基于ReentrantLock详解AQS源码

文章目录

  • 一、公平锁实现FairSync:
    • 加锁:
    • 释放锁:
  • 二、非公平锁实现NonfairSync:
  • 三、图解案例:

  • AQS的全称是AbstractQueuedSynchronizer,它的定位是为Java中几乎所有的锁和同步器提供一个基础框架。
  • AQS是基于FIFO的队列实现的,并且内部维护了一个状态变量state,通过原子更新这个状态变量state即可以实现加锁解锁操作。

AQS成员变量:

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

    protected AbstractQueuedSynchronizer() { }
    
    static final class Node {
        // 共享模式
        static final Node SHARED = new Node();
        // 独占模式
        static final Node EXCLUSIVE = null;

        // 标识线程已经取消
        static final int CANCELLED =  1;
        // 标识线程需要去unparking它的后继节点
        static final int SIGNAL    = -1;
        // 标识线程等待在一个条件上
        static final int CONDITION = -2;
        // 标识后面的共享锁需要无条件的传播(共享锁需要连续唤醒读的线程)
        static final int PROPAGATE = -3;

        // 当前节点保存的线程对应的等待状态(node状态可选值:0,SIGNAL,CANCELLED,CONDITION,PROPAGATE)
        // waitStatus == 0 默认状态
        // waitStatus > 0 取消状态
        // waitStatus == -1 表示当前node如果是head节点时,释放锁之后需要唤醒它的后继节点
        volatile int waitStatus;

        // 指向前驱节点
        volatile Node prev;
        // 指向后继节点
        volatile Node next;

        // 封装当前Node的线程,排队的线程
        volatile Thread thread;

        // 下一个等待在条件上的节点(Condition锁时使用)
        Node nextWaiter;

        // 当前节点是否是共享模式
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        // 返回前驱节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

    // 队列的头节点: 任何时刻,头结点对应的线程就是当前持锁线程~
    private transient volatile Node head;

    // 队列的尾节点:(阻塞队列不包含头结点head,是从head.next 开始,到 tail 结束,这个区间是阻塞队列)~
    private transient volatile Node tail;

    // 控制加锁解锁的状态变量
    // 独占模式下:0 表示未加锁状态, >0 表示已加锁状态
    private volatile int state;
    
    // 独占模式下:表示当前持有锁的线程~
    private transient Thread exclusiveOwnerThread;
}

一、公平锁实现FairSync:

加锁:

ReentrantLock lock = new ReentrantLock();
lock.lock();
final void lock() {
    acquire(1);
}
public final void acquire(int arg) {
    //条件1:线程去尝试获取锁,获取成功则返回true,获取失败则返回false
    //条件2:
    // 	条件2.1:addWaiter方法,将当前线程封装成node入队,并且返回当前入队的节点
    // 	条件2.2:入队后调用 acquireQueued方法 (该方法包含挂起当前线程、以及线程唤醒后相关的逻辑)
    // 		   (令当前线程不断去竞争资源,直到成功获取锁才停止自旋)
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
        // 中断当前线程
        selfInterrupt();
    }
}
// 尝试获取锁
protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取竞态变量的值
    int c = getState();
    // c==0,表示AQS处于无锁状态
    if (c == 0) {
        // 条件1:hasQueuedPredecessors(),判断队列中是否有等待的线程,这里是公平锁才要判断,如果是非公平锁没有这段逻辑
        // true:有等待的线程
        // false:没有等待的线程
        
        // 条件2:表示当前队列没有等待的线程,当前线程进行cas尝试获取锁,如果case设置state=1成功,说明获取到锁
        
        // 公平锁这里尝试获取锁,仅有队列为空或者队列中第一个节点是自己才进行锁的争抢,否则就要排队了
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 条件1和条件2成立,说明当前线程获取到了锁,设置当前线程为锁的持有者
            setExclusiveOwnerThread(current);
            // 返回true,说明获取到了锁
            return true;
        }
    }
    // getExclusiveOwnerThread:返回当前持有锁的线程
    // 当前线程是持有锁的线程,说明可重入了
    else if (current == getExclusiveOwnerThread()) {
        // c + acquires:重入次数+1
        int nextc = c + acquires;
        // 防止无限可重入导致nextc超过int的最大值,所以抛出异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置state值
        setState(nextc);
        // 返回true:表示当前线程获取到了锁
        return true;
    }
    // 返回false,说明当前线程没有获取到锁
    return false;
}
// 判断队列中是否有等待的线程
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    // 条件1:如果h==t:表示队列为空,返回false,没有线程等待
    // 条件2:如果h.next != null && s.thead == Thread.currentThread():表示队列不为空,当前线程是队列中等待的第一个节点
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
// 将当前节点加入等待队列
private Node addWaiter(Node mode) {
    // 创建一个Node节点,传入当前线程和模式,这里是独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // 获取尾节点
    Node pred = tail;
    // 如果尾节点不为空,说明队列已经存在节点,则尝试将当前节点链接到队尾,链接成功则返回当前节点
    if (pred != null) {
        // 设置当前节点的前驱为尾节点
        node.prev = pred;
        // cas设置新的尾节点为node
        if (compareAndSetTail(pred, node)) {
            // 如果设置新的尾节点成功,则将旧的尾节点的后继指向新的尾节点,形成双向链表,此时当前节点加入队列成功
            pred.next = node;
            // 返回当前节点
            return node;
        }
    }
    // 执行到这里有以下2种情况:
    // 1.tail == null 当前队列是空队列
    // 2.cas设置当前newNode 为 tail 时失败了,被其他线程抢先一步了
    // 自旋入队,只有入队成功才结束自旋:
    enq(node);
    return node;
}
// 自旋入等待队列
private Node enq(final Node node) {
    for (;;) { //自旋
        // 获取尾节点
        Node t = tail;
        // 如果尾节点为空,说明当前队列是空的
        if (t == null) { // Must initialize
            // cas初始化head节点,为一个空节点
            if (compareAndSetHead(new Node())){
             	// 初始化成功,则头节点和尾节点相等
                tail = head;   
            }
        } else { // 尾节点不为空,说明队列中有线程在等待
            // 设置当前节点的前驱为尾节点
            node.prev = t;
            // cas设置新的尾节点为node
            if (compareAndSetTail(t, node)) {
                // 如果设置新的尾节点成功,则将旧的尾节点的后继指向新的尾节点,形成双向链表,此时当前节点加入队列成功
                t.next = node;
                // 返回当前节点
                return t;
            }
        }
    }
}
// 抢占锁+挂起线程+线程唤醒之后执行的地方
final boolean acquireQueued(final Node node, int arg) {
    // false:表示当前线程抢占锁没有发生异常
    // true:表示当前线程抢占锁发生异常,需要将当前节点出队
    boolean failed = true;
    try {
        // 当前线程是否被中断唤醒的,因为执行到parkAndCheckInterrupt时会挂起线程,之后会被别的线程唤醒,做一个标记
        boolean interrupted = false;
        // 自旋进行锁抢占
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 条件1:p == head,即当前节点的前驱节点是否是头节点
            // true: 说明当前节点是头节点的第一个后继节点,有资格去争抢锁
            // false: 说明当前节点不是头节点的第一个后继节点,还是老老实实排队吧,等有资格再说
         	
            // 条件2:tryAcquire(arg),尝试去获取锁
         
            if (p == head && tryAcquire(arg)) {
                // 抢到锁了,则将head指向下一个节点,并把oldHead置空
                // node为头节点,清空节点的内容为一个空节点
                setHead(node);
                // 将旧的头节点的后继设置为null
                p.next = null; // help GC
                // 当前线程抢占锁成功,没有发生异常
                failed = false;
                // 当前线程没有被中断 false
                return interrupted;
            }
            // shouldParkAfterFailedAcquire:获取锁失败,需要被挂起  
            // 第一次进入方法,一般不会挂起,会设置pred.waitStatus=-1,自旋第二次进入就会被挂起
            // true:需要被挂起
            // false: 不需要挂起
            
            // parkAndCheckInterrupt:挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()){
                // 设置中断状态 true,表示当前线程是被中断信号唤醒的
                interrupted = true;
            }
        }
    } finally {
        // 抢占锁出现异常,需要将当前节点从队列中移除
        if (failed){
         	cancelAcquire(node);   
        }
    }
}
// 设置头节点,并且置空头节点的线程和前置指针
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
// 判断当前线程是否需要被挂起 true:需要挂起  false:不需要挂起
// pred:前驱节点
// node:当前线程节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // ws==-1:前驱节点释放锁之后回去唤醒下一个节点
    if (ws == Node.SIGNAL)
        // 挂起当前线程
        return true;
    // ws>0:表示前驱节点处于取消状态
    if (ws > 0) {
        // 链表从后往前走,跳过已经取消的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 直到遇到ws==0:表示默认状态,然后把pred的后驱指向当前node
        // 本次调用,node不需要被挂起,下次重试进来就会被挂起
        pred.next = node;
    } else {
        // 此时ws一定是0,但是不立即挂起它
        // cas将pred.waitStatus设置为-1
        // 下次重试进来时当前线程会被挂起
        
        // 将当前node节点的前一个节点的waitStatus设置为-1,表示前一个节点释放锁的时候会唤醒当前节点
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 不需要被挂起
    return false;
}
// 挂起当前线程,并返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
// 将node从队列中取消掉
private void cancelAcquire(Node node) {
    // 当前node为空,则忽略
    if (node == null) return;
	
    // 当前node线程置空
    node.thread = null;

    // 从后往前找,跳过已经取消的Node节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext:表示不是取消节点的下一个节点,即指向第一个取消的节点
    Node predNext = pred.next;

    // 设置当前节点为已取消状态
    node.waitStatus = Node.CANCELLED;

    // 如果取消的是最后一个节点,则将尾节点cas设置为最后一个未取消的节点,跳过已取消的节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // cas把pred的next节点设置为null
        compareAndSetNext(pred, predNext, null);
    } else {
        // 走到else代表当前节点不是tail节点,或者是cas操作的时候tail发生了变化
        // 如果不是tail节点,不能直接把tail节点指向到上面while循环得出的prev节点
        int ws;
        // 这里是的if代码块,是为了尝试一次,如果不成功再去复杂的处理。
        // 这里的if判断条件如下:
            // 1.如果上面while循环得到的pred节点不是head节点
            // 2.如果上面while循环得到的pred节点为-1,如果不为-1,cas改变成-1也。
            // 3.如果上面while循环得到的pred节点的线程指向不为null(如果为null代表在取消的过程中)
        // 因为&&是拼接,所以上面任意一个条件为false就会进入到else条件中。
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            
            Node next = node.next;
            // 当前节点的下一个节点如果不为空,且下一个节点的状态为-1,说明node是一个中间节点,需要将node后面的节点链接起来
            if (next != null && next.waitStatus <= 0)
                // cas将pred.next与next连接起来
                compareAndSetNext(pred, predNext, next);
        } else {
            // 直接唤醒当前节点的下一个节点。
            // 唤醒的目的是为了去执行shouldParkAfterFailedAcquire方法去处理取消节点。
            unparkSuccessor(node);
        }
		// 把当前节点的下一个节点指向自己
        node.next = node; // help GC
    }
}
// 唤醒当前节点的下一个节点,node为pred前驱节点
private void unparkSuccessor(Node node) {
    // 将node.waitStatus设置为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //s为node节点的下一个节点
    Node s = node.next;
    // 下一个节点为空或者下一个节点已经取消
    // 条件1:s == null
    // s 什么时候为null?
    // 1.当前节点就是tail节点时,s==null
    // 2.当前节点入队未完成时(1.设置新节点的prev指向pred  2.CAS设置新节点为tail  3.(未完成)pred.next -> 新节点)
    // 需要找到可以被唤醒的节点...
    
    // 条件2:s.waitStatus > 0 前提是 s == null
    // 如果条件2成立,则说明当前node节点的后继节点是取消状态,需要找一个合适的可以被唤醒的节点...
    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;
    }
    // 如果s不为空,说明找到了一个可以唤醒的节点,则进行唤醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

释放锁:

lock.unlock();
public void unlock() {
     sync.release(1);
}
public final boolean release(int arg) {
    // 尝试释放锁,true:释放锁成功  false:释放锁失败
    if (tryRelease(arg)) {
        // 判断头节点是否是正常节点,如果head是正常节点则唤醒头节点的下一个节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒操作
            unparkSuccessor(h);
        return true;
    }
    return false;
}
// 尝试释放锁
protected final boolean tryRelease(int releases) {
    // 获取state-1
    int c = getState() - releases;
    // 不是当前线程释放锁,则报错
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 如果c==0,说明是可重入最后一次释放锁,可以释放锁
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 设置state=0,说明AQS属于无锁状态
    setState(c);
    return free;
}

至此,ReentrantLock的公平锁实现方式源码已经注释完毕!


二、非公平锁实现NonfairSync:

公平锁的实现,在加锁时,只要队列中有等待的线程,就需要排队进行等待,很公平。

对于非公平锁,在加锁的时候会先执行cas抢占锁的逻辑,此时线程会跟头节点的下一个节点进行抢占锁,体现非公平的表现。

final void lock() {
    // 非公平锁加锁直接先cas抢一波,抢不到再进行入队操作
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
public final void acquire(int arg) {
    // tryAcquire尝试获取锁,非公平锁和公平锁的实现不一样
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// 非公平锁
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;
}

// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 差别在于有无hasQueuedPredecessors方法,这是判断队列中是否有等待的线程
// 公平锁需要判断,进行有序排队,而非公平锁不需要排队,直接先抢占一波,抢占不到再入队

三、图解案例:

下面用一幅图来描述三个线程加锁的流程,线程一加锁成功,线程二,三加锁失败挂起入队,然后线程一释放锁,唤醒线程二,然后线程二释放锁,唤醒线程三:

1.线程一抢到锁成功,设置Sate=1,并且设置持有锁线程为线程一
基于ReentrantLock详解AQS源码_第1张图片
2.线程二加锁失败,由于头节点不存在,所以线程二在入队时会顺便创建头节点,然后再链接到头节点后边,并且设置前一个节点的waitStatus=-1,表示前一个节点释放锁后会唤醒后一个节点,然后挂起线程二:
基于ReentrantLock详解AQS源码_第2张图片
3.线程三加锁失败,则入队挂起:
基于ReentrantLock详解AQS源码_第3张图片
4.线程一释放锁,设置头节点的waitStatus=0,然后唤醒下一个节点进行抢占锁,如果线程二抢到了锁:
基于ReentrantLock详解AQS源码_第4张图片
接着将线程二的节点作为头节点,head指针指向这个节点,并且断开原来的头节点:
基于ReentrantLock详解AQS源码_第5张图片
5.线程二释放锁,唤醒下一个节点进行抢占锁,线程三抢占到了锁,断开原来头节点,重新设置新的头节点:
基于ReentrantLock详解AQS源码_第6张图片6.线程三释放锁,把AQS中的state设置为0,清空持有锁线程,队列中的节点还是存在一个头节点,这个节点的内容都是空的,只要头节点创建出来了就一直存在,持有锁的线程会关联头节点。

7.上述流程可以说是公平锁的抢占方式,每次都是队列头节点的下一个节点抢到锁,严格排队抢锁,如果是非公平锁,则在唤醒head的下一个节点进行抢锁时,其他线程也可以在此期间抢锁,如果被其他线程抢到锁,则head的下一个节点还是会重新挂起,然后新的抢占锁的线程会重新关联head节点。


参考:

https://zhuanlan.zhihu.com/p/463668014

https://csp1999.blog.csdn.net/article/details/116604866

https://xiaohuang.blog.csdn.net/article/details/130046506

你可能感兴趣的:(并发,java)