主要介绍ReentrantLock两种锁的加锁和释放锁的具体过程。
java.util.concurrent.lock.ReentrantLock,Java SE 5.0引入
使用简单示例
代码块
Plain Text
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
1.1 公平锁 非公平锁
代码块
Plain Text
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
首先Java中的ReentrantLock 默认的lock()方法采用的是非公平锁。
也就是不用考虑其他在排队的线程的感受,lock()的时候直接询问是否可以获取锁,而不用在队尾排队。
1.1.1 公平锁
下面分析下公平锁的具体实现。
重点关注java.util.concurrent.locks.AbstractQueuedSynchronizer类
几乎所有locks包下的工具类锁都包含了该类的static子类,足以可见这个类在java并发锁工具类当中的地位。
这个类提供了对操作系统层面线程操作方法的封装调用,可以帮助并发设计者设计出很多优秀的API
ReentrantLock当中的lock()方法,是通过static 内部类sync来进行锁操作
public void lock()
{
sync.lock();
}
//定义成final型的成员变量,在构造方法中进行初始化
private final Sync sync;
lock过程
调用到了AQS的acquire方法:
图片加载失败,点击重试
从方法名字上看语义是,尝试获取锁,获取不到则创建一个waiter(当前线程)后放到队列中。
先看下tryAcquire方法:
图片加载失败,点击重试
留空了
看下FairSync的tryAcquire方法:
图片加载失败,点击重试
上面在else中直接setState()而不需要CAS
CAS
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,
unsafe对象是通过compareAndSwapInt方法实现的CAS操作
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
CAS操作有3个操作数,内存值M,预期值E,新值U,如果M==E,则将内存值修改为U,否则啥都不做。
getState方法是AQS的方法,因为在AQS里面有个叫state的标志位 :
图片加载失败,点击重试
回到tryAcquire方法:
代码块
Plain Text
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState(); //获取父类AQS中的标志位
if (c == 0) {
if (!hasQueuedPredecessors() &&
//如果队列中没有其他线程 说明没有线程正在占有锁!
compareAndSetState(0, acquires)) {
//修改一下状态位,注意:这里的acquires是在lock的时候传递来的,从上面的图中可以知道,这个值是写死的1
setExclusiveOwnerThread(current);
//如果通过CAS操作将状态为更新成功则代表当前线程获取锁,因此,将当前线程设置到AQS的一个变量中,说明这个线程拿走了锁。
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果不为0 意味着,锁已经被拿走了,但是,因为ReentrantLock是重入锁,
//是可以重复lock,unlock的,只要成对出现行。一次。这里还要再判断一次 获取锁的线程是不是当前请求锁的线程。
int nextc = c + acquires;//如果是的,累加在state字段上就可以了。
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
到此,如果如果获取锁,tryAcquire返回true,反之,返回false,回到AQS的acquire方法。
如果没有获取到锁,按照我们的描述,应该将当前线程放到队列中去,只不过,在放之前,需要做些包装。
先看addWaiter方法:
图片加载失败,点击重试
用当前线程去构造一个Node对象,mode是一个表示Node类型的字段,仅仅表示这个节点是独占的,还是共享的,或者说,AQS的这个队列(CLH队列 Craig, Landin, and Hagersten)中,哪些节点是独占的,哪些是共享的。
这里lock调用的是AQS独占的API,当然,可以写死是独占状态的节点。
创建好节点后,将节点加入到队列尾部,此处,在队列不为空的时候,先尝试通过cas方式修改尾节点为最新的节点,如果修改失败,意味着有并发,这个时候才会进入enq中死循环,“自旋”方式修改。
将线程的节点接入到队里中后,当然还需要做一件事:将当前线程挂起!这个事,由acquireQueued来做。
在解释acquireQueued之前,我们需要先看下AQS中队列的内存结构,我们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。
而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,代表有一个线程已经获取锁,事实上,队列的第一个节点就是代表持有锁的节点):
图片加载失败,点击重试
黄色节点为队列默认的头节点,每次有线程竞争失败,进入队列后其实都是插入到队列的尾节点(tail后面)后面。这个从enq方法可以看出来,上文中有提到enq方法为将节点插入队列的方法:
图片加载失败,点击重试
再回来看看
代码块
Plain Text
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//如果当前的节点是head说明他是队列中第一个“有效的”节点,因此尝试获取,上文中有提到这个类是交给子类去扩展的。
setHead(node);//成功后,将上图中的黄色节点移除,Node1变成头节点。
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
//否则,检查前一个节点的状态位,看当前获取锁失败的线程是否需要挂起。
parkAndCheckInterrupt())
//如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。直到被唤醒。
interrupted = true;
}
} finally {
if (failed) //如果有异常
cancelAcquire(node);// 取消请求,对应到队列操作,就是将当前节点从队列中移除。
}
}
这块代码有几点需要说明:
1. Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢?
原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点代表了一个线程的状态,有的线程可能“等不及”获取锁了,需要放弃竞争,退出队列,有的线程在等待一些条件满足,满足后才恢复执行等等,总之,各个线程有各个线程的状态,但总需要一个变量来描述它,这个变量就叫waitStatus,它有四种状态:
图片加载失败,点击重试
分别表示:
-
节点取消
-
节点等待触发
-
节点等待条件
-
节点状态需要向后传播。
只有当前节点的前一个节点为SIGNAL时,当前节点才能被挂起。
2. 对线程的挂起及唤醒操作是通过使用UNSAFE类调用JNI方法实现的。当然,还提供了挂起指定时间后唤醒的API,在后面我们会讲到。
到此为止,一个线程对于锁的一次竞争才告于段落,结果有两种,要么成功获取到锁(不用进入到AQS队列中),要么,获取失败,被挂起,等待下次唤醒后继续循环尝试获取锁,值得注意的是,AQS的队列为FIFO队列,所以,每次被CPU假唤醒,且当前线程不是出在头节点的位置,也是会被挂起的。AQS通过这样的方式,实现了竞争的排队策略。
unlock过程
图片加载失败,点击重试
图片加载失败,点击重试
unlock方法调用了AQS的release方法,同样传入了参数1,和获取锁的相应对应,获取一个锁,标示为+1,释放一个锁,标志位-1。
同样,release为空方法,子类自己实现逻辑:
代码块
Plain Text
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常。
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//因为是重入的关系,不是每次释放锁c都等于0,直到最后一次释放锁时,才通知AQS不需要再记录哪个线程正在获取锁。
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁,成功后,找到AQS的头节点,并唤醒它即可:
图片加载失败,点击重试
值得注意的是,寻找的顺序是从队列尾部开始往前去找的最前面的一个waitStatus小于0的节点。
流程
1、调用AQS的release()
2、调用Sync的tryRelease()尝试释放锁
3、AQS的state减去参数,如果没有重入state为0,将当前锁占有线程置为null,返回成功
4、找到CLH的头结点,现在头结点next节点,不为空且waitStatus不大于0就唤起它,否则就从队列尾部往前找到一个waitStatus<0的
到此,ReentrantLock的lock和unlock方法已经基本解析完毕了。
1.1.2 非公平锁
介绍非公平锁
代码块
Plain Text
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
非公平锁NonfairSync的lock()与公平锁的lock()在获取锁的流程上是一直的,但是由于它是非公平的,所以获取锁机制还是有点不同。通过前面我们了解到公平锁在获取锁时采用的是公平策略(CLH队列),而非公平锁则采用非公平策略它无视等待队列,直接尝试获取。如下:
lock()通过compareAndSetState尝试设置所状态,若成功直接将锁的拥有者设置为当前线程(简单粗暴),否则调用acquire()尝试获取锁;
代码块
Plain Text
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在非公平锁中acquire()的实现和公平锁一模一样,但是他们尝试获取锁的机制不同(也就是tryAcquire()的实现不同)。
tryAcquire内部调用Sync的nonfairyTryAcquire:
代码块
Plain Text
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
与公平锁相比,非公平锁的不同之处就体现在if(c==0)的条件代码块中:
代码块
Plain Text
//----------------非公平锁-----
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//----------------公平锁-----
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
是否已经发现了不同之处。公平锁中要通过hasQueuedPredecessors()来判断该线程是否位于CLH队列中头部,是则获取锁;而非公平锁则不管你在哪个位置都直接获取锁。
AQS(AbstractQueuedSynchronizer):为Java中管理锁的抽象类。该类为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。该类提供了一个非常重要的机制,在JDK API中是这样描述的:为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。子类可以维护其他状态字段,但只是为了获得同步而只追踪使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法来操作以原子方式更新的 int 值。 这么长的话用一句话概括就是:维护锁的当前状态和线程等待列表。
CLH (Craig, Landin, and Hagersten):AQS中“等待锁”的线程队列。我们知道在多线程环境中我们为了保护资源的安全性常使用锁将其保护起来,同一时刻只能有一个线程能够访问,其余线程则需要等待,CLH就是管理这些等待锁的队列。
CAS(compare and swap):比较并交换函数,它是原子操作函数,也就是说所有通过CAS操作的数据都是以原子方式进行的。
JUC (Java Util Concurrency)
JNI (Java Native Interface)
参考: