一下详细分析原因
官方曰,线程安全就是多线程访问时,采⽤了加锁机制,当⼀个线程访问该类的某个数据时,进⾏保护,其他线程不能进⾏访问直到该线程读取完,其他线程才可使⽤。不会出现数据不⼀致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。说白了,线程安全就是多个线程修改同一个变量的时候,修改的结果需要和单线程修改的结果相同。如果修改的结果和预期不符,那就是线程不安全。
代码例子:
结果为0,为什么不是1?
解释:因为for循环里新增了一个新的线程,来负责向list里add一个元素。但是我们打印的list.size 是主线程。如果在新的线程 new Thread 没执行完add 方法, 主线程就执行打印的代码,那么就是 0啊 。所以就是说,主线程等一等,让 for循环里面的新的线程 new Thread 先插入数据。
可以看到结果与期望值1是一致的。
情况①:
正常运行的情况,可以看到 10个线程 不争不抢 :
情况②:
多个线程抢占资源,有竞争,(仅仅对于往list塞数据这个动作来说)
情况③:
出现 ‘不安全’情况了 ,多线程操作 ArrayList 导致出现 add赋值 出现 null
出现 null 情景分析 ,先看看源码:
Object[] elementData : 保存所有元素值的 数组
size : elementData中存储的元素个数
再看看 add 函数的源码 :
ensureExplicitCapacity
将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。
elementData[size++] = e :
e是传入的值, 把这个值赋值在 elementData数组的 size++ 位置 。
很显然,这两步没有和在一块操作。
也就说如果出现这个扩容的触发和后面赋值并发情况 ,那么就有不安全问题产生。
ArrayList是基于数组实现,数组大小一旦确定就无法更改。
ArrayList的扩容: 将旧数组容器的元素拷贝到新大小的数组中(Arrays.copyOf函数)。
通过new ArrayList<>()实例的对象初始化的大小是0,所以第一次插入就肯定会触发扩容。如下代码:
那什么时候DEFAULT_CAPACITY = 10 默认值起作用呢?其实官方有注释:
添加第一个元素时,任何elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList都将扩展为DEFAULT_CAPACITY。
第一个数据是 null (其实应该称为 执行扩容操作,并发导致出现null值 )分析 :
第一个线程A 插入数据时属于首次add ,发现需要扩容 , 线程A 去扩容去了。
然后 我们是多线程操作场景, for循环第二次,触发new第二个线程B来了,线程B去add的时候,因为线程A第一次扩容可能并没完成,所以导致线程B 扩容所拿到list的elementDate是旧的,并不是线程A第一次扩容后对象, 线程B 拿到的 size还是 0 ,所以线程B 也认为自己是第一次add ,也需要扩容。
想一下 A 、B 线程的并发 一起进入扩容场景:
那么线程A 是第一次add的时候,他知道他要去扩容,他自己扩容完,自己整了个list的新elementDate ,然后 就开始赋值 elementDate[size++] = A的UUID值。
在线程A这个操作的过程中,线程 B 在做什么?
线程 B一开始 不巧也是以为要扩容,他拿着一个旧的 list的elementDate 也整了一个新的数组 ,
然后把 整个 list的 elementDate 引用指向 B线程自己弄出来的对象
this.elementData = B新构建的对象(这对象全部值为null);
然后做什么?
然后 线程B 开始执行 elementDate[size++] = B的UUID值。
线程A 的值赋值在他创建出来的 elementDate 里面,然后触发 size++ 。
但是线程 B 呢, 把 this.elementData 指向了自己的新弄出来的, 所以 A 的值无情被抛弃,但是线程 B 开始赋值的时候,
看看这个size在源码里的情况:
size是大家共用的,size 被线程A 加1了 ,所以就出现 线程 B 赋值的时候执行 elementDate[size++] = B的UUID值,出来的结果是:
[null , B的UUID值]
情况④:
java.util.ConcurrentModificationException 并发冲突
直接定位报错函数:
modCount是修改记录数,expectedModCount是期望修改记录数;
初始化的时候 expectedModCount=modCount ;
ArrayList的add函数、remove函数 操作都有对modCount++操作,当expectedModCount和modCount值不相等, 那就会报并发错误了
怎么办? 怎么安全起来?
1.使用 Vector :
List<String> resultList = newVector<>();
看看vector怎么保证安全的,add 方法synchronized加锁实现:
主意:Vector是一个线程安全的列表,底层采用数组实现。其线程安全的实现方式非常粗暴:Vector大部分方法和ArrayList都是相同的,只是加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了。JAVA官方文档中这样描述:如果不需要线程安全性,推荐使用ArrayList替代Vector。
2.使用 Collections里面的synchronizedList:
List<String> resultList =Collections.synchronizedList(new ArrayList<>());
synchronizedList同步块保证安全的:
但是迭代器未加锁,需要手动实现同步:
所以使用Collections.synchronizedList注意两个地方:
1.迭代操作必须加锁,可以使用synchronized关键字修饰;
2.synchronized持有的监视器对象必须是synchronized (list),即包装后的list,使用其他对象如synchronized (new Object())会使add,remove等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。
3.使用 CopyOnWriteArrayList :
List<String> resultList = new CopyOnWriteArrayList();
CopyOnWriteArrayList 使用ReentrantLock保证安全的:
CopyOnWriteArrayList 的set 也是上锁:
1.set add remove 都选择使用了Arrays.copyOf复制操作
2.get 多线程过程读取数据不是实时,那就可能出现 数据不一致问题,但是最终数据是一致的(读多写少就很合适)。
以上分析借鉴,非常感谢!
CopyOnWriteArrayList是java.util.concurrent包下面的一个实现线程安全的List,顾名思义,CopyOnWriteArrayList在进行写操作(add,remove,set等)时会进行Copy操作,可以推测出在进行写操作时CopyOnWriteArrayList性能应该不会很高。
可以看到CopyOnWriteArrayList底层实现为Object[] array数组。
每次添加元素时都会进行Arrays.copyOf操作,代价非常昂贵。
读的时候是不需要加锁的,直接获取。删除和增加是需要加锁的。
有两点必须讲一下。我认为CopyOnWriteArrayList这个并发组件,其实反映的是两个十分重要的分布式理念:
(1)读写分离
我们读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array,但是修改的时候,操作的是一个新的Object[] array,读和写操作的不是同一个对象,这就是读写分离。这种技术数据库用的非常多,在高并发下为了缓解数据库的压力,即使做了缓存也要对数据库做读写分离,读的时候使用读库,写的时候使用写库,然后读库、写库之间进行一定的同步,这样就避免同一个库上读、写的IO操作太多。
(2)最终一致
对CopyOnWriteArrayList来说,线程1读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,但是线程1拿到的还是最老的那个Object[] array,新添加进去的数据并没有,所以线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,但是对于之后的线程一定是一致的,它们拿到的Object[] array一定是三个线程都操作完毕之后的Object array[],这就是最终一致。最终一致对于分布式系统也非常重要,它通过容忍一定时间的数据不一致,提升整个分布式系统的可用性与分区容错性。当然,最终一致并不是任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求非常非常高,就必须做成强一致性的。
性能对比
通过前面的分析可知
Vector对所有操作进行了synchronized关键字修饰,性能应该比较差
CopyOnWriteArrayList在写操作时需要进行copy操作,读性能较好,写性能较差
Collections.synchronizedList性能较均衡,但是迭代操作并未加锁,所以需要时需要额外注意
测试结果:
总结:
- CopyOnWriteArrayList的写操作与Vector的遍历操作性能消耗尤其严重,不推荐使用。
- CopyOnWriteArrayList适用于读操作远远多于写操作的场景。
- Vector读写性能可以和Collections.synchronizedList比肩,但Collections.synchronizedList不仅可以包装ArrayList,也可以包装其他List,扩展性和兼容性更好。