JAVA提供了很多并发的基础模块,来帮助我们构建线程安全的类。这里做一下简要的介绍。帮助构建一个并发模块框架。
同步容器类
早起的Vector和HashTable都是同步容器。同步容器类,通过将它们的状态封装起来,并对每个公有方法都进行同步,使得每次都只有一个线程能访问容器的状态。
同步容器类将并发的操作转换为串行的。当然同步容器是线程安全的。
我们也可以使用同步封装器,Collections.synchronizedXXX方法来创建同步容器。
同步容器存在的问题
同步容器是线程安全的,但是有些必需的符合操作需要客户端加锁来保护。常见的符合操作有:迭代,跳转(根据指定的顺序查找当前元素的下一个元素),条件运算(如:如果存在某元素则把它删除掉)
同步容器的迭代器
对于同步容器来说,如果在迭代过程中,有线程修改了容器中的元素,那么将会导致“及时失败”,将会抛出ConcurrentModificationException。
及时失败,不会做什么操作,只是提醒我们出现了并发错误。
我们可以在迭代时对容器加锁来避免这种异常,但是如果容器很大,迭代加锁可能会导致活跃性问题。
但是对于容器的toString操作也会导致隐性的迭代,有时hashCode或是equals这些操作也会导致隐性的迭代调用。这种情况下,我们很难使用加锁来避免异常。
当然我们也可以使用容器的副本来避免,这就需要根据实际情况来判别了。
并发容器
同步容器将所有的状态访问都串行化,这样会严重的降低并发性,尤其是多个锁同时竞争一个锁,会严重的影响到吞吐量。
从JAVA5开始,增加了Concurrent包,提供了多个支持并发操作的容器。通过使用并发容器来替代同步容器,可以极大的提高伸缩性并降低风险。
如:ConcurrentHashMap就是一个常见的并发容器。它采用了一种叫做分段锁的技术,可以支持支持加载多个锁,保证多个线程同时对容器进行读写操作。所以它的迭代器就不支持及时失败,而是采用了弱一致性,在迭代是允许对容器进行修改,创建时会遍历所有元素,并在迭代器被构造后将修改操作反映给容器。其中也提供了一些额外的原子操作,如:如果没有则添加,若相等则移除等。
对于并发容器size和isEmpty都是估计的,因为数据始终都是在变化中。
阻塞队列和生产者-消费者模式
阻塞队列是JAVA5新增的属性,提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。
put和take方法将阻塞直到达到可以执行状态的发生,而offer和poll不会等待直接返回一个失败。
阻塞队列支持生产者-消费者模式。
类库中BlockingQueue有很多实现。其中LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,类似于LinkedList和ArrayList,但是具有更强的并发性。PriorityBlockingQueue是一个可以按照优先级排序的队列,可以按照默认的顺序排序也可以自定义比较器(Comparable)。
还存在一个特殊的BlockingQueue--SynchronousQueue,它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同,它维护的是一组线程,这些线程在等待着把元素加入或移出队列。
java6中还新增了双端队列,Deque和BlockingDeque。它们实现了在队列头和尾同时搞笑插入和移除。具体实现有ArrayDeque和LinkedBlockingDeque。它不光支持生产者-消费者模式,还支持工作密取--每个消费者都面对一个专属的生产者,如果对应的生产者队列为空,可以从其他生产者队列的尾部读取数据。
阻塞方法和中断方法
当线程被阻塞时它通常被挂起,并处于某种阻塞状态(BLOCKED、WAITING或TIMED_WAITING)。
Thread提供了interrupt方法,用来中断线程或查询线程是否已经中断。中断是一种协作机制。一个线程不能直接强制终止其他正在执行的线程,只能等待被中断线程自己在适当的时机终止操作。
同步工具类
闭锁
闭锁可以延迟线程的进度直到其到达终止状态。当达到终止状态时,将不会在改变状态。闭锁是一扇门,在闭锁到达结束状态前,所有的线程都无法通过。
例如:魔兽中,只有等所有的玩家到达后才可以开始游戏。
CountDownLatch就是一种灵活的闭锁。存在一个递减的计数器,每个线程对应一个值,当计数器的值减为0以后,表示所有线程都以达到状态。await方法将会阻塞直到计数器值为0。
FutureTask
FutureTask也是一种闭锁。有Callable来实现具体计算,相当于可返回结果的Runnable。它有3中状态,等待运行、正在计算和运行完成。
提供get方法,只有状态为运行完成时,get方法才可以返回结果,否则就会一直阻塞。
信号量
计数信号量可以用来控制同时访问某个特定资源的操作数量,或是执行某个指定操作的数量。
Semaphore管理一组虚拟许可,只有获得许可方可执行,否则就会一直等待直到获得许可。
栅栏
闭锁是一次性对象,一旦进入终止状态就不能被重置。栅栏(Barrier)类似于闭锁,可以阻塞一组线程直到某个事件发生。
栅栏和闭锁的区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,栅栏用于等待其他线程。
CyclicBarrier可以使一定数量的线程反复地在栅栏位置汇集。每个线程都会对应到一个栅栏,每个线程到达栅栏处将会调用栅栏的await方法,来等待其他线程也到达栅栏,等所有线程都到达栅栏后,这些线程才可以继续执行。
Exchanger是一种两方栅栏,各方在栅栏位置上交换数据。