JUC第八讲:Condition源码分析

news2024/11/18 13:31:38

JUC第八讲:Condition源码分析

本文是JUC第八讲,Condition详解。任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

文章目录

  • JUC第八讲:Condition源码分析
    • 概述
    • 1、Condition的使用
    • 2、重要入口方法
    • 3、基础属性
    • 4、重点方法
      • 4.1、await方法
      • 4.2、addConditionWaiter方法
      • 4.3、unlinkCancelledWaiters方法
      • 4.4、fullyRelease方法
      • 4.5、isOnSyncQueue方法
      • 4.6、findNodeFromTail方法
      • 4.7、signal方法
      • 4.8、doSignal方法
      • 4.9、transferForSignal方法
      • 4.10、signalAll方法
      • 4.11、doSignalAll方法
    • 5、总结

概述

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

通过对比Object的监视器方法和Condition接口,可以更详细地了解Condition的特性,对比项与结果如下表。

  • Object对象 wait方法、notify方法
    • 前置条件:获取对象的锁
  • Condition对象 await方法 signal 方法
    • 前置条件:调用Lock.lock() 获取锁
  • LockSupport对象 park方法和 unpark方法

img

1、Condition的使用

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。 Condition的使用方式比较简单,需要注意在调用方法前获取锁。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * Condition接口与示例
 * @param <T>
 */
public class BoundedQueue<T> {
 
    // 线程池
    private static ExecutorService THREAD_POOL = new ThreadPoolExecutor(2, 4, 60, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(1000), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());
	// 对象数组
    private Object[] items; 
    // 添加的下标,删除的下标和数组当前数量
    private int addIndex, removeIndex, count;
    private Lock lock = new ReentrantLock(); // 定义一个可重入锁
    private Condition notEmpty = lock.newCondition(); // 添加一个Condition
    private Condition notFull = lock.newCondition(); // 添加一个Condition
 
    public BoundedQueue(int size) {
        items = new Object[size];
    }
 
    /**
     * 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
     * @param t
     * @throws InterruptedException
     */
    public void add(T t) throws InterruptedException {
        lock.lock(); // 获取锁
        try {
          	// 场景1:如果数组满了,notFull进入等待
            while (count == items.length) { 
                System.out.println("items满了,add方法进入等待.");
                notFull.await(); // 等待remove方法里的notFull.signal()
            }
			// 场景2:item添加对象
            items[addIndex] = t;
            if (++addIndex == items.length) // 调整数组索引,避免越界
                addIndex = 0;
            ++count; // count+1,代表添加了一个对象
            notEmpty.signal(); // 走到这里,数组里至少有1个对象,必不为空,因此唤醒notEmpty
        } finally {
            System.out.println("add: " + t);
            lock.unlock(); // 释放锁
        }
    }
 
    /**
     * 由头部删除一个元素,如果数组空,则删除线程进入等待状态,
     * 直到有新添加元素(注意这里并没有真的删除元素,只是把count-1当作是删除)
     * @return
     * @throws InterruptedException
     */
    @SuppressWarnings("unchecked")
    public T remove() throws InterruptedException {
        lock.lock(); // 获取锁
        try {
          	//场景1:如果数组为空,notEmpty进入等待
            while (count == 0) { 
                System.out.println("items为空,remove方法进入等待.");
                notEmpty.await(); // 等待add方法里的notEmpty.signal()
            }
			//场景2:如果数组非空,item移除对象
            Object x = items[removeIndex]; // item移除对象(假移除)
            if (++removeIndex == items.length) // 调整数组索引,避免越界
                removeIndex = 0;
            --count; // count-1,代表移除了一个对象
            notFull.signal(); // 走到这里,数组里至少有1个空位,必不为满,因此唤醒notFull
            return (T) x;
        } finally {
            System.out.println("remove");
            lock.unlock(); // 释放锁
        }
    }
 
    public static void main(String args[]) throws InterruptedException {
        int count = 3; // 可以加大数组的size来看更多的过程
        BoundedQueue<Integer> bq = new BoundedQueue<Integer>(count);
 
        // 开启一个线程执行添加操作
        THREAD_POOL.submit(new Callable<Object>() {
            public Object call() throws InterruptedException {
                for (int i = 0; i < count * 2; i++) {
                    bq.add(i);
                    Thread.sleep(200); // 通过睡眠来制造添加和移除的速度差
                }
                return null;
            }
        });
 
        // 开启一个线程执行移除操作
        THREAD_POOL.submit(new Callable<Object>() {
            public Object call() throws InterruptedException {
                Thread.sleep(1000);
                for (int i = 0; i < count * 2; i++) {
                    bq.remove();
                    Thread.sleep(50); // 通过睡眠来制造添加和移除的速度差
                }
                return null;
            }
        });
    }
}

输出如下,由于调用remove方法的线程先睡眠了1秒,所以,add方法会先将item数组填满,填满后notFull进入等待。之后,remove方法的线程醒来开始进行移除,当移除之后会唤醒notFull,此时add和remove是并发操作的,但是由于remove的速度更快(通过sleep控制,add每次要睡200毫秒,remove只要50毫秒),所以items必然会被移除到为空,此时notEmpty进入等待,直到add方法往item添加了对象,如此反复。
img

2、重要入口方法

Condition的实现主要包括:条件队列、等待和通知。其中条件队列放的是AQS里的Node数据结构,使用nextWaiter来维护条件队列。等待和通知共有7个方法。

  • signal():唤醒该条件队列的头节点。
  • signalAll():唤醒该条件队列的所有节点。
  • awaitUninterruptibly():等待,此方法无法被中断,必须通过唤醒才能解除阻塞。
  • await():当前线程进入等待。
  • awaitNanos(long):当前线程进入等待,有超时时间,入参的单位为纳秒。
  • awaitUntil(Date):当先线程进入等待,直到当前时间超过入参的时间。
  • await(long, TimeUnit):当前线程进入等待,有超时时间,入参可以自己设置时间单位。

这些方法其实大同小异,因此本文只对常用的signal()、signalAll()和await()方法展开详解。搞懂了这3个方法,搞懂其他几个方法也基本没什么阻碍。

3、基础属性

Condition的实现是ConditionObject,而ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为条件队列),该队列是Condition对象实现等待/通知功能的关键。

private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter; // 条件队列的头节点
/** Last node of condition queue. */    
private transient Node lastWaiter;  // 条件队列的尾节点
 
/**
 * Creates a new {@code ConditionObject} instance.
 */
public ConditionObject() { }

通过源码可知,条件队列的节点使用的是AQS的Node数据结构。

另外,由于ConditionObject是AQS的内部类,因此必然和AQS是有很多关联的,因此看本文之前必须先了解AQS的实现原理。(如果你对AQS不熟悉,可以参考我的这一篇文章:JUC第十二讲:JUC锁: 锁核心类AQS详解)

条件队列的基本数据结构如下图中的“条件队列”:

img

4、重点方法

4.1、await方法

// 阻塞当前线程,直接被唤醒或被中断
public final void await() throws InterruptedException {
    // 如果当前线程被中断过,则抛出中断异常
  	if (Thread.interrupted())
        throw new InterruptedException();
  	// 添加一个waitStatus为CONDITION的节点到条件队列尾部
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);    // 释放操作。我们知道只有在拥有锁(acquire成功)的时候才能调用await()方法,因此,调用await()方法的线程的节点必然是同步队列的头节点。所以,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的条件队列中。
  	// 0为正常,被中断值为THROW_IE或REINTERRUPT
    int interruptMode = 0;
  	// isOnSyncQueue:判断node是否在同步队列(注意和条件队列区分。调用signal方法会将节点从条件队列移动到同步队列,因此这边就可以跳出while循环)
    while (!isOnSyncQueue(node)) {
        // node如果不在同步队列则进行park(阻塞当前线程)
      	LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)    // 检查线程被唤醒是否是因为被中断,如果是则跳出循环,否则会进行下一次循环,因为被唤醒前提是进入同步队列,所以下一次循环也必然会跳出循环
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)   // acquireQueued返回true代表被中断过,如果中断模式不是THROW_IE,则必然为REINTERRUPT(见上面的checkInterruptWhileWaiting方法)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();   // 移除waitStatus为CANCELLED的节点
    if (interruptMode != 0) // 如果跳出while循环是因为被中断
        reportInterruptAfterWait(interruptMode);    // 则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程
}
  • 1、如果当前线程被中断过,则抛出中断异常。
  • 2、调用addConditionWaiter方法(详解见下文addConditionWaiter方法)添加一个waitStatus为CONDITION的节点到条件队列尾部。
  • 3、调用fullyRelease方法(详解见下文fullyRelease方法)释放锁。
  • 4、调用isOnSyncQueue方法(详解见下文isOnSyncQueue方法)来阻塞线程,直到被唤醒或被中断。
  • 5、调用acquireQueued方法(详解见acquireQueued方法详解)来尝试获取锁,并判断线程跳出while循环是被唤醒还是被中断。
  • 6、如果跳出while循环是因为被中断,则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程。

4.2、addConditionWaiter方法

// 添加一个waitStatus为CONDITION的节点到条件队列尾部
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();   // 移除waitStatus不为 CONDITION的节点(条件队列里的节点 waitStatus 都为 CONDITION)
        t = lastWaiter; // 将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)
    }
  	// 以当前线程新建一个waitStatus为CONDITION的节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
  	// t为空,代表条件队列为空
    if (t == null)
      	// 将头节点赋值为node
        firstWaiter = node;
    else
      	// 否则,队列不为空。将t(原尾节点)的后继节点赋值为node
        t.nextWaiter = node;
    lastWaiter = node;  // 将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的
    return node;
}
  • 1、如果条件队列的尾节点不为null并且waitStatus不为CONDITION,则调用unlinkCancelledWaiters方法(详解见下文unlinkCancelledWaiters方法)移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION),并将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)
  • 2、以当前线程新建一个waitStatus为CONDITION的节点。
  • 3、如果t为空,代表条件队列为空,将头节点赋值为node;否则,队列不为空。将t(原尾节点)的后继节点赋值为node。
  • 4、最后将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

4.3、unlinkCancelledWaiters方法

// 从条件队列移除所有waitStatus不为CONDITION的节点
private void unlinkCancelledWaiters() {
  	// t赋值为条件队列的尾节点  
    Node t = firstWaiter; 
    Node trail = null;
    while (t != null) {
      	// 向下遍历
        Node next = t.nextWaiter;
      	// 如果t的waitStatus不为CONDITION
        if (t.waitStatus != Node.CONDITION) {
          	// 断开t与t后继节点的关联
            t.nextWaiter = null;
            if (trail == null)  // 如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点
                firstWaiter = next; 
            else
                trail.nextWaiter = next;    // 否则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;  // 如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION
        t = next;   // t指向下一个节点
    }
}
  • 1、将t赋值为条件队列的尾节点 。
  • 2、从t开始遍历整个条件队列。
  • 3、如果t的waitStatus不为CONDITION,则断开t与t后继节点的关联。
  • 4、如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点。
  • 5、如果trail不为null,则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)
  • 6、如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION。
  • 7、最后将 t指向下一个节点,准备开始下一次循环。

例子图解过程:

img

img

img

4.4、fullyRelease方法

// 释放锁
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
      	// 当前的同步状态
        int savedState = getState();
      	// 独占模式下release(一般指释放锁)
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
          	// 如果release失败则将该节点的waitStatus设置为CANCELLED
            node.waitStatus = Node.CANCELLED;
    }
}

调用release方法(详解见release方法详解)释放锁,如果释放失败,则将该节点的waitStatus设置为CANCELLED。

4.5、isOnSyncQueue方法

// 判断node是否再同步队列中
final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null) // 如果waitStatus为CONDITION 或 node没有前驱节点,则必然不在同步队列,直接返回false
        return false;
    if (node.next != null) // If has successor, it must be on queue 如果有后继节点,必然是在同步队列中,返回true
        return true;
    /*
     * node.prev can be non-null, but not yet on queue because
     * the CAS to place it on queue can fail. So we have to
     * traverse from tail to make sure it actually made it.  It
     * will always be near the tail in calls to this method, and
     * unless the CAS failed (which is unlikely), it will be
     * there, so we hardly ever traverse much.
     */
    return findNodeFromTail(node);  // 返回node是否为同步队列节点,如果是返回true,否则返回false
}

返回node是否为同步队列节点,如果是返回true,否则返回false。因为只有该节点的线程被唤醒(signal())才会从条件队列移到同步队列。

4.6、findNodeFromTail方法

// 从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

从同步队列的尾节点开始向前遍历,如果node为同步队列节点则返回true,否则返回false。

4.7、signal方法

public final void signal() {
  	// 检查当前线程是否为独占模式同步器的所有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
      	// 唤醒条件队列的头节点
        doSignal(first);
}
  • 1、检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException;
  • 2、拿到条件队列的头节点,如果不为null,则调用doSignal方法(详解见下文doSignal方法)唤醒头节点。

4.8、doSignal方法

// 将条件队列的头节点移到同步队列
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)  // 将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null
            lastWaiter = null;
      	// 断开first节点对first节点后继节点的关联
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&   // transferForSignal:将first节点从条件队列移动到同步队列
             (first = firstWaiter) != null);    // 如果transferForSignal失败,并且first节点不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 firstWaiter为null
}
  • 1、将first节点赋值为first节点的后继节点(相当于移除first节点),如果first节点的后继节点为空,则将lastWaiter赋值为null。
    断开first节点与first节点后继节点的关联。
  • 2、调用transferForSignal方法(详解见下文transferForSignal方法)将first节点从条件队列移动到同步队列。
  • 3、如果transferForSignal失败,并且first节点的后继节点(firstWaiter)不为null,则向下遍历条件队列的节点,直到节点成功移动到同步队列 或者 first节点的后继节点为null。

4.9、transferForSignal方法

// 将node节点从条件队列移动到同步队列,如果成功则返回true。
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))  // 如果不能更改节点的waitStatus,则表示该节点已被取消,返回false
        return false;
 
    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    Node p = enq(node); // 否则,调用enq方法将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点
  	// 将ws赋值为node前驱节点的等待状态
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // 如果node前驱节点的状态为CANCELLED(ws>0) 或 使用CAS将waitStatus修改成SIGNAL失败,则代表node的前驱节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点
        LockSupport.unpark(node.thread);
    return true;
}
  • 1、如果不能更改节点的waitStatus,则表示该节点已被取消,返回false。
  • 2、调用enq方法(详解见enq方法详解)将node添加到同步队列,注意:enq方法返回的节点是node的前驱节点。因此,此时p节点为node的前驱节点。
  • 3、将ws赋值为node前驱节点(p节点)的waitStatus。
  • 4、如果p节点的waitStatus为CANCELLED(ws>0) 或 使用CAS将p节点的waitStatus修改成SIGNAL失败,则代表p节点无法来唤醒node节点,因此直接调用LockSupport.unpark方法唤醒node节点。

4.10、signalAll方法

public final void signalAll() {
  	// 检查当前线程是否为独占模式同步器的所有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
      	// 唤醒条件队列的所有节点
        doSignalAll(first);
}
  • 1、检查当前线程是否为独占模式同步器的所有者,在ReentrantLock中即检查当前线程是否为拥有锁的线程。如果不是,则抛IllegalMonitorStateException。
  • 2、拿到条件队列的头节点,如果不为null,则调用doSignalAll方法(详解见下文doSignalAll方法)唤醒条件队列的所有节点。

4.11、doSignalAll方法

// 将条件队列的所有节点移到同步队列
private void doSignalAll(Node first) {
  	// 因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null
    lastWaiter = firstWaiter = null;
    do {
      	// next赋值为first节点的后继节点
        Node next = first.nextWaiter;
      	// 断开first节点对first节点后继节点的关联
        first.nextWaiter = null;
      	// transferForSignal:将first节点从条件队列移动到同步队列
        transferForSignal(first);
      	// first赋值为next节点
        first = next;
    } while (first != null);    // 循环遍历,将条件队列的所有节点移动到同步队列
}
  • 1、因为要移除条件队列的所有节点到同步队列,因此这边直接将firstWaiter和lastWaiter赋值为null。
  • 2、next赋值为first节点的后继节点 。
  • 3、断开first节点对first节点后继节点的关联
  • 4、调用transferForSignal方法(详解见上文transferForSignal方法)将first节点从条件队列移动到同步队列。
  • 5、first赋值为next节点,准备下一次循环。
  • 6、如果first不为null,则进入下一次循环。

5、总结

  • 1、调用await和signal方法都需要先获得锁,否则会抛异常
  • 2、调用await方法会新建一个waitStatus为CONDITION、线程为当前线程的节点到条件队列尾部,然后当前线程会释放掉锁,并进入阻塞状态,直到该节点被移到同步队列或者被中断。该节点被移动到同步队列,并不代表该节点线程能立马获得锁,还是需要在同步队列中排队并在必要时候(前驱节点为head)调用tryAcquire方法去获取,如果获取成功则代表获得了锁。
  • 3、调用signal方法会将条件队列的头节点移动到同步队列。

参考

  • AbstractQueuedSynchronizer.ConditionObject源码(JDK 1.8)
  • 《Java并发编程的艺术》

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

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

相关文章

【LeetCode-中等题】513. 找树左下角的值

文章目录 题目方法一&#xff1a;前序递归方法二&#xff1a;层序遍历 题目 方法一&#xff1a;前序递归 在递归遍历到叶子结点时&#xff0c;对比此时的节点深度&#xff0c;若当前节点深度大于当前最大深度&#xff0c;就更新value值&#xff0c;最后记录下的value即为最下最…

elementui 菜单选中优化

/** 父级菜单悬浮样式**/ .el-submenu__title:hover {color:#1890ff!important; } /** 父级菜单箭头悬浮样式**/ .el-submenu__title:hover>.el-submenu__icon-arrow{font-size: 13px!important;} /** 子菜单悬浮样式**/ .el-menu-item:hover{color:#1890ff!important; } /*…

Linux操作系统基础详解,计算机专业必看!

目录 Linux操作系统 Linux 简介 Linux 接口 Linux 组成部分 Shell Linux 应用程序 Linux 内核结构 Linux 进程和线程 基本概念 Linux 进程间通信 Linux 中进程管理系统调用 Linux 进程和线程的实现 Linux 调度 Linux 启动 Linux 内存管理 基本概念 Linux 内存…

【owt】 Intel® Media SDK for Windows: MSDK2021R1

https://www.intel.com/content/www/us/en/developer/articles/tool/media-sdk.html官方网不提供下载了: 2021地址 直接下载: MSDK2021R1.exe老版本 Intel Media SDK(Windows版本) 大神的介绍:owt-client-native 需要 https://github.com/open-webrtc-toolkit/owt-client…

spring security auth2.0实现

OAuth 2.0 的认证/授权流程 jwt只是认证中的一步 4中角色 资源拥有者&#xff08;resource owner&#xff09;、客户端&#xff08;client 第三方&#xff09;、授权服务器&#xff08;authorization server&#xff09;和资源服务器&#xff08;resource server&#xff09;。…

vue 使用cornerstone解析 .dcm 文件

// 首先下载依赖 npm install --save cornerstone-core cornerstone-math cornerstone-tools hammerjs cornerstone-web-image-loader 下载之后再package.json中可以看到最后图片的依赖// 下面是完成的组件代码 <template><div id"dicom_canvas" refdicom_c…

FL Studio21.1无限试用版体验新功能变化介绍

许多刚刚接触音乐创作的新朋友&#xff0c;通过各种渠道了解到FL Studio&#xff0c;但并不知道我们的历史以及在音乐创作方面所产生的影响&#xff0c;今天分享一篇来自coco玛奇朵博主Rio的深度科普文章&#xff0c;相信对新人会有很大启发。 FL Studio 21.1 通过钢琴卷中的音…

C++:类中的静态成员函数以及静态成员变量

一、静态成员变量 静态成员&#xff1a;在类定义中&#xff0c;它的成员&#xff08;包括成员变量和成员函数&#xff09;&#xff0c;这些成员可以用关键字static声明为静态的&#xff0c;称为静态成员。 静态成员变量需要在类外分配空间&#xff0c;static 成员变量是在初始…

速码!!BGP最全学习笔记:IBGP和EBGP基本配置

实验1&#xff1a;配置IBGP和EBGP 实验目的 熟悉IBGP和EBGP的应用场景掌握IBGP和EBGP的配置方法 实验拓扑 想要华为数通配套实验拓扑和配置笔记的朋友们点赞关注&#xff0c;评论区留下邮箱发给你! 实验步骤 1.IP地址的配置 R1的配置 <Huawei>system-view …

基于单片机火灾报警器仿真设计

一、系统方案 1、本设计采用51单片机作为主控器。 2、DS18B20采集温度值送到液晶1602显示。 3、MQ2采集烟雾值&#xff0c;送到液晶1602显示。 4、按键设置温度报警值&#xff0c;大于报警值&#xff0c;声光报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计…

微信小程序快速入门01(含案例)

文章目录 前言一、组件1.常用视图容器类组件viewscroll-viewswiper、swiper-item 2.text、rich-text3.其他常用组件buttonimagenavigator 二、小程序API三、数据绑定1.定义页面数据2.绑定数据 四、事件绑定1.什么是事件2.小程序中常用的事件3.事件对象 的属性列表target和curre…

Seata--分布式事务

1 分布式事务基础 1.1 事务 事务指的就是一个操作单元&#xff0c;在这个操作单元中的所有操作最终要保持一致的行为&#xff0c;要么所有操作都成功&#xff0c;要么所有的操作都被撤销。简单地说&#xff0c;事务提供一种“要么什么都不做&#xff0c;要么做全套”机制。 1…

代码随想录算法训练营day60|84.柱状图中最大的矩形 |完结撒花~

84.柱状图中最大的矩形 力扣题目链接 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 1 < heights.length <10^5 0 < heights[i] < 10^…

ChatGPT WPS AI 一键核对两表数据差异

业务需求,找出两个表中不相同的内容。如下图: 像这样的表格中,要找出不同的值,手动核对效率不高。 现在我们有了ChatGPT,可以由人工智能来完成这一操作,高效,快速,准确定位差异值。 指令:请找出A1:G14 单元格区域和I1:O14单元格区域的不相同部分,将两部数据区域不相…

10.1网站编写(Tomcat和servlet基础)

一.Tomcat: 1.Tomcat是java写的,运行时需要依赖jre,所以要装jdk. 2.建议配置好环境变量. 3.默认端口号8080(业务端口)可能会被占用,建议改一下(本人改成了9999). 4.另一个默认端口是8005(管理端口). 二Servlet基础(编写一个hello world代码): 整体分为7个步骤,分别是创建…

精品Python比赛报名系统竞赛

《[含文档PPT源码等]精品基于Python实现的比赛报名系统设计与实现》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程等 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技术&#xff1a;JavaScript…

LeetCode 接雨水 木桶理论、dp预处理

原题链接&#xff1a; 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题面&#xff1a; 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a…

C语言之字符函数字符串函数篇(2)

目录 字符串查找 strstr strstr的使用 strstr的模拟实现 分析 考虑点 代码 strt strtok的使用 循环改进 错误信息报告 strerror 错误码的错误信息 strerror的使用 perror 字符操作 字符分类函数 字符转化函数 今天我们接着讲字符串函数&#xff0c;也…

Android 富文本SpannableString

一、认识SpannableString 为什么要使用富文本 在Android开发中&#xff0c;有很多UI会画出一些特别炫酷的界面出来&#xff0c;比如一个字符串里有特殊的字会有其他颜色并加粗、变大变小、插入小图片、给某几个文字添加边框&#xff0c;如果我们使用笨办法用几个TextView或者Im…

解决Spring Boot 2.7.16 在服务器显示启动成功无法访问问题:从本地到服务器的部署坑

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…