为什么不是线程安全的
我们都知道在java中,经常会用到三大集合类Set,List,Map。但是像ArrayList, HashMap,HashSet这些常用的集合类是线程不安全的。在高并发的场景下使用这些集合类会导致很多的问题,比如丢失数据,数据的不一致性等等,甚至导致异常,给生产环境带来严重的损失。
首先我们以List集合来举一个例子,来看看会导致的问题。
List<String> list = new ArrayList<>();
// 3个线程向list集合中添加数据
for (int i = 0; i < 3; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(list);
}).start();
}
来看一下运行结果:
第一种运行结果
[null, e6bd]
[null, e6bd]
[null, e6bd]
第二种运行结果
[ce2f, 0c63, 3ce0]
[ce2f, 0c63, 3ce0]
[ce2f, 0c63, 3ce0]
第三种运行结果
[55fe, e03f]
[55fe, e03f]
[55fe, e03f]
可能会出现空的情况,也可能丢失数据,也有可能正常,这个只是3个线程同时访问,我们加大线程的数量到30(代码省略,只需将for循环的3改成30即可!),就会报错:==java.util.ConcurrentModificationException==
Exception in thread "Thread-2" Exception in thread "Thread-3" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.chunqiu.learn.Test1.lambda$main$0(Test1.java:20)
at java.lang.Thread.run(Thread.java:748)
当然,Set和Map集合的线程不安全也是一样的,下边看一下两者线程不安全的体现。
Set:
Set<String> set = new HashSet<>();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0,4));
System.out.println(set);
}).start();
}
Map:
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
String param = UUID.randomUUID().toString().substring(0,4);
map.put(param, param);
System.out.println(map);
}).start();
}
当然,报错都是java.util.ConcurrentModificationException。
出错原因
这个异常翻译成中文就是并发修改异常。为什么会报这样的一个异常呢?
首先先举一个生活中的例子:假如我们进考场签到,进考场之前,我们需要签到,每个人在花名册上签上自己的名字,假如张三正在签到,但是忘了自己的名字怎么写了,签的很慢,后边的李四等不及了,很嫌弃张三,就从张三的手中抢签到的笔,此时张三正在写字,这样就容易导致在争抢的过程中在花名册上留下一道长长的笔迹。这个由于多个线程同时在争抢添加操作所导致的异常就是我们的java.util.ConcurrentModificationException。
我们来看一下List中的添加方法。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
ArrayList的add方法为了保证效率并没有加锁,无法保证线程安全。那么应该如何保证线程的安全性呢?
二、如何保证线程安全
List的线程安全
List保证线程安全有三种方式:
- 使用线程安全的子类:Vector。 Vector的方法使用synchronized修饰,进行了加锁,可以保证线程安全。
- 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。==Collections.synchronizedList(new ArrayList<>())==
- 使用JUC包中的一个类:CopyOnWriteArrayList。
CopyOnWriteArrayList
原理介绍:CopyOnWriteArrayList采用的是一种写时复制的思想,也就是读写分离。 CopyOnWriteArrayList容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后新的容器newElements中添加元素,添加完元素之后,在将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWriteArrayList容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWriteArrayList容器也是有一种读写分离的思想,读和写不同的容器。
查看CopyOnWriteArrayList的源码,我们发现有两个变量:ReentrantLock类型的lock和volatile关键字修饰的数组对象array。 (volatile保证多线程对共享变量的可见性。)volatile的具体介绍可参见我之前的文章:Java关键字volatile全面解析和实例讲解
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
我们看一下CopyOnWriteArrayList的add方法。
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();
}
Set
Set保证线程安全有两种方式:
- 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。==Collections.synchronizedSet(new HashSet<>())==
- 使用JUC包中的一个类:CopyOnWriteArraySet。
查看CopyOnWriteArraySet的源码发现,其实底层还是CopyOnWriteArrayList。
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
在此呢多说一个面试题吧。 HashSet的底层是一个HashMap。 但是HashMap是K,V键值对的形式,HashSet只有一个,那HashSet的底层怎么是一个HashMap呢? 那就看一下源码:
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
/**
构造方法:
构造一个新的空集; 后备HashMap实例具有默认初始容量 (16) 和负载因子 (0.75)。
*/
public HashSet() {
map = new HashMap<>();
}
/**
添加方法,发现添加的元素食作为map的key,value是一个固定的对象
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
Map
Map保证线程安全有两种方式:
- 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。==Collections.synchronizedSet(new HashSet<>())==
- 使用JUC包中的一个类:ConcurrentHashMap。ConcurrentHashMap的内容较多,后续会专门出一片文章进行讲解。