JDK1.8 CopyOnWriteArrayList源码分析

CopyOnWriteArrayList是线程安全的ArrayList,本文基于JDK1.8对CopyOnWriteArrayList源码分析。

1.类结构

CopyOnWriteArrayList类层级结构图:

CopyOnWriteArrayList实现了List的所有方法 ,主要成员变量有以下两个:

     //可重入锁,用于对写操作加锁
    final transient ReentrantLock lock = new ReentrantLock();
    //Object对象数据,用来存储数据,使用volatile关键字修饰,目的是一个线程对字段的修改使另一个线程立即可见
    private transient volatile Object[] array;

可以看到CopyOnWriteArrayList没有和容量有关的属性或者常量,下面根据源码进一步分析,就可以知道原因了。

2.方法解析
2.1 构造函数
CopyOnWriteArrayList() 无参构造函数
public CopyOnWriteArrayList() {
        //无参构造方法,创建一个长度为0的数组
        setArray(new Object[0]);
    }

无参构造函数创建了一个长度为0的对象数组

CopyOnWriteArrayList(Collection<? extends E> c)
public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            //指定集合c的集合类型就是CopyOnWriteArrayList,则直接强转为CopyOnWriteArrayList再调用getArray()方法,赋值给elements
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            //将集合转为对象数组
            elements = c.toArray();
            if (c.getClass() != ArrayList.class)
                //指定集合c的集合类型不是ArrayList(ArrayList底层是对象数组),就将对象数组拷贝复制到elements
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        //设置array值
        setArray(elements);
    }
CopyOnWriteArrayList(E[] toCopyIn)
public CopyOnWriteArrayList(E[] toCopyIn) {
        //将入参拷贝一份赋值给array
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
2.2 add(E e)

add(E e)往CopyOnWriteArrayList末尾添加元素

public boolean add(E e) {
        //获取可重入锁
        final ReentrantLock lock = this.lock;
        //加锁,同一时间只能有一个线程进入
        lock.lock();
        try {
            //获取当前数组array
            Object[] elements = getArray();
            //获取当前array数组长度
            int len = elements.length;
            //复制一个新数组,新数组长度为当前array数组的长度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新数组末尾添加元素
            newElements[len] = e;
            //将新数组赋值给array属性
            setArray(newElements);
            return true;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

add操作使用ReentrantLock可重入锁来确保线程安全。通过add方法,我们可以看出CopyOnWriteArrayList修改操作的基本思想为:

a.复制一个新数组,新数组长度为当前数组长度+1,刚好能够容纳要添加的元素

b.在新数组里操作(添加、修改或者删除)

c.将新数组赋值给array属性,替换旧数组

这种思想称为“写时复制”,所以叫做CopyOnWriteArrayList。

此外,CopyOnWriteArrayList没有类似ArrayList中的grow()方法扩容的操作

2.3 add(int index, E element)
add(int index, E element)在指定下标添加指定元素
public void add(int index, E element) {
        //获取可重入锁
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //获取当前数组array
            Object[] elements = getArray();
            //获取当前数组array的长度
            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)
                //当numMoved=0,说明是在末尾添加,和add(E e)方法一致
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                //否则创建一个新数组,新数组长度为当前array数组的长度+1
                newElements = new Object[len + 1];
                //将原数组index之前的元素复制到新数组
                System.arraycopy(elements, 0, newElements, 0, index);
                //将原数组index下标后所有(共numMoved个)元素复制到新数组
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //将新数组的index位置设置为指定元素element
            newElements[index] = element;
            //将新数组赋值给array属性
            setArray(newElements);
        } finally {
            //释放锁
            lock.unlock();
        }
    }
2.4 remove(int index)

remove(int index)删除指定下标元素

public E remove(int index) {
        //获取可重入锁
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //获取当前数组array
            Object[] elements = getArray();
            //获取当前数组array的长度
            int len = elements.length;
            //获取array数组index下标的旧值
            E oldValue = get(elements, index);
            //计算需要移动的元素个数
            int numMoved = len - index - 1;
            if (numMoved == 0)
                //如果删除的是最后一个元素,将当前array设置为新数组,新数组的长度为旧数组长度-1
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                //创建一个新数组,数组长度为旧数组长度-1
                Object[] newElements = new Object[len - 1];
                //分段复制,将原数组index之前的元素复制到新数组
                System.arraycopy(elements, 0, newElements, 0, index);
                //分段复制,将原数组index+1之后的元素复制到新数组
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                //将新数组赋值给array属性
                setArray(newElements);
            }
            //返回旧值
            return oldValue;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

通过代码可以发现,CopyOnWriteArrayList中的增删改操作都是在新数组中进行的,通过加锁的方式确保同一时刻只有一个线程可以操作,操作后赋值给array属性,替换旧数组。

2.5 remove(Object o)
remove(Object o)删除指定元素
public boolean remove(Object o) {
        //获取当前数组array
        Object[] snapshot = getArray();
        //获取对象o的下标
        int index = indexOf(o, snapshot, 0, snapshot.length);
        //如果index<0,表示数组中不存在对象o,返回false,否则调用remove()方法删除对象
        return (index < 0) ? false : remove(o, snapshot, index);
    }

private boolean remove(Object o, Object[] snapshot, int index) {
        //获取可重入锁
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //获取当前数组array
            Object[] current = getArray();
            //获取当前数组array的长度
            int len = current.length;
            //如果入参数组snapshot和当前数组current 不相等,说明当前数组已经被其他线程修改
            if (snapshot != current)
                //定义一个循环体,设置别名为:findIndex,使用break findIndex可以跳出(多重循环)循环体
                findIndex: {
                //在指定的下标index和当前数组array的长度 取最小值
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    //集合结构发生修改,且元素未被删除
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                //待删除的元素下标index大于数组长度len,则该元素已经被其他线程删除,方法结束返回false
                if (index >= len)
                    return false;
                //指定下标index元素值和待删除的元素值匹配,直接退出循环体
                if (current[index] == o)
                    break findIndex;
                //遍历当前数组index下标之后的部分,获取指定元素o的下标
                index = indexOf(o, current, index, len);
                //若获取不到,方法结束,返回false
                if (index < 0)
                    return false;
            }
            //创建新数组,新数组的长度为当前数组长度的len-1
            Object[] newElements = new Object[len - 1];
            //分段复制,将原数组index之前的元素复制到新数组
            System.arraycopy(current, 0, newElements, 0, index);
            //分段复制,将原数组index+1之后的元素复制到新数组
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            //将新数组赋值给array属性
            setArray(newElements);
            return true;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

可以看到,remove(Object o)方法没有加锁,实际上是调用的方法remove(Object o, Object[] snapshot, int index) 加锁了。可能导致在调用remove(Object o)方法之后到获取到锁之前,当前数组array被其他线程修改,所以要判断入参snapshot 和当前数组current是否相等。

知识点:

a.break只可跳出一层循环,给break 设置别名,可用于跳出多重循环。语法格式为:

for (int i = 0; i < 3; i++) {
    flag:  for (int j = 0; j < 5; j++) {
      if (j == 3) {
       break flag;
      }
       System.out.print("a");
    }
    System.out.println("b");
}

#输出如下:
aaab
aaab
aaab
当j=3时,直接跳出了if和内层for循环
        

b.分段复制  

//分段复制,将原数组index之前的元素复制到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//分段复制,将原数组index+1之后的元素复制到新数组
System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
2.6 set(int index, E element)
set(int index, E element) 设置指定位置的值
public E set(int index, E element) {
        //获取可重入锁
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //获取当前数组array
            Object[] elements = getArray();
            //获取指定下标index的旧值
            E oldValue = get(elements, index);

            if (oldValue != element) {
                 //如果旧值和新值不相等,获取当前数组array的长度
                int len = elements.length;
                //复制一个新数组,长度和旧数组一致
                Object[] newElements = Arrays.copyOf(elements, len);
                //为新数组index下标赋值为新值
                newElements[index] = element;
                //新数组赋值给array属性,替换旧数组
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                //即使新值和旧值一致,为了确保volatile语义,需要重新设置array 目的:因为array属性被volatile修改,防止指令重排序
                setArray(elements);
            }
            //返回旧值
            return oldValue;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

可以看到,set操作时,当新值和指定下标index的旧值相等,仍然需要重新设置array,这是因为数组array被volatile修饰,设置后可保证代码的执行前后顺序,防止指令重排后续再单独分析volatile关键字的原理。

2.7 get(int index)
get(int index)获取指定下标的元素
public E get(int index) {
        //调用getArray()获取当前数组array,
        return get(getArray(), index);
    }

final Object[] getArray() {
        return array;
    }

private E get(Object[] a, int index) {
        //获取数组a 下标index的值
        return (E) a[index];
    }

可以看到get方法没有加锁,所以在并发情况下可能出现以下情况:

a.线程1调用get(int index)方法取值,内部通过getArray()方法获取到array属性值

b.线程2调用CopyOnWriteArrayList的增删改方法,内部通过setArray()方法在新数组中修改了值,还未赋值给array属性,替换旧数组

c.线程1还是从旧的array数组中取值

所以get方法是弱一致性的。

2.8 size()
public int size() {
        //获取数组元素个数
        return getArray().length;
    }

size()返回当前array数组长度,因为CopyOnWriteArrayList中的array数组每次当刚好能容纳下所有元素,并不像ArrayList那样会预留一定的空间。所以CopyOnWriteArrayList没有size属性,元素个数和数组长度是相等的。

2.9 迭代器
 public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
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;
        }
        ......
    }

可以看到,迭代器没有在锁中进行,也是弱一致性的。如果没有其他线程对CopyOnWriteArrayList进行增删改操作,那么snapshot还是创建迭代器时获取的array。如果有其他线程对CopyOnWriteArrayList进行增删改操作,旧的数组会被新数组给替换掉,但是snapshot还是原来的旧的数组的引用:

CopyOnWriteArrayList<String> tempList = new CopyOnWriteArrayList<>();
        tempList.add("hello");
        Iterator<String> iterator = tempList.iterator();
        tempList.add("world");
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
}
# 输出结果为hello
3. 总结

a.CopyOnWriteArrayList体现了写时复制的思想,增删改都是在复制的新数组中操作的

b.CopyOnWriteArrayList的增删改方法通过ReentrantLock可重入锁确保线程安全

c.CopyOnWriteArrayList的取值方法是弱一致性

d.同一时刻只能有一个线程对CopyOnWriteArrayList进行增删改操作,而读操作没有限制,并且

CopyOnWriteArrayList的增删改都需要复制一个新数组,增加了内存的消耗,所以CopyOnWriteArrayList适合读多写少的情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值