JDK容器学习之CopyOnWriteArrayList:线程安全保障机制


声明:本文转载自https://my.oschina.net/u/566591/blog/1553650,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

JDK容器学习之CopyOnWriteArrayList

列表容器常见的有 ArrayListLinkedList,然而两者都是非线程安全的,若应用场景对线程安全有需求,则可以使用CopyOnWriteArrayList来代替传统的Vector

I. 存储结构

先看下类中定义的成员变量, 一个数组和一个锁

/** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock();  /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array; 

array: 保存了列表中的数据

lock: 修改时加锁,用于保证线程安全

底层数据结构依然是数组,相交于ArrayList而言,少了一个表示数组长度的size变量,获取列表长度是通过下面的方法

public int size() {     return getArray().length; }  final Object[] getArray() {     return array; } 

留一个问题:

为什么获取链表的长度个ArrayList的使用姿势不同,这样做有什么好处

II. 读取,删除,添加实现逻辑

1. 读取数据

读数据,带两个疑问进行看源码

  • 读取是否加锁
  • 若加锁性能如何保证;若不加锁线程安全如何保证

先看实现源码

private E get(Object[] a, int index) {     return (E) a[index]; }  /**  * {@inheritDoc}  *  * @throws IndexOutOfBoundsException {@inheritDoc}  */ public E get(int index) {     return get(getArray(), index); } 

结果比较清晰

  • 读数据不加锁
  • 线程安全保障先给个简单的说明,后面内容详细补充
    • 数组定义为volatile,确保最新改动对多线程可见
    • private transient volatile Object[] array;

2. 删除元素

直接在源码中加上一些注释

public E remove(int index) {   final ReentrantLock lock = this.lock;   // 加锁,保证同一时刻只能有一个线程对链表进行写操作   lock.lock();   try {       Object[] elements = getArray();       int len = elements.length;       E oldValue = get(elements, index);       int numMoved = len - index - 1;       if (numMoved == 0) { // 删除最后一个元素时           //直接进行数组拷贝,然后将tables数组引用指向拷贝后的数组           setArray(Arrays.copyOf(elements, len - 1));       } else {           // 创建一个新的数组,将旧数组内容拷贝到新数组中           // 然后将tables数组引用指向新的数组           Object[] newElements = new Object[len - 1];           System.arraycopy(elements, 0, newElements, 0, index);           System.arraycopy(elements, index + 1, newElements, index,                            numMoved);           setArray(newElements);       }       return oldValue;   } finally {       lock.unlock();   } } 

从删除的实现,可确定以下几点:

  • 修改加锁,确保同一时刻只有一个线程对数组进行修改
  • 修改并不是在原数组上进行的,而是创建一个新的数组,在新的数组上进行操作操作,然后将tables引用指向新的数组
  • 修改必然会涉及到数组内容的拷贝

3. 新增元素

ArrayList新增元素时,可能导致数组扩容;CopyOnWriteArrayList在列表的修改时,采用数组拷贝,在新的数组上进行操作,从这点出发,应该不存在扩容的问题,因为每次修改都会导致数组的重新拷贝

从代码出发,验证上面的观点

public void add(int index, E element) {     final ReentrantLock lock = this.lock;     // 新增,先加锁     lock.lock();     try {         Object[] elements = getArray();         int len = elements.length;         if (index > len || index < 0) {             // 数组越界判断             throw new IndexOutOfBoundsException("Index: "+index+                                                 ", Size: "+len);         }         Object[] newElements;         int numMoved = len - index;         // 将原数组拷贝到新的数组中         if (numMoved == 0) { // 添加在最后一个             newElements = Arrays.copyOf(elements, len + 1);         } else { // 添加到中间,需要两次拷贝             newElements = new Object[len + 1];             System.arraycopy(elements, 0, newElements, 0, index);             System.arraycopy(elements, index, newElements, index + 1,                              numMoved);         }                  // 直接将数据添加到新的数组         newElements[index] = element;         // 将tables引用指向新的数组         setArray(newElements);     } finally {         lock.unlock();     } } 

从实现得出以下几个结论

  • CopyOnWriteArrayList没有数组扩容一说,因为每次修改都会创建一个新的数组
  • 修改加锁,确保只有一个线程对列表进行修改

新增元素示意图

输入图片说明

III. 线程安全测试

在List的遍历过程中,新增,删除or修改其中元素值时,会出现什么问题?

先写个测试demo

public class CopyOnWriteTest {     List<String> list = new CopyOnWriteArrayList<>(             new String[]{                     "1", "2", "3", "4", "5", "6", "7", "8", "9"             }     );      private void modify() {         new Thread(() -> {             list.add(8, "a8");             list.remove(9);             list.set(6, "6666");             System.out.println("----修改完成----");         }).start();     }      @Test     public void testModify() throws InterruptedException {         Iterator<String> iterable = list.iterator();         int i = 0;         while (iterable.hasNext()) {             if (i++ == 1) {                 modify();             } else if (i == 4) {                 Thread.sleep(1000);             }             System.out.println("index: " + i + " value: " + iterable.next());         }                  Thread.sleep(1000);         System.out.println(list);     } } 

输出结果

index: 1 value: 1 index: 2 value: 2 index: 3 value: 3 ----修改完成---- index: 4 value: 4 index: 5 value: 5 index: 6 value: 6 index: 7 value: 7 index: 8 value: 8 index: 9 value: 9 [1, 2, 3, 4, 5, 6, 6666, 8, a8] 

发现在迭代的过程中,对列表进行修改,是不会影响迭代过程的,遍历的依然是原来的数组;(顺带说一句,如果换成ArrayList会抛并发修改的异常)

探究下原理,主要是因为 CopyOnWriteArrayList的迭代器的实现方式

static final class COWIterator<E> implements ListIterator<E> {     /** Snapshot of the array */     private final Object[] snapshot;     /** Index of element to be returned by subsequent call to next.  */     private int cursor;      private COWIterator(Object[] elements, int initialCursor) {         cursor = initialCursor;         snapshot = elements;     }      public boolean hasNext() {         return cursor < snapshot.length;     }      public boolean hasPrevious() {         return cursor > 0;     }      @SuppressWarnings("unchecked")     public E next() {         if (! hasNext())             throw new NoSuchElementException();         return (E) snapshot[cursor++];     }      @SuppressWarnings("unchecked")     public E previous() {         if (! hasPrevious())             throw new NoSuchElementException();         return (E) snapshot[--cursor];     }      public int nextIndex() {         return cursor;     }      public int previousIndex() {         return cursor-1;     }      /**      * Not supported. Always throws UnsupportedOperationException.      * @throws UnsupportedOperationException always; {@code remove}      *         is not supported by this iterator.      */     public void remove() {         throw new UnsupportedOperationException();     }      /**      * Not supported. Always throws UnsupportedOperationException.      * @throws UnsupportedOperationException always; {@code set}      *         is not supported by this iterator.      */     public void set(E e) {         throw new UnsupportedOperationException();     }      /**      * Not supported. Always throws UnsupportedOperationException.      * @throws UnsupportedOperationException always; {@code add}      *         is not supported by this iterator.      */     public void add(E e) {         throw new UnsupportedOperationException();     } } 

从源码分析可得知

  1. 构造方法,确保迭代器持有一份对数组的引用,后续的迭代是针对这个数组进行的;若在迭代过程中,列表发生修改,使得List的数组引用指向新的数组,也不会改变迭代器中对原数组的引用,所以依然遍历的是旧数组
  2. 因为上面的原则,迭代过程中,不允许对数组进行修改

IV. 对比&小结

List容器中,VectorCopyOnWriteArrayList都是线程安全的,下面则主要对比下两者的实现逻辑

1. Vector

  • 所有接口都加锁
  • 多线程访问时,导致锁的竞争,导致效率低下

2. CopyOnWriteArrayList

  1. 底层结构:数组
  2. 读取接口,无锁
  3. 修改列表,加锁,确保始终只有一个线程在修改列表内容
  4. 修改方式:
    • 将原数组内容拷贝到新的数组,直接修改新数组
    • 然后将新数组赋值给列表的数组引用(array)
  5. 每次修改都会先上锁,然后进行数组拷贝,所以性能较 ArrayList低;读取无锁,所以读的性能比Vector高(没有竞争)
  6. 遍历时,是对列表中当前所指向的数组进行遍历,遍历过程中对数组的修改,不会影响遍历的内容
  7. 默认初始容量为0

扫描关注,java分享

本文发表于2017年10月20日 12:34
(c)注:本文转载自https://my.oschina.net/u/566591/blog/1553650,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1714 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1