面试准备 -- AQS 独占锁ReentrantLock详解

上篇文章简述了 AQS 的提供的几个支持,结果忘记写 AQS 内置的队列,在这里和大家说一句–抱歉,后面笔者会对博客进行重写添加。那今天,我们来学习 AQS 是如何实现独占锁功能的。

说到独占锁,我们就会想到 synchronized 关键字,既可以锁方法,也可以使用锁住某一块代码,简直就是个万能锁。当然今天我们肯定不是讲它,而是讲 AQS 提供的独占功能的类 – ReentrantLock。我们先来看看上篇文章的两个锁的对比图:

synchronizerLock
超时中断×
独占
共享×
等待队列

看完上图,我们可以知道,ReentrantLock 在 synchronizer 基础上添加了的更多的功能。现在我们来看看 ReentrantLock 是如何实现这些功能的。

先看看该类的类图:
在这里插入图片描述
我们从类图可以知道,ReentrantLock 实现了 Lock 接口,并且在内部中有三个内部类,内部类继承 AQS 类,其他暂且先忽略。现在我们先看看 Lock 接口抽象的方法:

public interface Lock {
	/**
	 * 拿锁(如果没拿到会阻塞当前线程)
	 */
    void lock();
	/**
	 * 拿可中断的锁
	 */
    void lockInterruptibly() throws InterruptedException;
	/**
	 * 尝试去拿锁(拿成功返回 true )
	 */
    boolean tryLock();
	/**
	 * 尝试去拿锁(有超时时间,超时没拿到就中断)
	 */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	/**
	 * 释放锁
	 */
    void unlock();
	/**
	 * 线程阻塞/唤醒(后面篇章讲解)
	 */
    Condition newCondition();
}

ReentrantLock 锁抽象:
ReentrantLock 中有三个内部类,其中 Sync 类是锁抽象,用于实现公平锁和非公平锁。先简单看看 Sync 内部类:

abstract static class Sync extends AbstractQueuedSynchronizer {
       
        /**
         * 抽象方法,用于区分公平锁和非公平锁具体实现
         */
        abstract void lock();

        /**
         * 
         */
        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 tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
        
        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
        /**
		* 阻塞/唤醒,后续篇章介绍
		*/ 
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
        /**
		* 判断锁状态,0-为未上锁,1-被占用
		*/ 
        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }
		/**
		* 判断锁状态,0-为未上锁,1-被占用
		*/ 
        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }
		/**
		* 判断锁状态,0-为未上锁,1-被占用
		*/ 
        final boolean isLocked() {
            return getState() != 0;
        }
        //省略下面代码。。。
    }

ReentrantLock 公平策略:
ReentrantLock 中公平策略和非公平策略的实现是不一样的,先看看 FairSync 类的具体方法:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
		//acquire 是父类方法,内部户调用 tryAcquire 方法拿锁
        final void lock() {
            acquire(1);
        }
        /**
         * 公平策略的 tryAcquire 方法,和非公平锁不同
         */
        protected final boolean tryAcquire(int acquires) {
        	//获取当前线程
            final Thread current = Thread.currentThread();
            //获取当前锁状态
            int c = getState();
            //0-说明没被占用
            if (c == 0) {
            	//判断是否是第一个入队的线程
                if (!hasQueuedPredecessors() &&
                	// CAS 修改当前锁状态
                    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;
        }
    }

在公平策略模式下,当我们调用 lock.lock()加锁时,首先调用的是:

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

由于使用的是公平模式,在 new 的时候已经构建好了,接下来就是调用 FairSync 内的 lock 方法,如下:

 static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
		//省略部分代码
}

当我们点击 acquire 方法时,这个方法在 AQS 类中,我们来看看该方法实现:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
       	    //这里涉及到入队了,链表 Node 的方法,暂不解释
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先 tryAcquire 方法就是我们公平锁中实现的方法。

tryAcquire 方法解析:

static final class FairSync extends Sync {
        /**
         * 公平策略的 tryAcquire 方法,和非公平锁不同
         */
        protected final boolean tryAcquire(int acquires) {
        	//获取当前线程
            final Thread current = Thread.currentThread();
            //获取当前锁状态
            int c = getState();
            //0-说明没被占用
            if (c == 0) {
            	//判断是否是第一个入队的线程
                if (!hasQueuedPredecessors() &&
                	// CAS 修改当前锁状态
                    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;
        }
    }

tryAcquire 方法在假设是有线程占用了的话,肯定返回的是 false,接着调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 方法,先看看 addWaiter 方法是怎么样的!

addWaiter 方法解析:

    private Node addWaiter(Node mode) {
    	//包装一个当前节点,将线程和将当前Node节点的下一个等待节点指向传入节点
        Node node = new Node(Thread.currentThread(), mode);
        //拿到尾节点,尝试将节点添加到尾节点,这里是做优化
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //正是入队
        enq(node);
        return node;
    }

enq 方法解析:

  private Node enq(final Node node) {
        for (;;) {
        	//拿到尾节点
            Node t = tail;
            //为空,构建一个空节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	//将前置节点指向尾节点
                node.prev = t;
                //CAS 设置尾节点
                if (compareAndSetTail(t, node)) {
                	//将前置的节点的下一个节点指向当前节点。
                    t.next = node;
                    return t;
                }
            }
        }
    }

上面理解起来可能比较难,接下来我们用图来解析是如何进行的:

现在有三条线程,分别为 A B 。这时,线程 A 开始开始来拿锁,拿到锁后,就占住。线程 A 占住锁后,队列还是处于空状态。
在这里插入图片描述
这时线程 B 来了,还是会去尝试拿锁。发现锁已经被占用了,就返回的是 false ,接下来就尝试去进入队列,也是就是调用 enq 入队方法。这时,我们发现队列是空的,CAS 尝试构建一个对象,构建成功后将头尾指针都指向这个空节点。
在这里插入图片描述
这时,头结点和尾节点后指向同一个,结果如下图所示:
在这里插入图片描述
都走完后,结束第一轮循环,走下一个循环。这时我们发现尾节点已经不是空的了,走 else 分支:
在这里插入图片描述
上面代码很清晰的可以看出,我们传入的 node 对象的前置节点指向 尾指针指向的对象,之后 CAS 尝试设置尾节点,成功的话,尾指针指向的节点的 next 指向我们传入的 node 节点。
开始是这样的:
在这里插入图片描述
之后,将 pre 指向尾节点(也可以说头节点,因为暂时头尾都指向同一个)
在这里插入图片描述
接下来,这里可能会出现并发情况,会尝试 CAS 设置尾指针指向。如果 CAS 成功设置,那么会将头接的 next 指向我们的传入的节点,结果如下图:
在这里插入图片描述
到此,我们线程 B 已经入队成功了。但是这仅仅完成了一半,接下来才是重点。如果我们还记得 acquire 方法的话,最终会调用 acquireQueued() 这个方法。该方法也是在 AQS 类中做了具体实现。

acquireQueued 解析:

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)) {
                	//将传入节点设为头节点
                    setHead(node);
                    //将原来的头节点的 next 指向置空
                    p.next = null; // help GC
                    //这个是判断是否让线程继续阻塞
                    failed = false;
                    return interrupted;
                }
               	//判断线程是否应该阻塞,parkAndCheckInterrupt 是对当前线程进行阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果线程被中断了,将 interrupted 设置为 true
                    interrupted = true;
            }
        } finally {
        	
            if (failed)
                cancelAcquire(node);
        }
    }

**shouldParkAfterFailedAcquire 方法解析: **
上面代码中,shouldParkAfterFailedAcquire 方法是判断该线程是否应该被阻塞,而 parkAndCheckInterrupt 则是对线程进行阻塞,下面我们看看源码实现:

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 		//前置节点状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
        	//大于 0, 表示该节点已经无效了,需要移除,
        	//这时,不断循环找到没有失效的节点
        	//将该节点的 next 指向自己
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	//若 pre 节点状态不正确,都将其变成 SIGNAL 阻塞状态  
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这里要说一下 Node 的几个状态:

状态描述
CANCELLED1该状态表示线程可能被中断
SIGNAL-1表示当前线程需要需要被阻塞,处于这个状态下才可以安全的阻塞
CONDITION-2暂时用不到,这时线程间阻塞唤醒使用
PROPAGATE-3与共享模式相关,暂时不理

上面代码中,只有处于 SIGNAL 状态时,才可以安全的将其阻塞。大于 0 的情况只有线程已经被中断,所以需要不断循环找前置没有失效的节点。

parkAndCheckInterrupt 方法解析:
parkAndCheckInterrupt 方法很简单,看名字就知道这是阻塞和检查线程是否被中断。LockSupport 该类会在后续篇章讲解。

   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

讲到这里,线程 B 入队到阻塞的情况基本讲解完毕。下面我们开始看看线程 A 执行完毕释放锁后,程序是如何继续运行的。
正常来说,我们释放锁时,会调用 Lock,unlock()方法来进行释放,unlock 内部调用 release 方法,代码如下:

   public void unlock() {
        sync.release(1);
    }
	public final boolean release(int arg) {
		//tryRelease 在 Sync 类中,不在 AQS 类里
		//重写了父类 AQS tryRelease
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
            	//唤醒阻塞的线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease 方法解析:

 protected final boolean tryRelease(int releases) {
 			// releases 传的是 1 ,getState 中 1-表示占用
            int c = getState() - releases;
            //判断是否是当前线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //等于 0  说明可以释放
            //置空线程
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //改变锁状态
            setState(c);
            return free;
        }

unparkSuccessor 方法解析:
该方法在 AQS 类中,是 AQS 一个很重要的类,用于唤醒线程。

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
        	//修改线程状态
            compareAndSetWaitStatus(node, ws, 0);
        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)
            LockSupport.unpark(s.thread);
    }

解析较为模糊,我们用图的方式来解析:

当我们线程 B 入队并阻塞时,waitState 的状态值是 -1
在这里插入图片描述
接着,线程 B 被唤醒后继续往下走,会走如下代码:
在这里插入图片描述
线程 B 尝试去拿到锁,如果成功拿到锁之后将头尾指针指向同一个节点。(仅仅两个线程会出现这种情况,多个的话,最后一个节点才是尾指针指向。)
在这里插入图片描述
后面线程 B 释放锁时,会将 waitStatus 设置为 0,后面都一样了。当然,假设阻塞队列中有最后一个线程阻塞唤醒时,unparkSuccessor 方法什么都不做。

ReentrantLock 非公平策略:
非公平锁的实现和公平锁的实现不一样,接下来我们先看看非公平锁的类:

   static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
		//尝试上锁
        final void lock() {
        	//未被占用,尝试 CAS 修改状态并保存当前线程
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	//占用则自旋尝试取锁
                acquire(1);
        }
		//重写的方法,非公平实现
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

当我们调用 Lock.lock()时,首先是判断锁是否被占用,如未占用,会 CAS 尝试拿到锁,被占用的情况下回调用 acquire 方法,但是该方法和公平锁一样,都是在父类 AQS 中,最后调用的还是 tryAcquire 方法,就是上面代码中的 tryAcquire 方法。下面是非公平锁的调用实现:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //拿到当前线程
            int c = getState();
            if (c == 0) {
            	//CAS 尝试修改
                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;
        }

其他入队,阻塞实现和公平锁一致,就不重复写了。下面要说 AQS 的中断机制。

AQS 的锁中断机制:
如果我们有业务是允许中途中断的,那么我们需要使用可中断锁了,可中断锁使用很简单,就是调用如下方法即可。

  lock.lockInterruptibly();

接下来我们来看看 lockInterruptibly 的代码,如下可以知道内部调用 acquireInterruptibly 方法,但是这个方法是在父类也就是 AQS 中做了实现,我们来看看父类实现的这个方法。

 public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

acquireInterruptibly 方法解析:

 public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        //判断线程是否中断
        if (Thread.interrupted())
            throw new InterruptedException();
         //尝试去拿锁
        if (!tryAcquire(arg))
       		//拿锁失败
            doAcquireInterruptibly(arg);
    }

doAcquireInterruptibly 方法解析:
是不是该方法似曾相识?上面我们我们中断后只是标识该线程被中断,而这里直接抛异常。其他步骤都一模一样,就不重复讲解了。

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //差别在这里!
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

差不多讲完了将 ReentrantLock 源码解析完了,剩下一个限时去拿锁的方式了,下面我们继续来看看。

AQS 锁的限时等待机制:
AQS 里有对限时等待拿锁的支持,Lock 接口也提供了该方法的抽象,有尝试去拿锁的,也有一定时间内不断尝试去拿锁的,方法如下:

//尝试去拿锁	
lock.tryLock();
//10 秒内不断尝试去拿锁
lock.tryLock(10,TimeUnit.SECONDS);

限时等待方法实现也不难,当我们调用有时间的 tryLock 时,内部调用代码如下:

 public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

tryAcquireNanos 方法解析:

  public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        //判断线程是否被中断
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试拿锁,不成功则限时去尝试拿
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

tryAcquire 方法就不写了,写到烂了,我们来看看 doAcquireNanos 这个方法:

doAcquireNanos 方法解析:

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        //代码差不多,就加个这句
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                //还有下面这两个判断
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                    //时间范围内,且状态修改为 SIGNAL,则阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    //如果成功就阻塞线程
                    LockSupport.parkNanos(this, nanosTimeout);
 				
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

好了,到这里基本就写完了 AQS 的独占锁详解,后面 AQS 对中断机制与限时等待机制的支持都是基于最开始的 doAcquireShared 进行部分修改。相关实现我们看代码可以知道都是 CAS 操作来保证,就不继续讲解了。

补充:
显示锁和内置锁的效率对比图
(以下图片来源于网络)
在这里插入图片描述
最后:
既然 AQS 对锁支持的那么好,而且按照上图中效率对比,AQS 对锁的支持都比 synchronized 好,那 synchronized 应该可以淘汰吧?答案是不一定,《深入理解 java 虚拟机》 中作者也说明,synchronized 锁有很大的优化空间,而且上图的图标是基于 JDK1.5 的。在后续的版本中,对 synchronized 做了极大的优化,优化后的内置锁效率上也是很高了。

在日常开发中,synchronized 关键字使用更方便、更广泛。即使 AQS 提供的锁支持比内置锁多了更多的功能。如果是简单的锁变量之类的,优先还是使用内置锁,除非有特别的业务要可以对这个资源进行中断,超时,那就使用显示锁吧。

吐血,这一篇居然写了 7 个小时,真是没想到,没想到呀!不过很庆幸终于写完了!

有兴趣的同学欢迎关注公众号,一起学习!
在这里插入图片描述

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值