本篇继续之前的系列,解析ReentrantLock公平锁。
公平锁源码解析
之前的解析文章就已经提到,ReentrantLock默认创建的对象是非公平的:
|
|
要创建公平锁需要自己指定:
|
|
通过比较FairSync类和NonfairSync类,可知两者内部的tryAcquire方法在实现有所不同,具体到代码:
|
|
实现公平锁的同步器FairSync会在尝试获得锁(tryAcquire方法)的过程中多出一行代码!hasQueuedPredecessors()
。实现如下:
|
|
在这个方法中,如果h == t,结合系列文章(一)的分析,要么之前还没有线程,要么前面只有一个线程,等待队列没有等待的线程,这时候直接返回false,当前线程尝试去获得锁;如果s != null且s.Thread是当前线程,那么当前线程就是head的后继节点,所以即便等待队列存在其它线程,也返回false并尝试去获取锁。
公平&非公平的具体体现
原理初探
公平锁和非公平锁在实现上基本相同,这就不得不让人产生困惑,锁的公平与不公体现在哪?现在假设有这样一个场景,开6个线程,当第1个启动的线程持有锁的时候,其余5个线程进入等待队列等待,等持有锁的线程释放锁的时候,接下来等待的线程拿锁的过程遵守公平&非公平原则吗??
看代码,这里创建一个非公平锁:
|
|
输出如下:
|
|
多次测试都是相同的输出结果,不严谨的表明了线程是按照开启的顺序依次拿锁执行的。这不难通过分析来证明:当其它线程都进入等待队列等待的时候,唤醒是按进入队列的先后顺序依次由前置节点唤醒,这是由源码的设计决定的。所以无论是否设置公平锁,此时获得锁的顺序都是公平的。
接下来再构造一个简单的模型,思路是开启若干线程,每个线程在执行的过程中经过若干轮的尝试获得锁、释放锁,通过观察程序执行的结果来分析公平锁非公平锁的工作原理,这个例子引用自ReentrantLock(重入锁)以及公平性。
构造观察模型
不妨设计5个线程,每个线程内循环30次,之所以设置30次是为了最后输出效果好一点点,线程内循环次数过少会因为程序运行快而使得等待队列没有等待的线程。
老规矩还是先放代码:
|
|
简要说明一下这段代码,开5个线程,每个线程在执行“Job”任务时,会在run方法内循环30次,每1次都有一个获得锁释放锁的过程,并在run方法中打印当前持有锁的名称以及同步器内的等待队列的线程名。
这里再啰嗦一下:ReentrantLock内部拥有一个返回同步器等待队列的方法(返回的是一个list),访问受限(protected),所以在这里创建一个新的子类ReentrantLock02,目的就是方便程序调用getQueuedThreads方法。
|
|
输出的每一行由相同的格式构成,比如这样一行:Lock by: Thread-2 Waiting Threads: [Thread-4, Thread-3, Thread-0, Thread-1]
,表示当前的锁被Thread-2持有,等待队列由前到后的顺序是:Thread-1、Thread-0、Thread-3、Thread-4。
结果其实已经体现了非公平锁的非公平性,每次新获得锁的线程,并不是等待队列里排最靠前的线程,这是因为上一个持有锁的线程释放锁的时候,如果有某个线程正好“乱入”,那么它就有可能在竞争胜利的情况下获得锁,而不会经过!hasQueuedPredecessors()
的判断。
在锁的公平和非公平这篇文章的最后提到了一个问题:为什么非公平锁下会出现线程连续获取锁的情况?实际上,由于本人将每个线程执行的循环设置为30次,输出结果已经表明线程并不是一直连续获取锁。而在循环次数少的情况下如果某个线程连续获取锁,我认为是因为线程的启动需要耗费资源和时间,而线程在由挂起到唤醒的过程中也会需要资源和时间,一个线程unlock()后也是优先执行tryAcquire方法,所以它比要被唤醒的线程更可能先获得锁。
将代码稍作修改就可以得到公平锁,由于前文已经将公平与否的原理分析清楚,这里不再赘述了,公平锁会按照等待队列的顺序依次获得锁,新近启动的线程也不能“插队”,例如(选取输出的一部分):
|
|
小结
公平锁虽然保证了所谓“公平”,但是线程状态的切换非常频繁,而且非公平锁等待队列中的线程被唤醒获得锁的过程也挺公平的,这就造成实际使用中通常采用非公平锁,但具体采用什么锁还是要结合具体的场景。
参考
ReentrantLock(重入锁)以及公平性:借用AQS内部的方法展现公平锁实现原理的例子;