Java专栏:线程并发安全中你必须学会的CopyOnWriteList

Part1CopyOnWriteList简介

ArrayList是线程不安全的,于是JDK新增加了一个线程并发安全的List——CopyOnWriteList,中心思想就是,简单来说是,而我们下面分析的源码具体实现也是这个思想的体现。

继承体系:

image.png

我们单独看一下CopyOnWriteList的主要属性和下面要主要分析的方法有哪些。从图中看出:

每个CopyOnWriteList对象里面有一个array数组来存放具体元素

使用ReentrantLock独占锁来保证只有写线程对array副本进行更新。

CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁

下面还是主要看CopyOnWriteList的实现

成员属性

构造方法

(1)无参构造,默认创建的是一个长度为0的数组

(2)参数为Collection的构造方法

(3)创建一个包含给定数组副本的list

上面介绍的是CopyOnWriteList的初始化,三个构造方法都比较易懂,后面还是主要看看几个主要方法的实现

添加元素

下面是add(E e)方法的实现 ,以及详细注释

总结一下add方法的执行流程

调用add方法的线程会首先获取锁,然后调用lock方法对list进行加锁(了解ReentrantLock的知道这是个独占锁,所以多线程下只有一个线程会获取到锁)

只有线程会获取到锁,所以只有一个线程会去更新这个数组,此过程中别的调用add方法的线程被阻塞等待

获取到锁的线程继续执行

首先获取原数组以及其长度,然后将其中的元素复制到一个新数组中(newArray的长度是原长度+1)

给定数组下标为len+1处赋值

将新数组替换掉原有的数组

最后释放锁

总结起来就是,多线程下只有一个线程能够获取到锁,然后使用复制原有数组的方式添加元素,之后再将新的数组替换原有的数组,最后释放锁(别的add线程去执行)。

最后还有一点就是,数组长度不是固定的,每次写之后数组长度会+1,所以CopyOnWriteList也没有length或者size这类属性,但是提供了size()方法,获取集合的实际大小,size()方法如下

获取元素

使用get(i)可以获取指定位置i的元素,当然如果元素不存在就会抛出数组越界异常。

当然get方法这里也体现了的弱一致性问题。我们用下面的图示简略说明一下。图中给的假设情况是:threadA访问index=1处的元素

获取array数组

访问传入参数下标的元素

因为我们看到get过程是没有加锁的(假设array中有三个元素如图所示)。假设threadA执行之后之前,threadB执行remove(1)操作,threadB或获取独占锁,然后执行写时复制操作,即复制一个新的数组,然后在newArray中执行remove操作(1),更新array。threadB执行完毕array中index=1的元素已经是item3了。

然后threadA继续执行,但是因为threadA操作的是原数组中的元素,这个时候的index=1还是item2。所以最终现象就是虽然threadB删除了位置为1处的元素,但是threadA还是访问的原数组的元素。这就是

image.png

修改元素

修改也是属于,所以需要获取lock,下面就是set方法的实现

看了set方法之后,发现其实和add方法实现类似。

获得独占锁,保证同一时刻只有一个线程能够修改数组

获取当前数组,调用get方法获取指定位置的数组元素

判断get获取的值和传入的参数

相等,为了保证volatile语义,还是需要重新这只array

不相等,将原数组元素复制到新数组中,然后在新数组的index处修改,修改完毕用新数组替换原数组

释放锁

删除元素

下面是remove方法的实现,总结就是

获取独占锁,保证只有一个线程能够去删除元素

计算要移动的数组元素个数

如果删除的是最后一个元素,那么上面的计算结果是0,就直接将原数组的前len-1个作为新数组替换掉原数组

删除的不是最后一个元素,那么按照index分为前后两部分,分别复制到新数组中,然后替换即可

释放锁

迭代器

迭代器的基本使用方式如下,hashNext()方法用来判断是否还有元素,next方法返回具体的元素。

那么在CopyOnWriteArrayList中的迭代器是怎样实现的呢,为什么说是弱一致性呢(),下面就说一下CopyOnWriteArrayList中的实现

在上面的代码中我们能看处,list的iterator()方法实际上返回的是一个COWIterator对象,COWIterator对象的snapshot成员变量保存了list中array存储的内容,但是snapshot可以说是这个array的一个快照,为什么这样说呢

我们传递的是虽然是当前的,但是可能有别的线程对进行了修改然后将原本的替换掉了,那么这个时候list中的和引用的就不是一个了,作为原的快照存在,那么迭代器访问的也就不是更新后的数组了。这就是弱一致性的体现

我们看下面的例子

运行结果如下。实际上再上面的程序中我们先向list中添加了几个元素,然后再thread中修改list,同时让,并等待thread执行完然后打印list中的元素,发现 main线程并没有发现list中的array的变化,输出的还是原来的list,这就是弱一致性的体现。在此我向大家推荐一个架构学习交流圈。交流学习伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

main线程中的list的元素:item1 main线程中的list的元素:item2 main线程中的list的元素:item3

总结

CopyOnWriteArrayList是如何保证时线程安全的:使用ReentrantLock独占锁,保证同时只有一个线程对集合进行操作

数据是存储在CopyOnWriteArrayList中的array数组中的,并且array长度是动态变化的(操作会更新array)

在修改数组的时候,并不是直接操作array,而是复制出来了一个新的数组,修改完毕,再把旧的数组替换成新的数组

使用迭代器进行遍历的时候不用加锁,不会抛出ConcurrentModificationException异常,因为使用迭代器遍历操作的是数组的副本(当然,这是在别的线程修改list的情况)

set方法细节

注意到set方法中有一段代码是这样的

其实就是说要指定位置要修改的值和数组中那个位置的值是相同的,但是还是需要调用set方法更新array,这是为什么呢,参考Why setArray() method call required in CopyOnWriteArrayList,总结就是为了维护happens-before原则。首先看一下这段话

java.util.concurrent 中所有类的方法及其子包扩展了这些对更高级别同步的保证。尤其是:线程中将一个对象放入任何并发 collection 之前的操作 happen-before 从另一线程中的 collection 访问或移除该元素的 。在此我向大家推荐一个架构学习交流圈。交流学习伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

可以理解为这里是为了保证set操作之前的系列操作happen-before与别的线程访问array(不加锁)的,参照下面的例子

假设存在以上场景,如果能保证只会存在这样的轨迹:(1)->(2)->(3)->(4).根据上述java API文档中的约定有

(2)happen-before与(3),在线程内的操作有(1)happen-before与(2),(3)happen-before与(4),根据happen-before的传递性读写nonVolatileField变量就有(1)happen-before与(4)

所以Thread 1对nonVolatileField的写操作对Thread 2中a的读操作可见。如果CopyOnWriteArrayList的set的else里没有setArray(elements)的话,(2)happen-before与(3)就不再有了,上述的可见性也就无法保证。所以就是为了保证set操作之前的系列操作happen-before与别的线程访问array(不加锁)的后续操作

– END –
最新BAT java经典必考面试题链接:https://pan.baidu.com/s/1B_Lc1tluda0fbdrFnexOMQ
提取码:gw8d

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值