CopyOnWriteArrayList

CopyOnWriteArrayList

并发包中的并发List只有CopyOnWriteArrayListCopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略;

写时复制策略产生的弱一致性问题

在这里插入图片描述

CopyOnWriteArrayList类图中,每个CopyOnWriteArrayList对象里面有一个array数组对象来存放具体元素,ReentrantLock独占锁对象来保证同时只有一个线程对array进行修改;

public class CopyOnWriteArrayList<E> implements List<E>{
	final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    
    final void setArray(Object[] a) {array = a;}
    final Object[] getArray() {return array;}
    
    public CopyOnWriteArrayList() {setArray(new Object[0]);}
    
}

CopyOnWriteArrayList中用来添加元素的函数有add(E e)add(int index,E element)addIfAbsent(E e)等,它们的原理类似,所以下面以add(E e)举例讲解。

    public boolean add(E e) {
        //1. 获取独占锁(保证并发安全)
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //2. 获取array
            Object[] elements = getArray();
            int len = elements.length;
            //3. 复制array到新数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //4. 添加新元素到新数组
            newElements[len] = e;
            //5. 使用新数组替换添加前的数组
            setArray(newElements);
            return true;
        } finally {
            //6. 释放锁
            lock.unlock();
        }
    }

**写时复制:**可以看到上面的案例,在向CopyOnWriteArrayList中添加一个新元素时,CopyOnWriteArrayList通过从原array中复制出一个新的数组,在新复制出来的数组上进行新元素的添加;

    public E get(int index) {
        return get(getArray(), index);
    }
	//步骤A
	final Object[] getArray() {
        return array;
    }
	//步骤B
	private E get(Object[] a, int index) {
        return (E) a[index];
    }

如上代码,可以看到。调用get(int index)方法可以分为两个步骤,首先获取array数组(步骤A),然后通过下标访问指定位置的元素(步骤B)。可以看到整个过程没有进行加锁同步,那么问题就来了。

假设,线程X执行完步骤A后执行步骤B前,另一个线程Y进行了remove或者add操作,那么线程X在执行步骤B时的执行结果是什么样的呢?

    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)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                //分两次复制删除后剩余的元素到新数组
                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();
        }
    }

假设这个时候CopyOnWriteArrayListarray的内容如下所示(有1,2,3三个元素)

在这里插入图片描述

线程X执行完步骤A后执行步骤B前,另一个线程Yremove操作,删除元素1。此时先获取独占锁,然后进行写时复制操作(如上remove代码),在新复制的数组中删除get需要访问的元素1,然后让array指向新复制的数组。这个时候线程X开始执行步骤B步骤B操作的数组是线程Y删除之前的数组,不受影响;这其实就是写时复制策略产生的弱一致性问题

在这里插入图片描述

弱一致性的迭代器

所谓的弱一致性是指返回迭代器后,其他线程对list的增删改对迭代器不可见;

//迭代器的基本使用
public class IteratorDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("test1");
        list.add("test2");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

看一下CopyOnWriteArrayList中的迭代器

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;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
    }

如上代码,CopyOnWriteArrayList.iterator()获取的迭代器为COWIteratorCOWIterator对象的snapshot变量保存当前list内容,cursor是遍历list时数据的下标;

为什么说snapshotlist的快照呢?明明是指针的传递引用啊,而不是副本。这个就涉及到我们之前说到的addremove时的操作了。如果其他线程没有对list进行增删改,那么snapshot本身就是listarray,因为它们是引用关系。但是如果在遍历期间其他线程对list进行了增删改,那么snapshot就是快照了。因为增删改后list里面的数组被新数组替换了。

list中数组被新数组替换,老数组被snapshot引用。获取迭代器后,由于操作的是不同数组,所以其他线程对list进行的增删改不可见,这就是弱一致性

public class CopyOnWriteArrayListDemo {
    private static volatile CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>();

    static {
        copyOnWriteArrayList.add("hello");
        copyOnWriteArrayList.add("hello1");
        copyOnWriteArrayList.add("hello2");
        copyOnWriteArrayList.add("hello3");
        copyOnWriteArrayList.add("hello4");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread updateThread = new Thread(() -> {
            copyOnWriteArrayList.set(0, "hello world");
            copyOnWriteArrayList.remove(2);
            copyOnWriteArrayList.remove(3);
        }, "updateThread");
        //保证在修改线程启动之前获取迭代器
        Iterator iterator = copyOnWriteArrayList.iterator();
        //启动线程
        updateThread.start();
        //main等待修改线程执行完毕
        updateThread.join();
        //迭代元素
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
执行结果:
hello
hello1
hello2
hello3
hello4
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王叮咚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值