• 作者:老汪软件技巧
  • 发表时间:2024-12-24 10:05
  • 浏览量:

出处: /post/745150…

大家好,我是Coder哥,今天我们来聊一下CopyOnWrite这一编程思想。

看到CopyOnWrite 我相信作为一个Java开发第一时间想到的是CopyOnWriteArrayList这个容器,没错,这个容器确实是这一思想的精髓,也是面试中的常客,但是很多人面试的时候也只是回答一下 List并发时可以用 CopyOnWriteArrayList 来对标ConcurrentHashMap, 再深入就不知道回答啥了。那我们之前文章 【锁思想】高并发下线程饥饿?看看读写锁是怎么避免饥饿的中有聊过读写锁,也在【锁思想】自旋 or CAS 它俩真的一样吗?一文搞懂聊过无锁并发的实现,我们还聊过【锁思想-终章】解锁高性能编程的密码:掌握JVM锁优化的黄金法则锁优化相关的知识,那么CopyOnWrite 思想和其他锁思想有什么关系呢?,本篇文章就分析一下CopyOnWrite 这一思想和其他锁思想的联系,从以下个方面入手:

CopyOnWrite 是什么?CopyOnWrite 在CopyOnWriteArrayList中的应用。CopyOnWrite、读写锁、无锁并发的关系和各自的优劣势。CopyOnWrite在其他场景中的应用。总结CopyOnWrite 是什么?

从字面的意思上说就是写时复制,简单来说,每当有写操作时,我们不直接修改原数据,而是复制一份新数据进行修改,修改完成后,再用新的数据替换掉旧数据,这样能达到读写分离的效果。这种思想的核心优势是:

这一思想用处非常广泛,其中之一就是 CopyOnWriteArrayList,我们结合CopyOnWriteArrayList详细的了解一下。

CopyOnWrite 在CopyOnWriteArrayList中的应用。

CopyOnWriteArrayList 是 Java 中并发包 java.util.concurrent 下的一个容器,它通过实现 CopyOnWrite思想,提供了一种线程安全的 List 实现。接下来我们结合CopyOnWriteArrayList源码来具体说明 CopyOnWrite 思想的应用。

CopyOnWriteArrayList 整体的结构代码如下:

/** 可重入锁对象 */
final transient ReentrantLock lock = new ReentrantLock(); 
/** CopyOnWriteArrayList底层由数组实现,volatile修饰,保证数组的可见性 */
private transient volatile Object[] array; 
/**
* 获取数组
*/
final Object[] getArray() {    
  return array;
} 
/**
* 设置数组
*/
final void setArray(Object[] a) {    
  array = a;
} 
/**
* 初始化CopyOnWriteArrayList相当于初始化数组
*/
public CopyOnWriteArrayList() {    
  setArray(new Object[0]);
}

从上面代码中我们能看到以下两点:

类中会有一个 ReentrantLock 锁, 用来保证Write的安全。volatile 能保证多线程修改的时候的可见性。

然后我们看一下Add方法的具体实现:add 方法

public boolean add(E e) {     
  // 加锁    
  final ReentrantLock lock = this.lock;    
  lock.lock();    
  try {         
    // 得到原数组的长度和元素        
    Object[] elements = getArray();        
    int len = elements.length;         
    // 复制出一个新数组        
    Object[] newElements = Arrays.copyOf(elements, len + 1);         
    // 添加时,将新元素添加到新数组中        
    newElements[len] = e;         
    // 将volatile Object[] array 的指向替换成新数组        

无锁并发编程__远程教育解决致富瓶颈

setArray(newElements); return true; } finally { lock.unlock(); } }

add 方法是Write操作。

首先需要利用 ReentrantLock 的 lock 方法进行加锁,获取锁之后,得到原数组的长度和元素,也就是利用 getArray 方法得到 elements 并且保存 length。然后利用 Arrays.copyOf 方法Copy出一个新的数组,得到一个和原数组内容相同的新数组,并且把新元素添加到新数组中。最后完成添加动作后,转换引用所指向的对象,利用 setArray(newElements) 操作就可以把 volatile Object[] array 的指向替换成新数组同时保证了对其他线程的可见性,最后在 finally 中把锁解除。

上述步骤体现了 CopyOnWrite 思想: 写操作是在原容器的副本上进行的,而在读取数据时并不会对容器加锁。需要注意的是,如果在容器副本创建期间有新的读取操作进入,读取到的数据仍然是旧的数据。这是因为在副本创建过程中,原容器的引用并未发生变化,只有在写操作完成后,引用才会指向新的副本。

然后我们看一下读操作代码如下:

public E get(int index) {    
  return get(getArray(), index);
}
final Object[] getArray() {    
  return array;
}
private E get(Object[] a, int index) {    
  return (E) a[index];
}

可以看出,get 相关的操作完全没有加锁,确保了读操作的高效性。

从上面的介绍我们可以知道,CopyOnWrite 实现了读写分离,可以同时读,也能读的时候写,也支持并发读写并且能保证并发读写的安全,那么CopyOnWrite 和读写锁、无锁并发有什么关系?以及他们各自适用的场景都是什么?

CopyOnWrite、读写锁、无锁并发的关系和各自的优劣势。

之前文章 【锁思想】高并发下线程饥饿?看看读写锁是怎么避免饥饿的中有聊过读写锁,也在【锁思想-终章】解锁高性能编程的密码:掌握JVM锁优化的黄金法则聊过CAS,从中我们知道读写锁,CAS的原理,那么我们分别简单的再介绍一下:

无锁并发是什么?它的常用实现方式有哪些?

无锁并发(Lock-free concurrency) 是指在多线程环境下,线程不需要使用传统的锁机制(如 synchronized 或 ReentrantLock)来控制对共享资源的访问。通过无锁的并发技术,可以避免使用锁带来的性能瓶颈、死锁等问题,提升系统的吞吐量和响应性。

在无锁并发中,线程对共享数据的操作并不会被显式的锁住,而是通过特定的算法保证线程安全。这通常依赖于硬件提供的原子操作(例如 CAS 操作)来实现并发数据结构的修改。

无锁并发主要有两类技术:

读写锁是什么?它的优势是什么?

读写锁(Read-Write Lock) 是一种特殊类型的锁,它允许多个线程同时读取共享资源,但在写操作时,只允许一个线程访问资源。换句话说,读写锁是通过区分读操作和写操作,来优化并发性能的一种机制。它的主要目的是提升在多读少写的场景下的性能。

在 Java 中,读写锁由 java.util.concurrent.locks.ReadWriteLock 接口定义,常用的实现类是 ReentrantReadWriteLock。

读写锁的适用

CopyOnWrite 与读写锁的区别是什么?

从上面的学习我们知道CopyOnWrite 的核心优势是读操作无需加锁,写操作时通过复制数据执行写,那么他们两个的区别如下:

上面的第二条理论上也是无锁并发的一种方式,读写共存不用加锁.

那么除了在CopyOnWriteArrayList中有应用,在我们编程中其他的应用场景有哪些呢?

CopyOnWrite在其他场景中的应用。

例子一:Nacos 源码中的应用

在 Nacos 源码中,CopyOnWrite 思想也有广泛的应用。例如,Nacos 在管理配置中心时,会频繁进行读取操作,但写入操作相对较少。为了提高读取性能并避免加锁,Nacos 使用了类似 CopyOnWrite 的机制来管理一些只读数据的容器。

以 Nacos 中的 ConfigService 为例,它需要频繁地读取配置信息,而配置的修改(如修改某个配置项)相对较少。为了避免每次读取都加锁,Nacos 采用了类似 CopyOnWrite 的策略,即每次配置修改时,会复制一份新的配置,而读取时则直接读取当前配置的副本,这样可以确保读操作高效且线程安全。

例子二:缓存系统

另一个典型的应用场景是缓存系统。在缓存系统中,读取操作通常是非常频繁的,而写操作(如缓存更新)则相对较少。为了确保在高并发环境下读取操作的高效性,很多缓存系统采用了类似 CopyOnWrite 的思想。当缓存需要更新时,系统会将旧缓存的副本复制一份,并在副本上进行修改,然后将副本替换掉原缓存。这样,读取操作就不会被写操作阻塞,极大地提高了缓存的读取性能。

总结

CopyOnWrite 编程思想是通过写时复制来实现线程安全的,它适用于读多写少的场景,能够极大提升并发读操作的效率。CopyOnWriteArrayList 作为其在 Java 中的实现,通过每次写操作都复制一份新的副本,保证了读操作的无锁执行,同时避免了并发写时的锁竞争。虽然它在写操作频繁时可能会带来内存占用和性能开销的问题,但在大多数读多写少的场景中,仍然是一个非常有效的解决方案。

到最后了,感谢各位能看到这里

《锁思想》 系列文章:

参考文章:/posts/017.h…