一、异常演示
循环创建线程,将数据放入集合的同时,从集合中读取数据。
/** * list集合线程不安全问题 */ public class ThreadDemo04 { 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(); } } }
运行结果:
出现了并发修改异常
那么如何解决呢?
二、解决方案
1、vector
直接将ArrayList替换为Vector即可
// List<String> list = new ArrayList<>(); List<String> list = new Vector<>();
运行结果:
为什么会这样呢?vector和ArrayList有什么不同呢?
进入Vector的源码可以发现,里面几乎所有的方法都加了synchronized关键字
这样确实可以避免线程安全问题,不过效率比较低。
2、Collections工具类
使用Collections工具类中的synchronizedList()方法生成线程安全的集合。
// List<String> list = new ArrayList<>(); // List<String> list = new Vector<>(); List<String> list = Collections.synchronizedList(new ArrayList<>());
运行结果:
看一下jdk1.8的API对他的介绍:
3、CopyOnWriteArrayList 写时复制技术
// List<String> list = new ArrayList<>(); // List<String> list = new Vector<>(); // List<String> list = Collections.synchronizedList(new ArrayList<>()); List<String> list = new CopyOnWriteArrayList<>();
运行结果:
三、写时复制技术
1 、特性
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时,它具有以下特性:
1、它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
2、 它是线程安全的。
3、 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
4、 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
5、 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
2、原理
写时复制技术中,读操作支持并发读,即多个线程可以同时读到集合内的元素。写操作是独立写,当一个写线程执行写操作时,所有其他写线程阻塞;当该线程写的时候,将原集合中的数据复制一份,在拷贝的集合中完成写操作后,再将该拷贝集合和原集合合并。
为什么需要拷贝,在原数组直接修改不行吗?这篇文章讲得很清楚:CopyOnWriteArrayList写时复制的原理_Endwas的博客-CSDN博客
最后我们来看一下源码:
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(); } }