Java 中的线程安全容器

作者: roseduan写字的地方 | 来源:发表于2019-05-25 07:54 被阅读1次

    一、同步容器

    常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,最简单的将这些容器变为线程安全的方式,是给这些容器所有的方法都加上 synchronized 关键字。

    Java 的 Collections 中实现了这些同步容器:

    image

    简单的使用如下:

    List<String> list = Collections.synchronizedList(new ArrayList<>());
    
    Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());
    
    Set<String> set = Collections.synchronizedSet(new HashSet<>());
    

    循环遍历同步容器

    如果在遍历同步容器的时候,组合了多个方法,这会可能会存在竞态条件,仍然不是线程安全的。解决的办法便是对容器加锁。例如下面这样:

    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        
        //省略添加数据的操作
        
        String[] str = new String[list.size()];
        int k = 0;
        synchronized (list){
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()){
                str[k ++] = iterator.next();
            }
        }
    }
    

    二、并发容器

    Java 中还提供了一系列并发容器,相比于同步容器,其性能更好。并发容器共分为了四类:List、Map、Set、Queue。

    1. List

    List 中一个最主要的实现类是 CopyOnWriteArrayList ,CopyOnWrite,即写时复制,这样的好处是读操作是无锁的。

    其实现原理是内部维护了一个数组,内部变量 array 指向了这个数组。需要写时,并不是在原数组上操作,而是将数组复制一份,在拷贝的数组中进行写。完成后,将 array 指向新的数组。这样一来,读写之间不互斥,效率得到了很大的提升。

    需要注意的是 CopyOnWriteArrayList 适用于读多写少的场景,并且需要接受读写的暂时不一致,因为在写的时候,并行的读操作可能并不能马上看到写的结果。

    2. Map

    Map 的两个主要实现类是 ConcurrentHashMapConcurrentSkipListMap,两者主要的区别是:前者是无序的,后者是有序的。

    2.1 ConcurrentHashMap

    在 Java 1.7 中,ConcurrentHashMap 的实现使用的是分段锁技术,其内部主要的数据结构是 Segment 和 HashEntry,ConcurrentHashMap 包含了一个 Segment 数组,每个 Segment 又包含一个 HashEntry 数组,每个 HashEntry 是一个存储数据的链表结构。

    其中 Segment 继承了 ReentrantLock,每个 Segment 都有对应的锁,需要修改数据的时候,需要获取这把锁。修改不同的 Segment 数据,则完全可以并行,效率得到了提升。示意图如下:

    image

    Java 1.8 又对 ConcurrentHashMap 做了较大的改进,放弃了分段锁的技术。结构和 Java 1.8 中的 HashMap 类似,采用的是数组+链表/红黑树来实现。为了降低哈希冲突的成本,在链表长度超过 8 时,将链表转换为红黑树。使用 CAS 和 synchronized 解决并发问题,锁住链表或者红黑树的头节点,只要没有哈希冲突,则不会出现并发问题。示意图如下:

    image
    2.2 ConcurrentSkipListMap

    ConcurrentSkipListMap 保证有序的主要原因是,底层使用的是跳表这种数据结构,关于跳表的介绍,你可以查看数据结构中的内容。

    3. Set

    Set 的两个实现是 CopyOnWriteArraySetConcurrentSkipListSet

    和前面说到的 CopyOnWriteArrayList 、ConcurrentSkipListMap 实现的原理类似。

    4. Queue

    队列可以从两方面进行分类:

    • 单端和双端:单端队列指的是只能在队首出队,队尾出队,而双端队列指的是队首和队尾均可入队和出队。

    • 阻塞和非阻塞:阻塞队列指的是,当队列满的时候,入队列阻塞;当队列空的时候,出队列阻塞。

    Java 中,单端队列使用 queue 标识,双端队列使用 deque 标识。

    4.1 单端阻塞队列

    常用的实现类有:ArrayBlockingQueueLinkedBlockingQueuePriorityQueue

    4.2 单端非阻塞队列

    其实现类是 ConcurrentLinkedQueue

    4.3 双端阻塞队列

    其实现类是 LinkedBlockingDeque

    4.4 双端非阻塞队列

    其实现类是 ConcurrentLinkedDeque

    使用其实都非常地简单,就是入队出队之类的操作,这里不再赘述了。

    相关文章

      网友评论

        本文标题:Java 中的线程安全容器

        本文链接:https://www.haomeiwen.com/subject/hytazqtx.html