AQS是如何实现线程等待和唤醒的

AQS简介

首先我们认识下AQS(AbstractQueuedSynchronizer),是Java并发的一个框架,比如说一些并发类: 独占锁 : ReentrantLock锁(独占) 共享锁 Semaphore信号量、CoutDownLatch计数器、CyclicBarrier(循环栅栏)等... AQS核心是通过FIFO等待的队列来实现线程的阻塞和唤醒的,分别有两种队列Condition和CLH等待队列;

AQS三个关键定义

  • state:标记共享变量,volatile修饰,可重入;
arduino 复制代码
//state =0 表示当前锁没有被占用,state>0表示被占用
private volatile int state;
  • queue:线程获取锁失败后,进入queue中排队等待唤醒
  • cas:比较然后交换,修改state的值; 这里存在三个参数分别是:内存值、预期值以及新值 执行逻辑:比较内存值与预期值,如果相等则去更新新值,如果不等则返回失败;

关于AQS的队列结构,之前写过AQS为什么使用双向FIFO队列 - 掘金,这里就不做详细介绍了;

AQS加锁过程

关于加锁,主要是依靠ReentrantLock类,通过它的tryAcquire方法尝试获取锁

java 复制代码
static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        //尝试加锁
        protected final boolean tryAcquire(int acquires) {
        //获取当前线程对象
            final Thread current = Thread.currentThread();
            //获取当前state值
            int c = getState();
            //判断是否被占用
            if (c == 0) {
            //多个线程请求锁,利用cas来更新
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                //更新成功说明就获取到了锁对象,设置当前线程持有锁对象
                    setExclusiveOwnerThread(current);
                    //获取锁成功
                    return true;
                }
            }
            //如果当前线程已经持有锁,因为可重入,可以继续state ++
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                //可重入锁,获取成功
                return true;
            }
            //没有获取锁成功,返回false
            return false;
        }
    }

addWaiter

scss 复制代码
public final void acquire(int arg) {
//获取锁失败,调用addWaiter进入等待队列
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}

如果返回false加锁失败,直接就丢弃么,不是的,需要加入到queue中等待队列进行等待,静态内部类Node的addWaiter方法;

ini 复制代码
 private Node addWaiter(Node mode) {
 //将当前线程封装成Node
        Node node = new Node(Thread.currentThread(), mode);
       //获取尾节点
        Node pred = tail;
        //不为空
        if (pred != null) {
        //将prev节点设置为tail尾节点
            node.prev = pred;
            //进行cas,尾节点和tail进行交换
            if (compareAndSetTail(pred, node)) {
            //tail的next节点变成node
                pred.next = node;
                return node;
            }
        }
        //入队不成功 执行enq方法
        enq(node);
        return node;
    }

enq方法

ini 复制代码
    private Node enq(final Node node) {
    //死循环
        for (;;) {
        //将node设置为尾节点
            Node t = tail;
            //如果为空
            if (t == null) { 
            //创建新的Node新节点,设置为head
                if (compareAndSetHead(new Node()))
                //经过循环node将到队列尾部
                    tail = head;
            } else {
                node.prev = t;
                //cas将node设置为tail
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

LockSupport

park阻塞

上面说了加锁失败的线程都将进入队列等待,这时候就出现了一个park的概念,LockSupport类的park方法,对AQS中的node线程进行阻塞,阻塞的主要目的是:防止自旋一直获取锁消耗CPU的资源,需要的时候可以调用unpark方法唤醒线程;

scss 复制代码
    public static void park(Object blocker) {
    //获取当前线程
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        //调用UNSAFE的park方法,将线程进入休眠等待状态
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

unpark唤醒

唤醒需要被唤醒的线程

arduino 复制代码
    public static void unpark(Thread thread) {
    //传入需要被唤醒的线程
        if (thread != null)
            UNSAFE.unpark(thread);
    }

Condition队列

上面说了线程的阻塞和唤醒,那么需要被阻塞和唤醒的线程要是需要搭载在队列上面的,所以AQS就提供了Condition队列;

Wait

重要的两个节点condition的头节点和尾节点

java 复制代码
//condition对应的队列的头节点
private transient Node firstWaiter;
//condition对应的队列的尾节点
private transient Node lastWaiter;

await主要是是将node阻塞线程进行入列操作

scss 复制代码
        public final void await() throws InterruptedException {
        //线程中断抛出异常
            if (Thread.interrupted())
                throw new InterruptedException();
                //加入等待队列
            Node node = addConditionWaiter();
            //将锁释放 如果释放失败就将waitstatus状态改完CANCELLED
            long 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);
        }

unlinkCancelledWaiters剔除不为conditon节点的node

ini 复制代码
        private void unlinkCancelledWaiters() {
        //获取等待队列
            Node t = firstWaiter;
            Node trail = null;
            //不为空就一直循环
            while (t != null) {
                Node next = t.nextWaiter;
                //waitStatus不为CONDITION队列
                if (t.waitStatus != Node.CONDITION) {
                 //切断不为condition节点的下一个节点,切断它们之间的关系
                    t.nextWaiter = null;
                    //如果trail 为空,firstWaiter也不是CONDITION节点,需要被剔除
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

Signal

前面说线程加入等待队列,下面来看线程是如何被唤醒的

java 复制代码
public final void signal() {
//判断当前线程是否持有锁
    if (!isHeldExclusively())
// 没有持有就抛出异常
        throw new IllegalMonitorStateException();
//获取condition里面第一个节点
    Node first = firstWaiter;
//如果不为空 调用doSignal
    if (first != null)
        doSignal(first);
}

transferForSignal方法

typescript 复制代码
private void doSignal(Node first) {
            do {
            //循环,如果队列的第一个和下一个为空,则将后续节点都剔除
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
                //不为空调用transferForSignal方法
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal节点唤醒

arduino 复制代码
     final boolean transferForSignal(Node node) {
     
     //将nodewaitStatus从CONDITION改为0 
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
         //返回firstWaiter前节点
        Node p = enq(node);
        int ws = p.waitStatus;
        // 如果前节点的waitStatus > 0
        //或者将前节点waitStatus改成 Node.SIGNA 失败了,也会触发节点唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

SignalAll(唤醒condition全节点)

方法大致差不多,就是调用了signalAll方法

java 复制代码
public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

doSignalAll

ini 复制代码
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

checkInterruptWhileWaiting(唤醒后的操作)

唤醒后干嘛呢,要看这里了,checkInterruptWhileWaiting方法

checkInterruptWhileWaiting这一步是检查唤醒线程的状态,根据不同的状态进行设置异常或者中断

arduino 复制代码
            private int checkInterruptWhileWaiting(Node node) {
           
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }
  • 如果线程中断,设置为/THROW_IE/ ,抛出异常
  • 如果signal没有发生异常,则设置为/REINTERRUPT/
  • 如果线程没有中断就返回0

transferAfterCancelledWait

java 复制代码
            final boolean transferAfterCancelledWait(Node node) {
//将node的WaitStatus从Node.CONDITION改为0
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
  
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

acquireQueued抢夺锁

ini 复制代码
        final boolean acquireQueued(final Node node, long arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果Node是头节点,就去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    //帮助gc
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        // 如果抢占锁失败,就将线程从队列清除
            if (failed)
                cancelAcquire(node);
        }
    }
  • acquireQueued不出现异常的话,就将interruptMode设置为/REINTERRUPT/
  • node.nextWaiter != null ,就是没有经过唤醒,就去清除它
  • interruptMode !=0 调用reportInterruptAfterWait方法

interruptMode为异常状态就抛出异常,状态为REINTERRUPT,调用中断方法

java 复制代码
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

总结

基于可重入锁ReentrantLock加锁过程,出现的condition线程等待队列,在队列中根据node节点状态,来决定阻塞还是被唤醒,释放锁唤醒queue上的其他节点来争夺锁,线程唤醒后会在condition队列中进行剔除,将waitStatus状态改为0,加锁锁的queue;

相关推荐
救救孩子把9 分钟前
深入理解 Java 对象的内存布局
java
落落落sss11 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
万物皆字节17 分钟前
maven指定模块快速打包idea插件Quick Maven Package
java
夜雨翦春韭24 分钟前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
我行我素,向往自由30 分钟前
速成java记录(上)
java·速成
一直学习永不止步36 分钟前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
邵泽明37 分钟前
面试知识储备-多线程
java·面试·职场和发展
Yvemil71 小时前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
煸橙干儿~~1 小时前
分析JS Crash(进程崩溃)
java·前端·javascript