Java并发之CopyOnWriteArrayList源码------写时复制如何保证线程安全?

一.场景引入


还是继续上次的例子,三十个线程,每个都向list中加一个8位字符串并打印,观察结果
public class NotSafeDemo {

    public static void main(String[] args) {

        //List<String> list = new ArrayList<>();
		List<String> list = new CopyOnWriteArrayList<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());


        for (int i = 1; i <=  30; i++) {

            new Thread(()->{

                list.add(UUID.randomUUID().toString().substring(0,8)); //8位随机字串 写操作
                System.out.println(list);

            },String.valueOf(i)).start();

        }


    }

}


我注释掉了ArrayList的那句,大家可看我之前的文章查看结果用ArrayList会怎样
这次换了CopyOnWriteArrayList,我们来试试控制台怎么样:

"C:\Program Files\Java\jdk1.8.0_181\bin\java.exe" "-javaagent:D:\idea\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58770:D:\idea\IntelliJ IDEA 2019.2.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;C:\Users\李肇京\IdeaProjects\JUC\out\production\JUC" JUC_01_SellTicket.Collection.NotSafeDemo
[90456d38, dd8c1029, 1ef5a376]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1, ae9f2d2e]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1, ae9f2d2e, cd11cfbf]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1, ae9f2d2e, cd11cfbf, f6c3b44b]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1, ae9f2d2e, cd11cfbf, f6c3b44b, 496c8324, fe28e9f2]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b, cae337e1, ae9f2d2e, cd11cfbf, f6c3b44b, 496c8324, fe28e9f2, acf663ea]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f, 44e8fc9b]
[90456d38, dd8c1029, 1ef5a376, 91329f03, a79fdb00, 073c520f]
[90456d38, dd8c1029, 1ef5a376]
  • 不管试几次,发现都没有出现java.util.ConcurrentModificationException这个异常
  • 在之前的分析中,我们发现原因是list的toString方法,其内部会调用迭代器的.next()函数
  • 这个函数在遍历的时候,每次都会调用checkForComodification()进行检查
  • 当并发的情况下,我在读这个list的时候,有其他线程把它改了,所以出现异常

二.CopyOnWriteArrayList介绍


什么是CopyOnWrite:
  • Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

  • Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。


CopyOnWriteArrayList
  • CopyOnWriteArrayList 在列表有更新时直接将原有的列表复制一份,并再新的列表上进行更新操作,完成后再将引用移到新的列表上(java的第一个特性,速度快)。旧列表如果仍在使用中(比如遍历)则继续有效。
  • 如此一来就不会出现修改了正在使用的对象的情况(读(老对象)和写(新复制的对象)分别发生在两个对象上,第二个特性,读写分离),同时读操作也不必等待写操作的完成,免去了锁的使用加快了读取速度

只是看这些是不能理解它的设计思想和原理的,下面我们看一下源码。


三.CopyOnWriteArrayList源码分析

首先,看一下成员变量

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //序列化编号
    private static final long serialVersionUID = 8673264195747942595L;

    //可重入锁
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    //数组,仅通过get和set方法操作
    private transient volatile Object[] array;

可以看出CopyOnWriteArrayList的底层实际上是通过一个数组来保存数据的,而这个数组是通过set和get方法进行操作的,注意only,是仅通过get和set来操作


然后来看我们的这个关键add方法:

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//拿到当前数组
            int len = elements.length;//获得长度
            //拷贝当前数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //把要添加的元素放在新数组中
            newElements[len] = e;
            //调用set方法将新数组设置为当前数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();//解锁
        }
    }
  • 首先,整个写操作是在加锁的基础上的,避免了线程安全问题
  • 内部实际上就是创建了一个新数组,把要改动的地方在新数组上实施,然后把这个数组设置为当前数组。
  • 此时,其他线程有两种状态,要么是想读,要么是想写,使用这种方式,读的线程依旧工作,读的仍然是之前的数组,写的对象阻塞,避免线程安全问题

四.总结:

CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。

这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。

“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。


五.缺点

  • 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
  • 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值