这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
线程安全的集合
WangScaler: 一个用心创作的作者。
声明:才疏学浅,如有错误,恳请指正。
不安全的集合
日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的,举个例子。
1 | java复制代码package com.wangscaler.securecollection; |
你的本意可能是起三个线程去填充这个ArrayList集合,然而结果却总是超出意料,上述的代码执行的结果可能是null;null;3也可能是null;2;3,当然也有可能达到你的预期效果1,2,3。
为什么会出现这种情况呢?我们翻开源码
1 | java复制代码public boolean add(E e) { |
但是三行代码的执行的时候字节码如下
1 | java复制代码 0 aload_0 |
在程序执行的时候,线程是交替执行的。我们上面的例子有三个线程,分别是线程1、线程2、线程3。
看字节码 putfield #284 <java/util/ArrayList.size : I>
是在aastore
之前的,也就是说size写回主内存是在数组写回之前的。所以就有可能出现线程1数组还没写进去的时候,线程2就开始执行了,此时size已经是加一之后的了,所以此时线程2将新值保存到数组里,也就出现了null;2;3的情况。
HashMap也是线程不安全的,同样HashSet也是,因为HashSet的底层就是HashMap,话不多说上源码
1 | java复制代码public HashSet() { |
那HashMap和HashSet的区别是啥呢?我们知道HashMap是键值对的形式,而HashSet的value值是固定的,源码如下。
1 | java复制代码private static final Object PRESENT = new Object(); |
HashMap除了线程不安全,在jdk8之前HashMap扩容的时候还会产生死链的情况,
如何解决?
1、遗留的安全集合
- Vector用于ArrayList
- HashTable用于HashMap
举例如下:将上述的List<Integer> list = new ArrayList<>();
修改为List<Integer> list = new Vector<>();
即可。为什么这个就可以解决问题呢?打开源码
1 | java复制代码public synchronized boolean add(E e) { |
是个同步方法,通过互斥锁使问题得到解决,但是在多线程中极大的影响效率,已经被弃用了。HashTable同样因为同步的问题,被弃用了。
2、Collections
通过Collections的修饰将不安全的集合变成安全的集合。
- synchronizedList用于ArrayList
- synchronizedMap用于HashMap
- synchronizedSet用于HashSet
在原有不安全集合上包装了一个线程安全的类,来达到预期的效果。举例如下:将上述的List<Integer> list = new ArrayList<>();
修改为List<Integer> list = Collections.synchronizedList(new ArrayList<>());
即可解决。原理是什么呢?还是看源码
1 | java复制代码SynchronizedList(List<E> list) { |
在所有的方法上加了synchronized修饰,从而达到同步的效果。
3、JUC
- Bloacking
+ ArrayBlockingQueue
+ LinkedBlockingQueue
+ LinkedBlockingDeque
+ ...
- CopyOnWrite
+ CopyOnWriteArrayList对应ArrayList
+ CopyOnWriteArraySet用于HashSet,底层还是CopyOnWriteArrayList
- Concurrent(推荐使用,弱一致性。)
+ ConcurrentHashMap用于HashMap
只能保证一个操作是原子的,比如先检查key在不在(get),不在再添加(put)两个操作无法保证原子性,应该使用computeIfAbsent()
+ ConcurrentSkipListMap
+ ConcurrentSkipListSet
+ ...
举例如下:将上述的List<Integer> list = new ArrayList<>();
修改为List<Integer> list = new CopyOnWriteArrayList<>();
适合读多写少的场景。看下源码
1 | java复制代码public boolean add(E e) { |
在写入的时候加锁,并复制一份,将新加入的写进新数组,最终把新数组写回原资源,从而保证数据的原子性。这个过程中只是给增加方法加锁,不影响读的操作。
结语
多线程中同步方法大大影响了工作效率,所以ConcurrentHashMap通过volatile结合自旋锁的方式,广受大家喜爱,同样也是面试官常问的题目之一,值得大家好好去读一下源码。
本文转载自: 掘金