并发编程学习笔记(二十三、CopyOnWriteArrayList源码分析)
目录:
- CopyOnWriteArrayList是什么
- 为什么要有CopyOnWriteArrayList
- 如何使用CopyOnWriteArrayList
- CopyOnWriteArrayList源码分析
CopyOnWriteArrayList是什么
CopyOnWriteArrayList是一个写入时复制的集合,与其说它是一种新的数据结构还不如说它是一种设计思想。
首先从它的名字来看,猜测它是ArrayList一种扩展,是在写入时复制到另一个容器的扩展,那这种扩展有什么用呢,往下看。
为什么要有CopyOnWriteArrayList
参考:https://baijiahao.baidu.com/s?id=1666117483794506320&wfr=spider&for=pc
——————————————————————————————————————————————————————————————————————
存在即合理,我们之前说CopyOnWriteArrayList是ArrayList的一种扩展,那我们这里就来讨论下,它扩展的到底是什么。
首先我们知道传统的ArrayList是非线程安全的,在多个线程读写的情况下会出现ConcurrentModificationException的异常,如果我们操作集合的场景是要求线程安全的话那就得使用Vector或是自己包装下ArrayList。
当然这种使用方式会影响读写效率的,因为他们都是采用独占锁实现的,同一时刻只能有一个线程能操作集合。
而CopyOnWriteArrayList正是为了解决这一问题。
——————————————————————————————————————————————————————————————————————
1、独占锁效率低:采用读写分离思想解决:
既然独占锁的效率低下,那我们可以换一种方式,采用读写分离式的思想将读操作和写操作进行分开即可。
读操作不加锁,所有线程都不会阻塞;写操作加锁,线程会阻塞。
2、写线程获取到锁,其他线程包括读线程阻塞:
但是这时候又出现了另外一个问题了:写线程获取到锁之后,其他的读线程会陷入阻塞。
3、复制思想:解决问题2:
这咋办呢?我们可以再转化一下思想:
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写回内存,其他的线程就会读到了脏数据。这就是CopyOnWriteArrayList的思想和原理,就是拷贝一份写;所以使用条件也很局限,那就是在读多写少的情况下比较好。
如何使用CopyOnWriteArrayList
使用方式和ArrayList类似,因为他们都是List的子类。
1 public static void main(String[] args) throws InterruptedException { 2 // 塞10个数据用于测试 3 CopyOnWriteArrayList<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>(); 4 for (int i = 0; i < 10; i++) { 5 copyOnWriteArrayList.add(i); 6 } 7 8 // 定时打印集合元素 9 new Thread(() -> { 10 for (int i = 0; i < copyOnWriteArrayList.size(); i++) { 11 try { 12 Thread.sleep(1000); 13 } 14 catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 System.out.println(Thread.currentThread().getName() + "输出元素:" + copyOnWriteArrayList.get(i)); 18 } 19 }).start(); 20 // 定时删除集合元素 21 new Thread(() -> { 22 for (int i = 0; i < copyOnWriteArrayList.size(); i++) { 23 try { 24 Thread.sleep(800); 25 } 26 catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 if (copyOnWriteArrayList.get(i) % 2 == 0) { 30 System.out.println(Thread.currentThread().getName() + "删除元素:" + copyOnWriteArrayList.remove(i)); 31 } 32 } 33 }).start(); 34 35 Thread.sleep(10000); 36 for (Integer integer : copyOnWriteArrayList) { 37 System.out.println("CopyOnWriteArrayList剩余元素:" + integer); 38 } 39 }
CopyOnWriteArrayList源码分析
前面我们说到CopyOnWriteArrayList并不算是一种新的数据结构,而是一种思想,其核心思想就是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
根据上面所说的我们来看下CopyOnWriteArrayList添加元素的add方法:
1 public boolean add(E e) { 2 final ReentrantLock lock = this.lock; 3 lock.lock(); 4 try { 5 Object[] elements = getArray(); 6 int len = elements.length; 7 Object[] newElements = Arrays.copyOf(elements, len + 1); 8 newElements[len] = e; 9 setArray(newElements); 10 return true; 11 } finally { 12 lock.unlock(); 13 } 14 }
源码很简单,和上面的所说的实现一样,首先是我们所说的为了线程安全而加的重入锁,然后我们拿到CopyOnWriteArrayList中的数据源array,之后复制一份新的数组并将传入的值e添加到新数组,最后重新设置CopyOnWriteArrayList中的数据源为新数组。
以及set、remove方法均是如此,你可以对比着看下。
1 public E set(int index, E element) { 2 final ReentrantLock lock = this.lock; 3 lock.lock(); 4 try { 5 Object[] elements = getArray(); 6 E oldValue = get(elements, index); 7 8 if (oldValue != element) { 9 int len = elements.length; 10 Object[] newElements = Arrays.copyOf(elements, len); 11 newElements[index] = element; 12 setArray(newElements); 13 } else { 14 // Not quite a no-op; ensures volatile write semantics 15 setArray(elements); 16 } 17 return oldValue; 18 } finally { 19 lock.unlock(); 20 } 21 }
1 public E remove(int index) { 2 final ReentrantLock lock = this.lock; 3 lock.lock(); 4 try { 5 Object[] elements = getArray(); 6 int len = elements.length; 7 E oldValue = get(elements, index); 8 int numMoved = len - index - 1; 9 if (numMoved == 0) 10 setArray(Arrays.copyOf(elements, len - 1)); 11 else { 12 Object[] newElements = new Object[len - 1]; 13 System.arraycopy(elements, 0, newElements, 0, index); 14 System.arraycopy(elements, index + 1, newElements, index, 15 numMoved); 16 setArray(newElements); 17 } 18 return oldValue; 19 } finally { 20 lock.unlock(); 21 } 22 }