【Java 并发】AbstractQueuedSynchronizer 中的 Condition

news2024/11/15 19:31:09

1 简介

任何一个 Java 对象都天然继承于 Object 类, 在线程间实现通信的往往会应用到 Object 的几个方法, 比如 wait(), wait(long timeout), wait(long timeout, int nanos) 与 notify(), notifyAll()
几个方法实现等待 / 通知机制。同样的, 在 Java Lock 体系下也有同样的方法实现等待/通知机制。

从整体上来看 Object 的 wait 和 notify / notify 是与对象监视器配合完成线程间的等待/通知机制, 而 Condition 与 Lock 配合完成等待通知机制,
前者是 Java 底层级别的, 后者是语言级别的, 具有更高的可控制性和扩展性

两者除了在使用方式上不同外, 在功能特性上还是有很多的不同:

  1. Condition 能够支持不响应中断, 而通过使用 Object 方式不支持
  2. Condition 能够支持多个等待队列 (new 多个 Condition 对象), 而 Object 方式只能支持一个
  3. Condition 能够支持超时时间的设置, 而 Object 不支持

参照 Object 的 wait 和 notify / notifyAll 方法, Condition 也提供了同样的方法

2 Condition 实现原理分析

看一下 Condition 的示意图, 方便后续的理解

Alt 'AQS 的 Condition 实现'

2.1 等待队列

创建一个 Condition 对象是通过 lock.newCondition(), 而这个方法实际上是会 new 出一个 ConditionObject 对象, 该类是 AQS 的一个内部类。
Condition 是要和 lock 配合使用的, 也就是 Condition 和 Lock 是绑定在一起的。

我们知道在 Lock 是借助 AQS 实现的, 而 AQS 内部维护了一个同步队列, 如果是独占式锁的话, 所有获取锁失败的线程的会尾插入到同步队列。
同样的, Condition 内部也是使用同样的方式, 内部维护了一个等待队列, 所有调用 Condition.await 方法的线程会加入到等待队列中, 并且线程状态转换为等待状态。

另外注意到 ConditionObject 中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;

/** Last node of condition queue. */
private transient Node lastWaiter;

Node 类有这样一个属性:

//后继节点
Node nextWaiter;

进一步说明, 等待队列是一个单向队列。调用 Condition.await 方法后线程依次尾插入到等待队列中。
总的来说 ConditionObject 内部的队列的样子是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同时还有一点需要注意的是: 可以多次调用 lock.newCondition() 方法创建多个 condition 对象, 也就是一个 lock 可以持有多个等待队列。
而在利用 Object 的在实现上只能借助对象 Object 对象监视器上的一个同步队列和一个等待队列, 而并发包中的 Lock 拥有一个同步队列和多个等待队列

如图所示:

MultiConditionInAbstractQueuedSynchronizer
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图所示, ConditionObject 是 AQS 的内部类, 因此每个 ConditionObject 能够访问到 AQS 提供的方法, 相当于每个 Condition 都拥有所属同步器的引用。

3 await 的实现原理

当调用 Condition.await() 方法后会使得当前获取 lock 的线程进入到等待队列, 如果该线程能够从 await() 方法返回的话一定是该线程获取了与 Condition 相关联的 lock 或者被中断

await() 方法源码为:

public final void await() throws InterruptedException {

    // 当前线程为中断状态
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 1. 把当前的线程封装为 Node 节点, 放到队列的尾部, 同时返回这个节点    
    Node node = addConditionWaiter();
    
    // 2. 释放当前线程所占用的 lock, 在释放的过程中会唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);

    int interruptMode = 0;

    // 判断当前节点是否在 AQS 的同步队列里面
    while (!isOnSyncQueue(node)) {
        // 3. 把当前线程挂起, 进入阻塞状态
        LockSupport.park(this);
        // 0 不是被中断, 结束循环了
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }

    // 4. 自旋等待获取到同步状态, 既 Lock 
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
        
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();

    // 5. 处理被中断的状态    
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

大概的流程是这样的:
当前线程调用 Condition.await() 方法后, 会使得当前线程释放 lock, 然后加入到等待队列中, 直至被 signal/signalAll 后会使得当前线程从等待队列中移至到同步队列中去, 再次获得了 lock 后才会从 await 方法返回, 或者在等待时被中断会做中断处理

这里涉及几个问题

  1. 是怎样将当前线程添加到等待队列中去的
  2. 释放锁的过程
  3. 怎样才能从 await 方法退出

3.1 await 中的入队操作 - addConditionWaiter 方法

private Node addConditionWaiter() {

    // 当前线程是否为持有锁的线程
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();

    // 取到最后的节点    
    Node t = lastWaiter;
    
    // 尾节点不为空, 同时状态不为 CONDITION (-2)
    if (t != null && t.waitStatus != Node.CONDITION) {
        // 把当前链表里面状态值不是 Conditon 的进行删除
        unlinkCancelledWaiters();
        // 重试设置 t 为新的尾节点
        t = lastWaiter;
    }
    
    // 把当前节点封装为 Node
    Node node = new Node(Node.CONDITION);

    // 尾节点为空, 当前节点就是头节点了
    if (t == null)
        firstWaiter = node;
    else
        // 把当前节点放到队列的尾部
        t.nextWaiter = node;

    // 更新当前节点为尾节点    
    lastWaiter = node;
    return node;
}

private void unlinkCancelledWaiters() {
    
    // 头节点
    Node t = firstWaiter;
    // 记录最后一个状态是 Condition 的节点, 用于后面赋值给尾节点
    Node trail = null;
    
    // t 节点不为空
    while (t != null) {
        // 获取下一个节点
        Node next = t.nextWaiter;

        // t 节点的状态不为 CONDITION
        if (t.waitStatus != Node.CONDITION) {
            // 设置 t 的下一个节点为 null
            t.nextWaiter = null;


            // 因为 trail 记录的是遍历中最新的一个状态不是 Condition 的节点
            // 为 null, 当前一直在移动头节点, 那么只需要把 状态不为 Condition 的 t 节点的下一个节点为头节点即可
            // 不为 null, trail 表示当前遍历中, 最新的那个状态为 Condition 的节点, 将 t 节点的下一个节点设置到 trail 后面即可
            if (trail == null)
                // 设置当前的头节点为 t 节点的下一个节点
                firstWaiter = next;
            else
                // trail 的下一个节点等于 t 节点的下一个节点
                trail.nextWaiter = next;
            
            // 没有下一个节点了
            if (next == null)
                // 将尾节设置为 trail
                lastWaiter = trail;
        } 
        else
            // 设置 trail = t
            trail = t;
        // t = t 的下一个节点    
        t = next;
    }
}

3.2 await 中的锁释放操作 - fullyRelease 方法

将当前节点插入到等待对列之后, 会使当前线程释放 lock, 由 fullyRelease 方法实现, fullyRelease 源码为:

final int fullyRelease(Node node) {
    try {
        // 获取同步状态
        int savedState = getState();

        // 调用 AQS 的方法释放锁
        if (release(savedState))
            // 释放成功
            return savedState;

        // 释放失败, 抛出异常    
        throw new IllegalMonitorStateException();
    } catch (Throwable t) {

        // 释放锁的节点的状态修改为 Cancelled 取消状态
        node.waitStatus = Node.CANCELLED;
        throw t;
    }
}

调用 AQS 的模板方法 release 方法释放 AQS 的同步状态并且唤醒在同步队列中头节点的后继节点引用的线程。

如果释放成功则正常返回, 若失败的话就抛出异常。这样就解决了上面的第 2 个问题了。

3.3 await 中的判断当前节点是否在等待队列的操作 - isOnSyncQueue 方法

final boolean isOnSyncQueue(Node node) {
    
    // 当前节点的状态为 condition 或没有上一个节点, 也就是头节点了, 直接返回 false
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;

    // 没有下一个节点, 也就是理论理论上的头节点, 直接返回 true
    if (node.next != null) 
        return true;

    // 从链表的尾节点开始向上找, 是否有等于这个节点
    return findNodeFromTail(node);
}

private boolean findNodeFromTail(Node node) {

    // 从尾节点一直往前找
    for (Node p = tail;;) {
        // 当前节点和 node 一样 返回 true
        if (p == node)
            return true;
        // p 节点为 null, 没有数据了, 返回 false    
        if (p == null)
            return false;
        p = p.prev;
    }
}

整理一下, 逻辑如下

  1. 当前节点为 condition 状态或者没有上一节点, 也就是头节点, 直接返回 false
  2. 当前节点没有下一个节点了, 也就是理论上的头节点, 直接返回 true
  3. 从链表的尾节点开始往前找是否有和判断的节点一样的, 有返回 ture, 没有 false

3.4 await 中的线程线程唤醒中断判断 - checkInterruptWhileWaiting 方法

这里简单说一下 Condition.signal() 方法的原理

  1. 找到当前的等待队列的头节点
  2. 通过 CAS 将头节点的状态从 condition 设置为 0 (加入等待队列的线程的状态默认为 condition)
  3. 第二步的 CAS 失败了, 会重新从等待队列中找下一个节点, 然后从第二步继续开始
  4. 第二步的 CAS 成功了, 把当前的节点放到同步队列的尾部, 同时返回上一次的尾节点
  5. 上一次的尾节点的状态为取消状态, 或者通过 CAS 将上一次的尾节点的状态设置为 signal 状态失败的话, 调用 LockSupport.unpark(上一次尾节点里面的线程), 对齐进行唤醒

上面大体就是 signal 的步骤, 理解完上面的步骤, 才能理解下面的代码, 重点, 重点。

在上面 await 方法的源码中, 知道线程是通过 LockSupport.park(this) 挂起的。 那么什么时候这个阻塞的方法会继续执行, 也就是对应的线程苏醒。

  1. 通过调用 LockSupport.unpark(当前线程), 唤醒当前线程
  2. 别的线程调用了当前线程的 interrupt 方法, 中断当前线程, 这是 LockSupport.park(this), 也会被唤醒, 不会抛中断异常的
  3. 线程假唤醒

所以线程在 LockSupport.park(this) 处苏醒, 继续走下去的, 可能性有 3 种, 这时候需要先判断当前线程是否为中断导致的属性


private int checkInterruptWhileWaiting(Node node) {

    // 判断当前线程在等待期间, 是否被设置了中断标识, 这个方法会返回 3 种情况
    // 0: 正常情况, 没有设置中断标识, 表示是正常的同步队列唤醒或者外部直接通过 LockSupport.unpark() 唤醒这个线程
    // THROW_IE (-1): 线程唤醒后, 抛出异常
    // REINTERRUPT (1): 线程唤醒后, 设置中断标识为 true

    // THROW_IE 和 REINTERRUPT 都是表示线程在等待中被设置了中断标识, 他们是如何区别的?
    // 需要分析 transferAfterCancelledWait 方法中的, 将当前节点的状态通过 CAS 从 condition 设置为 0 是否成功
    // 那么什么时候是成功, 什么时候会失败, 这时就涉及到上面说的 signal()/signalAll() 方法的流程了
    // 1. 直接调用了这个线程的 interrupt() 方法, 这时候节点的状态还是 condition 的, 成功
    // 2. 一个线程调用了 signal()/ signalAll() 方法, 这时候执行到了上面步骤的第二步, 并成功了, 这时候另一个线程直接中断了线程, 这时候节点的状态不是 condition 的, 失败了
    // 3. 一个线程调用了 signal()/ signalAll() 方法, 这时候还未执行到上面步骤的第二步, 这时候另一个线程直接中断了线程, 这时候节点的状态还是 condition 的, 成功
    
    // 上面 3 种情况, 可以简单的概况为 
    // 1. 调用了 signal()/ signalAll() 后, 中断线程
    // 2. 中断了线程后, 调用了 signal()/ signalAll()

    // 如果是第一种情况, 则由 signal() / signalALl() 将中断线程的节点放到同步队列的尾部
    // 如果是第二种情况, 则由 transferAfterCancelledWait() 将中断线程的节点放到同步队列的尾部

    // 在第一种情况下, 存在将中断线程的节点从等待队列移动到同步队列的过程, 
    // 这个过程会出现一个很端的时间, 在同步队列找不到这个节点, 所以在后面做多了一层判断, 直到在同步队列中找到了线程的节点, 才返回

    // 2 种不同的中断方式
    // 第一种返回了 REINTERRUPT, 线程唤醒后, 设置中断标识为 true
    // 第二种返回了 THROW_IE, 线程唤醒后, 抛出异常

    return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :  0;
}

final boolean transferAfterCancelledWait(Node node) {

    // 把 node 节点的状态从 condition 设置为 0,
    // 设置成功, 把节点放到同步队列的尾部, 返回 true
    
    // 在 signal/signalAll 中会先将节点设置为 signal 状态, 即 0, 然后把节点从等待队列移动到同步队列
    // 在这里将节点的状态从 condition 设置为 0, 设置成功了, 表示在 signal/signalAll 之前, 线程就被中断了
    // 设置失败, 在表示在 signal/signalAll 之后, 线程才被中断

    if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
        enq(node);
        return true;
    }

    // 将中断线程的节点从等待队列移动到同步队列的过程, 存在极端时间的节点找不到
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

3.5 await 的退出流程

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

        // 唤醒后判断当前线程的是否被中断过
        // 等于 0, 表示没有被中断
        // 不等于 0, 表示中断过, 存在 2 种中断方式
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }

    // acquireQueued 的返回值, true 线程需要中断, false 线程不需要中断
    
    // acquireQueued 返回需要中断, 则将不是 THROW_IE 的中断模式设置为 REINTERRUPT
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        // 也就是将 0 的情况也设置为 REINTERRUPT
        interruptMode = REINTERRUPT;

    // 节点的下一个节点不为 null, 移除当前链表中的取消状态的节点    
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();

    // 中断模式不为 0, 根据中断模式设置线程的中断状态   
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
    // 中断模式为 THROW_IE, 抛出异常
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    // 中断模式为 REINTERRUPT, 设置线程的中断标识为 true    
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

上面就是 await() 方法的整体流程梳理。

上面的第 3 个问题: 怎样才能从 await 方法退出 也就能得到答案了

  1. 当前线程被中断, 从等待队列移动到同步队列后, 重新获取锁
  2. 调用 signal/signalAll 将当前线程的节点从等待队列移到同步队列, 重新获取锁

3.6 await 类似的其他挂起线程的方法

3.6.1 超时机制的支持

condition 还额外支持了超时机制, 使用者可调用方法 awaitNanos(), awaitUtil() 这两个方法的实现原理, 基本上与 AQS 中的 tryAcquire 方法如出一辙。

2 者的实现方式和 await 类似, 只是在死循环中添加了对超时时间的判断。同时在超时后, 将当前线程的节点的状态从 condition 设置为 0, 追加到同步队列的尾部

3.6.2 不响应中断的支持

要想不响应中断可以调用 condition.awaitUninterruptibly() 方法, 该方法的源码为:

public final void awaitUninterruptibly() {

    // 节点入到等待队列
    Node node = addConditionWaiter();
    // 释放锁
    int savedState = fullyRelease(node);

    // 是否需要设置中断标识
    boolean interrupted = false;
    // 判断当前节点不在同步队列中, 在的话, 结束循环
    while (!isOnSyncQueue(node)) {
        // 挂起线程
        LockSupport.park(this);
        // 线程苏醒后, 判断中断标识是否为 true
        if (Thread.interrupted())
            interrupted = true;
    }
    // 从同步队列中获取锁成功了, 同时返回需要设置中断 或者上面的判断后需要中断
    if (acquireQueued(node, savedState) || interrupted)
        // 设置线程的中断标识为 true
        selfInterrupt();

}

这段方法与上面的 await 方法基本一致, 只不过减少了对中断的处理, 并省略了 reportInterruptAfterWait 方法抛被中断的异常。

4 signal/signalAll 实现原理

调用 condition 的 signal 或者 signalAll 方法可以将等待队列中等待时间最长的节点移动到同步队列中, 使得该节点能够有机会获得 lock。
按照等待队列是先进先出 (FIFO) 的, 所以等待队列的头节点必然会是等待时间最长的节点, 也就是每次调用 condition 的 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;
    // transferForSignal 进行节点的真正处理
    // 在 transferForSignal 处理失败时, 最新的头节点不为空, 继续处理新的头节点
    } while (!transferForSignal(first) &&  (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {

    // 1. 将节点的状态从 condition 设置为 0
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    // 2. 把这个节点放到 AQS 里面的同步队列的尾部, 同时获取到上一次的尾节点
    Node p = enq(node);

    int ws = p.waitStatus;
    // 上一次的尾节点状态为 1 (取消状态) 或者 通过 CAS 将这个节点的状态设置为 signal 失败, 则唤醒这个线程
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

signal方法的逻辑:

  1. 将头节点的状态更改为CONDITION
  2. 调用 enq 方法, 将该节点尾插入到同步队列中

现在我们可以得到结论: 调用 condition 的 signal 的前提条件是当前线程已经获取了 lock, 该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列, 而移入到同步队列后才有机会使得等待线程被唤醒,
即从 await 方法中的 LockSupport.park(this) 方法中返回, 从而才有机会使得调用 await 方法的线程成功退出。

signalAll

sigllAll 与 sigal 方法的区别体现在 doSignalAll 方法上, 前面我们已经知道 doSignal 方法只会对等待队列的头节点进行操作, 而 doSignalAll 的源码:

private void doSignalAll(Node first) {
    // 将头尾节点都设置为 null
    lastWaiter = firstWaiter = null;

    do {
        // 获取头节点的下一个节点
        Node next = first.nextWaiter;
        // 设置投节点的下一个节点为 null
        first.nextWaiter = null;
        // 尝试将头节点设置到同步队列的尾部
        transferForSignal(first);
        // 头节点等于头节点的下一个节点
        first = next;
    } while (first != null);
}

该方法会不断地将等待队列中的每一个节点都移入到同步队列中, 即 “通知” 当前调用 condition.await() 方法的每一个线程。

5 await 与 signal-signalAll 的结合思考

文章开篇提到等待/通知机制, 通过使用 condition 提供的 await 和 signal/signalAll 方法就可以实现这种机制, 而这种机制能够解决最经典的问题就是 “生产者与消费者问题”。
await 和 signal 和 signalAll 方法就像一个开关控制着线程 A (等待方) 和线程 B (通知方) 。它们之间的关系可以用下面一个图来表现得更加贴切:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如图:

  1. 线程 awaitThread 先通过 lock.lock() 方法获取锁成功后调用了 condition.await 方法进入等待队列
  2. 另一个线程 signalThread 通过 lock.lock() 方法获取锁成功后调用了condition.signal 或者 signalAll 方法, 使得线程 awaitThread 能够有机会移入到同步队列中,
  3. 当其他线程释放 lock 后使得线程 awaitThread 能够有机会获取 lock, 从而使得线程 awaitThread 能够从 await 方法中退出执行后续操作。如果 awaitThread 获取 lock 失败会直接进入到同步队列

举个例子

public class AwaitSignal {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(new Waiter());
        waiter.start();
        Thread signaler = new Thread(new Signaler());
        signaler.start();
    }

    static class Waiter implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
            } finally {
                lock.unlock();
            }
        }
    }

    static class Signaler implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
}

输出结果

Thread-0当前条件不满足等待  
Thread-0接收到通知, 条件满足

6 参考

详解Condition的await和signal等待/通知机制
Condition的await-signal流程详解

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

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

相关文章

代码随想录算法训练营第day27|93.复原IP地址 、 78.子集 、 90.子集II

93.复原IP地址 93. 复原 IP 地址 - 力扣(LeetCode) 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 . 分隔。 例如:"0.1.2.201" 和 …

AI论文速读 | UniTS:构建统一的时间序列模型

题目:UniTS: Building a Unified Time Series Model 作者:Shanghua Gao(高尚华), Teddy Koker, Owen Queen, Thomas Hartvigsen, Theodoros Tsiligkaridis, Marinka Zitnik 机构:哈佛大学(Harvard&#x…

学完排序算法,终于知道用什么方法给监考完收上来的试卷排序……

由于每个老师批改完卷子之后装袋不一定是有序的,鼠鼠我被拉去当给试卷排序的苦力。面对堆积成山的试卷袋,每一份试卷袋的试卷集又很重,鼠鼠我啊为了尽早下班,决定用一种良好的办法进行排序。 1.插入排序 首先考虑的是插入排序。…

Python 井字棋游戏

井字棋是一种在3 * 3格子上进行的连珠游戏,又称井字游戏。井字棋的游戏有两名玩家,其中一个玩家画圈,另一个玩家画叉,轮流在3 * 3格子上画上自己的符号,最先在横向、纵向、或斜线方向连成一条线的人为胜利方。如图1所示…

阿里云-零基础入门NLP【基于机器学习的文本分类】

文章目录 学习过程赛题理解学习目标赛题数据数据标签评测指标解题思路TF-IDF介绍TF-IDF 机器学习分类器TF-IDF LinearSVCTF-IDF LGBMClassifier 学习过程 20年当时自身功底是比较零基础(会写些基础的Python[三个科学计算包]数据分析),一开始看这块其实挺懵的&am…

【C语言】数9的个数

编写程序数一下 1到 100 的所有整数中出现多少个数字9 1,首先产生1~100的数字。然猴设法得到数9个数,例如个位:19%109,十位:91/109。 2,每次得到数九的时候,就用一个变量来进行计数。 代码如…

Python--成员方法、@staticmethod将成员方法静态化、self参数释义

在 Python 中,成员方法是指定义在类中的函数,用于操作类的实例对象。成员方法通过第一个参数通常命名为 self,用来表示调用该方法的实例对象本身。通过成员方法,可以实现类的行为和功能。 成员方法的定义 在类中定义成员…

苍穹外卖-day10:Spring Task、订单状态定时处理、来单提醒(WebSocket的应用)、客户催单(WebSocket的应用)

苍穹外卖-day10 课程内容 Spring Task订单状态定时处理WebSocket来单提醒客户催单 功能实现:订单状态定时处理、来单提醒和客户催单 订单状态定时处理: 来单提醒: 客户催单: 1. Spring Task 1.1 介绍 Spring Task 是Spring框…

电脑装win11(作si版)

装win11经历 前言:因为我的u盘今天到了,迫不及待试试装机 然后在一系列准备好工具后,便是开始拿学校的机房电脑来试试手了~~ 前期准备 下载好win11镜像(可以去微软官网下载) 下载Rufus工具 https://www.lanzoue.com/…

2023年度VSCode主题推荐(个人常用主题存档)

前言 早在2018年的时候发了一篇关于VSCode主题风格推荐——VS Code 主题风格设置,时过境迁,如今常用的主题皮肤早已更替。 今天下午在整理VSCode插件的时候,不小心把常用的那款(亮色)主题插件给删除了,无…

配置OGG 如何批量修改源端及目标端序列值_满足客户变态需求学会这招你就赚了

欢迎您关注我的公众号【尚雷的驿站】 **************************************************************************** 公众号:尚雷的驿站 CSDN :https://blog.csdn.net/shlei5580 墨天轮:https://www.modb.pro/u/2436 PGFans:ht…

鸿蒙App开发学习 - TypeScript编程语言全面开发教程(下)

现在我们接着上次的内容来学习TypeScript编程语言全面开发教程(下半部分) 4. 泛型 TypeScript 中的泛型(Generics)是一种编程模式,用于在编写代码时增强灵活性和可重用性。泛型使得在定义函数、类、接口等数据类型时…

DeformableAttention的原理解读和源码实现

本专栏主要是深度学习/自动驾驶相关的源码实现,获取全套代码请参考 目录 原理第一步看看输入:第二步,准备工作:生成参考点的偏移量生成参考点的权重生成参考点 第三步,工作: 源码 原理 目前流行3D转2DBEV方案的都绕不开的transfomer变体-DeformableAttention. 传统transform…

DataFunSummit 2023因果推断在线峰会:解码数据与因果,引领智能决策新篇章(附大会核心PPT下载)

在数据驱动的时代,因果推断作为数据科学领域的重要分支,正日益受到业界的广泛关注。DataFunSummit 2023年因果推断在线峰会,汇聚了国内外顶尖的因果推断领域专家、学者及业界精英,共同探讨因果推断的最新进展、应用与挑战。本文将…

【小白笔记:JetsonNano学习(一)SDKManager系统烧录】

参考文章:SDKManager系统烧录 小白烧录文件系统可能遇到的问题 担心博主删除文章,可能就找不到比较详细的教程了,特意记录一下。 Jetson Nano采用四核64位ARM CPU和128核集成NVIDIA GPU,可提供472 GFLOPS的计算性能。它还包括4GB…

24计算机考研调剂 | 【官方】山东师范大学(22自命题)

山东师范大学2024年拟接收调剂 考研调剂信息 调剂专业目录如下: 计算机技术(085404)、软件工程(085405) 补充内容 我校2024年硕士研究生调剂工作将于4月8日教育部“中国研究生招生信息网”(https://yz.ch…

海外问卷调查:代理IP使用方法

在进行问卷调查时,为了避免被限制访问或被封禁IP,使用代理IP已经成为了必要的选择。 其中,口子查和渠道查也不例外。 使用代理IP可以隐藏本机IP地址,模拟不同的IP地址,从而规避被封禁的风险。但是,对于很…

登录-前端部分

登录表单和注册表单在同一个页面中,通过注册按钮以及返回按钮来控制要显示哪个表单 一、数据绑定和校验 (1)绑定数据,复用注册表单的数据模型: //控制注册与登录表单的显示, 默认false显示登录 true时显…

linux 安装常用软件

文件传输工具 sudo yum install –y lrzsz vim编辑器 sudo yum install -y vimDNS 查询 sudo yum install bind-utils用法可以参考文章 《掌握 DNS 查询技巧,dig 命令基本用法》 net-tools包 yum install net-tools -y简单用法: # 查看端口占用情况…

3_springboot_shiro_jwt_多端认证鉴权_Redis缓存管理器

1. 什么是Shiro缓存管理器 上一章节分析完了Realm是怎么运作的,自定义的Realm该如何写,需要注意什么。本章来关注Realm中的一个话题,缓存。再看看 AuthorizingRealm 类继承关系 其中抽象类 CachingRealm ,表示这个Realm是带缓存…