JUC---CopyOnWriteArrayList源码解析(JDK13)

java.util.concurrent包系列文章
JUC—ThreadLocal源码解析(JDK13)
JUC—ThreadPoolExecutor线程池源码解析(JDK13)
JUC—各种锁(JDK13)
JUC—原子类Atomic*.java源码解析(JDK13)
JUC—CAS源码解析(JDK13)
JUC—ConcurrentHashMap源码解析(JDK13)
JUC—CopyOnWriteArrayList源码解析(JDK13)
JUC—并发队列源码解析(JDK13)
JUC—多线程下控制并发流程(JDK13)
JUC—AbstractQueuedSynchronizer解析(JDK13)


一、CopyOnWrite*系列

CopyOnWriteArrayList和CopyOnWriteArraySet​也是常用的并发集合类。​他们支持并发的读写​。线程安全的。不过有它的缺点。
本篇就分析下CopyOnWriteArrayList的原理和源码。CopyOnWriteArraySet​也是类似的。


二 、CopyOnWriteArrayList

CopyOnWriteArrayList诞生自JDK1.5,是为了取代Vector和SynchronizedList的,就和ConcurrentHashMap取代SynchronizedMap一样。Vector和SynchronizedList锁的粒度太大,并发效率低。并且迭代时无法编辑。

适用场景:

  • 多读少写的情况,比如系统缓存的白名单。偶尔更新一次,读却特别多。

读写规则:

  • 读取完全不用加锁,写入也不会阻塞读操作。只有写入与写入之间需要同步等待。

看一份代码:

public static void main(String[] args) {
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("A");
    list.add("B");
    list.add("C");
    list.add("D");
    // CopyOnWriteArrayList生成迭代器。迭代器里的数组就是生成迭代器时list的数组。
    // 后续的对list的修改时对这个数组的数据不产生影响。修改是在新的内存中新copy了一个同样的数组。
    // 修改完成后再把list数组指向这个新数组。
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println("list is " + list);
        String next = iterator.next();
        System.out.println("next is " + next);
        if (next.equals("B")) {
            list.remove("D");
        }
        if (next.equals("C")) {
            list.add("E");
        }
    }
}
运行结果:
list is [A, B, C, D]
next is A
list is [A, B, C, D]
next is B
list is [A, B, C]
next is C
list is [A, B, C, E]
next is D

这就是CopyOnWrite的意思。创建新副本,读写分离。add的时候会在新内存copy一份原数组,add完成之后让CopyOnWriteArrayList的array指向新数组,旧的就会被回收了。实现了读写不需要同步。写与写才需要同步。
上面把CopyOnWriteArrayList换成ArrayList是不行的,ArrayList不支持在迭代时操作数组。

ArrayList报错原因:
看看ArrayList里面迭代器的属性和方法

 public E next() {
 	// 获取下一个元素之前会判断当前数组长度是否变化了
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

// expectedModCount是生成迭代器时modCount赋给expectedModCount
int expectedModCount = modCount;

final void checkForComodification() {
	// modCount每次修改就+1,
	// expectedModCount是生成迭代器时modCount赋给expectedModCount
	if (modCount != expectedModCount)
		// 如果数组长度变化了,说明被修改了,直接报错
        throw new ConcurrentModificationException();
}

CopyOnWriteArrayList的add方法​

public boolean add(E e) {
	// 上锁,写必须同步
   synchronized (lock) {
       Object[] es = getArray();
       int len = es.length;
       // 复制了一个新数组,并且长度是原来的+1
       es = Arrays.copyOf(es, len + 1);
       // 新加的元素添加到数组最后
       es[len] = e;
       // add完成后,把array指向新数组
       setArray(es);
       return true;
   }
}final void setArray(Object[] a) {
    array = a;
}

CopyOnWriteArrayList的get方法​
可以发现get方法没有任何的加锁行为

public E get(int index) {
    return elementAt(getArray(), index);
}

static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}

初始化方法与锁

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    final transient Object lock = new Object();

    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }
 
    final void setArray(Object[] a) {
        array = a;
    }

    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
}

CopyOnWriteArrayList的缺点:

  • 迭代期间的数据可能过时了的
  • 内存占用更大,因为是copy一份数组嘛

  • 我的公众号:Coding抠腚
  • 偶尔发发自己最近学到的干货。学习路线,经验,技术分享。技术问题交流探讨。
    Coding抠腚
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章