线程安全的集合类哪里找

Lin2J
2021-03-21 / 0 评论 / 366 阅读
温馨提示:
本文最后更新于2021-03-21,若内容或图片失效,请留言反馈。

验证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();
        }
    }
}

输出(输出结果不是唯一的,也有可能不抛出异常):

ArrayListNotSafe

ConcurrentModificationException 可以作为一种检测bug的方式,但是不能在程序中依赖此异常。

线程安全的集合哪里找?

在众多的List 接口实现类中,总有一部分是为了线程安全而设计的。

使用 Collections.synchronizedList(List list) 方法

该方法像是一个包装操作,将传入的 list 进行包装,调用 list 的方法之前,进行同步处理。

返回列表对象的类,都是继承了一个 SynchronizedCollection 类,该类有一个成员变量 Object mutex,用来做同步处理时使用。

SynchronizedCollection

在调用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 关键字,用来保证线程安全。

该类的 listIteratoriterator 返回的迭代器是支持 fail-fast 的。

还有一个 elements() 方法,返回一个 Enumeration 对象,只有 hasMoreElements()nextElement()方法,不支持 fail-fast

VectorsynchronizedList 的区别是,一个是Vector对方法加锁,无法控制锁的粒度;二是Vector进行加锁的对象是 this 本身,无法控制锁的对象。

使用 CopyOnWriteArrayList

CopyOnWrite 也叫 COW

CopyOnWrite 容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object添加

而是先将当前容器 Object[] 进行复制,复制一个新的容器 Object[] newElement 并往其中里添加元素,添加完元素之后再将原容器的引用指向新的容器 setArray(new Element) 。

这样做的好处是可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList-add

CopyOnWriteArrayList-get

可以看到 add 方法中,是先复制原来的数组,然后增加新的元素,最后再赋值回原来的数组,这个过程是加锁的

CopyOnWriteArrayList 适合读多写少的场景,写多的情况下,频繁地加锁和复制,也是一笔很大的开销。

拓展

以上三种方式适用于 List ,但是 SetMap 也有类似的方式,去实现线程安全。

比如 Collections 中也有 synchronizedSetsynchronizedMap 方法,其原理也跟 synchronizedList 一样。

HashMapHashTable 的关系犹如ArrayListVector

java.util.concurrent 下也有 CopyOnWriteSet, ConcurrentHashMap 这种线程安全的集合。

而且, CopyOnWriteSet 底层依赖的是 CopyOnWriteArrayList ,比如它的 add 方法就是调用了 CopyOnWriteArrayListaddIfAbsent() 方法。