【Java 并发编程】深入理解 AQS - AbstractQueuedSynchronizer

news2024/12/24 2:16:29

深入理解 AQS - AbstractQueuedSynchronizer

  • 1. AQS
    • 1.1 什么是 AQS
    • 1.2 AQS 具备的特性
  • 2. AQS 原理解析
    • 2.1 AQS 原理概述
      • 2.1.1 什么是 CLH 锁
      • 2.1.2 AQS 中的队列
    • 2.2 AQS 共享资源的方式:独占式和共享式
      • 2.2.1 Exclusive(独占式)
      • 2.2.2 Share(共享式)
    • 2.3 AQS 底层使用了模板方法模式
    • 2.4 AQS 定义了两种队列
      • 2.4.1 AQS 定义了5个队列中节点状态
      • 2.4.2 同步等待队列
      • 2.4.3 条件等待队列
        • Condition 接口
  • 3. AQS 源码分析
    • 3.1 ReentrantLock 概述
    • 3.2 创建重入锁
    • 3.3 公平锁/⾮公平锁的 lock()
      • 3.3.1 公平锁 NonfairSync.lock() 方法
      • 3.3.2 非公平锁 NonfairSync.lock() 方法
      • 3.3.3 acquire()
      • 3.3.4 FairSync.tryAcquire(arg)
      • 3.3.5 NonFairSync.tryAcquire(arg)
      • 3.3.6 addWaiter(Node.EXCLUSIVE)
      • 3.3.7 acquireQueued(...)
    • 3.4 解锁
    • 3.4.1 tryRelease()
    • 3.4.2 unparkSuccessor

1. AQS

在介绍 AQS 之前,先一起回顾一下 CAS(Compare And Swap)。

1.1 什么是 AQS

AQS,全名 AbstractQueuedSynchronizer,是一个抽象同步队列,它的内部通过维护一个状态 volatile int state(共享资源的状态),一个 FIFO 线程等待队列来实现同步功能。

Java 并发包很多工具类底层都是基于 AQS 来实现的,比如:Lock 工具 ReentrantLock、栅栏 CountDownLatch、信号量 Semaphore 等。

1.2 AQS 具备的特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

2. AQS 原理解析

2.1 AQS 原理概述

AQS 内部维护 state 属性,state 用关键字 volatile 修饰,代表着该共享资源的状态一更改就能被所有线程可见,而 AQS 的加锁方式本质上就是多个线程在竞争 state,当 state 为 0 时代表线程可以竞争锁,不为 0 时代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个 FIFO 的等待队列中,这些线程会被 UNSAFE.park() 操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。

2.1.1 什么是 CLH 锁

  • 由 Craig、Landin 和 Hagersten 三位大佬发明,因此命名为 CLH 锁;

  • 单向链表实现的队列;

  • 是一个自旋公平(FIFO)锁,能确保无饥饿性;

  • 申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱节点释放了锁就结束自旋

一起看一下线程加锁的过程:

  1. 首先获得当前线程的当前节点 curNode,这里每次获取的 CLHNode 节点的 locked 状态都为false;

  2. 然后将当前 CLHNode 节点的 locked 状态赋值为 true,表示当前线程的一种有效状态,即获取到了锁或正在等待锁的状态;

注意,为了保证 locked 属性线程间可见,该属性被 volatile 修饰。

  1. 线程对 tail 域调用 getAndSet 方法,使自己成为队列的尾部,同时获取一个指向其前趋结点的引用 PreNode;

  2. 因为尾指针 tailNode 的总是指向了前一个线程的 CLHNode 节点,因此这里利用尾指针 tailNode 取出前一个线程的 CLHNode 节点,然后赋值给当前线程的前继节点 preNode,并且将尾指针重新指向最后一个节点即当前线程的当前 CLHNode 节点,以便下一个线程到来时使用;

  3. 根据前继节点(前一个线程)的 locked 状态判断,若 locked 为 false,则说明前一个线程释放了锁,当前线程即可获得锁,不用自旋等待;若前继节点的 locked 状态为 true,则表示前一线程获取到了锁或者正在等待,自旋等待。

在这里插入图片描述

如上图:有3个并发线程同时启动执行 lock 操作,假如3个线程的实际执行顺序为:T1、T2、T3

  1. 线程1 过来,执行了 lock 操作,获得了锁,此时 locked 状态为 true

  2. 线程2 过来,执行了 lock 操作,由于线程1 还未释放锁,此时自旋等待,locked 状态也为 true

  3. 线程3 过来,执行了 lock 操作,由于线程2 处于自旋等待,此时线程3 也自旋等待(因此 CLH 锁是公平锁),locked 状态也为 true

以下为 CLH 锁的释放锁过程:

  1. 首先从当前线程的线程本地变量中获取出当前 CLHNode 节点,同时这个 CLHNode 节点被后面一个线程的 preNode 变量指向着;

  2. 然后将 locked 状态置为 false 即释放了锁;

注意:locked 因为被 volitile 关键字修饰,此时后面自旋等待的线程的局部变量 preNode.locked 也为 false,因此后面自旋等待的线程结束 while 循环即结束自旋等待,此时也获取到了锁。这一步骤也在异步进行着。

  1. 然后给当前线程的表示当前节点的线程本地变量重新赋值为一个新的 CLHNode。

2.1.2 AQS 中的队列

AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配:

在这里插入图片描述
AQS 中的 CLH 变体等待队列拥有以下特性:

  • AQS 中队列是个双向链表,也是 FIFO 先进先出的特性

  • 通过 head、tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性

  • head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程

  • 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改:

// 共享变量,使用 volatile 关键字修饰保证线程可见性
private volatile int state;

资源的可用状态通过 protected 类型的 getState、setState、compareAndSetState 进行操作:

// 返回同步状态的当前值
protected final int getState() {
    return state;
}

// 设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}

// 如果当前状态值等于期望值,则原子地将同步状态设置为给定的更新值。
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2.2 AQS 共享资源的方式:独占式和共享式

AQS 定义了两种资源共享方式 :独占式(Exclusive)和共享式(Share)。

2.2.1 Exclusive(独占式)

只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁。

2.2.2 Share(共享式)

多个线程可以同时执行,如 Semaphore/CountDownLatch。

2.3 AQS 底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用) :

  1. 使用者继承 AbstractQueuedSynchronizer重写指定的方法(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)。

  2. AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,感兴趣的可以参考:https://refactoringguru.cn/design-patterns/template-method

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。

  • tryAcquire(int)独占方式。尝试获取资源,成功则返回 true,失败则返回 false。

  • tryRelease(int)独占方式。尝试释放资源,成功则返回 true,失败则返回 false。

  • tryAcquireShared(int)共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

  • tryReleaseShared(int)共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。

2.4 AQS 定义了两种队列

  • 同步等待队列: 主要用于维护获取锁失败时入队的线程。

  • 条件等待队列: 调用 await() 的时候会释放锁,然后线程会加入到条件队列,调用 signal()/signalAll() 唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁。

2.4.1 AQS 定义了5个队列中节点状态

  1. 值为0,初始化状态,表示当前节点在 sync 队列中,等待着获取锁。

  2. CANCELLED,值为 1,表示当前的线程被取消;

  3. SIGNAL,值为 -1,表示当前节点的后继节点包含的线程需要唤醒,也就是 unpark;

  4. CONDITION,值为 -2,表示当前节点在等待 condition,也就是在 condition 队列中;

  5. PROPAGATE,值为 -3,表示当前场景下后续的 acquireShared 能够得以执行;


/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;

2.4.2 同步等待队列

AQS 当中的同步等待队列是 CLH 的变体,见:2.1.12.1.2

  1. 当前线程如果获取同步状态失败时,AQS 则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到 CLH 同步队列,同时再一次尝试获取锁,如果获取失败则会阻塞当前线程;

  2. 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态;

  3. 通过 signal/signalAll 将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。

在这里插入图片描述

2.4.3 条件等待队列

AQS 中条件队列是使用单向列表保存的,用 nextWaiter 来连接:

  • 调用 await 方法阻塞线程;

  • 当前线程存在于同步队列的头结点,调用 await 方法进行阻塞(从同步队列转化到条件队列)

Condition 接口

在这里插入图片描述

  1. 调用 Condition#await 方法会释放当前持有的锁,然后阻塞当前线程,同时向 Condition 队列尾部添加一个节点,所以调用 Condition#await 方法的时候必须持有锁。

  2. 调用 Condition#signal 方法会将 Condition 队列的首节点移动到阻塞队列尾部,然后唤醒因调用 Condition#await 方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用 Condition#signal 方法的时候必须持有锁,持有锁的线程唤醒被因调用 Condition#await 方法而阻塞的线程。

3. AQS 源码分析

3.1 ReentrantLock 概述

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。当我们想要使⽤重⼊锁的时候,使⽤⽅式⼀般是如下3个步骤:

public class ReentrantLockTest {

    /** 1. 创建重入锁  */
    ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        // 2. 加锁 block until condition holds
        lock.lock();
        
        try {
            // do something
        } catch (Exception e) {
            // ...
        } finally {
            // 3. 解锁
            lock.unlock();
        }
    }
}

后面我们就是针对这3个步骤对其源码进⾏解析。在此之前,先一起看一下 Sync、FairSync、NonfairSync 是在哪⾥被使⽤的。

ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。

/** Synchronizer providing all implementation mechanics */
private final Sync sync;

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

观察源码可以发现 FairSync、NonfairSync 都是继承 Sync,而 Sync 又继承于 AbstractQueuedSynchronizer。

static final class FairSync extends Sync
static final class NonfairSync extends Sync
abstract static class Sync extends AbstractQueuedSynchronizer

3.2 创建重入锁

ReentrantLock 默认是非公平的:

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

当我们使⽤⽆参构造⽅法去创建重⼊锁的时候,底层使⽤的是⾮公平锁

  • 当⼊参 fair 等于 false 的时候,采⽤的就是⾮公平锁 - NonfairSync

  • 当⼊参 fair 等于 true 的时候,采⽤的就是公平锁 - FairSync

3.3 公平锁/⾮公平锁的 lock()

3.3.1 公平锁 NonfairSync.lock() 方法

公平锁直接就执行 acquire() 排队等待:

/**
 * 公平锁
 */
static final class FairSync extends ReentrantLock.Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 执行等待
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 和非公平锁相比,这里多了一个是否有线程在等待的判断
            if (!hasQueuedPredecessors() &&
                    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;
    }
}

3.3.2 非公平锁 NonfairSync.lock() 方法

/**
 * 非公平锁
 */
static final class NonfairSync extends ReentrantLock.Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * 立即执行抢占锁操作。如果抢占成功,则加锁;如果抢占失败,则等待
     */
    final void lock() {
        // AQS的state如果成功被设置为1,则表示本线程已抢占锁成功;否则,抢占锁失败
        if (compareAndSetState(0, 1))
            // 将 AOS.excLusive0wnerThread 设置为当前线程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 执行等待
            acquire(1);
    }

    // 是否获得非公平锁
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

先判断是否有锁,0 是无锁、1 是有锁,等于 0 的话则修改成 1 并且获得锁:

// 如果当前状态值等于期望值 0,则自动将同步状态设置为给定的更新值 1
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

设置当前独占锁的拥有者线程:

protected final void setExclusiveOwnerThread(Thread thread) {
    // 将当前线程设置到这把独占锁上
    exclusiveOwnerThread = thread;
}

抢锁失败就进行则等待:

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

公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tyAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

非公平锁如果这两次 CAS 都不成功,那么后面和公平锁是一样的, 都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态

3.3.3 acquire()

⽆论是公平锁还是⾮公平锁,他们都有机会调⽤相同的⽅法,即:acquire(1)

/**
 * 以独占模式获取,忽略中断 (interrupts)
 * 通过调用至少一次 tryAcquire(int) 来实现,成功返回。否则线程排队,可能重复阻塞和解除阻塞,调用 tryAcquire(int) 直到成功
 * 此方法可用于实现方法 Lock.lock()
 */
public final void acquire(int arg) {
    /**
     * tryAcquire(1): 进行抢锁操作,返回是否抢锁成功
     * addWaiter(Node.EXCLUSIVE): 构建一个独占式节点 Node,维护好该节点的前后指针 Node
     * acquireQueued(addWaiter(Node.EXCLUSIVE), 1) 判断获取锁失败的时候,是否应该挂起该线程,如果是,则挂起当前线
     */
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 设置当前线程的中断标识
        selfInterrupt();
}

也就是分为三步 tryAcquire(尝试抢锁)、addWaiter(构建节点加入到队列中)、acquireQueued(获取锁失败的时候将线程挂起)

3.3.4 FairSync.tryAcquire(arg)

/**
 * 进行抢锁操作,返回是否成功
 *
 * case1> 没人抢占锁(state==0),线程A尝试执行抢占锁操作,如果抢占成功,则返回true; 如果抢占失败,则返回false。
 * case2> 有人已经抢占了这个锁(state != 0),但是抢占这个锁的线程就是自己,那么对自己执行重入加锁操作,返回true;如果不是自己抢占的锁,那么返回false。
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    /** case1: 如果c等于0,说明可以抢占锁 */
    if (c == 0) {
        // 如果线程不需要排队 && 抢占锁成功(即:如果state=0,则将该值修改为1,CAS操作成功)
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    /** case2: 如果c不等于0,判断是否是重入操作 (即: 锁本来就是被自己抢占的,支持多次抢占) */
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

通过获取 state 来判断锁是否被获取,等于 0 的话不要排队直接获取锁,可以看到比 NonFairSync 多了 hasQueuedPredecessors 是否需要进入队列排队。

如果不等于 0,判断是否是重入;否则返回 false。

/**
 * 主要是用来判断线程需不需要排队。true:线程需要排队。false:线程不需要排队。
 * 因为队列是 FIF0 的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示线程需要排队;没有则返回 false 表示线程无需排队;
 */
public final boolean hasQueuedPredecessors() {
    // 读取头节点
    Node t = tail;
    // 读取尾节点
    Node h = head;
    // s是首节点h的后继节点
    Node s;

    /**
     * h != t -> 队列中有 >= 2个节点
     * (s = h.next) == null -> 头节点没有后继节点,即:只有自己一个node或者创建了h的后置节点,但是还没有执行h.next=node
     * s.thread != Thread.currentThread() -> 第二个节点承载的线程不是当前线程
     */
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

3.3.5 NonFairSync.tryAcquire(arg)

/**
 * 是否获得非公平锁
 */
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

/**
 * 进行抢锁操作,是否抢到非公平锁
 *
 * 处理内容:
 * 1>如果抢到锁,返回true
 *   1.1>如果当前线程第一次抢到锁:
 *        AQS.state由0变为1
 *        AQS.exclusiveOwnerThread=Thread.currentThread()
 *        返回true
 *   1.2>如果当前线程再次抢到锁(重入加锁):
 *        AQS.status++
 *        返回true
 * 2>如果没抢到锁,返回false
 */
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()) {
        /**
         * 获得当前独享线程,如果就是当前线程,那么执行重入操作
         * 执行tryLock()时:
         *      如果第二次进入,则 nextc = 0 + 1 = 1
         *      如果第三次进入,则 nextc = 1 + 1 = 2
         *      如果第四次进入,则 nextc = 2 + 1 = 3
         */
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

3.3.6 addWaiter(Node.EXCLUSIVE)

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;
}
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;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

3.3.7 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);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

3.4 解锁

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

3.4.1 tryRelease()

首先执行tryRelease,再执行 unpark 操作

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

3.4.2 unparkSuccessor

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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);
}

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

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

相关文章

用 GPT-4 来面试,简直开挂啊!

公众号关注 “GitHubDaily” 设为 “星标”&#xff0c;每天带你逛 GitHub&#xff01; 众所周知&#xff0c;ChatGPT 凭其超强的文本生成能力&#xff0c;成为了 2023 年最为火爆的 AI 应用之一。 几个月前&#xff0c;GPT-4 发布&#xff0c;又将 ChatGPT 的能力提升到了一个…

redis为何这么快

文章目录 概述基于内存的操作高效的数据存储结构设计高效的数据结构string底层实现SDS字符串长度处理杜绝缓冲区溢出减少内存重新分配的次数空间预分配惰性空间释放 list底层实现压缩列表(zipList)双端链表(linkList) hsah底层实现ziplist字典 set底层实现 zset底层实现ziplist…

“大厂的人一毕业,讲师就多了起来”——但培训行业,早就卷起来了

“大厂的人一毕业&#xff0c;讲师就多了起来”&#xff0c;很多中年产品经理都把去做培训当成一个后备选项&#xff0c;也许&#xff0c;作为十几年前就淌过路的人&#xff0c;可以给你一些信息。 总体来说&#xff0c;今年有个特别的体感&#xff0c;就是产品经理/产品思维/产…

【分布式应用】ELFK集群部署(Filebeat+ELK)Logstash的过滤模块

一、ELFK集群部署&#xff08;FilebeatELK&#xff09; ELFK ES logstashfilebeatkibana 实验环境 服务器类型系统和IP地址需要安装的组件硬件方面node1节点192.168.126.21JDK、elasticsearch-6.7.2、kibana-6.7.22核4Gnode2节点192.168.126.22JDK、elasticsearch-6.7.22核4…

chatgpt赋能python:Python中的快捷键:提高编程效率的利器

Python中的快捷键&#xff1a;提高编程效率的利器 作为一名有10年Python编程经验的工程师&#xff0c;我深刻体会到快捷键的重要性。在日常编程中&#xff0c;快捷键可以大大提高编程效率&#xff0c;让我们更快地完成工作。本文将介绍Python中一些常用的快捷键&#xff0c;并…

【Git】常用命令

Git命令游戏教程网站&#xff1a;https://learngitbranching.js.org/?localezh_CN 日常使用 命令 git push 1.不省略的写法 适合<本地分支名>和<远程分支名>不一样的情况 将本地的dev分支上的代码推送到远程主机名为origin中test的分支上。如果远程的test分支不存…

Go开发学习 | 如何使用日志记录模块包针对日志按天数、按大小分隔文件示例...

欢迎关注「全栈工程师修炼指南」公众号 点击 &#x1f447; 下方卡片 即可关注我哟! 设为「星标⭐」每天带你 基础入门 到 进阶实践 再到 放弃学习&#xff01; “ 花开堪折直须折&#xff0c;莫待无花空折枝。 ” 作者主页&#xff1a;[ https://www.weiyigeek.top ] 博客&…

比较专业的成体系OJ题库和近期整理的比赛题目

比较专业的成体系OJ题库和近期整理的比赛题目&#xff0c;一个优秀的OJ系统是不断剔除和补充题目的完善过程 &#xff0c;不是越多越好&#xff0c;而是符合孩子们学习阶段的需求&#xff0c; 类似的题目有2-3题就好&#xff0c;方便学生举一反三&#xff0c;另外同一题 要求回…

两个offer:一个996,月薪3万;一个885,月薪2万,怎么选?

找工作时&#xff0c;钱和闲&#xff0c;你选哪个&#xff1f; 一位网友拿到了两个offer&#xff0c;一个996&#xff0c;月薪3万&#xff0c;一个885&#xff0c;月薪2万&#xff0c;怎么选&#xff1f; 一部分网友选择885&#xff0c;因为自己是打工人&#xff0c;不是打工奴…

为什么职场中35岁之后很难找到合适的工作?

(点击即可收听) 为什么职场中35岁之后很难找到合适的工作 无论是初入职场还是,职场多年的老司机,都听过一个35岁危机的一个话题 无论是企业还是一些招聘者,针对35,甚至就是30的人,充满了不是这样,就是那样的偏见的理由 每个公司都喜欢有激情,有想法,有干劲的年轻人,无论哪个公司…

大学四年,因为这8个网站,我成为同学眼中的学霸

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 大学期间&#xff0c;几乎每一个教过我的老师都反应&#xff0c;我的学习态度不好&#x…

设计模式之~观察者模式

观察者模式又叫做发布-订阅&#xff08;Publish/Subscribe&#xff09;模式。 观察者模式observer&#xff1a;定义了一种一对多的依赖关系&#xff0c;让多个观察者对象同时监听某个主题对象。这个主题对象在状态发生变化时&#xff0c;会通知所有观察者对象&#xff0c;使他们…

《计算机组成原理》唐朔飞 第8章 CPU的结构和功能 - 学习笔记

写在前面的话&#xff1a;此系列文章为笔者学习计算机组成原理时的个人笔记&#xff0c;分享出来与大家学习交流。使用教材为唐朔飞第3版&#xff0c;笔记目录大体与教材相同。 网课 计算机组成原理&#xff08;哈工大刘宏伟&#xff09;135讲&#xff08;全&#xff09;高清_…

实现分布式实体追踪:提升系统可见性和故障排查能力

引言&#xff1a; 在当今复杂的分布式系统中&#xff0c;追踪用户请求的执行过程变得越来越重要。为了获得全面的系统可见性和更高效的故障排查能力&#xff0c;我们在Flipkart采用了分布式实体追踪的解决方案。本文将介绍我们的实施策略&#xff0c;以及如何使用结构化日志和集…

IMG CXM GPU:面向复杂消费级设备的无缝视觉体验

上周我们推出了一款新的GPU&#xff0c;即IMG CXM。它的三种配置可扩展&#xff0c;为可穿戴设备和高级数字电视等多种消费设备提供无缝用户界面。 消费级设备需要GPU提供什么&#xff1f; 涵盖智能手表和智能眼镜的可穿戴市场为移动中的消费者提供了易于访问的信息。鉴于屏幕尺…

一文解读 AIGC 驱动高绩效商业的落地与思考

本文根据神策数据智能业务负责人郭荣锋《AIGC 驱动高绩效商业的实践》的主题演讲整理所得&#xff0c;主要围绕神策对 AIGC &#xff08;即 AI-Generated Content&#xff0c;人工智能生成内容&#xff09;业务应用的理解、AIGC 的落地实践及心得体会等方面展开。 以下为本文的…

运行 100 万个并发任务究竟需要多少内存?

Laf 公众号已接入了 AI 绘画工具 Midjourney&#xff0c;可以让你轻松画出很多“大师”级的作品。同时还接入了 AI 聊天机器人&#xff0c;支持 GPT、Claude 以及 Laf 专有模型&#xff0c;可通过指令来随意切换模型。欢迎前来调戏&#x1f447; <<< 左右滑动见更多 &…

Tomcat文件夹属性

Tomcat安装完成后&#xff0c;其安装目录下包含bin、conf、lib、logs、temp、webapps、work等子目录&#xff0c;各个子目录简介如下&#xff1a; &#xff08;1&#xff09;bin目录。主要存放Tomcat的命令文件。&#xff08;解压缩版点击bin下的startup.bat&#xff0c;即可运…

4.2 字节流与字符流

在Java中&#xff0c;有两种基本的数据流类型&#xff1a;字节流和字符流。字节流处理原始二进制数据&#xff0c;而字符流处理Unicode字符。本章节我们将学习字节流与字符流的基本概念以及如何使用它们进行文件的输入输出操作。 4.2.1 字节流 字节流处理原始二进制数据&…

打造音视频极致消费体验

在观看视频时&#xff0c;用户最看重的是什么呢&#xff1f;清晰度&#xff1f;流畅度&#xff1f;还是播放时的稳定性&#xff1f;作为视频厂商&#xff0c;不仅要考虑到常见的指标&#xff0c;一些关乎用户体验的隐藏性指标也需要重点关注。如何持续升级优化代码并在成本和用…