这篇博文的特点是绘制了一套复杂场景每个关键帧的状态图,形象清晰, 便于验证理解。
以下转载自:http://www.cnblogs.com/leesf456/p/5383609.html
一、前言
在分析了AbstractQueuedSynchronier源码后,接着分析ReentrantLock源码,其实在AbstractQueuedSynchronizer的分析中,已经提到过ReentrantLock,ReentrantLock表示下面具体分析ReentrantLock源码。
二、ReentrantLock数据结构
ReentrantLock的底层是借助AbstractQueuedSynchronizer实现,所以其数据结构依附于AbstractQueuedSynchronizer的数据结构,关于AQS的数据结构,在前一篇已经介绍过,不再累赘。
三、ReentrantLock源码分析
3.1 类的继承关系
public class ReentrantLock implements Lock, java.io.Serializable
说明:ReentrantLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。
3.2 类的内部类
ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。
说明:ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
1. Sync类
Sync类的源码如下
View Code
说明:Sync类存在如下方法和作用如下。
2. NonfairSync类
NonfairSync类继承了Sync类,表示采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,源码如下。
View Code
说明:从lock方法的源码可知,每一次都尝试获取锁,而并不会按照公平等待的原则进行等待,让等待时间最久的线程获得锁。
3. FairSyn类
FairSync类也继承了Sync类,表示采用公平策略获取锁,其实现了Sync类中的抽象lock方法,源码如下。
View Code
说明:跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。
说明:可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。
3.3 类的属性
public class ReentrantLock implements Lock, java.io.Serializable { // 序列号 private static final long serialVersionUID = 7373984872572414699L; // 同步队列 private final Sync sync; }
说明:ReentrantLock类的sync非常重要,对ReentrantLock类的操作大部分都直接转化为对Sync和AbstractQueuedSynchronizer类的操作。
3.4 类的构造函数
1. ReentrantLock()型构造函数
View Code
说明:可以看到默认是采用的非公平策略获取锁。
2. ReentrantLock(boolean)型构造函数
View Code
说明:可以传递参数确定采用公平策略或者是非公平策略,参数为true表示公平策略,否则,采用非公平策略。
3.5 核心函数分析
通过分析ReentrantLock的源码,可知对其操作都转化为对Sync对象的操作,由于Sync继承了AQS,所以基本上都可以转化为对AQS的操作。如将ReentrantLock的lock函数转化为对Sync的lock函数的调用,而具体会根据采用的策略(如公平策略或者非公平策略)的不同而调用到Sync的不同子类。
所以可知,在ReentrantLock的背后,是AQS对其服务提供了支持,由于之前我们分析AQS的核心源码,遂不再累赘。下面还是通过例子来更进一步分析源码。
四、示例分析
4.1 公平锁
package com.hust.grid.leesf.abstractqueuedsynchronizer; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class MyThread extends Thread { private Lock lock; public MyThread(String name, Lock lock) { super(name); this.lock = lock; } public void run () { lock.lock(); try { System.out.println(Thread.currentThread() + " running"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } } public class AbstractQueuedSynchonizerDemo { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(true); MyThread t1 = new MyThread("t1", lock); MyThread t2 = new MyThread("t2", lock); MyThread t3 = new MyThread("t3", lock); t1.start(); t2.start(); t3.start(); } }
运行结果(某一次):
Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running
说明:该示例使用的是公平策略,由结果可知,可能会存在如下一种时序。
说明:首先,t1线程的lock操作 -> t2线程的lock操作 -> t3线程的lock操作 -> t1线程的unlock操作 -> t2线程的unlock操作 -> t3线程的unlock操作。根据这个时序图来进一步分析源码的工作流程。
① t1线程执行lock.lock,下图给出了方法调用中的主要方法。
说明:由调用流程可知,t1线程成功获取了资源,可以继续执行。
② t2线程执行lock.lock,下图给出了方法调用中的主要方法。
说明:由上图可知,最后的结果是t2线程会被禁止,因为调用了LockSupport.park。
③ t3线程执行lock.lock,下图给出了方法调用中的主要方法。
说明:由上图可知,最后的结果是t3线程会被禁止,因为调用了LockSupport.park。
④ t1线程调用了lock.unlock,下图给出了方法调用中的主要方法。
说明:如上图所示,最后,head的状态会变为0,t2线程会被unpark,即t2线程可以继续运行。此时t3线程还是被禁止。
⑤ t2获得cpu资源,继续运行,由于t2之前被park了,现在需要恢复之前的状态,下图给出了方法调用中的主要方法。
说明:在setHead函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued返回之前,sync queue就只有两个结点了。
⑥ t2执行lock.unlock,下图给出了方法调用中的主要方法。
说明:由上图可知,最终unpark t3线程,让t3线程可以继续运行。
⑦ t3线程获取cpu资源,恢复之前的状态,继续运行。
说明:最终达到的状态是sync queue中只剩下了一个结点,并且该节点除了状态为0外,其余均为null。
⑧ t3执行lock.unlock,下图给出了方法调用中的主要方法。
说明:最后的状态和之前的状态是一样的,队列中有一个空节点,头结点为尾节点均指向它。
使用公平策略和Condition的情况可以参考上一篇关于AQS的源码示例分析部分,不再累赘。
五、总结
再掌握了AQS后,再来分析ReentrantLock的源码,就会非常简单,因为ReentrantLock的绝大部分操作都是基于AQS类的。所以,进行分析时要找准方向,就会事半功倍。谢谢各位园友观看~