JUC之线程不安全的ArrayList、HashSet、HashMap
- 1 线程不安全的ArrayList
-
- 1.1 前言
- 1.2 单线程下的ArrayList
- 1.3 多线程下的ArrayList
- 1.4 线程不安全的ArrayList,解决方案
-
- 1.4.1 方案一: Vector
- 1.4.2 方案二: Collections.synchronized()
- 1.4.3 方案三: 采用JUC里面的方法-CopyOnWriteArrayList
- 2 线程不安全的HashSet
-
- 2.1 单线程下的HashSet
- 2.2 多线程下的HashSet
-
- 2.2.1 解决方案 CopyOnWriteArraySet
- 3 线程不安全的HashMap
-
- 3.1 解决方案一-Collections.synchronizedMap(new HashMap<>())
- 3.2 解决方案二-ConcurrentHashMap
1 线程不安全的ArrayList
1.1 前言
当我们执行下面语句的时候,底层进行了什么操作?
底层创建了一个空的数组,伴随着初始值为10。
当执行add方法后,如果超过了10,那么会进行扩容,扩容的大小为原值的一半,也就是5个,使用下列方法扩容。
1.2 单线程下的ArrayList
单线程环境的ArrayList是不会有问题的
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for(String element : list) {
System.out.println(element);
}
}
}
1.3 多线程下的ArrayList
当我们同时启动30个线程去操作List的时候:
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i=0; i<30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
输出结果:

这个时候出现了错误,也就是java.util.ConcurrentModificationException,这个异常是 并发修改的异常。
1.4 线程不安全的ArrayList,解决方案
1.4.1 方案一: Vector
使用Vector替换ArrayList,关于Vector如何实现线程安全的,而是在方法上加了锁,即synchronized。
这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,导致并发性下降。
1.4.2 方案二: Collections.synchronized()
采用Collections集合工具类,在ArrayList外面包装一层 同步 机制
代码:
private static void demo02() {
List<String> list = Collections.synchronizedList(new ArrayList<>());
for(int i=0; i<30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
输出结果:
来看一看Collections.synchronized()是怎么做到的:
- 首先进到synchronized()方法看源码:
JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap - 再来看一看ArrayList的源码: ArrayList实现了RandomAccess接口,所以ArrayList是支持随机访问的,所以创建一个SynchronizedRandomAccessList对象。
JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap - 再进入SynchronizedRandomAccessList类源码中瞅一瞅: SynchronizedRandomAccessList是一个静态类,再看看他的父类:
JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap SynchronizedRandomAccessList的父类是SynchronizedList,但是SynchronizedList又继承了SynchronizedCollection。JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap - 最后再看一看SynchronizedCollection 到顶了,由于SynchronizedCollection实现了Collection接口,重写了add方法,我们再看看add()方法:
JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap 可以看到synchronized修饰了最开始传进来的list对象,就使得在同一时刻只有一个线程能对list进行操作。JUC之线程不安全的ArrayList、HashSet、HashMap1 线程不安全的ArrayList2 线程不安全的HashSet3 线程不安全的HashMap
1.4.3 方案三: 采用JUC里面的方法-CopyOnWriteArrayList
CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想。
代码:
private static void demo03() {
List<String> list = new CopyOnWriteArrayList();
for(int i=0; i<30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
输出结果:
莫得问题!!!
写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的度,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
就是写的时候,把ArrayList扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList的引用指向扩容后的
查看底层add方法源码:
2 线程不安全的HashSet
2.1 单线程下的HashSet
private static void demo01() {
Set<String> set = new HashSet<>();
set.add("aaa");
set.add("bbb");
set.add("ccc");
for (String element : set){
System.out.println(element);
}
}
2.2 多线程下的HashSet
代码:
private static void demo02() {
Set<String> set = new HashSet<>();
for(int i=0; i<30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
输出结果:
可以看到跟多线程下ArrayList的一样的异常。
2.2.1 解决方案 CopyOnWriteArraySet
代码:
private static void demo03() {
Set<String> set = new CopyOnWriteArraySet<>();
for(int i=0; i<30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
输出结果
莫得问题!!!
点开进CopyOnWriteArraySet类的源码看一看。。。
底层还是使用CopyOnWriteArrayList进行实例化。。。
顺便说一下调用 HashSet.add()的方法,只需要传递一个元素,而HashMap是需要传递key-value键值对?
HashSet的底层结构就是HashMap。
再看下add方法的源码:
我们能发现但我们调用add的时候,存储一个值进入map中,只是作为key进行存储,而value存储的是一个Object类型的常量,也就是说HashSet只关心key,而不关心value。
3 线程不安全的HashMap
话就不多说了,多线程下的HashMap会和之前的多线程下的ArrayList和HashSet报同样的异常错误。
3.1 解决方案一-Collections.synchronizedMap(new HashMap<>())
3.2 解决方案二-ConcurrentHashMap
未完,待补充。。。