J.U.C中设计了高效的线程安全的容器,主要包括三类:Blocking类,CopyOnWrite 类,Concurrent 类。
CopyOnWrite类
? ??CopyOnWriteArrayList
CopyOnWriteArrayList适合读多写少的应用场景,底层实现采用了写入时拷贝的思想。增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,而此时读取会在旧数组上执行,这样保证了数组可以读读并发和读写并发,只有在并发写时才会阻塞。
相比Vector它具有高并发,高效率的优势。缺点是会出现弱一致性,get操作和迭代器遍历操作都会获取数组在那一刻的快照,如果此时出现写操作读线程还是读的旧数组。
?CopyOnWriteArrayList的底层包括一个Object数组和一个ReentrantLock,与ArrayList的区别主要在add,remove,set方法上。这些写方法的操作步骤基本相同:尝试获取锁——成功后获取原数组——创建新数组——复制数组——修改新数组——修改好的数组替换原数组。
????CopyOnWriteArraySet
对CopyOnWriteArrayList的一个包装,add方法调用CopyOnWriteArrayList的addIfAbsent来保证成员变量不重复。
Concurrent 类
? ? ConcurrentHashMap
? ? 见源码分析
? ? ConcurrentSkipListMap
? ??线程安全的高并发表,实现了ConcurrentNavigableMap接口,与TreeMap一样是一个有序表,但底层是用跳表来实现的。ConcurrentSkipListMap 的读写时间是log(N),和线程数几乎无关,并发的线程越多,它的性能优势越明显。其数据结构如下
详细分析见:https://www.cnblogs.com/java-zzl/p/9767255.html
? ? ConcurrentLinkedQueue
ConcurrentLinkedQueue只实现了Queue接口,是高效的并发安全的链表队列。它不是阻塞队列,没有实现put,take方法,但其数据结构与LinkedBlockingQueue完全一样,底层都是头结点为哨兵结点的链表结构。不同之处在于,ConcurrentLinkedQueue使用CAS和自旋的方式来实现并发安全而没有用锁,并且它是无限容量的。
ConcurrentLinkedQueue的offer,poll方法在入队出队时没有立刻更新head和tail指针,通常是两次入队/出队才更新一次head/tail指针,这样虽然让代码逻辑变得更复杂,但减少了head,tail的CAS修改次数,提高了性能。
同其他Concurrent容器一样,ConcurrentLinkedQueue的size方法也是非常数时间返回,它需要遍历整个链表。
Blocking类
? ??BlockingQueue接口
BlockingQueue继承了queue接口,?在queue基础上提供了在一定情况下让线程阻塞的入队出队方法(put,take),通常用于一个线程生产对象,而另外一个线程消费这些对象的场景,队列空时让出队的消费者线程阻塞,队列满时让入队的生产者阻塞。不同于Queue,BlockingQueue不允许插入null。除了SynchronousQueue比较特殊,其他BlockingQueue的实现类都是通过ReentrantLock来保证并发安全,通过ConditionObject来实现put,take方法的阻塞。
offer,poll提供了一个超时等待的版本,如果线程阻塞了指定时间后还没有成功入队/出队,就返回false。
LinkedBlockingQueue
LinkedBlockingQueue实现了BlockingQueue接口,使用链表存储数据,与LInkedList的数据结构基本一致。区别在于,初始化时会放入一个值为null的哨兵结点,LinkedBlockingQueue的头结点永远会是哨兵结点(哨兵结点是为了保证队列只有一个结点时也能够线程安全地并发入队出队)。初始化LinkedBlockingQueue时可以指定队列容量,也可以不指定,不指定时可以当做是无限容量的阻塞队列。
成员变量:
?putLock和takeLock两把ReentrantLock保证了并发安全,所有方法入队前都要获取putLock,出队前都要获取takeLock。两把锁的设计让LinkedBlockingQueue可以同时做入队和出队操作,提高了性能。
notFull,notEmpty两个条件变量用于实现put,take方法的阻塞操作,如果队列为空,take方法会调用notEmpty.await让当前线程进入等待,如果队列满了,put方法会调用notFull.await让当前线程进入等待。为了减少竞争,LinkedBlockingQueue使用了sigal方法而不是signalAll方法唤醒线程,在所有的入队,出队方法在结束前,都会在条件满足的情况下调用notFull和notEmpty的signal方法。
count变量用于记录队列中的结点数,使用了AtomicInteger类型保证了修改计数时的线程安全。
put/take方法:
基本上就是链表的入队出队操作加上加锁解锁和条件变量的使用,以下是源码:
ArrayBlockingQueue
ArrayBlockingQueue实现了BlockingQueue接口,使用循环队列(数组)存储数据,与ArrayDeque的数据结构基本一致。ArrayBlockingQueue是容量有限的阻塞队列,初始化时必须指定容量,还可以指定使用公平锁。ArrayBlockingQueue只使用了一把锁来保证并发安全,因此入队出队操作只能串行执行,效率要比LinkedBlockingQueue差一些。
PriorityBlockingQueue
线程安全版的PriorityQueue,基本上就等于PriorityQueue加上锁和条件变量,由于底层是数组,同样只有一把锁,并且只有notEmpty一个条件变量,当队列装满时会扩容,因此put方法的实现是直接调用offer,不会出现阻塞。
?SynchronousQueue
SynchronousQueue类实现了BlockingQueue接口,是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
SynchronousQueue没有使用锁和条件变量,阻塞线程使用的是park方法,源码分析参考https://www.jianshu.com/p/d5e2e3513ba3
DelayQueue
DelayQueue实现了BlockingQueue接口,是一个特殊的阻塞队列。队列中的元素必须实现Delayed 接口,具有一个到期时间。DelayQueue中元素只有到期后才能被取出。
Delayed接口:
继承了Comparable接口,并定义了一个getDelay方法,该方法返回对象剩余延迟时间,如果返回值<=0,则表示该对象的延迟时间已经到期。实现了Delayed接口的对象要同时实现getDelay和compareTo方法。
成员变量:
DelayQueue直接使用一个PriorityQueue来存储数据,优先队列会根据Delayed接口的compareTo方法来排序,通常会把延迟时间最短的结点放在头结点。
lock和available用来保证并发安全和队列为空是take方法阻塞。
leader保存了第一个调用take方法却因为延迟时间未到期而阻塞的线程。
put方法:
由于PriorityQueue会扩容,DelayQueue也是无线容量的,put方法直接调用offer方法,不会阻塞。offer方法就是简单的加锁后放入有限队列中。
take方法:
take方法在取出元素前,会调用元素的getDelay方法,如果值大于0表示,线程会进入delay时长的等待。只有第一个发现延迟未到期的线程会进入限时等待,后面的进来的线程会进入无限期等待,之后由限时等待的线程唤醒。这样设计的目的是让延迟到期后只会唤醒一个线程,尽量减少线程竞争。
BlockingDeque接口
BlockingDeque同时继承了BlockingQueue和Deque接口,是一个提供阻塞方法的双端队列。增加了BlockingQueue每个方法的XXXFirst,XXXLast版本。
LinkedBlockingDeque
链表方式实现了BlockingDeque接口,没有像LinkedBlockingQueue一样使用哨兵结点作为头结点,因此只有一把锁,其他逻辑与LinkedBlockingQueue相同。