一、ArrayList线程安全问题
打开ArrayList的源码可以看到,add方法里面并没有synchronize关键字,即线程不安全的,当我们使用多线程进行操作的时候,可能会报错ConcurrentModificationException(并发修改异常)错误。
源码:
1 | public class ThreadDemo3 { |
运行截图:
二、ArrayList线程安全问题解决方案:
2.1 Vector
写法大致与ArrayList一致:
1 | List<String> list = new Vector<>(); |
多次运行并没出现报错。
查看源码可发现有synchronize关键字:
Vector是一个线程安全的List, 但是它的线程安全实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率。所以并不推荐使用Vector。
2.2 Collections工具类
方案二是使用Collections工具类synchronizedList方法
1 | public class ThreadDemo3 { |
但这两种方案都比较古老,但也可以解决问题。更推荐的是第三中方案,来自 JUC 的 CopyOnWriteArrayList。
2.3 * CopyOnWriteArrayList
2.3.1 什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
2.3.2 源码:
1 | private transient volatile Object[] array; |
说明:setArray()的作用是给array赋值;其中,array是volatile transient
Object[]类型,即array是“volatile数组”。
关于volatile关键字,我们知道“volatile能让变量变得可见”,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。
关于transient关键字,它是在序列化中才起作用,transient变量不会被自动序列化。
2.3.3 底层原理:
- CopyOnWriteArrayList实现了List接口,因此它是一个队列。
- CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
- CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
4.CopyOnWriteArrayList的“动态数组”机制 – 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。
5.CopyOnWriteArrayList的“线程安全”机制 – 是通过volatile和监视器锁Synchrnoized来实现的。
6.CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的 保证。
7.CopyOnWriteArrayList通过监视器锁Synchrnoized来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。
2.3.4 CopyOnWrite的缺点
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
1.数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】
2.CopyOnWriteArrayList采用“写入时复制”策略,对容器的写操作将导致的容器中基本数组的复制,性能开销较大。所以在有写操作的情况下,CopyOnWriteArrayList性能不佳,而且如果容器容量较大的话容易造成溢出。
3.适用于数据量不大的场景,不适用于数据量大的场景。由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
4.CopyOnWriteArrayList在写操作中,使用了ReentrantLock锁以保证线程安全,并替换原array属性;但是读的时候直接读取array,可能会发生在写操作替换array前后。这就会导致读到旧数据,引发不一致。
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
三、Hashset线程不安全
Hashset它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。
demo:
1 | public class ThreadDemo3 { |
解决方案CopyOnWriteArraySet:
使用CopyOnWriteArraySet。
四、HashMap线程不安全
demo:
1 | public class ThreadDemo3 { |
解决方案ConcurrentHashMap:
使用ConcurrentHashMap。
ConcurrentHashMap是 J.U.C (java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的、支持高效并发的版本。
基于 JDK1.8的ConcurrentHashMap原理 大牛博客:https://www.cnblogs.com/ylspace/p/12726672.html