文章首发于个人博客,欢迎访问关注:https://www.lin2j.tech
验证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()
方法。