1 概述
本文介绍偏向锁相关原理,并不限定于Java中的偏向锁,但是Java中偏向锁的实现也是相同的原理,本文主要是对参考文献(Quickly Reacquirable Locks)中偏向锁实现重点部分的翻译,加入了自己的理解,参考文献称偏向锁为可快速获取的锁(QRL,Quickly Reacquirable Locks)。如何快速获取将会在第2节介绍过相关数据结构之后介绍。偏向锁主要是为了提高绝大部分情况下不存在竞争、只有一个线程在尝试获取锁的场景,通过相关数据结构可以减少CAS操作的数量,提高应用性能。偏向锁在有多个线程竞争获取时,会变成(论文中称为revoke,撤销)普通的锁(或默认锁),在Java中则会变先变为轻量级锁。
2 相关数据结构
相对于一般的锁来说,偏向锁使用了两个额外的域(这里不能等同于Java类成员域,在Java中其实是使用对象头中的Mark Word保存的)。第一个域是用来记录锁当前状态的域,可能的状态包括NEURAL,BIASED,REVOKING,DEFAULT。
- NEURAL:锁的初始状态,刚建立时的状态。
- BIASED:该锁被第一次获取且没有竞争时,锁状态会被持有的线程置为此状态。
- REVOKING:如果有另一个线程尝试获取当前被其他线程持有的BIASED状态的锁,则该锁会经过撤销过程变为普通的锁,即DEFAULT,撤销过程中该锁会处于中间状态REVOKING。
- DEFAULT:表示因为多个线程竞争获取偏向锁,则该锁会退化为普通的锁。
注意:撤销和释放(release)锁不同,这里的撤销(revoke)指的是从偏向锁退化为普通锁的过程。
初始状态下锁处于NEURAL状态,当该锁被第一次获取时,获取锁的线程会将锁状态变为BIASED。此时如果有另一个线程尝试获取同一个锁,那么其最终会将锁状态变为DEFAULT,在变为DEFAULT状态之前该锁会根据撤销协议先处于一个暂时的中间状态REVOKING。当锁状态为BIASED时,状态中会有一个域用来记录当前持有锁的线程标识符(后文使用线程ID代替)。对于一个不可再偏向(non-rebiasable,表示一旦撤销之后不可再次回到偏向状态)的锁,则锁状态只能按照NEURAL->BIASED->REVOKING->DEFAULT的状态进行切换,不可回退。但是偏向锁也可实现为可重偏向的锁(rebiasable)。
实现偏向锁需要额外增加的第二个域是一个可以指示布尔类型的位(bit)即可,用来表示偏向锁当前持有者对锁是获取状态还是释放状态。有了这个标识,当偏向锁被获取并且没有撤销时,当前持有者线程的获取和释放操作只要简单的改变该域的状态即可。
注意,一个线程获取偏向锁首先需要成为偏向锁的持有者(holder),即将自己的线程ID通过CAS写入锁状态域中,然后再获取锁(acquire),持有并不一定获取了锁,持有之后必须改变上面布尔标识位才获取了锁,但是获取锁之前必须要先成为锁的持有者;释放(release)锁之后也需要有其他线程显示竞争之后当前持有者才会失去持有者身份,锁的持有者可以通过改变上面介绍的布尔位进行锁的获取和释放动作。
将锁从初始的NEUTRAL状态转变为BIASED状态可以仅通过一次CAS操作完成。这里使用CAS操作是必须的,因为需要避免多个线程同时尝试获取NEUTRAL状态锁并置其状态为BIASED。
现在我们介绍第1节概述中快速获取的含义,根据上面的数据结构介绍,当一个线程获取了偏向锁之后,会在锁中记录线程ID,锁中也会有一个标识位用来表示该锁目前是释放的还是被获取的。因此以后线程在获取和释放锁时,只要检测锁是否记录自己的线程ID即可,如果检测成功,表示线程已经是偏向锁的持有者,直接通过改变上述布尔类型标识位域进行获取和释放锁即可。如果测试失败(表示该锁目前持有者不是自己),则再测试一下锁当前状态,如果还是BIASED状态,则使用CAS竞争锁,如果CAS成功,则尝试使用CAS将锁状态中线程ID置为自己的线程ID,这样后续在获取和释放锁时就不用使用CAS操作了。上面测试过程没有使用CAS操作,因此提高了性能,实现了快速获取锁。
实现上面atomic-free(表示尽可能减少CAS这样的原子操作)偏向锁的难点就在于如何协调获取偏向锁和撤销偏向锁的过程。必须处理偏向锁获取和释放同时发生偏向锁撤销时的多线程竞争问题,可以通过使用CAS将锁的状态改为REVOKING来避免后一种竞争:通过CAS将锁状态改为REVOKING也可能会有多线线程同时进行,但是CAS保证只有一个线程会成功改变锁状态,称为真正的撤销者(revoker),其他的线程则尝试着获取默认锁(默认锁即偏向锁撤销完成之后变成DEFAULT状态的锁)。
3 锁操作过程中的场景
下面介绍偏向锁操作过程(获取、释放、撤销等)的四种主要场景:
-
第一种场景是当锁还没有被任何线程获取过处于初始状态NEUTRAL时。在这种状态下,想要获取锁的线程首先会测试锁的状态,然后会使用CAS操作将锁状态中当前持有者线程置为自己的线程ID,这里的CAS操作是必须的,因为需要在多个线程同时获取偏向锁时保证原子性,CAS操作保证了最终只有一个线程会获取到偏向锁,那些获取失败的线程会进入撤销模式,也就是下面会介绍的第3种场景。当一个线程成功的在锁状态中写入自己的线程ID时,即成为该偏向锁的持有者,锁的状态会由NEUTRAL变为BIASED,同时该锁也会被这个线程获取。
-
第二种场景是偏向锁的当前持有者线程重新获取锁。在这种场景下,持有者线程只需要检测所的状态位保存自己的线程ID,再测试该锁状态不为REVOKING,即没有被撤销,最后对布尔状态位进行赋值即可。如果该锁被撤销了,锁的当前持有线程的获取动作会失败,需要进入获取默认锁的路径。
-
第三种场景发生在第一个非偏向锁持有者尝试获取同一个锁时,通过测试锁当前状态为BIASED,并且锁状态中保存的线程ID不是自己的线程ID可以知道进入此场景。该线程会通过CAS操作将锁的状态由BIASED改为REVOKING(REVOKING是偏向锁撤销为普通锁的中间状态,用来表示锁正在撤销过程中)。因为偏向锁的当前持有线程可能会随时改变锁的状态(比如随时获取、释放偏向锁),所以上面的CAS操作会使用循环进行多次尝试,且每次尝试CAS之前都必须重新读取一下锁状态。需要注意的是,可能会存在多个非持有者线程同时使用CAS尝试将锁状态从BIASED改为REVOKING,CAS则保证最终只有一个线程修改成功,修改成功的线程会变成该锁的撤销者(revoker)。其他修改失败的线程会被通知该锁现在已经不是偏向状态了,从而进入自旋状态直到撤销者成功的完成偏向锁的撤销过程,将锁状态改为DEFAULT,这些线程最终会转向去获取普通锁。偏向锁的撤销者会等待偏向锁的当前持有者释放锁,然后撤销者线程会成功的将锁变成普通锁,然后也会成为该普通锁的第一个获取者。
-
第四种场景发生在锁状态为DEFAULT时,进入这个场景有三步:首先,该线程尝试获取初始状态(NEUTRAL)或者偏向锁(BIASED)失败;然后,该线程通过获取普通锁的路径成功获取普通锁;最后,该线程成功获取锁,并从等待锁的阻塞中被唤醒。
参考文献后面还介绍了可再偏向(rebiasable)锁的实现原理,即在合适的时机再次尝试将锁从DEFALUT状态置为BIASED状态,这里不再介绍,有兴趣可以看看参考文献。
网友评论