CopyOnWriteArrayList

参考: 死磕 java集合 by 彤哥读源码

1. 概述

1.1 简介

CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过数组实现,每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样保证了只阻塞写操作,不阻塞读操作,实现读写分离。

1.2 继承体系

 

主要是继承了List接口,提供一系列根据索引操作数组的方法

 

 

2. 源码解析

2.1 属性字段

// 写时的加锁, 读不加锁
final transient Object lock = new Object();

// 存储元素的数组
private transient volatile Object[] array;

// 所有对array的操作都是通过getArray()和setArray()
final Object[] getArray() {
    return array;
}


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

 

2.2 构造函数

// 默认构造函数
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

// 集合构造
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] es;
    if (c.getClass() == CopyOnWriteArrayList.class)
        // 因为是读写分离的, 不必担心此集合被其它集合修改问题
        // 因此直接指向其数组
        es = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        es = c.toArray();
        // 其它类型集合不能保证, 因此需要复制
        if (es.getClass() != Object[].class)
            es = Arrays.copyOf(es, es.length, Object[].class);
    }
    setArray(es);
}


// 直接传递数组也需要复制
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

 

2.3 常规操作

// 尾插
public boolean add(E e) {
    // 写时加锁, 保证写的线程安全
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        // 数组长度和元素数量是一致的
        // 添加就是重新创建一个数组
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

 

// 插入指定位置
public void add(int index, E element) {
    // 写时加锁
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;
        // 尾插
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len + 1);
        // 非尾插
        else {
            // 新建容量+1的数组, 复制原数组元素
            newElements = new Object[len + 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index, newElements, index + 1,
                             numMoved);
        }
        // 添加元素
        newElements[index] = element;
        //
        setArray(newElements);
    }
}

 

// 移除元素
public E remove(int index) {
    // 加锁
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        E oldValue = elementAt(es, index);
        int numMoved = len - index - 1;
        Object[] newElements;
        // 尾部移除, 直接缩容
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len - 1);
        else {
            // 新建容量-1数组, 并复制
            newElements = new Object[len - 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index + 1, newElements, index,
                             numMoved);
        }
        setArray(newElements);
        return oldValue;
    }
}

 

// 这个是线程安全的?
public boolean remove(Object o) {
    Object[] snapshot = getArray();
    int index = indexOfRange(o, snapshot, 0, snapshot.length);
    // 调用remove之前数组实际上已经被改变了呢?
    return index >= 0 && remove(o, snapshot, index);
}

 

// get不用加锁, 即读不加锁
public E get(int index) {
    return elementAt(getArray(), index);
}

// 只要有改变元素内容的都要加锁
public E set(int index, E element) {
    // 加锁
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
        }
        // 没有数组长度的增加与减少, 设置的数组还是原数组
        setArray(es);
        return oldValue;
    }
}

 

 1 public boolean remove(Object o) {
 2     Object[] snapshot = getArray();
 3     int index = indexOfRange(o, snapshot, 0, snapshot.length);
 4     // 有元素才会进行处理, 不过因为上面没有加锁, 在此时集合的array可能已经改变了
 5     return index >= 0 && remove(o, snapshot, index);
 6 }
 7 
 8 // ??, 反正是删除
 9 private boolean remove(Object o, Object[] snapshot, int index) {
10     // 加锁
11     synchronized (lock) {
12         // 重新获取array
13         Object[] current = getArray();
14         int len = current.length;
15         // array已经被改变, 即快照无效
16         if (snapshot != current) findIndex: {
17             // min(index, len)之间查询, 因为可能缩容了
18             int prefix = Math.min(index, len);
19             for (int i = 0; i < prefix; i++) {
20                 // 
21                 if (current[i] != snapshot[i]
22                     && Objects.equals(o, current[i])) {
23                     // 提前定位到数据位置了, 而且这个数据和快照的数据还不是同一个对象
24                     index = i;
25                     break findIndex;
26                 }
27             }
28             // 没有数据
29             if (index >= len)
30                 return false;
31             // 
32             if (current[index] == o)
33                 break findIndex;
34             // 
35             index = indexOfRange(o, current, index, len);
36             if (index < 0)
37                 return false;
38         }
39         // 常规删除
40         Object[] newElements = new Object[len - 1];
41         System.arraycopy(current, 0, newElements, 0, index);
42         System.arraycopy(current, index + 1,
43                          newElements, index,
44                          len - index - 1);
45         setArray(newElements);
46         return true;
47     }
48 }

 

1 public int size() {
2     return getArray().length;
3 }

 

3. 总结

  • 使用synchronized加锁,保证线程安全
  • 写操作涉及到容量改变的都会加锁,并新建数组并拷贝数据,并在新数组中增删
  • 写操作不涉及到容量如set只是加锁
  • 使用读写分离的思想,读不加锁,写加锁
  • 写操作基本都是要加锁、要新建数组、要拷贝数据的,因此效率很低,适合于读多写极少的情况
  • 只保证最终一致性,不保证实时一致性(读时读到的可能是旧数组,而新数组还在处理中)
  • 没有size属性,因为数组容量和数组长度是相同的

 

posted @ 2020-04-19 12:33  YangDanMua  阅读(154)  评论(0编辑  收藏  举报