Java并发之Lock功能分析
可重入锁
ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。
重入锁相比 synchronized 有哪些优势:
- 可以在线程等待锁的时候中断线程(
lockInterruptibly
),synchronized 是做不到的。 - 可以尝试获取锁,如果获取不到就放弃,或者设置一定的时间,这也是 synchroized 做不到的。
- 可以设置公平锁,synchronized 默认是非公平锁,无法实现公平锁。
公平与非公平
ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。ReentrantLock还提供了公平锁和非公平锁的选择,构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
NonfairSync
1 | static final class NonfairSync extends Sync { |
FairSync
1 | static final class FairSync extends Sync { |
读写锁
所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为 如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读锁(锁降级);如果一个线程对资源加了读锁,其他线程可以继续加读锁。java.util.concurrent.locks中关于多写锁的接口:
1 | public interface ReadWriteLock { |
ReentrantReadWriterLock通过两个内部类实现Lock接口,分别是ReadLock,WriterLock类。与ReentrantLock一样,ReentrantReadWriterLock同样使用自己的内部类Sync(继承AbstractQueuedSynchronizer)实现CLH算法。
1 | /* |
ReentrantReadWriterLock使用一个32位的int类型来表示锁被占用的线程数(AbstractQueuedSynchronizer中的state),高16位用来表示读锁占有的线程数量,用低16位表示写锁被同一个线程申请的次数。
- SHARED_SHIFT,表示读锁占用的位数,常量16
- SHARED_UNIT,增加一个读锁,按照上述设计,就相当于增加 SHARED_UNIT;
- MAX_COUNT,表示申请读锁最大的线程数量,为65535
- EXCLUSIVE_MASK,表示计算写锁的具体值时,该值为 15个1,用 getState & EXCLUSIVE_MASK算出写锁的线程数,大于1表示重入。
比如,现在当前,申请读锁的线程数为13个,写锁一个,那state怎么表示?
上文说过,用一个32位的int类型的高16位表示读锁线程数,13的二进制为1101
,那state的二进制表示为 00000000 00001101 00000000 00000001
,十进制数为851969, 接下在具体获取锁时,需要根据这个851968这个值得出上文中的 13 与 1。要算成13,只需要将state 无符号向左移位16位置,得出00000000 00001101
,就出13,根据851969要算成低16位置,只需要用该00000000 00001101 00000000 00000001
& 111111111111111
(15位),就可以得出00000001
,就是利用了1&1得1,1&0得0这个技巧。
1 | /** |
上述这4个变量,其实就是完成一件事情,将获取读锁的线程放入线程本地变量(ThreadLocal),方便从整个上 下文,根据当前线程获取持有锁的次数信息。其实 firstReader,firstReaderHoldCount ,cachedHoldCounter 这三个变量就是为readHolds变量服务的,是一个优化手段,尽量减少直接使用readHolds.get方法的次数,firstReader与firstReadHoldCount保存第一个获取读锁的线程,也就是readHolds中并不会保存第一个获取读锁的线程;cachedHoldCounter 缓存的是最后一个获取线程的HolderCount信息,该变量主要是在如果当前线程多次获取读锁时,减少从readHolds中获取HoldCounter的次数。
sync
1 | /** |
Sync中提供了很多方法,但是有两个方法是抽象的,子类必须实现。
公平
1 | final boolean writerShouldBlock() { |
writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起。
非公平
1 | final boolean writerShouldBlock() { |
可以看到,非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;
而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。
如果当前占据的锁是读锁,那么紧接着的下一个读线程不应排队,排队s若不为null,
!s.isShared()
一定是true。当前线程需要排队。
如果当前占据的锁是写锁,那么一定是当前线程占据了写锁,同时在申请读锁。如果此时下一个是写线程在等待,当前线程需要排队。排队的原因,在注释中有说,为了防止等待写锁线程的无限饥饿
ReadLock
1 | /** |
1 | /** |
下面来看看读锁的释放:
1 | /** |
WriteLock
写锁是独占锁。如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。
1 | // WriteLock |
锁降级
Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。
不过从源码中也可以看出,确实会给写锁一些特殊照顾,如非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作。
Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。
1 | void processCachedData() { |
Condition
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。
1 | public interface Condition { |
以上是Condition接口定义的方法,await*对应于Object.wait,signal对应于Object.notify,signalAll对应于Object.notifyAll。特别说明的是Condition的接口改变名称就是为了避免与Object中的wait/notify/notifyAll的语义和使用上混淆,因为Condition同样有wait/notify/notifyAll方法。
每一个Lock可以有任意数据的Condition对象,Condition是与Lock绑定的,所以就有Lock的公平性特性:如果是公平锁,线程为按照FIFO的顺序从Condition.await中释放,如果是非公平锁,那么后续的锁竞争就不保证FIFO顺序了。
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
await & signal|signalAll
线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。
condition 源码分析
Condition实例的产生方式是newCondition()
方法,通过追踪方法。我们可以发现Condition的生产方法调用链是:
1 | // ReentrantLock |
先来看看 await()
方法
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
1 | public final void await() throws InterruptedException { |
addConditionWaiter
addConditionWaiter方法主要用于调用Condition.await 时将当前节点封装成 一个Node, 加入到 Condition Queue里面。
1 | private Node addConditionWaiter() { |
这里有1个问题, 何时出现 t.waitStatus != Node.CONDITION
?
- ConditionObject.await ->
- checkInterruptWhileWaiting ->
- transferAfterCancelledWait “compareAndSetWaitStatus(node, Node.CONDITION, 0)”
导致这种情况一般是 线程中断或 await 超时。注意: 当Condition进行 awiat 超时或被中断时, Condition里面的节点是没有被删除掉的, 需要其他 await 在将线程加入 Condition Queue 时调用addConditionWaiter而进而删除, 或 await 操作差不多结束时, 调用 “node.nextWaiter != null” 进行判断而删除 (通过 signal 进行唤醒时 node.nextWaiter 会被置空, 而中断和超时时不会)。
unlinkCancelledWaiters
1 | private void unlinkCancelledWaiters(){ |
fullyRelease
1 | final int fullyRelease(Node node) { |
signal()
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中.调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程.
1 | public final void signal() { |
doSignal
1 | private void doSignal(Node first) { |
signal()方法只是将Condition等待队列头结点移出队列,此时该线程节点还是阻塞的,同时将该节点的线程重新包装加入AQS等待队列,当调用unlock方法时,会唤醒AQS等待队列的第二个节点,假如这个新节点是处于第二个位置,那么它将会被唤醒,否则,继续阻塞。
await 不响应中断
1 | public final void awaitUninterruptibly(){ |