AQS之ReentrantLock源码解析

AQS?ReentrantLock?

  我们知道java并发编程的核心在于JUC(java.util.concurrent)包,而在JUC中的大多数同步器都是围绕一个共同的基础行为,例如等待队列、条件队列、独占获取、共享获取等。而这些行为的抽象就是基于AbstractQueuedSynchronizer(AQS)。简单来说AQS就是一个抽象了同步器公共行为的框架类。
  ReentrantLock就是基于AQS实现的一种互斥锁,与synchronized类似,但是功能要比synchronized强大,例如ReentrantLock支持公平与非公平锁等等。

代码层面AQS与ReentrantLock之间的关系

上面说了AQS的大致概念,但是AQS在代码中是如何体现的呢?

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
}

可以发现AQS在代码层面就是一个抽象类。因此我在最开始说AQS就是一个抽象了同步器公共行为的类。


而ReentrantLock与AQS之间又有什么关系呢?我们查看以下ReentrantLock的源码

public class ReentrantLock implements Lock, java.io.Serializable {
}
public interface Lock {
}

可以发现ReentrantLock实现了Lock接口,而Lock接口上面却什么都没有。但是我们说ReentrantLock是基于AQS实现的,又是怎么回事呢?接下来,我们看ReentrantLock的一个内部类:

/** Synchronizer providing all implementation mechanics */
private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {
}

static final class FairSync extends Sync {
}

static final class NonfairSync extends Sync {
}

可以发现,在ReentrantLock的内部有一个Sync的抽象内部类,这个类就继承了AQS。

接下来我们看一下这几个类的类关系图就会更加清晰的了解到AQS与ReentrantLock之间的关系
在这里插入图片描述

源码解析前置知识

1、AQS的父类
  在上面的类结构图中我们可以看到AQS这个抽象类上面是还有一个父类的,叫做AbstractOwnableSynchronizer,这个类很简单,他只有一个属性叫做exclusiveOwnerThread,以及这个属性的 set/get 方法,这个属性表示的就是当前锁是被哪一个线程所获取的。
  其源码如下:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;

    protected AbstractOwnableSynchronizer() { }

    /**
     * 表明当前锁是被这个线程所获取
     */
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

2、CLH队列
  CLH队列就是一个双向链表,每一个CLH队列的节点就是一个线程。这个节点在AQS中叫做Node,其源码中比较重要的几个属性值如下所示:

static final class Node {
        // 表示该节点是共享属性,多个线程可以同时执行,如Semaphore/CountDownLatch
        static final Node SHARED = new Node();
        // 表示该节点是独占属性,只有一个线程能执行,如ReentrantLock
        static final Node EXCLUSIVE = null;

        // 由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。需要注意的是,取消节点的线程不会再阻塞。
        static final int CANCELLED =  1;
        // 表示此节点的后续节点已经(或即将)被阻塞,因此当前节点在释放锁或者被取消后必须唤醒(unpark)后续节点
        // 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
        static final int SIGNAL    = -1;
        // 节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
        static final int CONDITION = -2;
        // 应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。
        static final int PROPAGATE = -3;
		
		// 0:以上数值均未按数字排列以简化使用。
		// 非负值表示节点不需要发出信号。所以,大多数代码不需要检查特定的值,只需要检查符号。
		
        // 该节点的信号量状态,即上面四种和0(Init,初始化状态)
        volatile int waitStatus;

        // 指向前一节点的指针
        volatile Node prev;

        // 指向下一节点的指针
        volatile Node next;

        // 当前节点表示的线程
        volatile Thread thread;

        // 表示该节点是共享还是独占,共享就是 SHARED 独占就是 EXCLUSIVE
        Node nextWaiter;

        /**
         * 获取当前节点的前置节点
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    }

    // 指向CLH双向链表队列的头节点
    private transient volatile Node head;

    // 指向CLH双向链表队列的尾部
    private transient volatile Node tail;

  使用一张图来表示CLH队列,如下:
在这里插入图片描述

注意:
由于CLH队列中存在头节点,因此为了区分这个头节点和CLH中第一个实际存储信息的节点,下文中提到的第一个节点都是指第一个实际存储信息的节点。

3、 LockSupport.park()/unpark()
  这两个方法类似于synchronized同步代码块中的wait()和notify(),都是用于阻塞和唤醒线程。但是不同的是park()/unpark()可以在程序的任意位置使用,并且需要注意的是park()对于interrupt 中断是有感应的。

4、 临界资源
  我们知道synchronized关键字的使用如下:

Object lock = new Object();
synchronized(lock){
}

  因此对于synchronized来说,各个线程所争夺的临界资源就是这个lock对象。但是对于ReentrantLock来说各个线程所争夺的临界资源又是什么呢?在AQS中有一个属性:

/**
* The synchronization state.
*/
private volatile int state;

  这个属性就是各个线程在ReentrantLock中所争夺的资源。

5、 CAS

  这个东西不知道的同学去百度一下吧,没啥说的,其实就是偷懒,hhhhh。。。。。。。


有了这些前置知识之后,我们开始进入ReentrantLock的源码解析,
希望读者在阅读文章的同时可以打开源码,看文章的同时也阅读一次源码。

首先看一段代码:

public static void main(String[] args) {
	ReentrantLock lock = new ReentrantLock(true);  // 公平锁

    try {
        lock.lock();  // 上锁

        /* 业务逻辑 */

    } finally {
        lock.unlock();  // 解锁
    }
}

接下来的公平锁分析就以这段代码为例

ReentrantLock之lock方法(公平锁)

在讲解源码之前,还是要提醒读者,要记住lock() 方法的作用:就是放行拿到锁的线程,阻塞没有拿到锁的线程!!!要时刻记住这个方法的作用,然后再去阅读源码。

进入ReentrantLock的lock方法

public void lock() {
    sync.lock();
}

进入sync.lock
可以发现这个是一个抽象类,我们进入公平锁的具体实现中:
在这里插入图片描述

final void lock() {
    acquire(1);
}

可以发现实际是调用了这个acquire(1),方法,并传入了一个1!!!
进入acquire(1) 方法
这个方法就是AQS实现的方法了,其源码如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在这个方法中有四个方法调用,我们直接进入这四个方法的源码实现:
1、tryAcquire(arg)

tryAcquire(arg):获取锁,获取到锁返回true,获取不到返回false

这个方法的具体实现是由子类实现的,我们进入他的公平锁实现:
在这里插入图片描述
源码如下(代码解释,我直接写在源码中):

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();  // 获取当前线程
        int c = getState();  // 得到临界资源state的状态
        if (c == 0) {  // 若是state为0,那就表示还没有线程获取锁
        	// hasQueuedPredecessors(): 如果当前线程前面有排队线程,则为true;如果当前线程位于队列的头部或队列为空,则为false。即判断当前线程是否可以获取锁
        	// compareAndSetState(0, acquires): cas替换state的值为1,cas成功表示获取锁成功
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                // 获取锁成功,那么设置获取锁的线程为当前线程
                setExclusiveOwnerThread(current);
                // 返回true,表示获取锁成功
                return true;
            }
        }
        // 如果当前线程就是获取了锁的线程
        else if (current == getExclusiveOwnerThread()) {
        	// 那么state += 1。这就是ReentrantLock可重入特性的体现!!!
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            // 重新设置state的值
            setState(nextc);
            // 返回true,表示获取了锁
            return true;
        }
        // 返回false,表示获取锁失败
        return false;
    }
}

以上就是ReentrantLock获取锁的逻辑。总结如下:

1、state = 0,表示没有线程获取锁。
  1.1、若是CLH队列中没有节点 或者 该线程中所表示节点就是CLH的第一个节点。就表示该线程可以尝试获取锁。那么cas替换state标记为1。cas成功就表示获取了锁,返回true
  1.2、因为这里是多线程的情况,因此当前线程进行判断的时候,可能已经线程获取到了锁,并且有线程在排队了,因此判断state=0之后,再进一步判断CLH队列的状态。若是有线程在排队了,或者cas失败,那么返回false,表示获取锁失败。

2、 state != 0 && 当前线程就是获取了锁的线程,那么直接将state += 1。即表示ReentrantLock是可重入的。


如果获取锁成功,那么整个lock方法也就结束了,该去执行同步代码块中的逻辑代码了。怎么样,是不是很简单!!!!但是如果获取锁不成功,那么就需要进行线程的阻塞了


2、addWaiter(Node.EXCLUSIVE)

addWaiter(Node.EXCLUSIVE):将线程节点node以独占方式加入到CLH这个双向链表的尾部,并返回该node

源码如下(代码解释,我直接写在源码中):

private Node addWaiter(Node mode) {
	// 以独占方式(EXCLUSIVE)创建一个节点
    Node node = new Node(Thread.currentThread(), mode);
    // 获取CLH队列的尾节点
    Node pred = tail;
	// 情况1:若是第一次执行的时候,head和tail指针都是指向null的,表示CLH双向链表还未初始化
    // 	因此不会进入这个if中,而是直接进入enq方法,进行CLH的初始化操作,然后自旋插入节点
    
    // 情况2:CLH中已经有Node了,但是当前Node进行CAS入队操作失败
    //	此时也会进入enq(node)方法中进行自旋入队

    // 若尾节点不为空,表示CLH队列中已经有节点了
    if (pred != null) {
    	// cas操作将该节点插入到CLH队列尾部,然后返回该节点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 该方法进行CLH双向链表的初始化与自旋式的插入操作
    enq(node);
    return node;
}

2.1、enq(node)

该方法进行CLH双向链表的初始化与自旋式的插入操作

private Node enq(final Node node) {
	// 死循环
    for (;;) {
        Node t = tail;
        // 如果尾节点为空,那么表示CLH队列还没有进行初始化操作
        if (t == null) { // Must initialize
        	// 直接new 一个新节点,cas将其作为头节点。
        	// 这里就说明了上文说的CLH队列是一个带头结点的双向链表。
        	// cas失败表示有其他线程抢先进行了cas,那么直接进入下一次循环(自旋)。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
        	// CLH已经进行了初始化
        	// cas将该节点插入到CLH的尾部。
        	// 同样若是cas失败就表示该位置被其他节点占用,那么自旋,直到插入成功。
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

以上就是节点插入到CLH中的逻辑。总结如下:

ReentrantLock会将线程封装为一个Node节点,然后利用cas将其插入到CLH队列的尾部。失败则自旋重试,直到成功。

3、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

acquireQueued(node, arg):传入刚才的添加到CLH的节点,修改node的前置节点的waitStatus状态,并阻塞该线程。若是外部interrupt中断唤醒线程,那么返回true,否则unpark正常唤醒返回false

源码如下(代码解释,我直接写在源码中):

final boolean acquireQueued(final Node node, int arg) {
		// 默认faile为true,即出错
        boolean failed = true;
        try {
        	// 默认中断表示为false。即未被中断
            boolean interrupted = false;
            for (;;) {
            	// 获取当前节点的前置节点
                final Node p = node.predecessor();
                // 若是前置节点就是头节点(即当前节点就是CLH中的第一个节点)
                // 并且 获取锁成功(这里进行获取锁的操作是因为在解阻塞之后需要进行一次获取锁,让解阻塞的线程获取锁)
                if (p == head && tryAcquire(arg)) {
                	// 拿到锁成功,那么重新设置CLH的头节点,即当前节点需要从CLH中移除
                    setHead(node);
                    p.next = null; // help GC
                    // 没有失败
                    failed = false;
                    // 返回中断标记。若是外部有调用中断,那么这里就是返回true
                    return interrupted;
                }
                // 若当前节点不是CLH中的第一个节点,或者说是第一个节点但是获取锁失败
				// shouldParkAfterFailedAcquire(p, node): 传入当前节点和前置节点,设置前置节点的waitState状态为SIGNAL来表示当前节点,若是出现问题,返回fasle,成功则返回true
				// parkAndCheckInterrupt(): 阻塞当前线程。若外部unpark唤醒,那么返回false。若是外部中断唤醒,那么返回true。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 进入这里说明外部有调用中断方法,那么这里设置中断标记为true。
                    // 随后再进行一次循环,再次阻塞在 parkAndCheckInterrupt() 方法中
                    interrupted = true;
            }
        } finally {
            if (failed)
            	// 有失败,执行取消逻辑
                cancelAcquire(node);
        }
    }

3.1、shouldParkAfterFailedAcquire(p, node)

该方法的作用是为了设置pred节点的waitStatus状态为SIGNAL,利用pred节点的waitStatus值表示当前节点是否为可唤醒,若当前node为第一个节点,那么会设置head节点的waitStatus状态为SIGNAL。
因此可知,CLH队列是利用pred节点的状态来标识当前node的信号量状态
第一次执行的得到的pred节点的waitStatus会为0,cas替换该状态为SIGNAL,会返回false
第二次执行该方法得到pred节点的waitStatus才会为SIGNAL,返回true

源码如下(代码解释,我直接写在源码中):

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取前置节点的waitStaus状态
    int ws = pred.waitStatus;
    // 如果前置节点为SIGNAL,表示可唤醒,那么返回true
    if (ws == Node.SIGNAL)
        return true;
    // 如果前一个结点是表示出现异常,需要取消的
    if (ws > 0) {
        // 从CLH中依次移除那些表示出现异常需要取消的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果当前节点的前一个结点的信号量状态既不是SIGNAL 也不是 >0 的
        // 那么就只可能有 -2和-3还有0
        
        // 此时将当前节点的前一个结点的信号量状态 利用cas替换成SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false,表示出现异常
    return false;
}

3.2、shouldParkAfterFailedAcquire(p, node)

使用LockSupport.park(this),阻塞当前线程

源码如下(代码解释,我直接写在源码中):

private final boolean parkAndCheckInterrupt() {
	// 阻塞当前线程
    LockSupport.park(this);
	// 阻塞唤醒有两种方式
	// 1、外部调用unpark()方法,正常唤醒,那么此方法直接返回fasle,表示不是中断唤醒
	// 2、外部调用interrupt()方法,那么就会解阻塞,此方法返回true。随后进入外部的循环,
	// 由于调用的是interrupted()这个静态方法,会重置中断标记,因此再次进入这里会再次阻塞。
	
	// 这是那个静态方法。返回中断标记位,并清空中断标记
    return Thread.interrupted();
}

以上就是阻塞线程的逻辑。总结如下:

ReentrantLock的线程阻塞是利用LockSupport的park方法。进行线程的阻塞。由于LockSupport的park方法对于中断是有感应的,因此ReentrantLcok也实现了对中断的感应。


到这里就是lock方法实现阻塞的全部逻辑了。


4、selfInterrupt()

acquireQueued(node, arg):使用当前线程调用一次interrupt方法

源码如下(代码解释,我直接写在源码中):

static void selfInterrupt() {
	// 使用当前线程调用一次interrupt方法
    Thread.currentThread().interrupt();
}

此方法很简单,就是使用当前线程调用一次interrupt方法。要这么做的原因其实很简单。首先需要知道什么情况下会进入这里。当 acquireQueued() 方法返回true的时候,也就是外部调用了中断的时候。因为在acquireQueued中使用了Thread.interrupted() 清除了中断标记,但是外部是调用了中断的,因此这里需要回显中断标记,因此需要调用一次interrupt方法。

以上就是在公平锁情况下的lock方法的源码说明,下面来说说解锁的源码,如果你已经懂了上面加锁的逻辑,那么其实解锁的源码逻辑也就不难了。

ReentrantLock之unlock方法(公平锁)

进入ReentrantLock的unlock方法

public void unlock() {
    sync.release(1);
}

进入release方法
release方法不分公平与非公平,因为解锁逻辑都是一样的。

public final boolean release(int arg) {
	// 设置state的值,尝试释放锁。
	// 返回true:表示可以唤醒下一个处于阻塞的线程
	// 返回false:表示可重入的情况,表示不需要唤醒下一个阻塞的线程
    if (tryRelease(arg)) {
    	// 如果需要唤醒下一个处于阻塞的线程
        Node h = head;
        // 如果CLH队列的头节点不为空,并且waitState不是0
        if (h != null && h.waitStatus != 0)
        	// 对第一个节点进行唤醒操作,让CLH的第一个节点进行锁的获取
        	// 传入头节点
            unparkSuccessor(h);
        // 返回释放成功
        return true;
    }
    // state的值设置失败,返回fasle,表示释放失败
    return false;
}

1.1、tryRelease(arg)

此方法进行state的值-1操作,表示释放锁资源,并返回是否唤醒下一个处于阻塞的线程

protected final boolean tryRelease(int releases) {
	// 当前state值-1
 	int c = getState() - releases;
 	// 如果锁的释放不是由锁的持有者释放的,那么抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 初始化free为false
    boolean free = false;
    // 如果state为0了,那么表示需要唤醒下一个线程了。
    if (c == 0) {
    	// 重置free为true
        free = true; 
        // 将持有锁的线程置为null
        setExclusiveOwnerThread(null);
    }
    // 重新设置state的值
    setState(c);
    return free;
}

1.2、tryRelease(arg)

此方法进行线程的解阻塞,注意传入的是头节点(因为CLH队列中是使用上一个节点的waitStatus表示下一个节点的状态)。

private void unparkSuccessor(Node node) {
	// 获取头节点的状态
    int ws = node.waitStatus;
    // 如果状态为负数,那么反正不是取消状态,cas将其替换为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 获取头结点的下一节点,即CLH中排在第一个的线程节点
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    	// 这里就是将异常节点移除
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 若其不为空
    if (s != null)
    	// 调用unpark,解阻塞。
        LockSupport.unpark(s.thread);
}

到此公平锁的加锁和解锁源码的解析完毕


总结:
1、加锁:
  外部调用lock方法,首先判断state状态,然后进行加锁逻辑。加锁成功的话,state+1。加锁失败,那么就插入到CLH队列的尾部,然后阻塞它。
2、解锁:
  外部调用unlock方法,首先state-1,若-1后state为0,那么唤醒CLH队列的第一个节点。

上文中了解了公平锁的lock和unlock逻辑之后,现在我们开始分析非公平锁的lock和unlock方法。其实非公平锁的逻辑和公平锁差不多,只是有些许不同。

ReentrantLock lock = new ReentrantLock(false);  // 非公平锁

ReentrantLock之lock方法(非公平锁)

final void lock() {
	// 非公平锁在进入lock之后,立马执行一次cas修改state状态,抢资源
    if (compareAndSetState(0, 1))
    	// cas成功,那么修改拥有锁的线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // 这个方法和公平锁调用的是一样的
}

1、 acquire(1)

这个方法和公平锁调用的是一样的,只是tryAcquire 方法的实现是调用非公平锁,剩下几个方法的实现和公平锁是一样的

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

在这里插入图片描述
2、tryAcquire(arg)

进入tryAcquire的非公平锁实现。

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

2、nonfairTryAcquire(acquires)

其实这个方法的逻辑和公平锁的tryAcquire 方法差不多。

final boolean nonfairTryAcquire(int acquires) {
 final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 非公平锁获取锁的条件,不需要进行CLH队列元素的判断,而是直接尝试获取
    	// 这就是非公平锁和公平锁获取锁的区别
        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;
}

这里给大家展示该方法与公平锁的tryAcquire 方法区别
在这里插入图片描述

总结:
非公平锁获取锁的条件,不需要进行CLH队列元素的判断,而是直接尝试获取,如果在这里没有获取到锁,那么后面还是需要乖乖的进入CLH队列,等待被唤醒。
因此不公平锁的不公平性在于进入lock方法,那么不管你CLH队列中是否有排队线程,我先进行锁的获取,因此就可能插队成功。但是如果没有插队成功,那么还是需要进入CLH队列排队。
但是我觉得吧,这样的话ReentrantLock的非公平性只是一种半非公平,因为我觉得既然是非公平,那么就应该让所有的线程去抢锁(个人观点,可能这样实现过于复杂吧,小小吐槽)。

ReentrantLock之unlock方法(非公平锁)

  非公平锁的释放锁逻辑和公平锁是一样的,调用的是一个方法,这里不再赘述。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值