【Java并发工具类】Java并发容器

前言

Java并发包有很大一部分都是关于并发容器的。Java在5.0版本以前线程安全的容器称之为同步容器。同步容器实现线程安全的方式:是将每一个公有方法都使用synchronized修饰,保证每次只有一个线程能访问容器的状态。可是这样的串行度过高,将严重下降并发性,当多个线程竞争容器的锁时,吞吐量将严重下降。所以,在Java 5.0版本时提供了性能更高的容器来改进以前的同步容器,咱们称其为并发容器html

下面咱们先来介绍Java 5.0以前的同步容器,而后再来介绍Java 5.0以后的并发容器。java

Java 5.0以前的同步容器

目前,Java中的容器主要可分为四大类,分别为ListMapSetQueue(Queue是Java5.0添加的新的容器类型),可是并非全部的Java容器都是线程安全的。例如,咱们经常使用的ArrayListHashMap就不是线程安全的。线程安全的类为VectorStackHashTable编程

如何将非线程安全的类变为线程安全的类? 非线程安全的容器类能够由Collections类提供的Collections.synchronizedXxx()工厂方法将其包装为线程安全的类。数组

// 分别将ArrayList、HashMap和HashSet包装成线程安全的List 、Map和Set
List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
Map map = Collections.synchronizedMap(new HashMap());

这些类实现线程安全的方式是:将它们的状态封装起来,并对每一个公有方法进行同步,使得每次只有一个线程能访问容器的状态。以ArrayList为例,可使用以下的代码来理解如何将非线程安全的容器包装为线程安全的容器。安全

// 包装 ArrayList 
SafeArrayList<T>{
    List<T> c = new ArrayList<>();
    // 控制访问路径,使用synchronized修饰保证线程互斥访问
    synchronized T get(int idx){
        return c.get(idx);
    }
    synchronized void add(int idx, T t) {
        c.add(idx, t);
    }
    synchronized boolean addIfNotExist(T t){
        if(!c.contains(t)) {
            c.add(t);
            return true;
        }
        return false;
    }
}

被包装出来的线程安全的类,都是基于synchronized同步关键字实现,因而被成为同步容器。而本来的线程安全的容器类Vector等,一样也是基于synchronized关键字实现的。数据结构

同步容器在复合操做中的问题

同步容器类都是线程安全的,可是复合操做每每都会包含竞态条件问题。这时就须要额外的客户端加锁来保证复合操做的原子性。并发

在下例$^{[2]}$中,定义了两个方法getLast()deleteLast(),它们都会执行“先检查后执行再运行”操做。每一个方法首先都得到数组的大小,而后经过结果来获取或删除最后一个元素。函数

public class UnsafeVectorHelpers {
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }

    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

若是线程同时调用相同的方法,这将不会产生什么问题。可是从调用者方向看,这将致使很是严重的后果。若是线程A在包含10个元素的Vector上调用getLast,同时线程B在同一个Vector上调用deleteLast,这些操做的交替若以下所示,那么getLast将抛出ArrayIndexOutOfBoundsException异常。工具

线程A在调用size()与getLast()这两个操做之间,Vector变小了,所以在调用size时获得的索引值将再也不有效。性能

因而咱们便须要在客户端加锁实现新操做的原子性。那么就须要考虑对哪一个锁对象进行加锁。 同步容器类经过加锁自身(this)来保护它的每一个方法,因而在这里咱们锁住list对象即可以保证getLast()和deleteLast()成为原子性操做。

public class SafeVectorHelpers {
    public static Object getLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            return list.get(lastIndex);
        }
    }

    public static void deleteLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            list.remove(lastIndex);
        }
    }
}

在对Vector中元素进行迭代$^{[2]}$时,调用size()和相应的get()之间Vector的大小可能发生变化的状况也会出现。

for(int i=0; i<vector.size(); i++){
    doSomething(vector.get(i));
}

与getLast()同样,若是在对Vector进行迭代时,另外一个线程删除了一个元素,而且删除和访问这两个操做交替执行,那么上面的方法将抛出ArrayIndexOutOfBoundsException异常。 一样,咱们能够经过在客户端加锁来防止其余线程在迭代期间修改Vector。

synchronized(vector){
    for(int i=0; i<vector.size(); i++){
        doSomething(vector.get(i));
    }
}

有得必有失,以上代码将会致使其余线程在迭代期间没法访问vector,所以也下降了并发性。

迭代器与ConcurrentModificationException

不管是使用for循环迭代,仍是使用Java 5.0引入的for-each循环语法,对容器类进行迭代的标准方式都是使用Iterator。一样,若是在使用迭代器访问容器期间,有线程并发地修改容器的大小,也是须要对迭代操做进行加锁,即以下${^{[1]}}$。

List list = Collections.synchronizedList(new ArrayList());
synchronized (list) {  
    Iterator i = list.iterator();
    while (i.hasNext())
        foo(i.next());
}   

在设计同步容器类的迭代器时没有考虑到并发修改的问题,当出现如上状况时,它们表现出来的行为是<mark>“及时失败”(fail-fast)</mark>的。当它们发现容器在迭代过程当中被修改时,就会当即抛出一个ConcurrentModificationException异常。

这种fail-fast的迭代器并非一种完备的处理机制,而只是“善意地”捕获并发错误,所以只能做为并发问题的预警指示器。这种机制的实现方式是:使用一个计数器modCount记录容器大小改变的次数,在进行迭代期间,若是该计数器值与刚进入迭代时不一致,那么hasNext()或next()将抛出ConcurrentModificationException异常。

可是,对计数器的值的检查时是没有在同步状况下进行的,所以可能会看到失效的计数值,致使迭代器没有意识到容器已经发生了修改。这是一种设计上的权衡,从而下降并发修改操做的检测代码对程序性能带来的影响。

更多的时候,咱们是不但愿在迭代期间对容器加锁。若是容器规模很大,在加锁迭代后,那么在迭代期间其余线程都不能访问该容器。这将下降程序的可伸缩性以及引发激烈的锁竞争下降吞吐量和CPU利用率。 一种替代加锁迭代的方法为**“克隆”**容器,并在副本上迭代。副本是线程封闭的,天然也就是安全的。可是克隆的过程也须要对容器加锁,且也存在必定的开销,需考虑使用。

隐藏的迭代器

容器的hashCode()equals()等方法也会间接地执行迭代操做,当容器做为另外一个容器的元素或键值时,就会出现这种状况。一样,containsAll()removeAll()retainAll()等方法,以及把容器做为参数的构造函数,都会对容器进行迭代。全部这些间接迭代操做均可能抛出ConcurrentModificationException异常。

Java 5.0的并发容器

在Java 5.0版本时提供了性能更高的容器来改进以前的同步容器,咱们称之为并发容器。并发容器虽然多,可是总结下来依旧为四大类:ListMapSetQueue

List

CopyOnWriteArrayList是用于替代同步List的并发容器,在迭代期间不须要对容器进行加锁或复制。写时复制(CopyOnWrite)的线程安全性体如今,只要正确地发布一个事实不可变的对象,那么在访问该对象时就再也不须要进一步的同步。而在每次进行写操做时,便会建立一个副本出来,从而实现可变性。“写时复制”容器返回的迭代器不会抛出ConcurrentModificationException,由于迭代器在迭代过程当中,若是对象会被修改则会建立一个副本被修改,被迭代的对象依旧是原来的。

CopyOnWriteArrayList仅适用于写操做很是少的场景,并且可以容忍短暂的不一致。CopyOnWriteArrayList迭代器是只读的,不支持增删改。由于迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

Map

ConcurrentHashMapConcurrentSkipListMap的区别为:ConcurrentHashMap的key是无序的,而ConcurrentSkipListMap的key是有序的。使用这二者时,它们的key和value都不能为空,不然会抛出NullPointerException异常。

Map有关实现类对于key和value的要求:

集合类 Key Value 是否线程安全
HashMap 容许为null 容许为null
TreeMap 不容许为null 容许为null
HashTable 不容许为null 不容许为null
ConcurrentHashMap 不容许为null 不容许为null
ConcurrentSkipListMap 不容许为null 不容许为null

与HashMap同样,ConcurrentHashMap也是一个基于散列的Map,它使用分段锁实现了更大程度的共享。任意数量的读取线程能够并发地访问Map,执行读取的线程和执行写入的线程能够并发地访问Map,而且必定数量的写入线程能够并发地修改Map。ConcurrentHashMap在并发环境下能够实现更高的吞吐量,而在单线程环境中只损失很是小的性能。

ConcurrentHashMap返回的迭代器也不会抛出ConcurrentModificationException,所以不须要在迭代期间对容器加锁。ConcurrentHashMap返回的迭代器具备弱一致性(Weakly Consistent),而并不是fail-fast的。弱一致性的迭代器能够容忍并发的修改,当建立迭代器会遍历已有的元素,并能够(可是不保证)在迭代器被构建后将修改操做反映给容器。

ConcurrentHahsMap是对Map进行分段加锁,没有实现独占。全部须要独占访问功能的,应该使用其余并发容器。

ConcurrentSkipListMap里面的SkipList自己就是一种数据结构,中文翻译为“跳表”。跳表插入、删除、查询操做平均时间复杂度为O(log n)。返回的迭代器也是弱一致性的,也不会抛出ConcurrentModificationException。

Set

Set接口下,两个并发容器是CopyOnWriteArraySetConcurrentSkipListSet,可参考CopyOnWriteArrayList和ConcurrentSkipListMap理解。

Queue

Java并发包中Queue下的并发容器是最复杂的,能够从下面两个维度来分类:

  1. 阻塞和非阻塞

    阻塞队列是指当队列已满时,入队操做阻塞;当队列为空时,出对操做阻塞。

  2. 单端和双端

    单端队列指的是只能从队尾入队,队首出队;双端指的是队首队尾皆可出队。

在Java并发包中,阻塞队列都有Blocking标识,单端队列是用Queue标识,而双端队列是Deque标识。以上两个维度可组合,因而分为四类并发容器:单端阻塞队列、双端阻塞队列、单端非阻塞队列、双端非阻塞队列。

在使用队列时,须要格外注意队列是否为有界队列(内部的队列是否容量有限),无界队列在数据量大时,会致使OOM即内存溢出。 在有Queue的具体实现的并发容器中,只有ArrayBlockingQueue和LinkedBlockingQueue是支持有界的,其他都是无界队列。

小结

这篇文章从宏观层面介绍了Java并发包中的并发工具类,对每一个容器类仅作了简单介绍,后续将附文介绍每个容器类。

参考: [1]极客时间专栏王宝令《Java并发编程实战》 [2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016

原文出处:https://www.cnblogs.com/myworld7/p/12350626.html

相关文章
相关标签/搜索