之前看过一些多线程的东西,今天温习了一下,阅读了Java Concurrent Programming 第四节 锁 又回顾了线程状态的流转,整理如下:
与锁相关的切入点:Lock和Condition。
Lock是一个接口,它提供了上锁(lock)和解锁(unlock)等抽象方法;
concurrent包里提供了许多不同策略的锁的实现,使用它们是为了较传统的synchronized同步块而言,使获取和使用独占资源的过程更高效。
传统的synchronized机制是,当某个线程获取到对象内建的锁(monitor lock)之后,其它线程就只能在同步块的入口处僵等,处于blocked状态。
如果等候时间很短,那问题不大;然而如果排队等候的线程很多,预期到要等候的时间较长,那么其它线程就在空转,因为blocked状态下线程不会释放所占用的系统资源,此时可以采用异步通知机制让这些线程休息,即切换到waiting状态并等待激活信号的到来,并释放所占资源。
concurrent包里的lock()方法通过LockSupport.park()来实现,即使得线程转为waiting状态。参考线程状态流转图
进一步地,为了同一把锁需要长时间等候的场景可能有多种,例如生产者和消费者之间有个传送带BlockingQueue是独占资源,每当写入Q都要获得同一把写锁。然而有两种情况(或者说状态/条件)可导致长时间等待的预期:一种是Q满了无法入队(理应使生产者等待),另一种是Q空了无法出队。(理应使消费者等待)。
Condition提供了上述长时间等待场景中分情况调度线程的能力,提供了等待(await)和激活(signal)功能(类似于传统synchronized机制中的wait和notify,注1)。
不同角色的线程得以遵循不同的Condition来调度。
至此,由一个Lock派生多个Condition的结构才阐述清楚。(关于Condition的翻译,注2)
几种抽象的锁:
ReadWriteLock:当一个线程申请读锁时,只要写入锁没有被其他线程持有,那么会立刻拥有读锁。当一个线程申请写入锁时,其他线程不能持有读锁和写入锁,否则会一直等待。写锁是排他锁,而读锁可以同时被多个线程持有。
ReentrantLock:考虑到递归的情况,有些锁需要被一个线程重复获取,就形成了“可重入锁”。
ReentrantReadWriteLock:同时具备上述两种功能。
几种涉及锁的场景:
实际编程中,对于某些常见的场景,直接面向锁编程仍然太罗嗦,所以诞生了以下几种工具类对各个场景予以直观的支持。
- Semaphore(信号量):把有限个资源提供给多线程竞争,还提供不同的竞争策略(公平/不公平),类比停车场车位紧张的场景。
- Mutex (互斥):即信号量为1的情况,类比只有一个洗手间的场景。
- Latch(字面义:闭锁、门闩):等待直到n个目标线程触发了某个事件( CountDownLatch:A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.)对于CountDownLatch,触发事件对应的动作是计数器减1,即CountDownLatch.countDown();注意它不能reset,因此是一次性的。类比百米冲刺的终点线,等待直到所有队员撞线后,再执行颁奖典礼。
- Barrier(字面义:屏障、栅栏) :在几个切面上阻拦先抵达的线程使之等待,直到所有目标线程到达,以便所有线程继续从同一个切面开始并发执行(CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. )注意它可以reset,因此每个切面可以复用同一个CyclicBarrier。这篇帖子提到CyclicBarrier可以很方便的协助实现MapReduce这样的复杂的并行计算场景,真是一语中的,不过现实生活中有否形象的例子呢?我想到了诺曼底登陆。假设德军在诺曼底有n道防线,击溃每一道防线都需要不同数量的盟军士兵登陆集结后发起冲锋;因为当时盟军已经获得了海路的绝对控制权,士兵被源源不断地送上岸,同时也会在战斗中牺牲。上述例子中,每个盟军士兵都是一个线程,而防线即Barrier,每道防线等待特定数量的活着的线程抵达集结,便被攻破,紧接着收缩形成下一道防线,相当于reset Barrier溃败的阈值。
另,注意到Doug Lea较早的concurrent包实现,ReadWriteLock有多个实现,主要是提供了读写锁竞争时的不同策略,摘录于此:
尽管读-写锁定的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁定的效果。这些策略的例子包括:
- 在 writer 释放写入锁定时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁定还是授予写入锁定。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
- 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁定的 reader 授予读取锁定。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
- 确定是否重新进入锁定:可以使用带有写入锁定的线程重新获取它吗?可以在保持写入锁定的同时获取读取锁定吗?可以重新进入写入锁定本身吗?
- 可以将写入锁定在不允许其他 writer 干涉的情况下降级为读取锁定吗?可以优先于其他等待的 reader 或 writer 将读取锁定升级为写入锁定吗?
当评估给定实现是否适合您的应用程序时,应该考虑所有这些情况。
互联网大多是读多写少的场景,因此才采用了现在的默认实现吧。