线程安全的集合
WangScaler: 一个用心创作的作者。声明:才疏学浅,如有错误,恳请指正。
不安全的集合
日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的,举个例子。
package com.wangscaler.securecollection;
import java.util.*;
/**
* @author WangScaler
* @date 2021/8/5 15:25
*/
public class NotSafeCollection {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
int number = 3;
Random random = new Random();
for (int i = 0; i < number; i++) {
new Thread(() -> {
int randomNumber = random.nextInt(10) % 11;
list.add(randomNumber);
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
list.forEach(System.out::println);
}
}
你的本意可能是起三个线程去填充这个ArrayList集合,然而结果却总是超出意料,上述的代码执行的结果可能是null;null;3也可能是null;2;3,当然也有可能达到你的预期效果1,2,3。
为什么会出现这种情况呢?我们翻开源码
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
但是三行代码的执行的时候字节码如下
0 aload_0
1 aload_0
2 getfield #284 <java/util/ArrayList.size : I>
5 iconst_1
6 iadd
7 invokespecial #309 <java/util/ArrayList.ensureCapacityInternal : (I)V>
10 aload_0
11 getfield #287 <java/util/ArrayList.elementData : [Ljava/lang/Object;>
14 aload_0
15 dup
16 getfield #284 <java/util/ArrayList.size : I>
19 dup_x1
20 iconst_1
21 iadd
22 putfield #284 <java/util/ArrayList.size : I>
25 aload_1
26 aastore
27 iconst_1
28 ireturn
在程序执行的时候,线程是交替执行的。我们上面的例子有三个线程,分别是线程1、线程2、线程3。
看字节码 putfield #284 <java/util/ArrayList.size : I>
是在aastore
之前的,也就是说size写回主内存是在数组写回之前的。所以就有可能出现线程1数组还没写进去的时候,线程2就开始执行了,此时size已经是加一之后的了,所以此时线程2将新值保存到数组里,也就出现了null;2;3的情况。
HashMap也是线程不安全的,同样HashSet也是,因为HashSet的底层就是HashMap,话不多说上源码
public HashSet() {
map = new HashMap<>();
}
那HashMap和HashSet的区别是啥呢?我们知道HashMap是键值对的形式,而HashSet的value值是固定的,源码如下。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap除了线程不安全,在jdk8之前HashMap扩容的时候还会产生死链的情况,
如何解决?
1、遗留的安全集合
- Vector用于ArrayList
HashTable用于HashMap
举例如下:将上述的
List<Integer> list = new ArrayList<>();
修改为List<Integer> list = new Vector<>();
即可。为什么这个就可以解决问题呢?打开源码public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
是个同步方法,通过互斥锁使问题得到解决,但是在多线程中极大的影响效率,已经被弃用了。HashTable同样因为同步的问题,被弃用了。
2、Collections
通过Collections的修饰将不安全的集合变成安全的集合。
- synchronizedList用于ArrayList
- synchronizedMap用于HashMap
- synchronizedSet用于HashSet
在原有不安全集合上包装了一个线程安全的类,来达到预期的效果。举例如下:将上述的List<Integer> list = new ArrayList<>();
修改为List<Integer> list = Collections.synchronizedList(new ArrayList<>());
即可解决。原理是什么呢?还是看源码
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
在所有的方法上加了synchronized修饰,从而达到同步的效果。
3、JUC
Bloacking
- ArrayBlockingQueue
- LinkedBlockingQueue
- LinkedBlockingDeque
- ...
CopyOnWrite
- CopyOnWriteArrayList对应ArrayList
- CopyOnWriteArraySet用于HashSet,底层还是CopyOnWriteArrayList
Concurrent(推荐使用,弱一致性。)
- ConcurrentHashMap用于HashMap
只能保证一个操作是原子的,比如先检查key在不在(get),不在再添加(put)两个操作无法保证原子性,应该使用computeIfAbsent()
- ConcurrentSkipListMap
- ConcurrentSkipListSet
- ...
- ConcurrentHashMap用于HashMap
举例如下:将上述的List<Integer> list = new ArrayList<>();
修改为List<Integer> list = new CopyOnWriteArrayList<>();
适合读多写少的场景。看下源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
在写入的时候加锁,并复制一份,将新加入的写进新数组,最终把新数组写回原资源,从而保证数据的原子性。这个过程中只是给增加方法加锁,不影响读的操作。
结语
多线程中同步方法大大影响了工作效率,所以ConcurrentHashMap通过volatile结合自旋锁的方式,广受大家喜爱,同样也是面试官常问的题目之一,值得大家好好去读一下源码。