面试准备 -- AQS 等待/通知机制

以前我们使用 Object 类提供的线程等待/通知机制,我们先来看看一个例子:

import java.util.LinkedList;
/**
 * @author : Gentle
 * @date : 2019/5/20 15:08
 * @description:
 */

public class Tests {
    public static void main(String[] args) throws InterruptedException {
        Tests tests = new Tests();

            new Thread(tests::waits).start();
            //让上面的线程先睡觉完毕
            Thread.sleep(1000);
            new Thread(tests::notifys).start();
    }
    //随便构建一个对象,用来当锁对象
    private final Object object = new Object();
	//等待的方法
    private void waits() {
        synchronized (object) {
            try {
                System.out.println("开始睡觉!");
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我被唤醒啦");
        }
    }
    //唤醒方法
    private void notifys() {
        synchronized (object) {
            System.out.println("开始唤醒啦");
            object.notifyAll();
        }
    }
}

上面的小例子,就是演示是 JDK 本身提供的 synchronized 关键字配合实现的等待/通知机制。下面我们开始来学习 AQS 是如何来实现等待/通知机制的!

Condition 简介:
Condition 接口可能在开发中很少遇到,但是这一点却十分重要,下面我们来看看该接口提供的方法!

ublic interface Condition {
	/** 线程等待方法 **/
    void await() throws InterruptedException;
	/** 线程等待方法,可中断 **/
    void awaitUninterruptibly();
	/** 线程等待方法,有时间限定 **/
    boolean await(long time, TimeUnit unit) throws InterruptedException;
	/** 线程等待方法 可指定某一个时刻**/
    boolean awaitUntil(Date deadline) throws InterruptedException;
	/** 唤醒一个线程 方法与 notify()类似**/
    void signal();
	/** 唤醒全部线程 方法与 notifyAll()类似**/
    void signalAll();
}

看完接口提供的方法,来写个简单例子!

 public static void main(String[] args) throws InterruptedException {
        final Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
		//睡觉线程
        new Thread(()->{
            lock.lock();
            try {
                System.out.println("开始睡觉");
                condition.await();
                System.out.println("醒啦!!!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
       //让上面线程先完成
        Thread.sleep(1000);
        //唤醒线程
        new Thread(()->{
            lock.lock();
            try {
                System.out.println("准备唤醒");
                condition.signal();
            } finally {
                lock.unlock();
            }
        }).start();
    }

根据如上例子,现在我们有两条线程,分别为线程 A 和线程 B。线程 A 调用 await()进行阻塞,等待线程 B 将线程 A 唤醒!

首先,我们从线程 A 调用 await()方法说起。
await() 方法解析:

public final void await() throws InterruptedException {
	//判断线程是否被中断
     if (Thread.interrupted())
         throw new InterruptedException();
     //添加进等待队列中
     Node node = addConditionWaiter();
     //尝试释放
     long savedState = fullyRelease(node);
     int interruptMode = 0;
     //判断当前节点状态是否正确
     while (!isOnSyncQueue(node)) {
         LockSupport.park(this);
     	//中断了
     	if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
         	break;
     }	
     //acquireQueued 是不是很熟悉?
     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
         interruptMode = REINTERRUPT;
    //节点失效了,清理失效的节点
    if (node.nextWaiter != null) // clean up if cancelled
         unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
	}

第一步是调用插入插入队列的方法 addConditionWaiter() 来看看具体实现

addConditionWaiter() 方法解析:

	private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //构建一个新节点且 Node.CONDITION = -2 
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            //如果等待队列是空的,那将首尾指针指向当前节点
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
                
            lastWaiter = node;
            return node;
        }

该方法在 AQS 类中,final 标记不允许重写!
fullyRelease()方法解析:

	final int fullyRelease(Node node) {
        boolean failed = true;
        try {
        	//拿到当前锁状态
            int savedState = getState();
            //判断锁是否释放成功
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
        	//修改等待状态为关闭
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

锁释放的方法也很简单,方法很多都在上一篇文章中进行过讲述,这里就不在赘述了。

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;
    }

我们先看看如下代码:
这里是可选情况
isOnSyncQueue()方法解析:

	final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
       // If has successor, it must be on queue
        if (node.next != null) 
            return true;
        return findNodeFromTail(node);
    }
    //找到队尾的节点
	private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
        	//当前节点是否为尾节点
            if (t == node )
                return true;
            //尾节点
            if (t == null)
                return false;
            t = t.prev;
        }
    }

isOnSyncQueue 方法主要是用于判断当前节点是否在等待队列中,如果不在等待队列中,那就开始阻塞线程!这时候线程 A 就开始了阻塞状态,等待被唤醒!

线程 B 开始调用方法唤醒
线程 B 开始工作了,线程 B 完成自己的工作后,需要唤醒线程 A,这时需要调用 signal()方法,下面我们来看看该方法实现

signal()方法解析:

 	public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //找到第一个等待的节点
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
    //唤醒实现
	private void doSignal(Node first) {
            do {
            	//置空
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                //置空
                first.nextWaiter = null;
               //修改锁状态,并将其加入拿锁的队列中
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal()方法解析:

 final boolean transferForSignal(Node node) {
 		//CAS 修改节点的状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //入队
        Node p = enq(node);
        int ws = p.waitStatus;
        //状态不正确,CAS 修改状态,,然后线程阻塞
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

该方法主要是修改节点状态,将原本阻塞的线程的状态修改为最开始的状态!然后让该节点进入拿锁的等待队列中。

这时,线程池 B 的锁还没有释放,所以被唤醒的节点会先加入拿锁的等待队列中,直到线程 B 将锁释放后,在从队列中取出线程!由于这里只有两条线程 A 和 B。所以 B 线程释放锁后,A 线程便会拿到锁。

接下来还是线程 A 被唤醒后继续执行:

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
			//被唤醒后,开始走下面代码
			//尝试去拿锁,并设置状态
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //如果当前节点的下一个等待节点不为空,就将当前节点的链接清除
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

上面说的 node.nextWaiter != null,如果理解不了,那来看看具体的方法实现。

unlinkCancelledWaiters()方法解析:

 	private void unlinkCancelledWaiters() {
 			//第一个等待的节点
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
            	//拿到当前节点的下一个等待节点
                Node next = t.nextWaiter;
                //判断下一个节点的等待状态是否为 -2 
                if (t.waitStatus != Node.CONDITION) {
                	//置空
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                //交换,继续往下一个节点找
                else
                    trail = t;
                t = next;
            }
        }

如果上面都没有问题,最后走的便是线程中断了,这个不需要太多的解释了,中断方式调用的是最基本的 Thread 类中的中断方法。

if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);

上面讲解完毕,如果感觉还是很懵逼,我们用图的形式来进行讲解!

当我们调用 Node node = addConditionWaiter() 方法时,构建节点情况如下:

构建一个 waitStatus = -2 的节点。
在这里插入图片描述
将头指针和尾指针都指向当前节点!
在这里插入图片描述

接下来会调用 fullyRelease(node) 该方法是释放锁。Condition 接口是需要显示锁配合的,所以在调用 await() 方法前要加锁。

接下来就是线程 A 就要开始睡觉啦! LockSupport.park(this) 就是让当线程 A 去睡大觉,等着线程 B 叫醒他!

这时,线程 B 来了,执行完内部业务后开始调用 Condition.signalAll 或 Condition.signal 进行线程唤醒。
也就是上面解析过的代码:
在这里插入图片描述
如果等待队列中只有一个等待的线程,就直接将 lastWaiter 置空之后再将当前节点的指向下一个节点的引用断掉,调用 transferForSignal()方法将 Node 中 waitStatus 从 -2 设置为 0。

do while()将 firstWaiter 和 lastWaiter 指针置空:
在这里插入图片描述
将线程 A 入队,并设置成等待状态!也就是将 waitStatis 设置为 0 (阻塞状态)。
在这里插入图片描述在这里插入图片描述
当线程 B 唤醒线程 A 时,线程 A 还是属于阻塞状态的!当线程 B 释放锁时,才是真正唤醒线程 A,线程 A 继续往下走。线程 A 会继续尝试去拿锁,
在这里插入图片描述
在这里插入图片描述
拿锁的解析在上一篇文章 ReentrantLock 中已经写过,这里就不在重复写了。

到此,等待唤醒机制已经写完了,signalAll()或者其他可中断的方式,只是在最基本的 await()基础上添加部分代码实现。

总结:
其实,说起来,AQS 的等待/通知机制和 ReentrantLock 中等待十分类似,都是将线程加入队列然后进行线程阻塞,区别是使用不同的状态参数!不过,这一套机制的实现,丰富了我们 API 的调用,也可以限时,中断等,对我们的业务也有很大帮助。
当然,有这套机制,不等于我们要忘记最基本的 wait()和 notify(),毕竟 synchronized 的实现等待/通知机制使用的就是它。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值