详细分析如何利用AQS实现独占锁的获取与释放
独占式可重入非公平锁的获取
独占锁默认就是非公平锁,我们要想了解独占锁的获取是否,首先必须知道同步队列为一个有头尾节点的双向链表
1.通过new ReentrantLock().lock();
进入lock的的代码实现区
public void lock() {
sync.lock();
}
......
sync = new NonfairSync();
2.再进入sync.lock()查看具体的lock()上锁的实现
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
3.首先就进行了一次CAS比较交换操作,尝试获取锁。我们进入AQS查看CAS的实现机制
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
4.通过底层的本地方式进行比较交换如果比较交换成功,返回true则直接进入AQS将当前锁的持有线程改为当前请求的线程。也就是完成了请求锁的这个线程拿到了锁。
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
5.如果CAS比较交换失败返回false则进入AQS提供的acquire(1)尝试再次获取锁。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
6.首先进入被ReentrantLock中的sync重写的tryAcquire操作。尝试再一次的获取锁。因为lock时可能有多个线程通过CAS比较操作,但是只能有一个成功,其他的线程就是进入acquire进入tryAcquire再次尝试获取锁。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//拿到当前线程
int c = getState();//获取锁状态
if (c == 0) {//当前锁没有被任何线程所持有
if (compareAndSetState(0, acquires)) {//再次尝试比较交换来获取锁
//比较交换成功
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程状态不为0或者比较交换失败
else if (current == getExclusiveOwnerThread()) {//判断当前线程是不是锁的持有线程
//如果是则满足可重入操作,nextc计时器+1这里的nextc类似于内建锁的monitor
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//更新状态
return true;
}
return false;
}
7.重新判断锁状态尝试获取锁,判断是否满足可重入状态,则成功返回true条件就是当前锁未被获取比较交换成功,或者当前锁的持有线程是请求锁线程满足可重入则返回true。否则返回false回到acquie函数中。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//static final Node EXCLUSIVE = null;
selfInterrupt();
}
8.如果tryAcquie返回true则if条件为false无需进行acquireQueued操作,直接返回。因为已经获取到锁了嘛。如果tryAcquire返回false怎进入下一个判断acquireQueue,但是我们发现参数就是一个方法,所以要先进入addWaiter操作。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//将当前线程包装成一个节点利用Node的重载构造,由上可知mode==null
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;//获取尾节点
if (pred != null) {//如果当前有尾节点则尾插将尾节点和node节点相连接
node.prev = pred;//node前驱指向尾节点
if (compareAndSetTail(pred, node)) {//利用如下给出的方法调用本地方法完成比较尾插操作。
pred.next = node;//尾节点的next指向node节点
return node;//尾插成功,将当前包装好的线程节点返回给上一次调用
}
}
//走到这说明尾节点为null及没有尾节点或者说尾插失败。
enq(node);
return node;
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
9.如果队列有值并且尾插成功则直接返回上一层。否则进入enq方法。
private Node enq(final Node node) {
for (;;) {
//哎呦看到没死循环哦,这里的死循环就是自旋啦。
Node t = tail;
if (t == null) { // Must initialize
//走到这说明没有尾节点为null,也就是addwaiter中失败的第一个条件,也可以理解为队列为空一个值都没有则初始化队列
if (compareAndSetHead(new Node()))//同样调用的native本地方法进行的初始化队列将node直接设置为头节点。所以这里就不打开展示了。
tail = head;//因为刚初始化的队列也只有node一个节点所以尾节点也指向node
} else {
//addwaiter失败的第二个第二个条件队列不为空但是尾插失败。可能是多个线程同时请求尾插,但是肯定只有一个线程成功对吧,所以其他的线程jiu'hui就会到这里来了。死循环反复尝试尾插直到成功为止。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
10.这里enq肯定会尾插成功返回的,不成功根本不返回的。回到上一侧addwaiter直接return node。由回到上一层。这时候acquireQueued的参数node,arg就全了可以进入了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//首先设置初始状态
try {
boolean interrupted = false;
for (;;) {
//又一个死循环哦
final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了报错即可。
if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
//尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
setHead(node);//将node节点中的值清零,将这个节点设置为头节点
p.next = null; // help GC
failed = false;
return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
}
//走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
11.走到这里说明虽然把node节点尾插进入同步队列了,但是不是同步队列中第一个啊,或者说虽然是但是获取锁失败了。则到了shouldParkAfterFailedAcquire(p, node) 方法,判断该节点是否为阻塞状态。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//获取node前驱节点的状态
if (ws == Node.SIGNAL)//SIGNAL表示当前节点的后继节点为阻塞状态
return true;
if (ws > 0) {
//说明前驱节点为1,也就是这个节点已经为取消状态,可以取消了。那么就do,一直往前找直到找到一个前驱节点状态不为1,并将其设置为node的前驱节点。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//因为独占锁的同步队列中不可能出现状态值为-2,-3状态所以走到这里说明找到了一个前驱节点不为1的节点。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//这里同样调用本地方法将该节点的前驱节点状态设置为SIGNAL
}
return false;//返回false表示不需要将该节点阻塞
}
12.通过shouldParkAfterFailedAcquire(p, node)将node节点的前驱节点设置为SIGNAL状态,表示该阻塞,我们回看acquireQueued方法。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//首先设置初始状态
try {
boolean interrupted = false;
for (;;) {
//又一个死循环哦
final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了报错即可。
if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
//尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
setHead(node);//将node节点中的值清零,将这个节点设置为头节点
p.next = null; // help GC
failed = false;
return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
}
//走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
13.shouldParkAfterFailedAcquire我们如果我们是一上来碰见的前驱节点就是SIGNAL则返回的ture,则进行park操作,如果不是一上来碰上的SIGNAL而是我们设置的SIGNAL我们就继续死循环再次判断是不是前驱是不是头节点,因为如果前驱为头节点那么该节点虽然为阻塞状态也该被唤醒了。如果前驱不是头节点会再次进入shouldPark操作。这是就是一上来碰见SIGNAL了就可以进行park操作了。通过这个false避免了将同步队列中的第一个节点状态。返回true进入parkAndCheckInterrupt。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//将当前线程阻塞
//如果这里外面来了一个中断呢?可是我现在已经是阻塞了,类比sleep操作,都会有一个把中断重置为false的操作。
return Thread.interrupted();//将重置中断状态,也就是将中断状态设置为false,如果真的有中断则interrupted返回true。
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);//设置阻断器
UNSAFE.park(false, 0L);//调用本地方法来实际阻塞该线程
setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
14.设置完中断状态返回上一层,执着的尝试获取锁,直到获取到锁为止返回。自旋着一直等到是第一个线程可以获取锁了为止。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//首先设置初始状态
try {
boolean interrupted = false;
for (;;) {
//又一个死循环哦
final Node p = node.predecessor();//拿到当前节点的前驱节点,因为是带头的链表所以head就是第一个节点的前驱,所以如果p==null一定是出错了,报错进入finally块。
if (p == head && tryAcquire(arg)) {//如果p是头节点,那么说明node是第一个线程了。就可以再次尝试获取锁。
//尝试获取锁成功,那么node节点就可以从同步队列中撤销啦。
setHead(node);//将node节点中的值清零,将这个节点设置为头节点
p.next = null; // help GC
failed = false;
return interrupted;//这时候inturrupted为false也就是不需要中断,因为都第一个了都获取锁了都从同步队列中移除了。
}
//走到这说明,前驱节点不是头节点,或者尝试获取锁失败了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//进来说明在我将线程的阻塞的过程中有了一个中断信号。
interrupted = true;//将中断状态这是为true
}
} finally {
if (failed)//如果是正常return进来的failed肯定为false,只有p==null也就是出错的前提下才会进入cancelAcquire。
cancelAcquire(node);//同步队列出错删除节点处理
}
}
15.直到称为第一个节点获取到锁成功return返回上一层,如果acquireQueue返回true表示阻塞过。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();//设置中断状态,也就是自己产生一个中断
}
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}
16.selfInterrupt()的代码很简单,就是“当前线程”自己产生一个中断。但是,为什么需要这么做呢?
我们在parkAndCheckInterrupt()中将线程阻塞并且判断以下现在有没有中断状态,如果遇到了中断状态先无视毕竟我现在是阻塞状态对吧,并把线程有过中断信号这件事返回给acquireQueued。获取到了acquireQueued判断了以下返回了一个中断过的信息,于是将interrputer变量改为true就是中断过啦。并在这个线程获取到锁后将这个是否中断过的信息返回给了acquire。
虽然你获取到了锁了,还拦截了中断避免了阻塞和中断碰撞的异常报错,但是不可否认你被中断的事实啊。所以线程在acquire重新将中断设回了true。这样,我可以在获取锁后,手动继续判断中断信号。
举个应用例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test{
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
for(int i=0;i<10;i++){
Thread thread = new Thread(()->{
try {
lock.lock();
if(Thread.interrupted()==false) {
System.out.println(Thread.currentThread().getName() + "运行呢");
}
}
catch (Exception e) {
}finally {
System.out.println(Thread.currentThread().getName()+"释放锁哦");
lock.unlock();
}
});
thread.start();
thread.interrupt();
}
}
}
运行结果,运行呢这句话是不会被执行的。因为虽然lock获取锁的时候拦截了中断信号,但是最后又给设置回去了。所以外部依然可以在获取着锁的前提下选择中断线程,当然锁还是会释放的哦。
下面给出一个详细的流程图再梳理一下思路吧
独占式可重入非公平锁的释放
1.lock.unlock()首先调用lock提供的unlock方法。当然这个方法是被ReentrantLock覆写过的哦。
public void unlock() {
sync.release(1);
}
2.进入release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.首先调用ReentrantLock覆写的tryRelease方法尝试释放锁。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//获取当前锁的状态并-1,参数穿的1,这里要和lock对应当时+的多少,现在就得-多少
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();//如果当前线程不是持有锁的线程,那咋释放锁啊。抛异常啊
boolean free = false;
if (c == 0) {//因为锁的可重入性所以c不一定等于0,也就是该锁的持有线程不一定能改回null。只有当是最后一层加锁退出,c剪为0才可以将锁释放并且返回true,并将持有锁线程设置为null。
free = true;
setExclusiveOwnerThread(null);
}
//修改状态,无论是否释放锁都要这个线程成功释放了一层锁就要修改状态。
setState(c);
return free;
}
4.返回上一层,如果try返回的false表示还没有完全释放锁。所以不进入if。
public final boolean release(int arg) {
if (tryRelease(arg)) {
//进入表示该线程已经释放锁了
Node h = head;
if (h != null && h.waitStatus != 0)
//如果队列不为空,并且队列头节点的状态为-1,你可能会好奇为啥是-1,!=0不是还会有别的状态嘛。如果是取消状态,都删了,0状态表示后继没有节点,如果有节点尝试获取锁的时候已经将进程设置为-1了。
unparkSuccessor(h);
return true;
}
return false;
}
5.如果同步队列初始化了,并且还有节点需要被唤醒调用unparkSuccessor()
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获取头节点的节点状态
if (ws < 0)//如果头节点状态为-1,表示后继节点阻塞在同步队列等待被通知呢。
compareAndSetWaitStatus(node, ws, 0); //调用本地方法将头节点的状态设置为0
Node s = node.next;//获取头节点后下一个节点
if (s == null || s.waitStatus > 0) {//如果这个节点被阻塞着但是状态为1也就是要申请从同步队列中取消。那么肯定不能唤醒这个线程了。取消该线程
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)//从尾往前找,直到找到node的下一个状态不为1的线程,或者找到了null的前一个不为1的节点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)//说明至少找到了一个状态不为取消状态的节点,调用本地方法将该线程唤醒。
LockSupport.unpark(s.thread);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
同样配上我自己分析的实际执行步骤
独占式获取锁释放锁的总结
1.线程获取锁失败,将线程调用addwaiter方法封装成node节点,进行尾插入队操作。在addwaiter中,方法enq完成对同步队列的头节点初始化,以及CAS尾插失败后的重试操作。
2.入队之后,排队获取锁的核心方法aquireQueue,节点排队获取锁是一个自旋过程。当且仅当,当前节点的前驱节点为头节点,并且成功获取同步状态时,节点出队并且该节点引用的线程获取到锁。
否则不满足条件时,会不断自旋将前驱节点的状态设置为SIGNAL,而后调用LockSupport.part()将当前线程阻塞。
3.释放锁时会唤醒后继节点(后继节点不为空)
其他的独占锁类比获取释放过程
1.响应中断的获取锁
acquire方法是一个不响应中断的方法。
acquireSharedInterruptibly获取锁相应中断,原理与acquire几乎一样。唯一区别在于parkAndCheckInterrupt()返回true表示线程阻塞时被遇到了中断信号,抛出中断异常后线程退出。
2.超时等待功能获取锁
超时等待获取锁tryAcquireNanos(),该方法在三种情况下会返回,
1.在超时时间内,当前线程成功获取到锁。
2.当前线程在超时时间内被中断。
3.超时时间结束,仍然未获取到锁,线程退出,返回false;
超时获取锁逻辑于可中断获取锁基本一致,唯一区别在于获取锁失败后,增加了一个时间处理,如果当前时间超过截至时间,线程不再等待,直接退出,返回false,否则将线程阻塞,置为等待状态排队获取锁。
重入锁的理解
重入:表示能够对共享资源重复加锁,即当前线程再次获取锁时不会被阻塞。
通过在获取锁过程tryAcquire中当同步状态不为0表示同步状态已经被线程获取。判断获取锁的线程是否为当前请求锁的线程,如果是同步状态计数器+1,返回true成功获取锁。表示持有线程重复进入同步代码块。
通过释放过程先将同步状态剪为0并且判断当前申请释放的线程是否为持有锁的线程,来表示锁成功被释放。
公平锁和非公平锁对比
1.非公平锁在lock中一上来就有一次CAS。而公平锁直接就是tryAcquire
2.非公平锁在tryAcquire中一上来就又一次CAS,而公平锁则是先判断以下同步队列是否为空,为空才进行CAS操作。
同上上述操作,公平锁保证如果同步队列中有节点就尾插,每次被唤醒都是同步队列中等待时间最长的节点。从而避免了线程等待时间过长饿死的情况,抱枕了请求资源顺序的绝对性,但是需要频繁的进行同步队列变量,频繁的方法切换,性能差,效率低。
通过上述操作,非公屏锁因为多了两次CAS自旋,保证了系统更大的吞吐量,但是可能操作线程长时间不被响应出现饥饿现象。