Java并发——同步容器与并发容器
同步容器类
早期版本的JDK提供的同步容器类为Vector和Hashtable,JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装。对每个共有方法都进行同步。
Collection类中提供了多个synchronizedXxx方法,该方法返回指定集合对象对应的同步对象。synchronizedXxx方法本质是对相应容器的包装。
例:使用Collections类获得同步容器。
public static void main(String[] args) { Collection c = Collections.synchronizedCollection(new ArrayList()); List list = Collections.synchronizedList(new ArrayList()); // 包装List Set set = Collections.synchronizedSet(new HashSet()); // 包装Set Map map = Collections.synchronizedMap(new HashMap()); // 包装Map }
synchronizedMap源码探究
Collections.synchronizedMap(new HashMap())方法内部会new一个SynchronizedMap对象。
该方法源码如下:
SynchronizedMap类是一个静态内部类。
该类源码如下:
SynchronizedMap的每个方法,都是对Map本身做了一次带锁的操作。该类本身并没有太多的应用,性能较差。
同步容器的问题
同步容器类在单个方法被使用时可以保证线程安全。复合操作则需要额外的客户端加锁来保护。
迭代器与ConcurrentModificationException
使用Iterator迭代容器或使用使用for-each遍历容器,在迭代过程中修改容器会抛出ConcurrentModificationException异常。想要避免出现ConcurrentModificationException,就必须在迭代过程持有容器的锁。但是若容器较大,则迭代的时间也会较长。那么需要访问该容器的其他线程将会长时间等待。从而会极大降低性能。
若不希望在迭代期间对容器加锁,可以使用"克隆"容器的方式。使用线程封闭,由于其他线程不会对容器进行修改,可以避免ConcurrentModificationException。但是在创建副本的时候,存在较大性能开销。
隐式迭代
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都会隐式的Iterate,也即可能抛出ConcurrentModificationException。
并发容器
针对于同步容器的巨大缺陷。java.util.concurrent中提供了并发容器。并发容器包注重以下特性:
- 根据具体场景进行设计,尽量避免使用锁,提高容器的并发访问性。
- 并发容器定义了一些线程安全的复合操作。
- 并发容器在迭代时,可以不封闭在synchronized中。但是未必每次看到的都是"最新的、当前的"数据。如果说将迭代操作包装在synchronized中,可以达到"串行"的并发安全性,那么并发容器的迭代达到了"脏读"。
CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是"克隆"容器对象。
ConcurrentLinkedQuerue是Query实现,是一个先进先出的队列。一般的Queue实现中操作不会阻塞,如果队列为空,那么取元素的操作将返回空。Queue一般用LinkedList实现的,因为去掉了List的随机访问需求,因此并发性更好。
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,那么获取操作将阻塞,直到队列中有一个可用的元素。如果队列已满,那么插入操作就阻塞,直到队列中出现可用的空间。
Map并发容器
ConcurrentMap<K,V>接口
该接口定义Map的原子操作:putIfAbsent、remove、replace。
若没有则增加
V putIfAbsent(K key, V value) :若不包含key,则放入value。若包含key,则返回key对应的value。等价于:
if (!map.containsKey(key)) return map.put(key, value); else return map.get(key);
若相等则移除
boolean remove(Object key, Object value) :当key对应到指定的value时,才移除该key-value对。等效于:
if (map.containsKey(key) && map.get(key).equals(value)) { map.remove(key); return true; } else { return false; }
若相等则替换
boolean replace(K key, V oldValue, V newValue) :当key对应到指定的value时,才替换key对应的value值。等效于:
if (map.containsKey(key) && map.get(key).equals(oldValue)) { map.put(key, newValue); return true; } else { return false; }
若拥有则替换
V replace(K key,V value):只有目前将键的条目映射到某一值时,才替换该键的条目。
等效于:
if (map.containsKey(key)) { return map.put(key, value); } else { return null; }
ConcurrentHashMap<K,V>类
ConcurrentHashMap是HashMap的线程安全版本。ConcurrentHashMap类实现了ConcurrentMap接口,具备putIfAbsent、remove、replace原子操作。此类与 Hashtable 相似(与 HashMap 不同)ConcurrentHashMap不允许将 null 用作键或值,这与ConcurrentHashMap的具体实现有关。
ConcurrentHashMap实现也采用了散列机制,但是采用了分段锁(Lock Striping)机制提供了并发性能。并发环境下实现更的吞吐量,而在单线程环境下只损失非常小的性能。
三种Map的比较:
- HashMap是根据散列值分段存储。
- SynchronizedMap在同步的时候锁住了所有的段。
- ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。
ConcurrentHashMap和其他并发容易一样改进了同步容器的问题,其迭代器不会抛出CurrentModificationException异常。ConcurrentHashMap返回迭代器具有弱的一致性。弱一致性的迭代器可以容忍并发修改,当创建迭代器时会创建已有的元素,可以(但是不保证)在迭代器被构造后将修改操作反映给容器。ConcurrentHashMap弱一致性使得像size()和isEmpty()方法变弱了,即size()和isEmpty()方法返回的结果在计算时可能是过期了。但这种方法在并发环境下用处很小,因为它们返回值总在不断变化。这些操作被弱化,换来的是对重要操作的性能优化(包括get、put、containsKey、remove等)。
ConcurrentSkipListMap类
ConcurrentSkipListMap是TreeMap的线程安全版本。
Queue 并发容器
Queue 队列
队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。
对于Queue而言是在Collection的基础上增加了offer/remove/poll/element/peek方法,另外重新定义了add方法。
抛出异常 |
返回特殊值 |
操作描述 |
|
插入 |
add(e) |
offer(e) |
将元素加入到队列尾部。 |
移除 |
remove() |
poll() |
移除队列头部的元素。 |
检查 |
element() |
peek() |
返回队列头部的元素而不移除此元素。 |
PriorityQueue类在非并发情况下实现了Queue接口。
ConcurrentLinkedQueue类
ConcurrentLinkedQueue是使用非阻塞的方式实现的基于链接节点的无界的线程安全队列,性能非常好。
多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
BlockingQueue 接口——阻塞队列
对于Queue来说,BlockingQueue是主要的线程安全版本。这是一个可阻塞的版本,也就是允许添加/删除元素被阻塞,直到成功为止。
BlockingQueue相对于Queue而言增加了两个操作:put/take。下面是一张整理的表格。
抛出异常 |
返回特殊值 |
阻塞 |
超时 |
操作描述 |
|
插入 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
将元素加入到队列尾部。 |
移除 |
remove() |
poll() |
take() |
poll(time,unit) |
移除队列头部的元素。 |
检查 |
element() |
peek() |
返回队列头部的元素而不移除此元素。 |
BlockingQueue非常适合做生产者消费者模型。一般而言BlockingQueue内部会使用锁,所以BlockingQueue的性能并不太高。
ArrayBlockingQueue类
一个由数组支持的有界阻塞队列。ArrayBlockingQueue是一个典型的"有界缓存区",由于数组大小固定,所以一旦创建了这样的“缓存区",就不能再增加其容量。
阻塞条件:试图向已满队列中执行put元素会导致操作受阻塞;试图从空队列中take元素将导致类似阻塞。
源码解析
ArrayBlockingQueue使用ReentrantLock保证线程安全,并用Condition对线程进行通讯。
put方法 若满,则等待。
take方法 若空,则等待。
LinkedBlockingQueue类
一个基于链表的有界阻塞队列。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
LinkedBlockingQueue的一个构造方法可以设定容量范围。如果未指定容量,则它等于Integer.MAX_VALUE。除非插入节点会使队列超出容量,否则每次插入后会动态地创建链接节点。
PriorityBlockingQueue类
一个支持优先级的无界阻塞队列,即该阻塞队列中的元素可自动排序。默认情况下,元素采取自然升序排列。可以自定义类实现compareTo()方法来指定元素排序规则。此队列在逻辑上是无界的。此类不允许使用 null 元素。需要注意的是,不能保证相同优先级元素的顺序。
DelayQueue类
DelayQueue是一种延时获取元素的无界阻塞队列。队列使用PriorityQueue类实现。队列中的元素必须实现Delay接口,在创建元素时,可以指定从队列获取元素的限制时长。只有在延迟期满,元素才能被提出队列。
这个队列的特性是,队列中的元素都要延迟时间(超时时间),只有一个元素达到了延时时间才能出队列,也就是说每次从队列中获取的元素总是最先到达延时的元素。
DelayQueue非常有用,可以用DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素。使用一个线程循环检查队列中的元素,一旦可以从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimeQueue就是使用DelayQueue实现的。
SynchronousQueue类
SynchronousQueue是一个不存储元素的阻塞队列。每个put操作必须等待一个take操作,否则不能继续添加元素。
SynchronousQueue内部其实没有任何一个元素,容量是0,严格说并不是一种容器。由于队列没有容量,因此不能调用peek操作。因为仅在试图要移除元素时,该元素才存在。除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。
SynchronousQueue更像是一种信道(管道),资源从一个方向快速传递到另一方向。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。
例:使用BlockingQueue实现生产者-消费者模型。服务端(ICE服务)接受客户端的请求(accept),请求计算此人的好友生日,然后将计算的结果存取缓存中(Memcache)中。该例采用了ExecutorService实现多线程的功能,尽可能的提高吞吐量,阻塞队列使用的是LinkedBlockingQueue。
import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; public class BirthdayService { final int workerNumber; final Worker[] workers; final ExecutorService threadPool; static volatile boolean running = true; public BirthdayService(int workerNumber, int capacity) { if (workerNumber <= 0) throw new IllegalArgumentException(); this.workerNumber = workerNumber; workers = new Worker[workerNumber]; for (int i = 0; i < workerNumber; i++) { workers[i] = new Worker(capacity); } boolean b = running; // kill the resorting threadPool = Executors.newFixedThreadPool(workerNumber); for (Worker w : workers) { threadPool.submit(w); } } Worker getWorker(int id) { return workers[id % workerNumber]; } class Worker implements Runnable { final BlockingQueue<Integer> queue; public Worker(int capacity) { queue = new LinkedBlockingQueue<Integer>(capacity); } public void run() { while (true) { try { consume(queue.take()); } catch (InterruptedException e) { return; } } } void put(int id) { try { queue.put(id); } catch (InterruptedException e) { return; } } } public void accept(int id) { // accept client request getWorker(id).put(id); } protected void consume(int id) { // do the work // get the list of friends and save the birthday to cache } }
Deque双向队列
Deque接口定义了双向队列。双向队列允许在队列头和尾部进行入队出队操作。Deque不仅具有FIFO的Queue实现,也有FILO的实现,也就是不仅可以实现队列,也可以实现一个堆栈。需要说明的是LinkedList实现了Deque接口,即LinkedList是一个双向队列。
Deque在Queue的基础上增加了更多的操作方法。
第一个元素 | 最后一个元素 | 描述 | |||
抛出异常 | 返回特殊值 | 抛出异常 | 返回特殊值 | ||
插入 | addFirst(e) | offerFirst | addLast(e) | offerLast(e) | 将元素加入列表 |
push(e) | add(e) | offer(e) | |||
移除 | removeFirst(e) | pollFirst(e) | removeLast() | pollLast() | 将元素从队列移除 |
remove()/pop() | poll() | ||||
检查 | getFirst() | peekFirst() | getLast() | peekLast() | 查看队列头或尾元素 |
element() | peek() |
对于非阻塞Deque的实现类有ArrayDeque和LinkedList。
BlockingDeque接口
BlockingDeque 方法有四种形式,使用不同的方式处理无法立即满足但在将来某一时刻可能满足的操作:第一种方式抛出异常;第二种返回一个特殊值(null 或 false,具体取决于操作);第三种无限期阻塞当前线程,直至操作成功;第四种只阻塞给定的最大时间,然后放弃。下表中总结了这些方法:
第一个元素 | 最后一个元素 | 描述 | |||||||
抛出异常 | 返回特殊值 | 阻塞 | 超时 | 抛出异常 | 返回特殊值 | 阻塞 | 超时 | ||
插入 | addFirst(e) | offerFirst | putFirst(e) | offerFirst(e,time,unit) | addLast(e) | offerLast(e) | putLast(e) | offerLast(e,time,unit) | 将元素加入列表 |
push(e) | add(e) | offer(e) | |||||||
移除 | removeFirst(e) | pollFirst(e) | takeFirst() | pollFirst(time,unit) | removeLast() | pollLast() | takeLast() | pollLast(time,unit) | 将元素从队列移除 |
remove()/pop() | poll() | ||||||||
检查 | getFirst() | peekFirst() | getLast() | peekLast() | 查看队列头或尾元素 | ||||
element() | peek() | |
BlockingDeque 是线程安全的,但不允许 null 元素,并且可能有(也可能没有)容量限制。
BlockingDeque 实现可以直接用作 FIFO BlockingQueue。继承自 BlockingQueue 接口的方法精确地等效于下表中描述的 BlockingDeque 方法:
BlockingQueue 方法 | 等效的 BlockingDeque 方法 |
插入 | |
add(e) | addLast(e) |
offer(e) | offerLast(e) |
put(e) | putLast(e) |
offer(e, time, unit) | offerLast(e, time, unit) |
移除 | |
remove() | removeFirst() |
poll() | pollFirst() |
take() | takeFirst() |
poll(time, unit) | pollFirst(time, unit) |
检查 | |
element() | getFirst() |
peek() | peekFirst() |
LinkedBlockingDeque类
LinkedBlockingDeque是一个基于链表的双向阻塞队列。双向队列因为多一个操作队列的入口,在多线程同时入队时,也就降低了一半竞争。在初始化LinkedBlockingDeque时,可以设置容量,防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。
写时复制 CopyOnWrite
CopyOnWrite表示读时共享,写时复制。对于读多写少的场景特别适用。
CopyOnWriteArrayList 与 CopyOnWriteArraySet
对于List或者Set而言,增、删操作其实都是针对整个容器,因此每次操作都不可避免的需要锁定整个容器空间,性能肯定会大打折扣。要实现一个线程安全的List/Set,只需要在修改操作的时候进行同步即可,比如使用java.util.Collections.synchronizedList(List<T>)或者java.util.Collections.synchronizedSet(Set<T>)。
当然也可以使用Lock来实现线程安全的List/Set。ReadWriteLock当然是一种实现。CopyOnWriteArrayList/CopyOnWriteArraySet确实另外一种思路。
CopyOnWriteArrayList/CopyOnWriteArraySet的基本思想是一旦对容器有修改,那么就"复制"一份新的集合,在新的集合上修改,然后将新集合复制给旧的引用。当然了这部分少不了要加锁。显然对于CopyOnWriteArrayList/CopyOnWriteArraySet来说最大的好处就是"读"操作不需要锁了。