验证ArrayList线程不安全
ArrayList 应当是开发中用到的最多的集合类,是动态列表,List 接口的实现类。
多数情况下,我们实在单线程环境使用,或者是在方法内部,以局部变量的形式使用,一般不会出现线程安全问题。
但是当ArrayList置身于多线程环境时,很容易因为自身的fail-fast 机制抛出异常 ConcurrentModificationException 。
比如下面的代码
/**
* 验证ArrayList的线程不安全,并提供几种线程安全的列表的解决方案
*
* @author linjinjia linjinjia047@163.com
* @date 2021/3/19 22:38
*/
public class ListThreadSafeDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; i++) {
final int j = i;
new Thread(() -> {
list.add(j);
System.out.println(list);
}, "" + i).start();
}
}
}
输出(输出结果不是唯一的,也有可能不抛出异常):
ConcurrentModificationException 可以作为一种检测bug的方式,但是不能在程序中依赖此异常。
线程安全的集合哪里找?
在众多的List 接口实现类中,总有一部分是为了线程安全而设计的。
使用 Collections.synchronizedList(List list) 方法
该方法像是一个包装操作,将传入的 list 进行包装,调用 list 的方法之前,进行同步处理。
返回列表对象的类,都是继承了一个 SynchronizedCollection 类,该类有一个成员变量 Object mutex,用来做同步处理时使用。
在调用List 的方法时,会先对 mutex 进行同步,然后再调用 c 对应的方法。追踪 synchronizedList 方法的源码,会很容易发现这一点。
Collections.synchronizedList(List list) 方法的注释中,指出了遍历返回列表时,建议手动进行同步,并给了个示例。
List list = Collections.synchronizedList(new ArrayList());
...
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
这是因为返回列表的 listIterator() 和 listIterator(int index) 方法都是直接返回 c 的迭代器,所以遍历需要自己进行同步。
使用Vector类
Vector 是 jdk1.0 的古老集合类,该类对大部分方法都加上了 synchronized 关键字,用来保证线程安全。
该类的 listIterator 和 iterator 返回的迭代器是支持 fail-fast 的。
还有一个 elements() 方法,返回一个 Enumeration 对象,只有 hasMoreElements() 和 nextElement()方法,不支持 fail-fast。
Vector 和 synchronizedList 的区别是,一个是Vector对方法加锁,无法控制锁的粒度;二是Vector进行加锁的对象是 this 本身,无法控制锁的对象。
使用 CopyOnWriteArrayList ⭐
CopyOnWrite 也叫 COW。
CopyOnWrite 容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object添加。
而是先将当前容器 Object[] 进行复制,复制一个新的容器 Object[] newElement 并往其中里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(new Element) 。
这样做的好处是可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。
可以看到 add 方法中,是先复制原来的数组,然后增加新的元素,最后再赋值回原来的数组,这个过程是加锁的。
CopyOnWriteArrayList 适合读多写少的场景,写多的情况下,频繁地加锁和复制,也是一笔很大的开销。
拓展
以上三种方式适用于 List ,但是 Set 和 Map 也有类似的方式,去实现线程安全。
比如 Collections 中也有 synchronizedSet 和 synchronizedMap 方法,其原理也跟 synchronizedList 一样。
HashMap 和 HashTable 的关系犹如ArrayList 和 Vector 。
java.util.concurrent 下也有 CopyOnWriteSet, ConcurrentHashMap 这种线程安全的集合。
而且, CopyOnWriteSet 底层依赖的是 CopyOnWriteArrayList ,比如它的 add 方法就是调用了 CopyOnWriteArrayList 的 addIfAbsent() 方法。