死磕JUC之AQS

news/2024/5/20 6:11:29 标签: aqs, juc, concurrent, 并发

AQS目录

AQS简介

FIFO队列

NODE节点

独占模式

共享模式

ConditionObject


AQS简介

   AQS是Concurrent包核心之一,全称是AbstractQueuedSynchronizer。ReetrantLock,Semaphore,CountDownLatch都有一个内部类Sync继承AQS。

   AQS的核心是通过一个共享变量state来同步状态,变量的状态由子类维护,state=0时则说明没有任何线程占有共享资源的锁,当state=1时则说明有一个线程正在使用该共享资源,其他线程必须加入同步队列等待。AQS做的是线程阻塞队列的维护和线程的阻塞和唤醒,其中共享变量的修改都是通过CAS(v,e,n)无锁操作完成。

  AQS的主要方法是acquire()和release(),这两个方法一般会被子类封装成lock()和unlock()。

  AQS通过内部类Node构建FIFO同步队列完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当condition调用await后,线程会加入等待队列,而当condition调用signal后,线程将从等待队列移入同步队列进行锁的竞争。

AQS:

transient volatile Node head;//同步队列队头(不存储实际信息,空信息节点)

transient volatile Node tail;//同步队列队尾

volatile int state;//0代表未占用

FIFO队列

NODE节点

Node节点数据结构:node节点是访问同步代码的线程的封装。

static final class Node {       
        static final Node SHARED = new Node();//共享模式     
        static final Node EXCLUSIVE = null; //独占模式      
        static final int CANCELLED =  1;//线程结束/消亡状态        
        static final int SIGNAL    = -1;//后序线程节点需要被唤醒        
        static final int CONDITION = -2;//线程再condition等待队列等待某一条件    
        static final int PROPAGATE = -3;//后序线程节点会传播唤醒操作,共享模式起作用
        volatile int waitStatus;//上述四种之一,代表线程状态
        volatile Node prev;//同步队列前驱
        volatile Node next;//同步队列后继       
        volatile Thread thread;//拥有该node的线程
        Node nextWaiter;//等待队列的后继节点
}

注意有两个队列,一个同步队列(前驱prev后继next都有),一个等待队列(只有后继nextWaiter,只有等待队列的节点的ws才可以是CONDITION,另外等待队列的节点的ws只可能是CANCELLED或CONDITION)

独占模式

独占模式获取锁:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

尝试获取锁,如果获取成功,acquire执行完毕,当前线程获取到锁。否则加入同步队列阻塞,等待前驱释放锁再次竞争锁资源。

tryAcquire()是由子类实现,例如ReetrantLock中有公平模式获取锁和非公平模式获取锁。

下面是addWaiter方法:pred指向尾节点,如果尾节点不空,则当前节点的前驱指向尾节点。再cas判断尾节点是不是pred,如果是就把尾节点改为当前节点node,然后pred的后继指向node。以上步骤是尝试快速入队操作,如果失败了才会调用enq(node)。

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

enq(node)

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 当前队列为空,必须初始化
                if (compareAndSetHead(new Node()))//可以看出头节点是空信息节点
                    tail = head;//只有一个节点,头尾一样
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {//如果tail==t,就把tail改为node
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter方法只是入队的操作,没有入队后的真正逻辑操作,而这些操作是acquireQueued完成的,如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;//失败标志
        try {
            boolean interrupted = false;//acquireQueued过程是否被中断过
            for (;;) {
                final Node p = node.predecessor();//p为node的前驱
                if (p == head && tryAcquire(arg)) {//如果node是老二并且tryAcquire成功
                    setHead(node);//设置node为头
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //需不需要挂起当前线程&&挂起该线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)//未知错误或异常
                cancelAcquire(node);//删除该节点
        }
    }

shouldParkAfterFailedAcquire作用是判断是否要挂起当前线程,有以下三个作用:

  • 确定是否需要park;
  • 跳过被取消的结点;
  • 设置前继的waitStatus为SIGNAL.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//node前驱的状态
        if (ws == Node.SIGNAL)//前驱是唤醒状态
            return true;//返回true直接挂起
        if (ws > 0) {//CANCELLED状态
            do {//向前找到第一个不是CANCELLED状态的节点,挂在该节点的后面
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;//挂在该节点的后面
        } else {//PROPAGATE或0
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//把前驱改为SIGNAL状态
        }
        return false;//返回false不能挂起
    }

parkAndCheckInterrupt执行真正的挂起操作:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//挂起当前线程
        return Thread.interrupted();//返回线程中断标志,并重置中断标志位为false
    }

该方法如果返回true,说明曾经中断过,则acquireQueued中interrupted=true.最后会在acquire中selfInterrupt将中断补上,为什么补中断,理由如下:在acquireQueued中,即使线程在阻塞状态被中断唤醒获取到cpu执行权,但是也需要在循环中重新判断,如果前面还有其他的等待线程,根据公平性原则,该线程仍然无法获取到锁资源,也就是说在成功获取锁资源真正执行起来之前,他的中断会被忽略并被清除。

acquire大致流程如下

独占模式释放锁:

public final boolean release(int arg) {
    // tryReease由子类实现,通过设置state值来达到同步的效果。
    if (tryRelease(arg)) {
        Node h = head;
        // waitStatus为0说明是初始化的空队列
        if (h != null && h.waitStatus != 0)
            // 唤醒后续的结点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

共享模式

共享模式获取锁:

public final void acquireShared(int arg) {
    //如果没有许可了则入队等待
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
} 

doAcquireShared和独占模式acquireQueued很像,只有其中setHeadAndPropagate不一样。

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//以共享的模式入队
        boolean failed = true;//失败标志
        try {
            boolean interrupted = false;//中途是否被中断过
            for (;;) {
                final Node p = node.predecessor();//p是node的前驱
                if (p == head) {
                    int r = tryAcquireShared(arg);//尝试获取锁
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

setHeadAndPropagate源码如下:

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        // 后继结是共享模式或者s == null
        // 如果后继是独占模式,那么即使剩下的许可大于0也不会继续往后传递唤醒操作
        // 即使后面有的结点是共享模式。
        if (s == null || s.isShared())
            // 唤醒后继结点
            doReleaseShared();
    }
    }

共享模式释放锁:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 队列不为空且有后继结点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 不管是共享还是独占只有结点状态为SIGNAL才尝试唤醒后继结点
            if (ws == Node.SIGNAL) {
                // 将waitStatus设置为0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue; // loop to recheck cases
                unparkSuccessor(h);// 唤醒后继结点
                // 如果状态为0则更新状态为PROPAGATE,更新失败则重试
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue; // loop on failed CAS
        }
        
        if (h == head) // loop if head changed
            break;
    }
}

ConditionObject

ConditionObject是AQS中定义的内部类,实现了Condition接口,ConditionObject是基于Lock实现的,在其内部通过链表来维护等待队列。Contidion必须在lock的同步控制块中使用,调用Condition的singnal方法并不代表线程可以马上执行,线程的执行始终都需要根据同步状态(即线程是否占有锁)。

为什么Condition必须在lock的同步控制块中使用?即为什么在使用Condition之前要获取锁。

ConditionObject对象是通过显式锁中的lock.newCondition()方法生成的,可以看出此时必须占有锁资源,这也是为什么Condition必须在lock的同步控制块中使用的原因。

当condition调用await后,线程会加入等待队列,而当condition调用signal后,线程将从等待队列移入同步队列进行锁的竞争。

ConditionObject中定义的变量:

private transient Node firstWaiter;//等待队列头节点
        
private transient Node lastWaiter;//等待队列的尾节点

await方法如下:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//加入等待队列
            //上面提到condition必须在同步块中使用,故当前线程获取了锁,在加入等待队列后需要将此锁释放
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //isOnSyncQueue判断node是不是同步队列的节点
            //线程调用signal或signalAll时,会从firstWaiter节点开始,将节点依次从等待队列中移除,并通过enq方法重新添加到同步队列中
            //因此当其他线程调用signal或者signalAll方法时,该线程可能从条件(等待)队列中移除,并重新加入到同步队列中
            //1. 如果没有加入到同步队列,则阻塞当前线程,同时调用checkInterruptWhileWaiting检测当前线程在等待过程中是否发生中断,设置interruptMode表示中断状态。
            //2. 如果isOnSyncQueue方法判断出当前线程已经处于同步队列中了,则跳出while循环
            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();//从队列中删除ws是CANCELLED的节点
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter方法如下:

 private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果最后一个节点的ws是CANCELLED的话
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();//从队列中删除ws是CANCELLED的节点
                t = lastWaiter;//t重新赋值为尾节点
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);//当前线程节点
            if (t == null)//如果等待队列为空
                firstWaiter = node;//头节点赋值为node
            else
                t.nextWaiter = node;//尾节点的后继赋值为node
            lastWaiter = node;//尾节点重新赋值为node
            return node;//返回当前线程节点
        }

fullyRelease方法如下:

final int fullyRelease(Node node) {
        boolean failed = true;//释放锁资源失败的标志
        try {
            int savedState = getState();//返回state的值,独占模式是1,共享模式大于等于1(重入)
            if (release(savedState)) {//如果释放成功了
                failed = false;//失败标志置false
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)//如果释放失败了,把node的ws改为CACELLED
                node.waitStatus = Node.CANCELLED;
        }
    }

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

        //更新节点状态,并通过enq方法,将节点重新加入同步队列中
    final boolean transferForSignal(Node node) {
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

signal方法的核心其实是doSignal和transferForSignal方法,doSignal的主要作用就是将条件(等待)队列中的头节点firstWaiter从队列中移除,transferForSignal方法的主要作用就是将doSignal方法中移除的firstWaiter节点通过enq方法重新添加到同步队列中,从这里也可以看出为什么会在await方法中调用isOnSyncQueue方法判断节点是否处于同步队列中了。
 


http://www.niftyadmin.cn/n/1037799.html

相关文章

jquery 获取用户滚动到底部事件

$("滚动元素选择器").scroll(function () {var scrollTop $(this).scrollTop();var ks_area $(this).innerHeight();var nScrollHight 0; //滚动距离总长(注意不是滚动条的长度)nScrollHight $(this)[0].scrollHeight;// alert(ks_area);if (scrollTo…

H5页面 写手机端 用户点击输入框页面被放大问题解决

主要就是移动端和pc端的区别 在head标签内加入移动端适配标签就好了 <meta name"viewport" content"widthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalable0">

java回调例子

public class Start{interface OnClickListener {void onClick();}class Button{public void setOnClickListener(OnClickListener l) {l.onClick();//通过这个回掉给设置监听器的地方}}public static void main(String[] args) {new Start().start();}public void start(){ne…

H5页面实现pdf文件预览功能

看着很高大上的东西&#xff0c;确实只需要一个A标签就行了 <a target_black hrefhttps://www.lilnong.top/static/pdf/B-4-RxJS%E5%9C%A8React%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8-%E9%BE%99%E9%80%B8%E6%A5%A0_.pdf>预览PDF文件</a>虽然看着很高大上 但真的不…

maven学习之路---入门

1. 什么是maven? 它是一个软件开发的管理工具&#xff0c;主要管理的工作是&#xff1a;依赖管理&#xff0c;项目构建 2. 使用maven的好处&#xff1f; 能够集中管理jar包 提供一键构建 3. maven的安装及配置 配置环境变量&#xff1a;MAVEN_HOME,PATH路径配置 本地仓库配置&…

前端优秀框架jQuery weui推荐

作为和微信风格类似的一款移动端开发工具 jQuery weu在移动端开发中也是能完美兼容微信&#xff0c;而且语法简单对前端萌新开发移动端H5页面相对友好 这边推荐的资源网站是 http://www.santii.com/weui 这这里你可以看到很完整的组件使用 但这个框架的依赖相对比较难找 可能需…

my spring 学习之路 ---01

1、什么是spring&#xff1f; Spring&#xff1a;SE/EE开发的一站式框架。  一站式框架&#xff1a;有EE开发的每一层解决方案。  WEB层 &#xff1a;SpringMVC  Service层 &#xff1a;Spring的Bean管理&#xff0c;Spring声明式事务  DAO层 &#xff1a;Spring的Jdbc…

My SpringMVC学习之路---01

1.SpringMVC是什么&#xff1f; Spring web mvc和Struts2都属于表现层的框架,它是Spring框架的一部分,我们可以从Spring的整体结构中看得出来,如下图&#xff1a; 2…Springmvc处理流程 如下图所示&#xff1a; 3. 入门程序 需求&#xff1a;使用浏览器显示商品列表 3.1 创建…