AQS之ReentrantLock

6 篇文章 0 订阅
2 篇文章 0 订阅

目录

AbstractQueuedSynchronizer

ReentrantLock

lock

unlock

公平锁和非公平锁的区别

总结


AQS是AbstractQueuedSynchronizer的简称,它是java并发包很重要的一个工具类,像比较常见的ReentrantLock、CountDownLatch等都是在AQS的基础上建立的。本文将从ReentrantLock的源码开始分析AbstractQueuedSynchronizer和ReentrantLock的工作原理。

AbstractQueuedSynchronizer

我们先来看下AbstractQueuedSynchronizer这个类里面都有些啥东西

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

    static final class Node {
        /** 标记节点处于共享模式 */
        static final Node SHARED = new Node();
        /** 标记节点处于独占模式 */
        static final Node EXCLUSIVE = null;

        /** waitStatus 为1时,表明线程取消了获取锁 */
        static final int CANCELLED =  1;
        /** waitStatus 为-1时,表明当前节点的下一个节点对应的线程需要被唤醒 */
        static final int SIGNAL    = -1;
        /** waitStatus 为-2时在condition的时候才使用,表名节点在等待某种条件*/
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        //取值为上面的1,-1,-2,-3和0
        volatile int waitStatus;

        //前一个节点
        volatile Node prev;

        //下一个节点
        volatile Node next;

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

        //condition条件队列中的下一个节点
        Node nextWaiter;
        ......
    }

     // 链表头节点,头节点属于当前持有锁的那个线程
    private transient volatile Node head;

    //链表尾节点,每个新的节点过来,都添加到链表的最后,成为尾节点
    private transient volatile Node tail;

    //关键性属性,0代表没有线程持有锁,大于0代表已经有线程持有了当前锁
    //谁能把这个值修改为1,谁就算是持有了锁。
    //当然锁重入时,这个值会加1,也就是说这个值可以大于1
    private volatile int state;
    // 继承自父类,表示当前持有锁的那个线程
    private transient Thread exclusiveOwnerThread;
    ......
}

AbstractQueuedSynchronizer类中的字段属性可以看出AQS队列的结构图如下:

AQS有四个重要的属性:head、tail、state、exclusiveOwnerThread。其本质上是一个双向链表,从head头节点到tail尾节点,链表中每个节点都是一个Node对象,每个Node对象有四个比较重要的属性:prev、next、waitstatus、thread。每个线程来了之后new node()添加到链表的最后面。

ReentrantLock

先来看一下ReentrantLock的构造方法

    public ReentrantLock() {
        // 默认无参构造,将sync对象初始化为非公平锁的实现
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        // 传入参数指定使用非公平锁实现 或者 公平锁
        sync = fair ? new FairSync() : new NonfairSync();
    }
    // sync 是啥?sync类继承自AQS,ReentrantLock的加锁和释放都是通过sync来实现的
    abstract static class Sync extends AbstractQueuedSynchronizer {}

lock

我们平常在使用ReentrantLock的时候,一般都是先执行lock.lock()加锁,然后在finally中执行lock.unlock()方法解锁。

而ReentrantLock分公平锁和非公平锁。我们先来看一下公平锁的lock()方法

    static final class FairSync extends Sync {
            final void lock() {
                acquire(1);
            }
    }
    // AQS中的方法
    public final void acquire(int arg) {
        //先执行tryAcquire(1),尝试直接获取锁,如果返回成功就结束了
        //如果执行失败,acquireQueued将当前线程park挂起,进入阻塞队列排队等待
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    // 尝试获取锁,锁重入或者没有线程在等待或者是第一个节点可以成功获取到锁
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        //没有线程获取到锁的时候为0
        int c = getState();
        if (c == 0) {
            //hasQueuedPredecessors,由于是公平锁的实现,要先看看有没有线程在队列中等待,
            //没有人在前面才去获取锁
            if (!hasQueuedPredecessors() &&
                    //如果没有线程在等待,使用一次CAS尝试获取锁,获取失败说明被别的线程抢到了锁
                    compareAndSetState(0, acquires)) {
                // 如果获取到锁,将exclusiveOwnerThread设置为当前线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //如果当前线程就是持有锁的线程,说明是锁重入的场景
        else if (current == getExclusiveOwnerThread()) {
            //由于是重入锁,那么这个时候不需要考虑并发安全问题,直接对state进行+1赋值就好了
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

tryAcquire尝试获取锁成功lock方法就返回了,如果tryAcquire失败,那么就要执行acquireQueued将线程加入到队列中

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            // 先看看addWaiter(Node.EXCLUSIVE)是用来干嘛的
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    // 用node节点封装线程加入到队列中
    private Node addWaiter(Node mode) {
        // mode 传入的是Node.EXCLUSIVE,代表当前处于独占锁模式,通过构造方法传入当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // 下面的代码尝试将当前node添加到阻塞队列的最后
        Node pred = tail;
        // tail尾节点不为空,代表队列不是空的
        if (pred != null) {
            // 将tail节点设置为当前节点的 prev节点
            node.prev = pred;
            //使用一次CAS尝试将当前节点设置为尾节点(tail)
            if (compareAndSetTail(pred, node)) {
                // CAS成功,将之前尾节点的next指向当前节点,这样就构成了双向链表。然后返回
                pred.next = node;
                return node;
            }
        }
        // 如果队列为空,或者有竞争导致的CAS入队失败,那么执行enq入队操作
        enq(node);
        return node;
    }

    // 进行入队操作,有必要的话对head进行初始化
    private Node enq(final Node node) {
        //使用CAS自旋插入队列的最后面,即设置当前线程为tail
        for (;;) {
            Node t = tail;
            // 如果队列是空的
            if (t == null) { // Must initialize
                //对head进行初始化,这个时候head节点的waitStatus还是0
                if (compareAndSetHead(new Node()))
                    //head初始化好了,将tail指向head
                    tail = head;
            } else {// 将当前node添加到阻塞队列的最后
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    //只有入队成功,这个方法才会return
                    return t;
                }
            }
        }
    }

    // 再回到acquireQueued方法,此时代表当前线程的node节点已经添加到队列中,
    //接下来需要进行线程挂起,正常流程,这个方法应该返回false
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //死循环/自旋获取锁
            for (;;) {
                // 获取当前node的前一个节点
                final Node p = node.predecessor();
                //如果p == head,即前一个节点是head,说明当前节点在阻塞队列中排在第一个,
                //head通常是当前持有锁的线程,但如果head是刚刚初始化才有的,
                //那说明当前head没有不属于任何线程,那么这个时候可以尝试去获取一下锁
                //或者说head节点释放了锁,那么这个时候也是有机会直接获取到锁的
                if (p == head && tryAcquire(arg)) {
                    //将当前线程设置为head
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //成功获取到锁直接返回
                    return interrupted;
                }
                //如果当前node不是阻塞队列的第一个节点 或者尝试获取锁失败,会执行下面的逻辑
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

   //判断当前线程没有获取到锁是否需要park挂起,注意:这个方法是在循环当中的,返回false,下次循环还会进来
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取当前节点 前一个节点的状态
        int ws = pred.waitStatus;
        //如果前一个节点的状态为-1,说明前节点正常,当前线程需要被挂起
        //当前线程的唤醒依赖于前节点
        if (ws == Node.SIGNAL)
           
            return true;
        if (ws > 0) {
            //如果前一个节点的状态大于0,说明前一个节点取消了排队
            //这个时候往前遍历,直到找到一个正常的前节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//如果前节点的状态不是1和-1,那么状态可能为-2,-3,0
            //正常情况下,每个线程进来new node,它的状态都是0,
            //这个时候需要把它前一个节点状态设置为SIGNAL(-1),这样才可能从第一个分支返回
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

通过shouldParkAfterFailedAcquire来判断当前线程是否需要被挂起,如果返回false说明当前线程不需要被挂起,下次循环还会再次判断。只有在前一个节点状态正常的情况下,才会返回true,代表当前线程需要被挂起。

当前线程的唤醒操作是由它的前节点来完成的,当前线程挂起后,需要等待它的前节点来将它唤醒。继续回到前面的判断条件

    // 继续回到前面的这个判断条件 
    if (shouldParkAfterFailedAcquire(p, node) &&
        //如果当前线程需要被挂起,执行parkAndCheckInterrupt()
        parkAndCheckInterrupt())
        interrupted = true;

    //使用LockSupport.park挂起当前线程
    private final boolean parkAndCheckInterrupt() {
        //当前线程挂起后,就暂停在这里额,等待被唤醒
        LockSupport.park(this);
        return Thread.interrupted();
    }

到这里,没有获取到锁的线程就挂在这里了,等待被唤醒。接下来看一下它的unlock方法

unlock

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

    public final boolean release(int arg) {
        //执行的是tryRelease(1)
        if (tryRelease(arg)) {
            Node h = head;
            //如果头节点不为空并且头节点状态不为0
            if (h != null && h.waitStatus != 0)
                //这个方法会唤醒head节点的下一个节点,也就是当前节点的下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    //释放锁操作
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        //如果当前线程没有持有锁,抛出异常。释放锁的前提是持有锁
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //如果c==0,说明锁已经完全释放了,否则还不能释放锁。
        //因为可重入锁场景下,每次重入state都会+1。state要减到0才算是完全释放锁
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

    //这个方法就是在唤醒head的下一个节点
    private void unparkSuccessor(Node node) {
        //获得head的状态
        int ws = node.waitStatus;
        //如果head的waitStatus小于0,那么这个时候把它置为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //取出head的下一个节点
        Node s = node.next;
        //本来是要唤醒head的下一个节点,但是下一个节点可能已经取消了等待(waitStatus=1),
        //所以需要找到阻塞队列(不包括head)中waitStatus<0的第一个节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从tail开始往前遍历,直到找出waitStatus<0排在等待队列最前面的那个节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //就是在这里唤醒的线程
            LockSupport.unpark(s.thread);
    }

之前没抢到锁被挂起的线程,被唤醒后继续执行。继续尝试获取锁

    //使用LockSupport.park挂起当前线程
    private final boolean parkAndCheckInterrupt() {
        //当前线程挂起后,就暂停在这里额,等待被唤醒
        LockSupport.park(this);
        //唤醒后继续执行
        return Thread.interrupted();
    }

公平锁和非公平锁的区别

// 公平锁
static final class FairSync extends Sync {
    final void lock() {
        //公平锁这里是直接acquire
        acquire(1);
    }
    
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //公平锁在tryAquire这里需要判断队列中是否有线程在等待,有就不会竞争锁,在后面等待。                                    
            //而非公平锁不判断是否有线程等待 ,直接竞争锁
            if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        ......
    }
}

// 非公平锁
static final class NonfairSync extends Sync {
    // 非公平锁在lock方法中会先执行一次CAS尝试加锁,成功就直接返回
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    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;
            }
        }
        ......
    }
}

从代码中来看,公平锁非公平锁有两处不同,一个是在lock()方法中,一个是在tryAcquire()中。

公平锁遵循先到先得,指的是先来的线程先获得锁。我们之所以说非公平锁相对于公平锁性能更好,就是因为公平锁多了些排队等候的操作。所以非公平锁的吞吐量要比公平锁高。但是非公平锁可能导致队列中的线程迟迟获取不到资源,造成线程饥饿

总结

最后总结一下从加锁到释放锁的整个流程:

  1. 首先执行lock.lock(),加锁的操作其实就是对state进行+1,对应的解锁操作就是-1。
  2. tryAquire()获取锁成功直接就返回了,如果失败,将当前线程node放入阻塞队列的尾部,进入队列后,使用自璇的方式尝试获取锁,当然只有一个线程能够获取到锁。其他线程都通过park挂起。获取不到锁的线程都会被挂起。 等待被前一个节点唤醒
  3. 接下来持有锁的线程执行unlock释放锁的同时唤醒它的下一个节点。让它来持有锁。

所以AQS队列就是用来管理这些竞争线程的,所有的线程都在AQS链表中排好队,获取不到锁的线程通过park挂起,阻塞在那里。等待被前一个节点唤醒。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值