一、集合类不安全问题之并发修改异常和写时复制处理方案
我们都知道ArrayList是线程不安全的,请编写一个不安全案例,并给出解决办法
package com.jiguiquan.www; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** ** 集合类不安全问题 * ArrayList:线程不安全 * @author jiguiquan * */ public class CollectionNotSafe { public static void main(String[] args) { List<String> list = new ArrayList<>(); //使用三个线程,分别向list中添加3组数据 for (int i = 0; i < 20; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(list); },String.valueOf(i)).start(); } } }
执行结果如下:
明显地,在多线程环境下,ArrayList的简单add操作就已经开始报异常了,异常类为“ConcurrentModificationException”
ConcurrentModificationException:并发修改异常,此异常在多线程集合类操作中很常见,就想空指针异常一样常见;
思考报错问题的思路基本分以下几步走——成为一个优秀的JAVA工程师必不可少的方法论和思维模式:
1、故障现象:java.util.ConcurrentModificationException;
2、导致原因:并发时争抢修改导致,好比一个人正在签名,另一个突然过来抢,那么名册上会出现一个长长的划痕,即并发修改异常;
3、解决方案(千万不要直接回答:加锁):
3.1、使用Vector代替ArrayList:
List<String> list = new Vector<>();
使用Vector可以顺利解决此并发修改问题;
但是我们对比以下Vector和ArrayList后发现:Vector是JDK1.0就出现的集合类,而ArrayList是JDK1.2才出现的,如果Vector很好的话,又不会出现这样的报错,那么又何必再出现ArrayList呢;
原因就在于,Vector是通过增加Synchronized锁的方式实现的线程安全,这种方式对并发的牺牲比较大,这也是为什么我们很少见到Vector的原因所在;
这里也是我们经常对比ArrayList和Vector的地方:线程安全用Vector,线程不安全用ArrayList;
3.2、使用Collections工具类中的synchronizedList()方法将普通ArrayList包装转化为一个线程安全的加锁的ArrayList;
List<String> list = Collections.synchronizedList(new ArrayList<>());
但是此方法和Vector一样,是通过加锁的方式实现的线程安全,也不是最优的;
3.3、使用JUC包提供的写时复制CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
以上三种解决方案,实际运行结果都如下:
以上三种方案都可以解决ArrayList的多线程环境下“并发修改异常”;
前两种没什么讲的,我们来看看第三种的源码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(); } }
分析:
在执行add操作的时候,即写入的时候,首先先将之前的数组先拷贝一份,然后执行增加元素操作,增加完之后,迅速将之前的数组更新为增加过元素的这个新数组,之前的作废;
4、优化建议(同样的错误不犯第二次):在读多写少的时候推荐使用写时复制 CopyOnWriteArrayList类;
二、集合类不安全值Set
不安全案例:
package com.jiguiquan.www; import java.util.HashSet; import java.util.Set; import java.util.UUID; /** ** 集合类不安全之Set * @author jiguiquan * */ public class SetNotSafe { public static void main(String[] args) { Set<String> set = new HashSet<>(); for (int i = 0; i < 20; i++) { new Thread(() -> { set.add(UUID.randomUUID().toString().substring(0, 8)); System.out.println(set); },String.valueOf(i)).start(); } } }
执行结果如下:
显然,跟刚刚的ArrayList报了一样的异常,并发修改异常;
1、故障现象:java.util.ConcurrentModificationException;
2、导致原因:
3、解决办法,和ArrayList类似:
3.1、使用Collections工具类下面的synchronizedSet()方法将普通的HashSet包装转化为线程安全的加锁的HashSet;
Set<String> set = Collections.synchronizedSet(new HashSet<>());
3.2、使用JUC并发包下面的写时复制CopyOnWriteArraySet:
Set<String> set = new CopyOnWriteArraySet<>();
解决方案后的运行结果如下:
三、集合类不安全之Map
不安全案例:
package com.jiguiquan.www; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** ** 集合类不安全之Map * @author jiguiquan * */ public class MapNotSafe { public static void main(String[] args) { Map<String, String> map = new HashMap<>(); for (int i = 0; i < 30; i++) { new Thread(() -> { map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8)); System.out.println(map); },String.valueOf(i)).start(); } } }
执行结果如下:
显然,跟刚刚的ArrayList报了一样的异常,并发修改异常;
1、故障现象:java.util.ConcurrentModificationException;
2、导致原因:
3、解决办法,和ArrayList类似:
3.1、使用Collections工具类中的synchronizedMap()方法将普通的HashMap包装为线程安全的带锁的Map;
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
3.2、使用JUC并发包下的ConCurrentHashMap代替普通的HashMap
Map<String, String> map = new ConcurrentHashMap<>();
解决方案后的运行结果如下:
常见的集合类安全性问题主要也就是ArrayList、HashSet、HashMap这几类;