同步容器类的问题
- 整个容器类加锁,线性访问容器实例,并发性能非常低
- 虽然单个操作是线程安全的,但是复合操作如果不另外加锁,本身无法保证并发安全
- 迭代器迭代过程中,如果发生元素的操作,会触发ConcurentModificationException异常,使用了“及时失败”机制
ConcurrentHashMap的优化手段
- 不是在每个方法上都在同一个锁上同步并使得每次只能有一线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程序的共享,这种机制称为分段锁。这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地访问Map,并且一定数量的写入线程可以并发地修改Map。
- 迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。迭代器具有弱一致性,而并非“及时失败”。
- 对于一些需要在整个Map上进行计算的方法,例如size/isEmpty,这些方法的语义被略微减弱了以反映容器的并发特性。
- 添加了额外的复合原子操作【没有才插入、映射到值了才移除、映射到旧值才替换、映射到某个值时才替换到新值】
CopyOnWriteArrayList
原理
使用场景
阻塞队列与生产-消费模式
问题
解决方法
串行线程封闭
优点
对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了串行线程封闭,从而将对象所有权从生产者交付给消费者。
线程封闭对象只能由单个线程拥有,通过安全地发布该对象“转移”所有权,实现了转移前由前一线程独占,转移后由后一线程独占。
实现方法
阻塞队列使得这种线程封闭的所有权转移变得容易,其次还可以通过ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet来完成这项工作。
闭锁(CountDownLatch)
使用场景
- 确保某个计算在其需要的所有资源都被初使化之后才继续执行。
- 确保某个服务在其依赖的所有其它服务都已经启动之后才启动。
- 等待直到某个操作的所有参与者(如多玩家游戏中的所有玩家)都就绪再继续执行。
使用方法
FutureTask也可以用作闭锁
信号量的作用
栅栏
栅栏与闭锁的关键区别在于,所有线程必须都同时达到栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其它线程。
Exchanger
是一种两方栅栏,各方在栅栏位置上交换数据。如果一方通过exchange方法申请交换,而另一方还没有来,则会一直阻塞直至对方也提出申请。
最简单的方案是,当缓冲区被填满时,由填充任务进行交换,当缓冲区为空时,由清空任务进行交换。但是如果新数据的到达率不可预测,那么一些数据的处理过程就将延迟。另一个方法是,不仅当缓冲被填满时进行交换,并且当缓冲被填充到一定程序并保持一定时间后,也进行交换。