CountDownLatch内部原理解析

news2024/11/28 12:40:04

文章目录

  • 1、CountDownLatch介绍
    • 1.1、功能介绍
    • 1.2、demo
    • 1.3、问题
  • 2、前置知识
    • 2.1、AQS整体结构
      • 2.1.1、整体结构
      • 2.1.2、state属性
      • 2.1.3、head和tail属性
  • 3、CountDownLatchAPI源码解析
    • 3.1、countDown方法
      • 3.1.1、Sync类
      • 3.1.2、releaseShared方法
      • 3.1.3、tryReleaseShared方法
    • 3.2、await方法
      • 3.2.1、tryAcquireShared方法
      • 3.2.2、doAcquireSharedInterruptibly方法
        • 3.2.2.1、addWaiter方法。
        • 3.2.2.2、setHeadAndPropagate方法
        • 3.2.2.3、shouldParkAfterFailedAcquire方法
        • 3.2.2.4、parkAndCheckInterrupt方法
        • 3.2.2.5、cancelAcquire方法
    • 3.3、doReleaseShared方法
  • 4、总结
  • 5、参考资料

1、CountDownLatch介绍

1.1、功能介绍

CountDownLatch,是一个同步器,常见的使用场景是:
我们在主线程中需要执行一些异步任务,但是在全部的异步任务都执行完成前,主线程需要阻塞等待,等待异步任务全部执行完成后,主线程才能响应客户端。

1.2、demo

基于以上场景,搞了一个demo。
下面的这个demo。总体上分为了三个小步骤,代表了CountDownLatch的三个核心API
1)、初始化一个CountDownLatch。构造器中传入了10,代表这个CountDownLatch需要执行10次countDown方法。
2)、然后在一个循环中提交异步任务,总共提交10个异步任务,每个异步任务执行一次countDown方法。
3)、循环执行完成后,执行CountDownLatch.await方法

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            5,
            10,
            60L,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy());
    @Test
    public void testDel(){
    	//初始化一个CountDownLatch
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
        	//给线程池提交异步任务,总共提交10个
            threadPoolExecutor.execute(() -> {
                try {
                    System.out.println("执行一个异步任务");
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    //执行countDownLatch.countDown(),该方法一定要写在finally中,确保无论什么情况,都能执行到
                    countDownLatch.countDown();
                }
            });
        }
        try {
            System.out.println("主线程等待");
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

1.3、问题

使用起来很简单,也就是三步。我有3个问题
1)、countDown方法执行后,在做什么操作?
2)、await方法执行后,countDown方法全部执行完成前,流程阻塞在了await处。CountDownLatch怎么实现的阻塞呢?
3)、countDown方法全部执行完成后,await方法被唤醒,继续执行下面的业务逻辑,怎么唤醒的呢?
这里多说一句。看源码,最好能带着问题看,这样看起来,不会太枯燥,因为你要搞明白一些问题。
说回文章。我们下面就围绕我的这3个问题来展开。如果你还有其他问题,看懂我的3个问题后,可以自己再研究一下源码

2、前置知识

CountDownLatch是借助AQS实现的,大家应该都听说过AQS。其实不光CountDownLatch,像我们常使用的ReenTrantLock、Semaphore等同步器类也都是借助AQS实现的。所以在介绍CountDownLatch前,需要先介绍AQS。
AQS全称是:AbstractQueuedSynchronizer
先从类名上剖析一下这个类。
Abstract:抽象的。说明内部有一些方法需要子类实现
Queued:队列。内部实现依赖了队列
Synchronizer:同步器。这个类是用来设计同步器的

2.1、AQS整体结构

2.1.1、整体结构

AQS抽象类的属性及主要方法,如下图(原图链接)
在这里插入图片描述
带颜色的都是方法,不带颜色的都是属性。
从上到下,难度由浅入深。像CountDownLatch相关的API就是和AQS的API层进行交互。从图上也能看出来,AQS内容很多,为了避免篇幅过大,我们只介绍和CountDownLatch有关系的。其他方法或属性,感兴趣可以自己研究下

AQS中和CountDownLatch有关系的属性如下:
private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;

2.1.2、state属性

state属性是AQS中的许可或者说资源,不管是CountDownLatch,还是ReentrantLock或者Semaphore,本质上都是在操作这个state属性。我们以CountDownLatch举例。
初始化时,new CountDownLatch(10),就是将state的值设置为了10。看一下源码

//CountDownLatch构造器
public CountDownLatch(int count) {
    if (count < 0){
    	throw new IllegalArgumentException("count < 0");
    }
    this.sync = new Sync(count);
}
//Sync构造器
Sync(int count) {
    setState(count);
}
//setState方法
protected final void setState(int newState) {
    state = newState;
}

countDown方法,最终会对state值减一
await方法,监控state值的变化,当变成0时,阻塞结束
这两个方法的源码,下面会详细的解析,这里就不细说了。

2.1.3、head和tail属性

看命名,就知道这两个属性是链表上的头节点和尾节点,初始值为null。数据类型是Node。说明,AQS内部存在一条Node链表
看一下Node的属性

static final class Node {
    ......
    //共享模式的Node
    static final Node SHARED = new Node();
    
    //独占模式的Node
    static final Node EXCLUSIVE = null;

    //节点的等待状态,初始值是0
    volatile int waitStatus;

    //前置节点
    volatile Node prev;

    //后置节点
    volatile Node next;

    //节点代表的线程。每一个需要阻塞的线程,都会被封装成一个Node
    volatile Thread thread;
    ......
}

那Node链表又长什么样子呢?
在这里插入图片描述
就长这个样子
其中的waitStatus值很重要,关于Node链表,AQS基本就是围绕Node节点的waitStatus在做文章。除了初始化值0之外,还有另外4个取值

//节点处于取消状态。这个值是大于0的,另外的3个值都是小于0的。记一下这个点,源码中会经常遇到
static final int CANCELLED =  1;

//节点处于被唤醒状态。
static final int SIGNAL = -1;

//节点处于条件状态,这个CONDITION一般在CONDITION队列中使用。关于CountDownLatch,我们主要是讨论Sync队列
static final int CONDITION = -2;

//对于共享模式的Node来说,这个状态代表了传播
static final int PROPAGATE = -3;

AQS重要属性说完了。下面开始介绍CountDownLatch的重要API源码实现

3、CountDownLatchAPI源码解析

3.1、countDown方法

public void countDown() {
    sync.releaseShared(1);
}

这里调用了Sync类的releaseShard方法,接下来,看一下Sync类的实现。

3.1.1、Sync类

Sync类定义如下:
可以看到Sync类继承了AQS,所以Sync类是连接CountDownLatch和AQS的桥梁。

private static final class Sync extends AbstractQueuedSynchronizer {
    ......

    //这个构造方法,我们在上面已经见过了,new CountDownLatch(10),设置CountDownLatch的许可数量时,就会使用到这个构造器。
    Sync(int count) {
        setState(count);
    }

    //获取state
    int getCount() {
        return getState();
    }

    //判断AQS中state的值状态。这是重写的AQS的方法
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    ......
}

3.1.2、releaseShared方法

在Sync类中,我们并没有看到releaseShared方法,说明releaseShared是父类AQS的方法。
我们进入AQS看一下releaseShard方法的逻辑

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

先执行了tryReleaseShared方法,如果返回true,再执行doReleaseShared方法。先看tryReleaseShared方法

3.1.3、tryReleaseShared方法

protected boolean tryReleaseShared(int arg) {
     throw new UnsupportedOperationException();
}

原方法是抛出异常,说明这个方法需要子类重新实现,这就又回到了CountDownLatch类的Sync类中,Sync类中实现了tryReleaseShared方法,从方法名中能看出,这是共享模式释放。啥是共享模式呢?这里简单提一句,共享模式的对立面是独占模式。类似ReentrantLock,加锁操作就是独占模式,同一时刻,只能有一个线程加锁成功。共享模式,就是多个线程可以同时执行,对于CountDownLatch来说,多个线程可以同时执行countDown操作,所以countDown操作是共享模式。内部相应的方法命名时就会添加shared标识。比如此处的tryReleaseShared方法
看一下tryReleaseShared的实现

protected boolean tryReleaseShared(int releases) {
    	//开启死循环
        for (;;) {
        	//获取state数量
            int c = getState();

            if (c == 0)
            	//返回失败
                return false;

            //如果state数量不是0,state减一
            int nextc = c-1;

            //CAS设置新的state值
            if (compareAndSetState(c, nextc))
            	//如果state执行减一操作后值为0,说明异步线程全部执行countDown完成,此时可以唤醒主线程
                return nextc == 0;
        }
    }

这个方法最终就是对state执行减一操作。
tryReleaseShared执行完成,如果返回结果是true,会回到releaseShared方法中,然后就会执行doReleaseShared方法。
这里停一下,我们先不看doReleaseShared方法实现。先回到我们的CountDownLatch使用场景上。我们知道CountDownLatch.countDown一般是子线程执行,子线程全部执行countDown完成前,主线程是阻塞在countDownLatch.await处的,等待CountDownLatch.countDown全部执行完成,然后才会继续执行。
所以在执行doReleaseShared方法前,一般会先执行countDownLatch.await方法。所以,我们先看countDownLatch.await的实现逻辑,然后再看doReleaseShared方法逻辑。

3.2、await方法

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

await方法抛出了InterruptedException,说明这个方法是能响应线程中断的。在await方法内部执行了Sync类的acquireSharedInterruptibly方法
这个方法,Sync类中并没有,所以是父类AQS中的。我们到AQS中看一下这个方法的逻辑

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //检查当前线程是否被中断。如果被中断,抛出中断异常
        if (Thread.interrupted())
            throw new InterruptedException();

        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

先是判断了线程是否被中断,如果被中断,会直接抛出中断异常
如果线程没有被中断,然后会执行tryAcquireShared。我们看一下tryAcquireShared的方法实现

3.2.1、tryAcquireShared方法

tryAcquireShared实现如下:

protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

可以看到,AQS中的tryAcquireShared方法逻辑是直接抛出了UnsupportedOperationException异常(不支持的操作),说明该方法是需要子类重写的。我们回到CountDownLatch的Sync类中看下Sync类重写后的方法实现

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

可以看到,就是判断state的值。如果getState == 0的话,就返回1,此时代表CountDownLatch.countDown已经全部执行完成。如果getState != 0的话,就返回-1,代表CountDownLatch.countDown未全部执行完成。
tryAcquireShared,如果返回-1的话,接着就会执行doAcquireSharedInterruptibly方法

3.2.2、doAcquireSharedInterruptibly方法

这是CountDownLatch实现await方法的关键逻辑

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {

        //将当前线程以共享模式加入Node链表中
        final Node node = addWaiter(Node.SHARED);

        //获取锁的结果,初始化值为true,代表获取锁失败
        boolean failed = true;
        try {
        	//开启一个死循环,这就实现了CountDownLatch.await阻塞的效果。
            for (;;) {
            	//获取当前节点的前置节点
                final Node p = node.predecessor();

                //如果前置节点是头节点,再次执行tryAcquireShared方法判断state的值
                if (p == head) {
                	//获取state的状态
                    int r = tryAcquireShared(arg);

                    //如果返回值r >= 0代表state是0,此时说明CountDownLatch.countDown已经执行完成,此时会结束死循环,主线程继续往下执行
                    if (r >= 0) {
                    	//将当前节点设置为头节点并且进行传播
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }

                //1、shouldParkAfterFailedAcquire判断是否需要执行线程阻塞。
                //2、parkAndCheckInterrupt是阻塞当前线程,避免一直获取不到锁,浪费cpu
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
            	//获取锁失败的话,执行cancelAcquire方法
                cancelAcquire(node);
        }
    }

doAcquireSharedInterruptibly方法中有很多逻辑,这个方法的主要作用是:
开启死循环,线程不断地获取锁。如果线程满足阻塞条件的话,就对该线程执行阻塞操作,然后休眠等待被唤醒,避免浪费cpu。被唤醒后,继续获取锁。获取锁成功,就退出死循环。
下面挨个分析其中的方法

3.2.2.1、addWaiter方法。

看这个方法名,也能知道这是添加一个等待者,也就是我们执行了CountDownLatch.await方法的主线程

private Node addWaiter(Node mode) {
		//创建一个Node,Node的线程是当前线程,节点模式是共享模式
        Node node = new Node(Thread.currentThread(), mode);
        
        //获取Node链表的尾指针
        Node pred = tail;
        //如果尾指针不为空,说明Node链表已经被初始化。将当前Node节点入队
        if (pred != null) {
        	//获取node节点的前向指针
            node.prev = pred;

            //将当前节点设置为尾节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果尾指针为空,说明Node链表未被初始化,需要先初始化Node链表,再将当前Node节点入队
        enq(node);
        return node;
    }

//enq方法逻辑
private Node enq(final Node node) {
		//开启死循环
        for (;;) {
        	//获取Node链表尾节点
            Node t = tail;

            //如果尾节点为空,说明Node链表未被初始化,需要初始化
            if (t == null) {

            	//初始化一个Node节点,利用CAS将其设置为头节点
                if (compareAndSetHead(new Node()))

                	//tail节点指向head节点
                    tail = head;
            } else {
            	//当前Node节点前向指针指向尾部节点
                node.prev = t;
                //将当前节点设置为尾节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里总结一下,addWaiter方法就是将执行CountDownLatch.await的主线程加入到等待链表中

3.2.2.2、setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;
        //将当前节点设置为头节点
        setHead(node);
        
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //取当前节点的下一个节点,对于我们这个场景来说,只有一个节点,没有下一个节点,所以下一个节点是null
            Node s = node.next;

            //如果下一个节点是null,执行doReleaseShared方法
            if (s == null || s.isShared())
                doReleaseShared();
}

doReleaseShared方法的逻辑如下

private void doReleaseShared() {
		//开启一个死循环
        for (;;) {
        	//取头节点。头节点就是我们的节点
            Node h = head;

            //头结点不为空 && 头结点不等于尾节点。这说明Node链表中间有其他元素
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

在CountDownLatch的流程中,只有一个主线程,并且在setHeadAndPropagate方法中,先执行了setHead(node)方法,将当前节点设置为头节点,所以此时的头节点和尾节点是相同的。所以h != null && h != tail结果为假,doReleaseShared方法会直接返回

执行完setHeadAndPropagate方法,下面就该执行shouldParkAfterFailedAcquire和parkAndCheckInterrupt了。这两个方法,前一个是判断能否执行阻塞,后一个是执行线程阻塞操作的。
我们看一下shouldParkAfterFailedAcquire的实现逻辑。

3.2.2.3、shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;

        //如果前置节点状态是SIGNAL,此时返回true,说明需要阻塞当前节点
        if (ws == Node.SIGNAL)
            return true;

        //waitStatus > 0,说明当前节点的前置节点是取消状态
        if (ws > 0) {
            
            do {
            	//向前遍历,删除已取消节点
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);

            //找到不是取消节点的节点,将当前节点放入该节点后
            pred.next = node;
        } else {

            //将当前节点的waitStatus置为SIGNAL状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
3.2.2.4、parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

逻辑里,调用了LockSupport.park方法,阻塞当前线程,然后返回了线程的中断状态
为什么要返回中断状态呢?
因为LockSupport.park方法是能响应线程中断的,但是响应完中断后,并不会抛出异常,那这个时候就会有一个问题。我们怎么知道park方法是怎么被唤醒的呢?
是CountDownLatch.countDown执行完成后,正常被唤醒的。
还是执行CountDownLatch.await方法的线程执行了中断唤醒的。
区分不出来吧?所以此处要执行Thread.interrupted(),如果线程执行了中断,此时会Thread.interrupted()会返回true。以此就可以判断LockSupport.park的唤醒原因

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
   throw new InterruptedException();

如果线程发生了中断,此时就会抛出中断异常

3.2.2.5、cancelAcquire方法

抛出中断异常后,doAcquireSharedInterruptibly方法中的finally代码块就会开始执行。
如果failed值为true的话,此时会执行cancelAcquire方法

if (failed)
   //获取锁失败的话,执行cancelAcquire方法
   cancelAcquire(node);

从方法名上看,cancelAcquire方法是取消获取的意思。意思就是取消获取锁,说明节点对应的thread不再竞争锁。在这个方法中,会将Node节点的waitStatus值置为CANCELLED。这个方法和我们要研究的CountDownLatch流程没有什么关系,就不看了
以上,就是await方法的流程。

3.3、doReleaseShared方法

我们在介绍countDown方法的时候说过,有一个方法等介绍完await方法再进行介绍,还有印象吧?就是在介绍countDown方法逻辑时,tryReleaseShared方法执行后,应该执行doReleaseShared方法。再看一下countDown流程中的这段源码

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared方法执行成功后,会执行doReleaseShared方法
看一下doReleaseShared方法的实现

private void doReleaseShared() {
        //开启一个死循环
        for (;;) {
        	//获取头节点
            Node h = head;
            //头结点不等于空 && 头节点不等于尾节点
            if (h != null && h != tail) {
            	//获取头结点的waitStatus值
                int ws = h.waitStatus;

                //如果头节点状态是SIGNAL
                if (ws == Node.SIGNAL) {
                	//将头结点状态改成0,初始状态
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    //唤醒后面节点
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;
            }
            if (h == head)
                break;
        }
    }

这个方法,对于CountDownLatch的流程来说,其实可以不用关注。为什么这样说呢?
对于CountDownLatch来说,Node链表上其实只有一个节点。并且在执行
setHeadAndPropagate方法时,将当前主线程代表的Node设置为了头节点,所以头尾是相同的。在执行h != null && h != tail判断条件时,就不满足判断条件了,所以在执行h == head判断条件时,结果就会为true,方法就直接返回了

4、总结

以上就是CountDownLatch核心API的源码解析。
我画了一张图,我们从整体上看一下CountDownLatch的操作流程图
在这里插入图片描述
CountDownLatch本身的API不多,也都很简单。但是因为CountDownLatch是借助AQS框架实现的,所以会涉及到一些AQS的逻辑,稍微复杂一点。

5、参考资料

1)、https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html。这是美团技术团队写的一篇文章,我最开始看不懂AQS源码的时候,搜到了这篇文章,连续读了3遍,然后对AQS的整体技术脉络有了一个大体的认识,后面再看CountDownLatch相关的代码就清楚多了。推荐大家也读一下。

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

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

相关文章

ICMP协议详解及尝试用ping和tracert捕抓ICMP报文

一、ICMP协议 1.1、定义 ICMP&#xff08;Internet Control Message Protocol&#xff0c;互联网控制消息协议&#xff09;是一个支持IP层数据完整性的协议&#xff0c;主要用于在IP主机、路由器之间传递控制消息。这些控制消息用于报告IP数据报在传输过程中的错误&#xff0c…

ChatGPT4深度解析:探索智能对话新境界

大模型chatgpt4分析功能初探 目录 1、探测目的 2、目标变量分析 3、特征缺失率处理 4、特征描述性分析 5、异常值分析 6、相关性分析 7、高阶特征挖掘 1、探测目的 1、分析chat4的数据分析能力&#xff0c;提高部门人效 2、给数据挖掘提供思路 3、原始数据&#xf…

保研复习 | 数据结构

目录 CH1 绪论☆ 数据项、数据元素、数据结构☆ 逻辑结构和存储结构的区别☆ 顺序存储结构和链式存储结构的比较☆ 算法的重要特性☆ 算法的复杂度 CH2 线性表☆ 单链表 CH3 栈、队列和数组☆ 栈和堆是什么&#xff1f;☆ 栈在括号匹配中的应用☆ 栈在表达式求值中的应用☆ …

14-41 剑和诗人15 - RLAIF 大模型语言强化培训

​​​​​​ 介绍 大型语言模型 (LLM) 在自然语言理解和生成方面表现出了巨大的能力。然而&#xff0c;这些模型仍然存在严重的缺陷&#xff0c;例如输出不可靠、推理能力有限以及缺乏一致的个性或价值观一致性。 为了解决这些限制&#xff0c;研究人员采用了一种名为“人工…

3dsMax怎样让渲染效果更逼真出色?三套低中高参数设置

渲染是将精心构建的3D模型转化为逼真图像的关键步骤。但要获得令人惊叹的渲染效果&#xff0c;仅仅依赖默认设置是不够的。 实现在追求极致画面效果的同时&#xff0c;兼顾渲染速度和时间还需要进行一些调节设置&#xff0c;如何让渲染效果更加逼真&#xff1f; 一、全局照明与…

昇思25天学习打卡营第13天|K近邻算法实现红酒聚类

K近邻算法&#xff08;K-Nearest-Neighbor, KNN&#xff09;是一种用于分类和回归的非参数统计方法&#xff0c;是机器学习最基础的算法之一。它正是基于以上思想&#xff1a;要确定一个样本的类别&#xff0c;可以计算它与所有训练样本的距离&#xff0c;然后找出和该样本最接…

数据结构基础--------【二叉树基础】

二叉树基础 二叉树是一种常见的数据结构&#xff0c;由节点组成&#xff0c;每个节点最多有两个子节点&#xff0c;左子节点和右子节点。二叉树可以用来表示许多实际问题&#xff0c;如计算机程序中的表达式、组织结构等。以下是一些二叉树的概念&#xff1a; 二叉树的深度&a…

win10使用小技巧二

1. 解决电脑更新后无法连接打印机问题 步骤&#xff1a;右击电脑 → 选择“管理” → 打开“服务和应用程序” → 双击“服务” → 找到“Print Spooler” → 双击打开 → 在“常规”中将“启动类型”改为“自动” → 点击“应用” → 点击“确定” → 重启电脑。 2. 提升电脑…

rocketmq-console可视化界面功能说明

rocketmq-console可视化界面功能说明 登录界面OPS(运维)Dashboard(驾驶舱)Cluster(集群)Topic(主题)Consumer(消费者)Producer(生产者)Message(消息)MessageTrace(消息轨迹) rocketmq-console是rocketmq的一款可视化工具&#xff0c;提供了mq的使用详情等功能。 本章针对于rock…

css使用伪元素after或者before的时候想要给after设置z-index无效

css使用伪元素after或者before的时候想要给after或者before设置一个层级关系&#xff0c;使该伪类写入的样式在box的下面&#xff0c;发现给box设置z-index无效&#xff0c; 需要找到父级元素&#xff0c;在父级元素上设置z-index值并且将伪类设置z-index:-1

在数字化时代,自助BI是数据价值最大化的必经之路

引言&#xff1a;在数字化时代&#xff0c;数据已成为企业最宝贵的资产之一。然而&#xff0c;仅仅拥有海量数据并不足以带来竞争优势&#xff0c;关键在于如何有效地分析并利用这些数据以指导决策、优化运营、提升客户体验&#xff0c;并最终实现业务的持续增长。在一章里笔者…

实验1 主成分分析

目 录 二、实验环境... 1 三、实验内容... 1 3.1 导入数据... 2 3.2 求相关系数矩阵.. 3 3.3 数据规范化处理.. 3 3.4 主成分分析... 4 四 实验心得... 5 一、实验目的 &#xff08;1&#xff09;理解主成分分析的思想&#xff1b; &#xff08;2&#xff09;掌握主成分分析方…

昇思25天学习打卡营第19天 | CycleGAN图像风格迁移互换

内容介绍&#xff1a; CycleGAN(Cycle Generative Adversarial Network) 即循环对抗生成网络&#xff0c;该模型实现了一种在没有配对示例的情况下学习将图像从源域 X 转换到目标域 Y 的方法。 该模型一个重要应用领域是域迁移(Domain Adaptation)&#xff0c;可以通俗地理解…

常用SQL语句(基础篇)

前言 查询的sql的结构是 select...from...where...group by...having...order by...limit... 写查询sql的时候需要按照如下顺序写 from&#xff0c;where&#xff08;and&#xff0c;or&#xff0c;&#xff01;&#xff09;&#xff0c;group by&#xff0c;select&#xf…

文件系统技术架构分析

一文读懂&#xff1a;什么是文件系统 &#xff0c;有哪几类&#xff1f; ▉ 什么是文件系统&#xff1f; 技术大拿眉头皱了皱&#xff0c;忍住快要爆发的情绪。解释到&#xff1a; 数据以二进制形式存储于介质&#xff0c;但高低电平含义难解。文件系统揭秘这些二进制背后的意…

智能交通(3)——Learning Phase Competition for Traffic Signal Control

论文分享 https://dl.acm.org/doi/pdf/10.1145/3357384.3357900https://dl.acm.org/doi/pdf/10.1145/3357384.3357900 论文代码 https://github.com/gjzheng93/frap-pubhttps://github.com/gjzheng93/frap-pub 摘要 越来越多可用的城市数据和先进的学习技术使人们能够提…

代码随想录 数组部分+代码可在本地编译器运行

代码随想录 数组部分&#xff0c;代码可在本地编译器运行 文章目录 数组理论基础704.二分查找题目&#xff1a;思路二分法第一种写法二分法第二种写法 代码 27.移除元素题目&#xff1a;思路-双指针法代码 977.有序数组的平方题目思路-双指针代码 209.长度最小的子数组题目&am…

《Windows API 每日一练》8.4 edit控件

编辑类是最简单的预定义窗口类&#xff0c;而另一方面却又是最复杂的。当你用“edit”作为类名创建子窗口时&#xff0c;可以基于CreateWindow调用的x坐标、y坐标、宽度和高度参数定义一个矩形。这个矩形包含可编辑的文本。一旦子窗口控件获得输入焦点&#xff0c;你就可以输入…

黑色矩形块检测数据集VOC+YOLO格式2000张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2000 标注数量(xml文件个数)&#xff1a;2000 标注数量(txt文件个数)&#xff1a;2000 标注…

使用ifconfig命令获取当前服务器的内网IP地址

如何使用ifconfig命令获取当前服务器的内网IP地址呢&#xff1f; ifconfig eth0 | grep inet | awk {print $2}