AQS原理(AbstractQueuedSynchronizer)

news2024/11/24 5:47:01

本篇为 [并发与多线程系列] 的第四篇,对应Java知识体系脑图中的 并发与多线程 模块。

这一系列将对Java中并发与多线程的内容来展开。



AQS原理(AbstractQueuedSynchronizer)

  • AQS原理(AbstractQueuedSynchronizer)
    • AQS整体结构
      • state状态值
      • Node对象
    • CLH队列锁
      • AQS 中的变体CLH
    • AQS中的两种模式
      • AQS 两种模式对比
      • AQS 的独占模式
        • 获取acquire
        • 释放release
      • AQS 的共享模式
        • 获取acquireShared
        • 释放releaseShared
    • 锁的属性:公平性,响应中断,超时
        • 公平性
        • 响应中断
        • 超时
    • 扩展设计:ConditionObject与等待队列
        • Condition中的操作
      • await 等待
      • signal 唤醒
    • AQS设计模式巧思:模版方法
    • AQS的应用



AQS原理(AbstractQueuedSynchronizer)

在Java多线程中,AQS(抽象队列同步器)是一个用来实现同步锁以及其他涉及到同步功能的核心组件,Java中的锁Lock就是基于AbstractQueuedSynchronizer来实现的。AQS的出现是用于优化锁的效率,替代synchronize关键字的解决方案。不过,Java1.6版本起synchronize经过锁优化之后,效率差距已经不大。

但是,AQS里很多的设计思路,都值得学习和借鉴,理解AQS原理才能更好的理解JUC(Java.util.concurrent)包内关于多线程的实现。它也是面试中高频关键知识点之一。

注:本文以下源码基于JDK1.8。


AQS中的重点

  • AQS整体结构、状态值、Node对象
  • 数据结构:CLH队列
  • 锁的模式:独占模式与共享模式(含源码篇幅较长)
  • 锁的属性:公平性,响应中断,超时
  • 扩展设计:ConditionObject与等待队列(含源码篇幅较长)
  • 设计模式巧思:模板方法
  • AQS的应用:ReentrantLock等

AQS整体结构

首先,我们先从AQS的类图看一下整体结构和设计。

AQS类图


由类图可以看出,AQS(AbstractQueuedSynchronizer)继承了AbstractOwnableSynchronizer,包含了Node、ConditionObject对象。

  • AbstractOwnableSynchronizer提供了实现独占模式的线程的方法,由AQS继承。

  • AQS有一些内置变量,status、head、tail等,这些都是AQS重要的结构设计,status是锁资源的标识;head、tail是CLH结构的设计。

  • 从AQS的方法内看出,提供了独占模式的方法,也提供了共享模式(shared)的方法,还包含有响应中断(interruptibly)的方法;

  • Node对象,是AQS基础对象,是一个最小资源单元对象,一个Node持有一个线程引用(thread),用Node来表示线程的状态;

  • ConditionObject对象,是AQS里的扩展设计,同样用了变体CLH的数据结构,实现了等待队列。


state状态值

在AQS中,同步状态标识status设置为全局变量,并用volatile关键字修饰,可以由多个线程及时获取到此时的锁的状态。

我们看一下state的源码:

     /**	
     * The synchronization state.	
     * 同步器的状态
     */	
    private volatile int state;	
    /**	
     * getter方法,获取state	
     */	
    protected final int getState() {	
        return state;	
    }	
    /**	
     * Setter方法,设置state	
     */	
    protected final void setState(int newState) {	
        state = newState;	
    }	
    /**	
     * 原子修改当前值,且expect的值等于旧值时修改成功。即CAS设置	
     */	
    protected final boolean compareAndSetState(int expect, int update) {	
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);	
    }

state有几个特点

  • state用volatile修饰,保证多线程中的可见性。
  • getState() 和 setState() 方法采用final修饰,限制AQS的子类重写它们两。
  • compareAndSetState()方法采用乐观锁思想的CAS算法,也是采用final修饰的,不允许子类重写。

例如在独占模式中,每次增加节点即调用获取方法acquire(1),其中头结点会CAS将state从0设为1,用state来表示锁的持有状态。因此state是用来标记锁资源的重要标识。


Node对象

在AQS中,Node对象是最小资源单元对象,是构成变体CLH的重要结构,一个Node持有一个线程引用(thread),用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;
        static final int PROPAGATE = -3;

  			// 此时节点处于 上面状态中的某个值 (-3 -- 1)
        volatile int waitStatus;
				// 指向前驱节点
        volatile Node prev;
				// 指向后继节点
        volatile Node next;
				// 线程引用
        volatile Thread thread;
				// 指向等待获取condition 唤醒条件的队列节点
        Node nextWaiter;
    }

在Node节点的定义中,存在五种等待状态:

				// 节点的等待状态
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

        /**
         *   CANCELLED: 因为超时、中断 被取消;节点进入取消状态永远不能再变化(即不能重新唤醒)。而且,取消状态的节点永远不会再被阻塞。
         *   SIGNAL: 此节点的后续节点已经被park方法阻塞,当此节点释放/取消时需要通过unpark方法唤醒后续节点。
         * 为了避免竞争,acquire方法会先提供一个信号,然后后续节点尝试原子获取(cas),如果失败就阻塞。
         *   CONDITION:  此节点正处于条件队列中(condition queue)等待获取条件,除非节点被移出,移出的同时会设置节点状态为0。
         *   PROPAGATE:  共享模式头结点的无条件传播。引入这个状态是为了优化锁竞争,让队列中的节点能有序的一个个唤醒。
         *   0: 以上四个之外的状态,一般是节点的初始态。
         */
        volatile int waitStatus;

AQS-Node状态


CLH队列锁

在AbstractQueuedSynchronizer源码的注释中写道:AQS是CLH队列锁的变体,CLH队列锁通常使用的是自旋锁。

CLH锁是一种基于链表的可扩展、高性能的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

在传统的CLH队列中,头结点拥有锁,后续节点会一直自旋等待状态改变,发现前驱节点释放锁并修改状态值后,结束自旋并尝试去争取锁,头结点出队。

CLH新增的节点Node会将自己的prev指向之前的 尾节点tail,并修改tail指向新节点,加入队尾。

因此CLH是一种头结点出列,新增插入尾节点的一种FIFO(先进先出)单链表队列,等待节点会自旋获取状态值。
CLH队列结构

AQS 中的变体CLH

在AQS中,数据结构使用的是变体的CLH,每个Node节点结构变得更为复杂一些。

在AQS中,Node是构成变体CLH的重要结构,一个Node持有一个线程引用(thread),在CLH基础上,多出了双向指针用于形成双向连边,每个Node对象都存在prev、next两个指针,形成了双向链表。因此AQS中的CLH结构入下:
AQS-变体CLH队列结构


我们可以看出,AQS中的变体仍然保持CLH的一些特性,FIFO,头结点持有锁,运行结束后出列;新增节点插入队尾。

与传统CLH不同的是,在Node结构中增加next指针,形成双向队列,可以循环遍历。而AQS的成员变量中存在head、tail,分别代表了队列的头节点、尾节点,加上双向队列的设计,持有他们AQS即持有了整个队列。


AQS中的两种模式

在AQS的类图中,AQS继承了AbstractOwnableSynchronizer,AbstractOwnableSynchronizer提供了实现独占模式的线程的方法。

/**
 * A synchronizer that may be exclusively owned by a thread.  This
 * class provides a basis for creating locks and related synchronizers
 * that may entail a notion of ownership.  The
 * {@code AbstractOwnableSynchronizer} class itself does not manage or
 * use this information. However, subclasses and tools may use
 * appropriately maintained values to help control and monitor access
 * and provide diagnostics.
 * 
 * 一种可能被线程独占的同步器。这个类提供了创建锁和关联同步器的基础,
 * 意味着同步器可能存在所有权的概念。 这个类{@code AbstractOwnableSynchronizer}
 * 本身不管理 或者拥有这些信息,然而,子类和工具可能使用适当的状态值(state)去帮助控制和
 * 监视所有权,并提供诊断(conditionObject)。
 *
 * @since 1.6
 * @author Doug Lea
 */
public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
  
    /**
     * Empty constructor for use by subclasses.
     * 供子类使用的空构造函数
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * The current owner of exclusive mode synchronization.
     * 当前持有独占模式同步器的线程
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * set方法,设置独占线程
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * get方法,获取独占线程
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

实际上,AQS提供了独占模式共享模式两种方式。我们可以从方法名称看到AQS内部提供的两套实现,其中包含了可中断的阻塞方法:

// 独占模式方法
# acquire(int arg) : void -- 不可中断的加锁
# acquireInterruptibly(int arg) : void --可中断的加锁
# tryAcquireNanos(int arg, long nanosTimeout): boolean --带超时时长的可中断加锁
  
# release(int arg) : boolean --解锁

  
// 共享模式方法
# acquireShared(int arg) : void
# acquireSharedInterruptibly(int arg) : void
# tryAcquireSharedNanos(int arg, long nanosTimeout) : boolean
  
# releaseShared(int arg) : boolean

AQS 两种模式对比

AQS中的主流程:加锁、解锁,分别存在两种模式:共享模式、独占模式;这两种的主操作流程其实没有区别,主要区别在于,独占模式只允许一个线程获得资源,而共享模式允许多个线程获得资源,但是获得资源是原子性的,即全都成功才成功。
AQS-加锁流程图
AQS-解锁流程图


AQS 的独占模式

AQS使用变体的CLH — 双向链表来管理请求同步的Node,新的Node将会被插到链表的尾端,而链表的head总是代表着获得锁的线程,链表头的线程释放了锁之后,后面的节点会监视到状态,去竞争共享变量state。下面看一下AQS是如何实现独占模式下的acquire和release的。

获取acquire

首先看一下获取acquire方法的整体流程。
AQS-独占模式加锁

获取方法即将节点插入同步队尾,自旋等待,直到获取到锁的操作。以下贴出整个调用链路的源码:

    // 获取acquire方法完整调用链路
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 用中断的方法,让线程可以退出等待状态,变为就绪
            selfInterrupt();
    }

    // tryAcquire提供了一个模板方法,AQS中没有具体实现,交由子类实现
    // CAS尝试竞争state状态值,state特点中说过:AQS中用final修饰state的get/set,因此子类需自行实现,这种交由子类实现的特性,让实现同步器的方法可以自行定制独占/共享模式
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // CAS将节点加入队尾
    private Node addWaiter(Node mode) {
        // Node EXCLUSIVE = null -> mode(nextWaiter)
        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;
    }

    // 循环重试一直到成功加入队尾,返回前置节点,即旧的尾节点
    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)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
   
    // 自旋竞争state,竞争state失败,证明其他线程持有锁;竞争成功,让前驱节点出队
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // 前驱节点为头结点 且此节点CAS竞争state成功
                if (p == head && tryAcquire(arg)) {
                    // 将当前节点设为头结点,并切断前驱结点,让其出队
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 设置前驱节点的状态为-1,并unpark此节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                // 最终失败,取消竞争操作
                cancelAcquire(node);
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

    // 检测并设置前驱节点的状态,设置前驱成功,则park阻塞当前线程
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; 
        if (ws == Node.SIGNAL)
            // SIGNAL=-1,前驱节点 成为了 头结点/获取到锁的节点
            return true;
        if (ws > 0) {
            // CANCELLED=1,前驱节点已被取消,跳过前驱节点并重试
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 基本上是从初始态0,CAS尝试设置前驱节点状态为-1 SIGNAL,标识当前节点正在等待被唤醒
            // waitStatus必须是0或者-3,保证不会在park之前被唤醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    // 阻塞当前线程,并检查中断状态
    private final boolean parkAndCheckInterrupt() {
        // 阻塞当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }


在以上源码中我们需要注意的是,在 acquireQueued方法中节点会自旋等待获取锁,而结束自旋的条件如下:

  • 自旋到前驱节点获得锁的时候,当前节点可以安心被park阻塞,interrupted=true;则当前驱节点释放锁,可以unpark当前节点,返回interrupted的值 (true)。
  • 前驱节点成为了头节点,头结点即拥有锁的节点;且tryAcquire 返回true,意味着当前CAS竞争state成功,前驱节点释放锁。
  • 当前节点竞争到锁,设置为头结点,将运行完毕的旧头结点指针设为空,成功出队。即节点的 出/入 同步队列都是在acquire方法中操作。

释放release

接下来我们看看release释放方法。
AQS-独占模式解锁

释放方法即将拥有资源的头结点设为初始态0,并唤醒条件适合的第一个后继节点,等待将头节点出队释放。以下贴出整个调用链路的源码:

    // 释放release方法完整调用链路
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            // 头结点 不为空 且 为-1 表明后继节点等待被唤醒
            if (h != null && h.waitStatus != 0)
                // 将节点CAS设置为0,并唤醒后继条件合适的一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    // tryRelease 同样也是提供了一个模板方法,AQS中没有具体实现,交由子类实现
    // ReentrantLock中的实现,释放state,CAS设置为0,保证资源的安全性
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 将节点CAS设置为0,并唤醒后继条件合适的一个节点
    private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0) //CAS 设置为0,验证后继节点是否在等待被唤醒
            compareAndSetWaitStatus(node, ws, 0);
 
        Node s = node.next;
        // 如果头结点的 下一个节点为空 或者已取消(只有取消状态=1 >0)
        if (s == null || s.waitStatus > 0) {
            s = null;
          
            // 双向链表的用处:
            // 自尾节点往前遍历,遍历到头结点后面第一个符合唤醒条件的节点,unpark唤醒它
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒指定的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

释放的方法比入队简单的多,理解起来也不复杂。

以上就是独占模式下的出入队,若是希望线程可以在竞争的时候被中断,可以使用acquireInterruptibly。如果希望加上获取锁的时间限制,也可以使用tryAcquireNanos(int, long)方法来获取。他们两个方法不再赘述,可参考独占模式下的出入队原理对照理解即可。


AQS 的共享模式

需要注意的是,共享模式和独占模式的区别在于,独占模式只允许一个线程获得资源,而共享模式允许多个线程获得资源,但是获得资源是原子性的,即全都成功才成功。


获取acquireShared

我们看一下获取acquireShared方法,以下贴出整个调用链路的源码:

    // 获取acquireShared方法完整调用链路
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    // 提供了一个模板方法,AQS中没有具体实现,交由子类实现
    // Semaphore中的实现,比较当前可运行的 线程资源 和 入队个数,返回 线程资源-入队个数
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    // 自旋等待竞争state
    private void doAcquireShared(int arg) {
        // 将节点加入同步队尾 Node SHARED = new Node() -> mode(nextWaiter)
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    // 前驱节点是头结点,CAS发现仍有可竞争的资源
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        // 将当前节点放到队头 状态设置为0,且切断与头结点的连接
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            // 使用中断的方法,让线程退出等待状态,变为就绪
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 直到前驱节点获得锁,park当前节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    // CAS将节点加入队尾,如果失败,enq方法自旋直至成功加入队尾
    private Node addWaiter(Node mode) {
        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;
    }

    // 当前节点设置为0,取消绑定的线程,取消前驱指针
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // 记录头结点用于检查后继节点
        setHead(node);
        
        // 无条件传播 或旧头结点为空 或旧头结点为-1 SIGNAL 或新头结点为空 或新头结点为-1 SIGNAL 则取新头结点后继节点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
          
            // 后继节点为空 或后继节点也是共享模式节点
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

    // 头结点设置为0并唤醒后继节点,若期间头结点被改变则设置其为无条件传播,下次可以跳过此节点
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    // CAS将运行的节点 -1设置为0,检测后继节点是否正等待唤醒
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; 
                    // 再次检查头结点cas设置为0,并唤醒后继第一个条件合适的节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    // 设置头结点为无条件传播,下次可以跳过,保证传播性
                    continue;   //CAS失败继续循环,继续循环,直到节点 = -1运行   
            }
            // 如果头结点被改变了继续循环,设置为无条件传播,进入循环获取新的头结点继续判断
            if (h == head)  
                break;
        }
    }

    // 再次cas设置头结点为0,并唤醒后继第一个条件合适的节点
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 再次检查节点是否设置为0,未设置CAS设置为0
        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);
    }

    // 检测前驱节点的状态,返回当前节点是否竞争失败,失败后需park阻塞
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; 
        if (ws == Node.SIGNAL)
            // 前驱节点 成为了 头结点/获取到锁的节点
            return true;
        if (ws > 0) {
            // 前驱节点已被取消,跳过前驱节点并重试
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // CAS尝试设置前驱节点状态为-1 SIGNAL,标识当前节点正在等待被唤醒
            // waitStatus必须是0或者-3,保证不会在park之前被唤醒
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    // park阻塞当前线程,并检查中断状态
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可以看到,共享模式下的获取acquire方法整体流程与独占模式的大体一致,区别点仅有两个地方:

  • tryAcquireShared的实现,在Semaphore中,此方法实现大概为获取当前可运行的线程个数,返回available - acquires,即可运行的线程数 - 入队阻塞个数。以此方法实现多线程共享模式。
  • addWaiter(mode) 传入的mode不同,共享模式下 SHARED = new Node()。
  • 共享模式可以获得多个资源,在自旋获取state时,若前驱节点已经获得资源,且资源数量>=0,则切断头结点,将当前节点设为新的头结点且状态设为0,用中断的方式由等待态变为就绪。

释放releaseShared

我们看一下releaseShared释放方法,以下贴出整个调用链路的源码:

    // 释放releaseShared方法完整调用链路
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    // tryReleaseShared 同样也是提供了一个模板方法,AQS中没有具体实现,交由子类实现
    // Semaphore中的实现是,CAS设置 上一次剩余可运行线程数 为 当前可运行线程数 + 释放个数
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

    // 确保一个释放行为的传播性,即使有其他正在进行的获取/释放操作;这个方法通常会尝试唤醒头结点的后继节点,如果它处于等待唤醒的状态,如果不是,那么设置为 PROPAGATE -3 传播状态,保证在此释放结束之内,传播能继续下去。此外,我们必须循环,以防在执行此操作时添加新节点。另外,与其他的unparkSuccessor用法不同,我们需要知道CAS重置状态是否失败,如果是,则重新检查。
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    // 头结点cas设置为0,检验后继阶段是否正在等待唤醒 -1 -> 0
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;   // 循环检查
                    // unpark唤醒后继节点
                    unparkSuccessor(h);
                }
                // cas设置为-3 无条件传播
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;      // cas失败继续循环
            }
            if (h == head)         // 如果头结点变更,取当前新头结点状态继续循环
                break;
        }
    }

    private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0) //CAS 设置为0
            compareAndSetWaitStatus(node, ws, 0);
 
        Node s = node.next;
        // 如果头结点的 下一个节点为空 或者已取消(只有取消状态=1 >0)
        if (s == null || s.waitStatus > 0) {
            s = null;
          
            // 双向链表的用处:
            // 自尾节点往前遍历,遍历到头结点后面第一个符合唤醒条件的节点,unpark唤醒它
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

锁的属性:公平性,响应中断,超时

接下来说一下锁的几个属性,公平性,响应中断,超时,这几个属性也都分别对应了一个方法。


公平性

为何会有公平性这一说呢,在AQS源码中并没有看到,但是到在应用工具类里,即继承了AQS的实现中,例如在ReentrantLock里,可以看到Sync和FairSync这一组继承AQS的实现类,里面存在nonfairTryAcquire和fairTryAcquire两个方法。

因此公平和非公平这是在应用层所衍生出来的锁属性。

那么它们有什么差异呢,总结来说,非公平性即立即尝试插一次队,我们从源码入手:
AQS-公平性源码对比
由源码可以看出,他俩只有一点差异,即非公平性的锁会立即尝试一次竞争锁,如果成功则插队;否则后续逻辑与公平性的锁一样,自旋等待前一节点成为头结点后才竞争锁。

响应中断

响应中断和超时,都是AQS里加锁操作时的异常处理场景,可以视为应用级别的锁属性,我们把AQS中存在的方法体列出来,可以看到存在可中断、带超时时长的加锁方法。

// 独占模式方法
# acquire(int arg) : void -- 不可中断的加锁
# acquireInterruptibly(int arg) : void --可中断的加锁
# tryAcquireNanos(int arg, long nanosTimeout): boolean --带超时时长的可中断加锁
  
# release(int arg) : boolean --解锁

  
// 共享模式方法
# acquireShared(int arg) : void --不可中断的加锁
# acquireSharedInterruptibly(int arg) : void --可中断的加锁
# tryAcquireSharedNanos(int arg, long nanosTimeout) : boolean --带超时时长的可中断加锁
  
# releaseShared(int arg) : boolean --解锁

简单来说,可中断即对线程的检查, 若线程已中断,抛出异常进行中断结束任务。以下是acquireInterruptibly的源码,我们来看看:

public final void acquireInterruptibly(int arg)
            throws InterruptedException {
  			// 检查线程的状态,若已中断抛出异常结束操作
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            // 和加锁acquire主流程一致,已经忘记了的话可以往前翻翻流程图
            doAcquireInterruptibly(arg);
    }

超时

和可中断相对应的另一种场景,就是超时,AQS允许设置超时时长的加锁,若超时同样抛出异常进行中断结束任务。我们看看tryAcquireNanos的源码:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        // 检查线程的状态,若已中断抛出异常结束操作
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

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;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                // 检查线程的状态,若已中断抛出异常结束操作
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

扩展设计:ConditionObject与等待队列

在AQS中,存在ConditionObject对象,也是采用的CLH变体结构设计的队列,可以视为AQS队列(文中后续称为Sync Queue)的双生队列,我们暂且称其为Condition Queue。

Condition Queue在AQS里的角色类似于缓冲区,即从Sync Queue移到Condition Queue上,可以自主操控节点处于等待环节;后续节点需要加锁时,可以移回Sync Queue等待系统调度,能操作线程暂停和恢复,从而实现精准顺序唤醒。
AQS与Condition


Condition中的操作

JUC包中存在Condition接口,它提供了await()和signal()两个阻塞和唤醒线程的方法,而ConditionObject就是其实现类。

在AQS中,ConditionObject 提供的await()和signal()方法,能够为多线程之间交互提供帮助,能让线程暂停和恢复,可以实现精准顺序唤醒,是很重要的方法。


await 等待

在Node的节点状态中,有等待状态 CONDITION = -2,此状态代表阶段处于等待状态,插入入ConditionObject 等待队列中。

在AQS中,调用了await方法时,当前正持有锁的头节点会被放入等待队列中。接下来我们看一下完整调用链路:

		// await完整调用链路
		public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
      			// 将当前线程绑定到等待节点,插入等待队列尾部,返回当前插入的节点
            Node node = addConditionWaiter();
      			// 释放当前持有锁的节点,保存当前持有锁的状态state,唤醒后继节点
            int savedState = fullyRelease(node);
      
      			// 0 表示等待过程,即在等待队列中未被中断,保证线程的有效性
      			// REINTERRUPT = 1;表示在移出等待队列,恢复到同步队列中时被重新中断
      			// THROW_IE = -1;表示在signal前被cancel中断退出等待队列,并抛出InterruptedException
            int interruptMode = 0;
      			
      			// 等待节点被锁定直到 要么被cancel中断,要么被重新中断,则break
      			// 要么被signal()方法唤醒,isOnSyncQueue:检查当前节点是否在同步队列中
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
              	// 不在同步队列中,即在等待队列中,检查是否被中断,保证线程的有效性
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
      			
      			// 已成功加入同步队尾,判断线程是被signal唤醒还是被cancel中断了
      			
      			// 尝试自旋竞争state 成功 且 在未signal前被中断
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)				
              	// 中断模式设置为 重新中断,此情况与signal 中的unpark对应,由于前驱节点CAS竞争失败,表明前驱节点已成为头结点,而作为后继节点却竞争成功,所以应重新中断
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) 
              	// 此节点的后继仍有等待节点,遍历等待队列清除不是等待的节点,包括当前节点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
              	// 再次检查中断状态,根据状态返回结果
                reportInterruptAfterWait(interruptMode);
        }

		// 先判断队尾节点状态,不为等待,则遍历等待队列清除不是等待状态的节点,并将当前节点插入等待队尾
		private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果最后一个等待节点已经取消等待,清除它
            if (t != null && t.waitStatus != Node.CONDITION) {
              	// 遍历等待队列,移出不是等待的节点
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
      			// 将当前线程绑定到Node 节点上,设置类型waiterState =CONDITION -2
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
              	// 尾节点为空,即等待队列为空,将新标记为等待的节点加入队尾
                firstWaiter = node;
            else
              	// 尾节点存在,等待队列有节点,插入队尾
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

		// 从等待头节点遍历等待队列,将不是等待状态的节点移出
		private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
              	// 取后继等待节点
                Node next = t.nextWaiter;
                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; //指向后继节点
            }
        }

		// 将当前运行的线程状态全部释放
		final int fullyRelease(Node node) {
        boolean failed = true;
        try {
          	// 获取当前state,即当前运行线程的状态,保存下来
            int savedState = getState();
          	// 释放线程(独占模式)
            if (release(savedState)) {
                failed = false;
              	// 保存出队前的状态,用于唤醒线程
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

		// 验证当前节点是否在同步队列中
		final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
          	// 节点状态仍为等待,或者前驱没有节点(同步队列从队尾插入,除了获取锁的头结点,一般都有前驱节点)
            return false;
        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;
        }
    }

		// 检查是否有中断,如果在single()之前中断,则返回-1;如果在single()之后,则返回1;如果没有中断,则返回0。
		private int checkInterruptWhileWaiting(Node node) {
      			// 在中断的前提下从等待队列移到同步队列成功返回 THROW_IE = -1;表示在single()之前,被cancel中断退出等待队列,并抛出InterruptedException 
      			// 在中断的前提下,节点状态已经从CONDITION 变为0了,且在同步队列中,返回 REINTERRUPT = 1;表示在signal()后被重新中断 
      			// 没有被中断返回 0 
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

		// 检测线程的中断状态
		public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

		// 节点取消等待状态将其从等待队列移出,放入同步队尾
		final boolean transferAfterCancelledWait(Node node) {
      	// 节点仍为等待状态,CAS从等待-2设置为0,设置成功证明等待节点还未被signal(),但是此时在signal前已经中断,则最后要抛出异常InterruptedException来表明节点状况
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
          	// 转移节点,从等待队列中拿出,循环插入同步队尾直到成功
            enq(node);
            return true;
        }
      	
      	// 节点已经不处于 CONDITION 状态,则节点已为0 ,即是在被single()之后
      	// 在一个转移过程中被取消是罕见的现象,即将节点从等待队列加入到队尾过程,所以自旋等待 signal方法中 enq 方法结束才可以操作,所以此时自旋即可
      	// 自旋判断节点是否已经在同步队列中,未在 则暂停线程让出资源
        while (!isOnSyncQueue(node))
            Thread.yield();
      	// 已经在同步队列,节点状态仍为 CONDITION,代表在signal后被重新中断了
        return false;
    }

		// 自旋竞争state,若竞争state失败,证明其他线程持有锁
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
              	// 前驱节点为头结点 且此节点CAS竞争state成功
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
              	// 自旋检测前驱节点state,竞争state,竞争失败执行park阻塞
              	// park的前提是检查到前驱节点拥有锁,释放后可以安心unpark此节点
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
              	// 最终失败,取消竞争操作
                cancelAcquire(node);
        }
    }

		// 根据状态返回结果,或者不操作,
		private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
              	//若为THROW_IE -1,则抛出中断异常;
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
              	// 若为 REINTERRUPT 1,表示再次被中断,则对当前线程进行中断操作
                selfInterrupt();
        }

signal 唤醒

与await 方法相对于的,就是signal 唤醒方法,它可以将被await 等待的线程进行唤醒。

接下来我们看一下signal 方法,以下是它的完整调用链路:

		//signal 方法完整调用链路
		public final void signal() {
    	if (!isHeldExclusively())
    		throw new IllegalMonitorStateException();
    	Node first = firstWaiter;
   		if (first != null)
    		doSignal(first);
    }

		// 同样也是提供了一个模板方法,AQS中没有具体实现,交由子类实现
		// 用于判断当前线程是否持有锁
		protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }
		// ReentrantLock中的实现:判断当前线程是否为拥有锁的独占线程
		protected final boolean isHeldExclusively() {
    	return getExclusiveOwnerThread() == Thread.currentThread();
    }

		// 遍历等待队列,将一个合适的等待节点设为0插入同步队尾,并unpark当前线程提供一个竞争机会
		private void doSignal(Node first) {
    	do {
    		if ( (firstWaiter = first.nextWaiter) == null)
          // 后继等待节点为空,即等待队列已经没有节点了,设置尾部指针为空
    			lastWaiter = null;
        // 当前等待首节点的后继指针切断,设为空
    		first.nextWaiter = null;
        
        // 等待首节点设置为0插入同步队尾失败 且 后继节点不为空,赋值为后继节点继续循环
    	} while (!transferForSignal(first) &&
    		(first = firstWaiter) != null);
    }

		// 将等待节点插入队尾,并设置为初始态=0,除了前置节点正在拥有锁的情况下,unpark当前线程提供一个竞争机会
		final boolean transferForSignal(Node node) {
        
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
          	// 设置等待节点状态为0失败
            return false;
				
      	// 设置节点为0成功,循环插入同步队尾直至插入成功,返回前置节点,即旧的尾节点
        Node p = enq(node);
      
        int ws = p.waitStatus;
      
      	// 只有CANCELLED = 1 > 0,如果 前置节点已经被取消,尝试唤醒当前线程
      	// 前置节点设置为 SIGNAL=-1 失败,意味着CAS竞争锁失败,尝试唤醒当前线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
      
      	// 只要成功插入同步队尾即算作成功
        return true;
    }

AQS设计模式巧思:模版方法

AQS设计为一个抽象类,但在他的类里却没有任何一个抽象方法,取而代之的是定义了很多抛出UnsupportedOperationException异常的空方法,这是为什么呢?

这就是AQS设计的巧思,模版方法的设计,AQS提供了一套加锁、解锁的模版方法,但是它的子类不需要实现所有方法,可以实现自己需要的方法即可,未实现的方法在调用是就会抛出异常,用这个设计保证了模版里方法的安全性。

这一个实现抽象时很好的设计思路,日常编码时非常值得借鉴。

protected boolean tryAcquire(int arg) {
    // 若子类未实现,调用此方法会抛出异常
    throw new UnsupportedOperationException();
}

AQS的应用

因为模版方法的设计,我们可以自定义实现自己的线程操作工具,在JUC包中也存在了一些官方提供的实现类工具,例如ReentrantLock、CyclicBarrier、CountDownLatch、Semaphore等常用的工具类,他们都各有特点,后续我们会展开说明。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188354.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java解决跨域问题时的403报错

什么是跨域问题&#xff1f; 当一个请求的url的协议&#xff0c;域名&#xff0c;端口三者之间任意一个与当前页面url不同 即为跨域 问题背景&#xff1a; 如图&#xff0c;前端端口为8090&#xff0c;而后端端口为8099&#xff0c;形成跨域&#xff0c;无法对接 试图利用Spr…

【MySQL面试高频误区】不可重复读和幻读到底有哪些区别?

欢迎关注公众号 【11来了】 &#xff0c;持续 MyBatis 源码系列内容&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址&#xff1a;点击查看文章导读&#xff01; 感谢你的关注&#xff…

zotero使用koofr+google drive/onedrive同步

最早了解到这个思路是来自 知乎-【从零开始使用Zotero】(3) Zotero文献同步方式 和 How to Sync Zotero Files Using WebDAV and Google Drive with Koofr: A Step-by-Step Guide 虽然周围很多人都在用Readpaper这种web端的了&#xff0c;但是经常逛Arxiv而且zotero的web插…

论文翻译 | Language Models are Few-Shot Learners 语言模型是少样本学习者(下)

6 更广泛的影响 语言模型对社会有着广泛的有益应用&#xff0c;包括代码和写作自动补全、语法辅助、游戏叙事生成、提高搜索引擎响应以及回答问题等。但它们也可能有潜在的危害性应用。GPT-3在文本生成质量和适应性方面优于较小的模型&#xff0c;并且增加了区分合成文本与人类…

使用GitLab CI构建持续集成案例

1. 部署GitLab &#xff08;1&#xff09;基础准备 解压软件包并导入镜像&#xff1a; [rootmaster ~]# curl -O http://mirrors.douxuedu.com/competition/Gitlab-CI.tar.gz [rootmaster ~]# tar -zxvf Gitlab-CI.tar.gz [rootmaster ~]# ctr -n k8s.io image import gitla…

408 笔记错题本

数据结构 树 线索二叉树 组成原理 第五章 计组 SISD 一两个指令和数据 不能采用数据并行技术&#xff0c;但是可以采用流水线技术&#xff0c;计组这门课 就是讨论的是SISD这个概念。 SIMD 应用&#xff1a;浮点机、向量处理机、优化for循环。 MISD 这个理论上是不存在…

考研论坛平台|考研论坛小程序系统|基于java和微信小程序的考研论坛平台小程序设计与实现(源码+数据库+文档)

考研论坛平台小程序 目录 基于java和微信小程序的考研论坛平台小程序设计与实现 一、前言 二、系统功能设计 三、系统实现 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂…

华为资源分享

紫光云文档中心提供弹性计算服务文档https://www.unicloud.com/document/product/ElasticComputeService/index.html报文格式华为报文格式资料Info-Finder&#xff08;在线工具&#xff09; 报文格式华为IP网络电子书华为IP网络相关电子书IP网络系列丛书 - 华为企业业务华为产品…

[云] DockerCoins 练习笔记

DockerCoins DockerCoins 由 5 个服务组成 rng (随机数生成器): 这是一个Web服务&#xff0c;它的任务是生成随机字节。随机字节通常用于加密、安全令牌生成、测试等场景。 hasher (哈希计算器): 这个服务接收数据&#xff08;通常是通过POST请求发送的数据&#xff09;&#…

202408第十五届蓝桥杯青少组省赛C++中级组题解

202408第十五届蓝桥杯青少组省赛C++中级组题解 单选题 1、定义 char a[]="hello\nworld",执行 cout<<a,输出结果是(B) A、helloworld B、hello world C、 hellonworld D、 hello\nworld 解析:\n输出时会换行。 2、 的结果是(C)。 A、 B、

商城网站建设怎么建

商城网站已经成为企业迅速拓展市场、提升品牌形象的重要利器。随着互联网技术的飞速发展&#xff0c;传统的实体店面已经不能满足消费者多样化的购物需求。因此&#xff0c;搭建一个功能强大、用户友好的商城网站成为企业不可或缺的一环。 首先&#xff0c;商城网站的建设需要具…

OpenCAEPoro安装与测试(ASC 2024)

首先简单总结一下经验&#xff1a; 在之前的测试和学习中&#xff0c;由于自己是虚拟机或者云服务器&#xff0c;有root权限&#xff0c;经常无脑增删文件&#xff08;主要是为了图省事&#xff0c;看见报错就加回来&#xff0c;多出来就删除&#xff09;&#xff0c;但是在经…

D 咖饮品机闪耀荆州动漫嘉年华

在今年的国庆佳节&#xff0c;恰逢金秋时节&#xff0c;荆州动漫嘉年华盛大开幕&#xff0c;这场充满活力与创意的盛会吸引了无数动漫爱好者的目光。而在众多精彩的展示中&#xff0c;D 咖饮品机的出现无疑为这场嘉年华增添了一抹别样的色彩。 走进嘉年华现场&#xff0c;热闹的…

ubuntu18.04运行OpenPCDet出现的问题

一、概述 在编译成功OpenPCDet的源代码之后&#xff0c;发现在运行demo时候&#xff0c;依旧出现了很多问题&#xff0c;在此对出现的问题进行总结记录。 二、问题分析 &#xff08;一&#xff09;ModuleNotFoundError:No module named av2 如图所示 这个问题比较简单&#x…

(C语言贪吃蛇)14.用绝对值方式解决不合理的走位

目录 前言 解决方式 运行效果 总结 前言 我们上节实现了贪吃蛇四方向走位&#xff0c;但是出现了一些不合理的走位&#xff0c;比如说身体节点和头节点重合等等&#xff0c;本节我们便来解决这个问题。 我们希望贪吃蛇撞到自身时游戏会重新开始&#xff0c;并且贪吃蛇的运动方…

TryHackMe 第6天 | Web Fundamentals (一)

这一部分我们要简要介绍以下 Web Hacking 的基本内容&#xff0c;预计分三次博客。 在访问 Web 应用时&#xff0c;浏览器提供了若干个工具来帮助我们发现一些潜在问题和有用的信息。 比如可以查看网站源代码。查看源代码可以 右键 网页&#xff0c;然后选择 查看网站源代码&…

Discord 用户突破2亿:CEO 谈发展规划、产品策略及游戏通信的未来

Discord,这个最初为游戏玩家打造的社区平台,如今已经发展成为一个拥有超过2亿月活跃用户的全球性社交网络。创始人兼CEO Jason Citron在经历了多次创业尝试后,最终将Discord打造成了一个不可或缺的游戏通信工具。以下是Jason Citron在接受GamesBeat采访时,对Discord的现状、…

招联金融2025校招内推

【投递方式】 直接扫下方二维码&#xff0c;或点击内推官网https://wecruit.hotjob.cn/SU61025e262f9d247b98e0a2c2/mc/position/campus&#xff0c;使用内推码 igcefb 投递&#xff09; 【招聘岗位】 后台开发 前端开发 数据开发 数据运营 算法开发 技术运维 软件测试 产品策…

MAE(平均绝对误差)和std(标准差)计算中需要注意的问题

一、MAE&#xff08;平均绝对误差&#xff09; 计算公式&#xff1a; yi​ 是第i个实际值y^​i​ 是第i个预测值 计算方法&#xff1a; MAE就是求实际值与预测值之间的误差&#xff0c;需要给出预测值和原始的实际值 二、std&#xff08;标准差&#xff09; 计算公式&#x…

螺蛳壳里做道场:老破机搭建的私人数据中心---Centos下Docker学习06(Docker网络连接)

如果要搭建基于docker的私人DC&#xff0c;除了虚拟机网络连接外&#xff0c;就得掌握docker的网络连接。磨刀不误砍柴工&#xff0c;或者说工欲善其事必先利其器&#xff0c;我们先学学典型的docker的网络连接方式。Docker的网络连接有四种&#xff1a;bridge、none、containe…