一下详细分析原因

官方曰,线程安全就是多线程访问时,采⽤了加锁机制,当⼀个线程访问该类的某个数据时,进⾏保护,其他线程不能进⾏访问直到该线程读取完,其他线程才可使⽤。不会出现数据不⼀致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。说白了,线程安全就是多个线程修改同一个变量的时候,修改的结果需要和单线程修改的结果相同。如果修改的结果和预期不符,那就是线程不安全。

代码例子:




java arraylist对应的线程安全容器 arraylist线程安全么_数组


结果为0,为什么不是1?

解释:因为for循环里新增了一个新的线程,来负责向list里add一个元素。但是我们打印的list.size 是主线程。如果在新的线程 new Thread 没执行完add 方法, 主线程就执行打印的代码,那么就是 0啊 。所以就是说,主线程等一等,让 for循环里面的新的线程 new Thread 先插入数据。


java arraylist对应的线程安全容器 arraylist线程安全么_赋值_02


可以看到结果与期望值1是一致的。

情况①:

正常运行的情况,可以看到 10个线程 不争不抢 :


java arraylist对应的线程安全容器 arraylist线程安全么_数组_03


情况②:

多个线程抢占资源,有竞争,(仅仅对于往list塞数据这个动作来说)


java arraylist对应的线程安全容器 arraylist线程安全么_数组_04


情况③:

出现 ‘不安全’情况了 ,多线程操作 ArrayList 导致出现 add赋值 出现 null


java arraylist对应的线程安全容器 arraylist线程安全么_赋值_05


出现 null 情景分析 ,先看看源码:


java arraylist对应的线程安全容器 arraylist线程安全么_Powered by 金山文档_06


Object[] elementData : 保存所有元素值的 数组

size : elementData中存储的元素个数

再看看 add 函数的源码 :


java arraylist对应的线程安全容器 arraylist线程安全么_数据_07


java arraylist对应的线程安全容器 arraylist线程安全么_java_08


ensureExplicitCapacity

将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。

elementData[size++] = e :

e是传入的值, 把这个值赋值在 elementData数组的 size++ 位置 。

很显然,这两步没有和在一块操作。

也就说如果出现这个扩容的触发和后面赋值并发情况 ,那么就有不安全问题产生。

ArrayList是基于数组实现,数组大小一旦确定就无法更改。

ArrayList的扩容: 将旧数组容器的元素拷贝到新大小的数组中(Arrays.copyOf函数)。


java arraylist对应的线程安全容器 arraylist线程安全么_数组_09


通过new ArrayList<>()实例的对象初始化的大小是0,所以第一次插入就肯定会触发扩容。如下代码:


java arraylist对应的线程安全容器 arraylist线程安全么_赋值_10


那什么时候DEFAULT_CAPACITY = 10 默认值起作用呢?其实官方有注释:


java arraylist对应的线程安全容器 arraylist线程安全么_赋值_11


添加第一个元素时,任何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);

然后做什么?


java arraylist对应的线程安全容器 arraylist线程安全么_数组_12


然后 线程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 并发冲突


java arraylist对应的线程安全容器 arraylist线程安全么_java_13


直接定位报错函数:


java arraylist对应的线程安全容器 arraylist线程安全么_java_14


modCount是修改记录数,expectedModCount是期望修改记录数;

初始化的时候 expectedModCount=modCount ;

ArrayList的add函数、remove函数 操作都有对modCount++操作,当expectedModCount和modCount值不相等, 那就会报并发错误了

怎么办? 怎么安全起来?

1.使用 Vector :

List<String> resultList = newVector<>();

看看vector怎么保证安全的,add 方法synchronized加锁实现:


java arraylist对应的线程安全容器 arraylist线程安全么_数组_15


主意:Vector是一个线程安全的列表,底层采用数组实现。其线程安全的实现方式非常粗暴:Vector大部分方法和ArrayList都是相同的,只是加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了。JAVA官方文档中这样描述:如果不需要线程安全性,推荐使用ArrayList替代Vector。

2.使用 Collections里面的synchronizedList:

List<String> resultList =Collections.synchronizedList(new ArrayList<>());

synchronizedList同步块保证安全的:


java arraylist对应的线程安全容器 arraylist线程安全么_Powered by 金山文档_16


但是迭代器未加锁,需要手动实现同步:


java arraylist对应的线程安全容器 arraylist线程安全么_java_17


所以使用Collections.synchronizedList注意两个地方:

1.迭代操作必须加锁,可以使用synchronized关键字修饰;

2.synchronized持有的监视器对象必须是synchronized (list),即包装后的list,使用其他对象如synchronized (new Object())会使add,remove等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。

3.使用 CopyOnWriteArrayList :

List<String> resultList = new CopyOnWriteArrayList();

CopyOnWriteArrayList 使用ReentrantLock保证安全的:


java arraylist对应的线程安全容器 arraylist线程安全么_赋值_18


CopyOnWriteArrayList 的set 也是上锁:


java arraylist对应的线程安全容器 arraylist线程安全么_数据_19


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性能较均衡,但是迭代操作并未加锁,所以需要时需要额外注意

测试结果:


java arraylist对应的线程安全容器 arraylist线程安全么_数据_20


总结:

  • CopyOnWriteArrayList的写操作与Vector的遍历操作性能消耗尤其严重,不推荐使用。
  • CopyOnWriteArrayList适用于读操作远远多于写操作的场景。
  • Vector读写性能可以和Collections.synchronizedList比肩,但Collections.synchronizedList不仅可以包装ArrayList,也可以包装其他List,扩展性和兼容性更好。