一、简介
java的集合框架中,容器主要分为List、Set、Queue、Map四大类,常用的容器ArrayList、LinkedList、HashSet、HashMap等都不是线程安全的,在多线程环境下使用这些容器需要我们进行同步处理。为了简化开发,Java提供了同步容器。
同步容器
简单地在在需要同步的方法上添加synchronized来实现同步的容器。主要的同步容器:
- Vector
- Stack
- Hashtable
- Collections.SynchronizedList()方法创建的List
- Collections.synchronizedSet()方法创建的Set
- Collections.synchronizedMap()方法创建的Map
同步容器的问题
同步容器都是以自身为锁,多个线程共同竞争容器的锁时,吞吐量降低。因此,在并发量比较高的场景,可以使用并发容器来替代。
并发容器
java.util.concurrent包中针对并发环境设计的容器。并发容器使用了写时复制、分段锁、CAS等技术,在并发环境下能提供更高的吞吐量。
二、并发容器结构图
三、并发List
CopyOnWriteArrayList
对应ArrayList,使用方法和ArrayList相同。内部实现基于写入时复制技术。
写入时复制(Copy-On-Write)
是计算机程序设计领域中的一种优化思想,是一种读写分离思想,在很多系统设计上都会用到,常用于读多写少的并发场景。Copy-On-Write的原理是,对于查询操作,不需要加锁,直接访问数据。对于写入(增删改)操作,需要对操作加互斥锁,保证只会有1个线程在操作。在操作数据前,先将数据复制一份副本,之后的操作是对副本进行的。操作完成后,将原数据的指针指向副本,完成了副本替换原数据。因此,写入操作的同时,读操作不会被阻塞,而是继续读取原数据。
Copy-On-Write存在的问题
内存占用:由于有复制操作存在,在写操作比较频繁或者数据比较大的场景,Copy-On-Write比较耗存。
数据一致性:写副本完成到原数据指针被替换之间,如果有读操作,则读到的是旧数据。Copy-On-Write只能保证数据的最终一致性,不能保证数据的实时一致性。
四、并发Set
CopyOnWriteArraySet
对应HashSet,使用方法和HashSet相同。内部实现基于CopyOnWriteArrayList。
ConcurrentSkipListSet
对应TreeSet,使用方法和TreeSet相同。内部实现基于ConcurrentSkipListMap。
五、并发Queue
Queue接口表示单向队列,单向队列只能在队列头部删除元素、在队列末尾添加元素。
Queue的核心方法:
- offer:将元素加入队列的末尾。
- poll: 将元素从队列的头部删除。
- peek:返回队列头部的元素,但不进行删除。
ConcurrentLinkedQueue
ConcurrentLinkedQueue类实现了Queue接口。表示基于链表的单向队列,它的实现使用了CAS无锁算法。
BlockingQueue接口
BlockingQueue接口扩展了Queue接口。表示阻塞单向队列。
阻塞队列和生产者-消费者模型
阻塞队列的工作模式是:当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列为空时,队列会阻塞获取元素的线程,直到队列不空。
阻塞队列常用于生产者-消费者模型,生产者向队列中添加元素,消费者则从队列中取出元素。线程池就是利用阻塞队列来实现任务的排队。
Queue的核心方法:
- put:将元素加入队列的末尾,如果队列已满,线程将被阻塞。
- take:将元素从队列的头部删除,如果队列为空,线程将被阻塞。
BlockingQueue的五个实现类
LinkedBlockingQueue
基于链表的阻塞队列,它的容量没有上限,可以指定其最大容量。ArrayBlockingQueue
基于数组的阻塞循环队列,在构造时需要指定最大容量。PriorityBlockingQueue
基于堆的阻塞优先级队列,它的容量没有上限。优先级队列的元素按优先级顺序出队,而不是先进先出。可以自定义compareTo()方法来指定元素的优先级排序规则。DelayQueue
基于堆的阻塞时间调度队列,它的容量没有上限。此队列中的元素必须实现Delayed接口,在创建元素时可以指定延迟时间,只有在延迟时间满后,才能从队列中获取元素。队列头部是延迟时间满后保存时间最长的元素。如果延迟时间都还没满,则队列没有头部。此队列不允许使用null元素。DelayQueue多应用于定时任务调度等场景。SynchronousQueue
不存储元素的阻塞队列。此队列每一个put操作必须等待一个take操作,否则不能继续添加元素,队列本身不存储元素,看起来像是一根管道,资源从一个方快速传递到另一方。还可以指定队列的公平性,默认非公平。公平的队列可保证线程以FIFO的顺序进行访问,但是公平通常会降低吞吐量。SynchronousQueue非常适合传递性场景,即把生产者线程处理的数据直接传递给消费者线程。
TransferQueue接口
TransferQueue接口扩展了BlockingQueue接口。它提供了线程之间直接交换对象的能力。TransferQueue是一个结合了ConcurrentLinkedQueue, SynchronousQueue(公平模式)和LinkedBlockingQueues特性的组合体,具有较高的性能,推荐使用。
TransferQueue的核心方法:
- tryTransfer(E e)
若当前存在一个正在等待获取的消费者线程,则立刻移交元素e。否则,返回false。 - transfer(E e)
若当前存在一个正在等待获取的消费者线程,则立刻移交元素e。否则,插入当前元素e到队列尾部,并且当前线程进入阻塞状态,直到有消费者线程取走该元素。 - hasWaitingConsumer()
如果至少有一位消费者在等待,则返回true。 - getWaitingConsumerCount()
返回等待的消费者数量的估计值。
LinkedTransferQueue
LinkedTransferQueue实现了TransferQueue接口。表示基于链表的阻塞队列,它的容量没有上限。当有消费者等待获取元素时,可以通过transfer()方法把生产者传入的元素立即传给消费者。
LinkedTransferQueue采用了一种预占模式。当消费者取元素时,如果队列为空,则生成一个元素为null的节点并入队,然后消费者等待在这个节点上。当生产者在插入元素时发现队列中有一个元素为null的节点,于是生产者就不入队了,直接将元素填充到该节点并唤醒在此节点等待的消费者,消费者醒来后取走元素。如果当前没有等待获取的消费者,则生产者在插入元素后会一直等待直到插入的元素被消费者取走。TransferQueue不会让生产者过度生产,生产者的生产速度取决于消费者的消费能力,因此不会造成OutOfMemory错误。
六、并发Deque
Deque扩展了Queue接口,表示双向队列,双向队列可以在首尾两端访问、插入或删除元素。当只从队列一端进行入队和出队时即为栈。
Deque作为单向队列:
- offer:将元素加入队列的末尾。
- poll:将元素从队列的头部删除。
- peek:返回队首的元素,但不进行删除。
Deque作为双向队列:
- offerFirst: 添加元素到头部。如果队列已满,则返回false。
- offerLast: 添加元素到尾部。如果队列已满,则返回false。
- pollFirst:将元素从队列的头部删除。
- pollLast:将元素从队列的末尾删除。
- peekFirst:返回队列头部的元素,但不进行删除。
- peekLast:返回队列末尾的元素,但不进行删除。
Deque作为栈:
- push:将元素入栈。
- pop:将元素出栈。
- peek:返回栈顶元素,但不进行删除。
ConcurrentLinkedDeque
ConcurrentLinkedDeque实现了Deque接口。表示基于链表的双向队列。使用了CAS的非阻塞算法来保证线程并发访问时的数据一致性。
BlockingDeque接口
BlockingDeque接口扩展了BlockingQueue接口。表示阻塞双向队列。
BlockingDeque的核心方法:
- putFirst: 添加元素到头部。如果队列已满,线程将被阻塞。
- putLast: 添加元素到尾部。如果队列已满,线程将被阻塞。
- takeFirst: 移除头部元素。如果队列为空,线程将被阻塞。
- takeLast: 移除尾部元素。如果队列为空,线程将被阻塞。
LinkedBlockingDeque
LinkedBlockingDeque实现了BlockingDeque接口。表示基于链表的双向阻塞队列。
七、并发Map
ConcurrentHashMap
对应HashMap,使用方法和HashMap相同。内部实现基于分段锁技术,JDK8中改为CAS无锁算法。
ConcurrentSkipListMap
对应TreeMap,使用方法和TreeMap相同。内部实现基于跳跃表(Skip-List)和CAS无锁算法。