CopyOnWrite 思想及在 Java 并发包中的具体体现

读多写少的场景下引起的问题?数组

假设如今咱们的内存里有一个 ArrayList,这个 ArrayList 默认状况下确定是线程不安全的,要是多个线程并发读和写这个 ArrayList 可能会有问题。安全

那么,问题来了,咱们应该怎么让这个 ArrayList 变成线程安全的呢?多线程

有一个很是简单的办法,对这个 ArrayList 的访问都加上线程同步的控制,好比说必定要在 Synchronized 代码段来对这个 ArrayList 进行访问,这样的话,就能同一时间就让一个线程来操做它了,或者是用 ReadWriteLock 读写锁的方式来控制,均可以。并发

咱们假设就是用 ReadWriteLock 读写锁的方式来控制对这个 ArrayList 的访问,这样多个读请求能够同时执行从 ArrayList 里读取数据,可是读请求和写请求之间互斥,写请求和写请求也是互斥的。性能

代码大概就是相似下面这样:this

public Object  read() { 
    lock.readLock().lock(); 
    // 对ArrayList读取 
    lock.readLock().unlock(); 
} 
public void write() { 
    lock.writeLock().lock(); 
    // 对ArrayList写 
    lock.writeLock().unlock(); 
} 

相似上面的代码有什么问题呢?spa

最大的问题,其实就在于写锁和读锁的互斥。假设写操做频率很低,读操做频率很高,是写少读多的场景。那么偶尔执行一个写操做的时候,是否是会加上写锁,此时大量的读操做过来是否是就会被阻塞住,没法执行?这个就是读写锁可能遇到的最大的问题。线程

引入 CopyOnWrite 思想解决问题翻译

这个时候就要引入 CopyOnWrite 思想来解决问题了。它的思想就是,不用加什么读写锁,把锁通通去掉,有锁就有问题,有锁就有互斥,有锁就可能致使性能低下,会阻塞请求,致使别的请求都卡着不能执行。code

那么它怎么保证多线程并发的安全性呢?

很简单,顾名思义,利用“CopyOnWrite”的方式,这个英语翻译成中文,大概就是“写数据的时候利用拷贝的副原本执行”。你在读数据的时候,其实不加锁也不要紧,你们左右都是一个读罢了,互相没影响。问题主要是在写的时候,写的时候你既然不能加锁了,那么就得采用一个策略。假如说你的 ArrayList 底层是一个数组来存放你的列表数据,那么这时好比你要修改这个数组里的数据,你就必须先拷贝这个数组的一个副本。而后你能够在这个数组的副本里写入你要修改的数据,可是在这个过程当中实际上你都是在操做一个副本而已。

这样的话,读操做是否是能够同时正常的执行?这个写操做对读操做是没有任何的影响的吧!

看下面的图,来体会一下这个过程:

     

关键问题来了,那那个写线程如今把副本数组给修改完了,如今怎么才能让读线程感知到这个变化呢?

这里要配合上 Volatile 关键字的使用, Volatile 关键字的核心就是让一个变量被写线程给修改以后,立马让其余线程能够读到这个变量引用的最近的值,这就是 Volatile 最核心的做用。

因此一旦写线程搞定了副本数组的修改以后,那么就能够用 Volatile 写的方式,把这个副本数组赋值给 Volatile 修饰的那个数组的引用变量了。只要一赋值给那个 Volatile 修饰的变量,立马就会对读线程可见,你们都能看到最新的数组了。

下面是 JDK 里的 CopyOnWriteArrayList 的源码:

// 这个数组是核心的,由于用volatile修饰了 
// 只要把最新的数组对他赋值,其余线程立马能够看到最新的数组 
private transient volatile Object[] array; 

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修饰的变量 
       setArray(newElements); 
       return true; 
   } finally { 
       lock.unlock(); 
   } 
} 

咱们能够看看写数据的时候,它是怎么拷贝一个数组副本,而后修改副本,接着经过 Volatile 变量赋值的方式,把修改好的数组副本给更新回去,立马让其余线程可见的。

由于是经过副原本进行更新的,万一要是多个线程都要同时更新呢?那搞出来多个副本会不会有问题?

固然不能多个线程同时更新了,这个时候就是看上面源码里,加入了 Lock 锁的机制,也就是同一时间只有一个线程能够更新。

那么更新的时候,会对读操做有任何的影响吗?

绝对不会,由于读操做就是很是简单的对那个数组进行读而已,不涉及任何的锁。并且只要他更新完毕对 Volatile 修饰的变量赋值,那么读线程立马能够看到最新修改后的数组,这是 Volatile 保证的:

private E get(Object[] a, int index) { 
    // 最简单的对数组进行读取 
    return (E) a[index]; 
} 

这样就完美解决了咱们以前说的读多写少的问题。若是用读写锁互斥的话,会致使写锁阻塞大量读操做,影响并发性能。可是若是用了 CopyOnWriteArrayList,就是用空间换时间,更新的时候基于副本更新,避免锁,而后最后用 Volatile 变量来赋值保证可见性,更新的时候对读线程没有任何的影响!

相关文章
相关标签/搜索