多线程——线程同步器CountDownLatch

news2024/11/17 17:40:55

多线程——线程同步器CountDownLatch

  • (一)CountDownLatch案例入门
    • 【1】CountDownLatch和join的区别
    • 【2】CountDownLatch案例一:等待线程等待工作线程
    • 【3】CountDownLatch案例二:主线程main等待子线程
    • 【4】CountDownLatch案例三:线程池主线程等待子线程
  • (二)CountDownLatch的实现原理和源码分析
    • 【1】构造方法
    • 【2】await() 源码解析:阻塞线程进入等待队列里自旋
    • 【3】countDown() 源码解析:计数器减1
    • 【4】如何唤醒所有调用 await() 等待的线程呢?
  • (三)CountDownLatch的流程总结

(一)CountDownLatch案例入门

【1】CountDownLatch和join的区别

在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。

之前可以使用join方法实现,但是join方法不够灵活,join方法需要线程Thread调用,而在项目中都避免直接操作线程,而是使用线程池来管理线程,这时候就没有办法直接调用这些线程的join方法了,这个时候就需要使用CountDownLatch了。

CountDownLatch是使用计数器来允许子线程运行完毕或者在运行中递减计数。

【2】CountDownLatch案例一:等待线程等待工作线程

下面代码演示2个等待线程通过CountDownLatch去等待3个工作线程完成操作:工作线程在执行结束后调用countDown方法,等待线程在执行开始前调用await方法

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        // 让2个线程去等待3个三个工作线程执行完成
        CountDownLatch c = new CountDownLatch(3);

        // 2 个等待线程
        WaitThread waitThread1 = new WaitThread("wait-thread-1", c);
        WaitThread waitThread2 = new WaitThread("wait-thread-2", c);

        // 3个工作线程
        Worker worker1 = new Worker("worker-thread-1", c);
        Worker worker2 = new Worker("worker-thread-2", c);
        Worker worker3 = new Worker("worker-thread-3", c);

        // 启动所有线程
        waitThread1.start();
        waitThread2.start();
        Thread.sleep(1000);
        worker1.start();
        worker2.start();
        worker3.start();
    }
}

/**
 * 等待线程
 */
class WaitThread extends Thread {

    private String name;
    private CountDownLatch c;

    public WaitThread(String name, CountDownLatch c) {
        this.name = name;
        this.c = c;
    }

    @Override
    public void run() {
        try {
            // 等待
            System.out.println(this.name + " wait...");
            c.await();
            System.out.println(this.name + " continue running...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 工作线程
 */
class Worker extends Thread {

    private String name;
    private CountDownLatch c;

    public Worker(String name, CountDownLatch c) {
        this.name = name;
        this.c = c;
    }

    @Override
    public void run() {
        System.out.println(this.name + " is running...");
        try {
            Thread.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.name + " is end.");
        c.countDown();
    }
}

运行结果:

wait-thread-1 wait...
wait-thread-2 wait...
worker-thread-3 is running...
worker-thread-2 is running...
worker-thread-1 is running...
worker-thread-1 is end.
worker-thread-3 is end.
worker-thread-2 is end.
wait-thread-1 continue running...
wait-thread-2 continue running...

Process finished with exit code 0

【3】CountDownLatch案例二:主线程main等待子线程

public class ThreadTest {

    /**初始化CountDownLatch,值为线程数量*/
    private static final CountDownLatch ctl = new CountDownLatch(10);
    
    public static void main(String[] args)throws Exception{
        System.out.println("主线程正在执行前:"+Thread.currentThread().getName());
        test3();
        ctl.await(20, TimeUnit.SECONDS);//最多等待20秒,不管子线程完没完
        System.out.println("主线程正在执行后:"+Thread.currentThread().getName());
    }


    public static void test3(){
        try {
            for (int i = 1 ;i <= 10;i ++){
                Thread.sleep(1000);
                new Thread(()->{
                    System.out.println("子线程正在执行:"+Thread.currentThread().getName());
                }).start();
                ctl.countDown();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

}

执行结果

主线程正在执行前:main
子线程正在执行:Thread-0
子线程正在执行:Thread-1
子线程正在执行:Thread-2
子线程正在执行:Thread-3
子线程正在执行:Thread-4
子线程正在执行:Thread-5
子线程正在执行:Thread-6
子线程正在执行:Thread-7
子线程正在执行:Thread-8
子线程正在执行:Thread-9
主线程正在执行后:main

【4】CountDownLatch案例三:线程池主线程等待子线程

利用 CountDownLatch 定义了要等待的子线程数量,这样在该统计数量不为0的时候,主线代码暂时挂起,直到所有的子线程执行完毕(调用countDown()方法)后主线程恢复执行。否则就需要使用轮询的方法判断子线程是不是都结束了,都结束了才能使用shutdown关闭线程池

(1)轮询的方式
需要注意的是调用isTerminated()前一定要先调用shutdown()或shutdownNow()方法

public static void main(String[] args) throws Exception {
    int n = 3;
    String[] tasks = {"发送短信消息完毕", "发送微信消息完毕", "发送邮箱消息完毕"};
    int[] executeTimes = new int[]{2, 5, 1};
    ExecutorService threadPool = Executors.newFixedThreadPool(n);
    long start = System.currentTimeMillis();

    for (int i = 0; i < n; i++) {
        int finalI = i;
        threadPool.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(executeTimes[finalI]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(tasks[finalI]);
        });
    }
    // 以下为关键代码
    threadPool.shutdown();
    while (true) {
        if (threadPool.isTerminated()) {
            break;
        } else {
            TimeUnit.SECONDS.sleep(1);
        }
    }
    System.out.println("所有消息都发送完毕了,继续执行主线程任务。\n耗时ms:" + (System.currentTimeMillis() - start));
}

(2)使用CountDownLatch
让主线程等待子线程全部结束后再关闭线程池

public static void main(String[] args) throws Exception {
    int n = 3;
    String[] tasks = {"发短信完毕", "发微信完毕", "发QQ完毕"};
    int[] executeTimes = new int[]{2, 5, 1};
    CountDownLatch countDownLatch = new CountDownLatch(n);
    ExecutorService executorService = Executors.newFixedThreadPool(n);

    long start = System.currentTimeMillis();
    for (int i = 0; i < n; i++) {
        int finalI = i;
        executorService.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(executeTimes[finalI]);
                System.out.println(tasks[finalI]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();
    System.out.println("所有消息都发送完毕了,继续执行主线程任务。\n耗时ms:" + (System.currentTimeMillis() - start));
    // 不要忘记关闭线程池,不然会导致主线程阻塞无法退出
    executorService.shutdown();
}

(二)CountDownLatch的实现原理和源码分析

【1】构造方法

CountDownLatch通过内部类Sync来实现同步语义。Sync继承AQS。CountDownLatch通过构造器给state赋值

public CountDownLatch(int count) {
     if (count < 0) throw new IllegalArgumentException("count < 0");
     this.sync = new Sync(count);
 }
//设置初始state为计数器初始值,这使用的是AQS的state,Lock锁中使用了这个变量用来表示重入次数
 Sync(int count) {
     setState(count);
 }

Sync内部类代码如下:

private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    // 设置同步状态的值
    Sync(int count) {
        setState(count);
    }

    // 获取同步状态的值
    int getCount() {
        return getState();
    }

    // 尝试获取同步状态,只有同步状态的值为0的时候才成功
    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    // 尝试释放同步状态,每次释放通过CAS将同步状态的值减1
    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            // 如果同步状态的值已经是0了,不要再释放同步状态了,也不要减1了
            if (c == 0)
                return false;
            // 减1
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

【2】await() 源码解析:阻塞线程进入等待队列里自旋

await()源码如下:

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

调用的是AQS的acquireSharedInterruptibly(int arg)方法:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 如果被中断,抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取同步状态
    if (tryAcquireShared(arg) < 0)
        // 获取同步状态失败,自旋
        doAcquireSharedInterruptibly(arg);
}

首先,通过tryAcquireShared(arg)尝试获取同步状态state值,如果同步状态state的值为0,获取成功。这就是CountDownLatch的机制,尝试获取latch的线程只有当latch的值减到0的时候,才能获取成功。

如果获取失败,则会调用AQS的doAcquireSharedInterruptibly(int arg)函数自旋,尝试挂起当前线程:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 将当前线程加入同步队列的尾部
    final Node node = addWaiter(Node.SHARED);
    try {
        // 自旋
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头结点,则尝试获取同步状态
            if (p == head) {
                // 当前节点尝试获取同步状态
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 如果获取成功,则设置当前节点为头结点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            // 如果当前节点的前驱不是头结点,尝试挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

这里,调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 挂起当前线程。

【3】countDown() 源码解析:计数器减1

countDown()源码如下:

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

调用的是AQS的releaseShared(int arg)方法:

public final boolean releaseShared(int arg) {
    // 尝试释放同步状态
    if (tryReleaseShared(arg)) {
        // 如果成功,进入自旋,尝试唤醒同步队列中头结点的后继节点
        doReleaseShared();
        return true;
    }
    return false;
}

首先,通过tryReleaseShared(arg)尝试释放同步状态,具体的实现被Sync重写了,源码:

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        // 同步状态值减1
        int nextc = c - 1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

如果同步状态值减到0,则释放成功,进入自旋,尝试唤醒同步队列中头结点的后继节点,调用的是AQS的doReleaseShared()函数:

private void doReleaseShared() {
    for (;;) {
        // 获取头结点
        Node h = head;
        if (h != null && h != tail) {
            // 获取头结点的状态
            int ws = h.waitStatus;
            // 如果是SIGNAL,尝试唤醒后继节点
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 唤醒头结点的后继节点
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

这里调用了unparkSuccessor(h)去唤醒头结点的后继节点。

【4】如何唤醒所有调用 await() 等待的线程呢?

此时这个后继节点被唤醒,那么又是如何实现唤醒所有调用await()等待的线程呢?回到线程被挂起的地方,也就是doAcquireSharedInterruptibly(int arg)方法中:

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 将当前线程加入同步队列的尾部
    final Node node = addWaiter(Node.SHARED);
    try {
        // 自旋
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头结点,则尝试获取同步状态
            if (p == head) {
                // 当前节点尝试获取同步状态
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 如果获取成功,则设置当前节点为头结点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            // 如果当前节点的前驱不是头结点,尝试挂起当前线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

该方法里面,通过调用shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()将线程挂起。

当头结点的后继节点被唤醒后,线程将从挂起的地方醒来,继续执行,因为没有return,所以进入下一次循环。

此时,获取同步状态成功,执行setHeadAndPropagate(node, r)。

// 如果执行这个函数,那么propagate一定等于1
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) {
        // 获取当前节点的下一个节点
        Node s = node.next;
        // 如果下一个节点为null或者节点为shared节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

这里,当前节点被唤醒,首先设置当前节点为头结点。

如果当前节点的下一个节点是shared节点,调用doReleaseShared(),源码:

private void doReleaseShared() {
    // 自旋
    for (;;) {
        // 获取头结点,也就是当前节点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果head没有改变,则调用break退出循环
        if (h == head)
            break;
    }
}

首先,注意if (h == head) break; 这里每次循环的时候判断head头结点有没有改变,如果没有改变则退出循环。因为只有当新的节点被唤醒之后,新节点才会调用setHead(node)设置自己为头结点,头结点才会改变。

其次,注意if (h != null && h != tail) 这个判断,保证队列至少要有两个节点(包括头结点在内)。

如果队列中有两个或以上个节点,那么检查局部变量h的状态:

(1)如果状态为SIGNAL,说明h的后继节点是需要被通知的。通过对CAS操作结果取反,将compareAndSetWaitStatus(h, Node.SIGNAL, 0)和unparkSuccessor(h)绑定在了一起。说明了只要head成功的从SIGNAL修改为0,那么head的后继节点对应的线程将会被唤醒。
(2)如果状态为0,说明h的后继节点对应的线程已经被唤醒或即将被唤醒,并且这个中间状态即将消失,要么由于acquire thread获取锁失败再次设置head为SIGNAL并再次阻塞,要么由于acquire thread获取锁成功而将自己(head后继)设置为新head并且只要head后继不是队尾,那么新head肯定为SIGNAL。所以设置这种中间状态的head的status为PROPAGATE,让其status又变成负数,这样可能被被唤醒线程检测到。
(3)如果状态为PROPAGATE,直接判断head是否变化。

(三)CountDownLatch的流程总结

CountDownLatch的原理和ReentrantLock的原理类似,都是基于AQS实现的。

CountDownLatch 用于阻塞当前 1 个或多个线程,其目的是让这些线程等待其它线程的执行完成。可以简单将其理解为一个计数器,当初始化一个 count=n 的 CountDownLatch 对象之后,需要调用该对象的 CountDownLatch#countDown 方法来对计数器进行减值,直到计数器为 0 的时候,等待该计数器的线程才能继续执行。

但是需要注意的一点是,执行 CountDownLatch#countDown 方法的线程在执行完减值操作之后,并不会因此而阻塞。真正阻塞等待事件的是调用 CountDownLatch 对象 CountDownLatch#await 方法的线程,该线程一直会阻塞直到计数器计数变为 0 为止。

流程如下:
(1)初始化一个 count=n 的 CountDownLatch 对象,对象中的数值为n,等于把state值设置为n。相当于重入锁重入了n次。等待队列里也放入n个等待的节点。
(2)每个线程调用countDown方法的时候,获取state值,如果state大于0,就使用CAS给state-1。如果state=0,就调用doReleaseShared方法唤醒等待队列里的等待线程。
(3)而每一个线程调用await方法,会先获取state值,如果state的值为0,获取锁成功,当前被阻塞的线程开始执行。如果state大于0,当前线程就进插入等待队列尾部,所以等待队列里可能会有多个等待的线程节点。当线程被头结点唤醒的时候开始自旋判断state是否为,成功的话就继续出队并唤醒后继节点,这样就依次唤醒等待线程了。
(4)所以当所有工作线程执行完countDown方法后,state值正好减到0了。这个时候等待线程获取state成功,然后被唤醒,收尾!
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

.NET Conf China 2022 精彩回顾 | IOT专场

12月充满惊喜 各种美好的节日纷至沓来 奖励一年辛苦劳作的我们 本月程序员第一场节日 就是精彩的 .NET Conf China 2022 .NET Conf China 2022是面向开发人员的社区峰会&#xff0c;延续 .NET Conf 2022的活动&#xff0c;庆祝 .NET7的发布和回顾过去一年来 .NET在中国的发…

举个栗子~Alteryx 技巧(4):教你设置 Alteryx Server 用户权限

分析用户在用 Alteryx Designer 创建好工作流之后&#xff0c;如何在企业内部实现共享和管理分析工作流程、模型及数据呢&#xff1f;方法很简单&#xff1a;发布到 Alteryx Server &#xff01; Alteryx Server 通过内置的数据治理、集中管理的安全性和高可用性&#xff0c;可…

svn:E720005 move .svn/tmp拒绝访问

cleanup也clean不了 此时要用sqlite3.exe来解决了到有.svn的目录地下&#xff0c;把sqllite3.exe拷贝到这个下面 在目录里面打开cmd窗口&#xff0c;执行 sqlite3 .svn/wc.db "select * from work_queue" 如果查出内容&#xff0c;那么执行 sqlite3 .svn/wc.db …

【Dubbo3高级特性】「性能优化技术」Dubbo3完成扩展使用高效的Kryo和FST序列化开发实战

系列文章目录 如果你看到了这里&#xff0c;那么接下来你将会认识Dubbo3的诞生将如何引领微服务领域更进一步&#xff0c;从而迈入云原生的领域&#xff0c;这当然不仅仅是Dubbo3&#xff0c;之前也介绍了Java生态另外一个云原生领域的技术Quarkus等技术&#xff0c;而本文内容…

Spring Boot 实现微信点餐系统

架构 前后端分离&#xff1a; 部署架构&#xff1a; 补充&#xff1a; setting.xml 文件的作用&#xff1a;settings.xml是maven的全局配置文件。而pom.xml文件是所在项目的局部配置。Settings.xml中包含类似本地仓储位置、修改远程仓储服务器、认证信息等配置。 maven的作用…

校招求职经验分享——我是如何本科进入大厂的

写在开头 最近签完了三方&#xff0c;大学前三年的生涯规划和努力最终还是得到了兑现&#xff0c;脑子里不断涌现这几年来的经历&#xff0c;一直想着写点什么东西记录一下&#xff0c;刚好大四空闲时间真的太多&#xff0c;心血来潮&#xff0c;不如还是写写吧&#xff0c;记录…

mysql cdc 整库迁移 (mysql to mysql)

技术思想 利用 mysql catalog&#xff0c;mysql cdc&#xff0c;flink jdbc 等技术实现 mysql 整库迁移至下游数据库&#xff0c;这里是示范 mysql to mysql &#xff0c;其他 sink 组件可自行扩展实现。 通过 flink ParameterTool&#xff0c;可以选择是整库同步还是多表亦或…

2023年天津天狮学院专升本报名考试的安排

天津天狮学院2023年高职升本科考试报名时间安排的通知 一、报名条件 报名条件和具体要求按照天津市招生委员会的文件规定执行。考生必须完成文化课报名环节&#xff0c;且填报天津天狮学院志愿&#xff0c;且具备我校提出的线上考试条件&#xff0c;方可报考我校专业课考试。考…

Java作业

这里写自定义目录标题java16次作业新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是…

python-网络编程

python-网络编程 网络编程的理论概述&#xff1a; 现在的生活离不开网络&#xff0c;例如手机&#xff0c;电脑&#xff0c;平板&#xff0c;都是网络的代名词&#xff0c;通过一些APP&#xff0c;浏览器&#xff0c;获取大量的信息如 文字、声音、视频&#xff0c;这都是从网…

[Linux]------线程同步和信号量

文章目录前言一、条件变量同步概念与竞态条件条件变量函数初始化销毁等待条件满足唤醒等待为什么pthread_cond_wait需要互斥量&#xff1f;条件变量使用规范二、生产者消费者模型为何要使用生产者消费者模型生产者消费者模型的优点基于BlockingQueue的生产者消费者模型C queue模…

1.MyBatis简介

1.概念 MyBatis是一款开源的持久层框架&#xff0c;它支持定制化SQL、存储过程以及高级映射。 与其它ORM框架不同&#xff0c;MyBatis没有将Java对象与数据表关联起来&#xff0c;而是作为Java方法和SQL语句的桥梁&#xff0c;一般称它为“半自动化ORM”框架。 2.Mybatis架构 …

【软件测试】在我刚上岗时,资深测试给我的建议让我受益匪浅......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 建立测试基线 当我还…

BNB Chain对Zebec生态大力扶持,ZBC或继续登录一线平台

在行业早期开始&#xff0c;流支付赛道就已经具备了早期的轮廓&#xff0c;而在流支付协议Zebec Protocol出现后&#xff0c;该领域被推向了一个新的发展高度&#xff0c;并得到加密领域以及传统商业领域的高度关注。而随着生态的商业进展不断推进、生态不断壮大&#xff0c;Ze…

C++代码优化(1):条款1~4

"不要烟火不要星光&#xff0c;只要问问你内心的想法。" 本栏仅仅 由对《Effictive C》其中的一系列条款&#xff0c;掺杂着自己一些愚钝的理解而写的。 ---前言 条款01: 尽量以const、enum、inline 替换 #define 在谈及上述好几个关键字 与define宏定义的关系&…

Intel i226芯片4端口千兆以太网卡 2.5GPoE工业相机图像采集卡介绍

PCIe-8634图像采集卡是一款基于 Intel i226芯片高性能千兆工业级 PCIe*4 POE网卡,具有传输速度高、兼容性强、性能稳定的特点&#xff0c;可广泛主要应用于网络高清监控、无线覆盖、工业自动化等领域。 RJ45千兆网络采用4 k Intel226千兆网络芯片,支持10/100/1000/2500Mbps传输…

microservices 简介

油鹳视频 Microservices explained - the What, Why and How? https://www.youtube.com/watch?vrv4LlmLmVWk&t2s microservices 是一种软件体系结构&#xff0c; microservices architecture(微服务架构) 是与传统的 monolithic architecture(整体式架构&#xff0c;一体…

微信转盘抽奖小程序如何制作?

微信转盘抽奖小程序如何制作&#xff1f;大概需要多少钱&#xff1f; 价格方面&#xff0c;平台按年收费&#xff0c;一年1498至2498元。 明码标价&#xff0c;7天退款制度&#xff0c;随时退。 微信转盘抽奖小程序如何制作步骤: 1.进入第三方微信转盘抽奖小程序制作平台官…

计算机结构体系:指令调度与循环展开题型 (非凭感觉的方法详解)

文章目录题目初始分析1.确定所使用的各个寄存器的作用2.将循环体内容语句和控制语句分开3.找出每一条循环体内容指令的代价并排序4.找出每一条循环体控制指令的代价并排序5.基于贪婪算法的最优循环展开体系结构这门课程中&#xff0c;指令调度和循环展开可以说是课程最困难的地…

负载均衡反向代理下的webshell

负载均衡(Load Balance) 是一种廉价的扩容的方案&#xff0c;它的概念不是本文的重点&#xff0c;不知道的可以去查资料学习。实现负载均衡的方式有很多种&#xff0c;比如 DNS 方式、HTTP 重定向方式、IP 负载均衡方式、反向代理方式等等。 其中像 HTTP 重定向方式、DNS方式等…