Java多线程篇(12)——ForkJoinPool

news2024/9/28 21:23:15

文章目录

  • 1、基本原理
  • 2、源码
    • 2.1、sumbit干了啥?
    • 2.2、工作线程如何运行?怎么窃取?
    • 2.3、fork干了啥?
    • 2.4、join干了啥?

在这里插入图片描述

1、基本原理

假设有大量的CPU密集型计算任务,比如计算1-100的总和,普通的写法是单线程循环累加1-100,这样固然可以。不过 Doug lea 觉得太慢了,于是设计了 ForkJoinPool 。
ForkJoinPool 设计理念是分治思想,将大任务拆分成多个小任务,然后再多线程去执行小任务,最后再将小任务的结果合在一起得到最终结果。

啊?如果是多线程执行任务的话,那我在拆分任务的时候是new一个有返回值的线程去执行不一样的吗,为什么还要大费周章写个 ForkJoinPool?
因为ForkJoinPool本质上是一个线程池,线程池最大的优势就是可以线程复用,无需创建大量线程浪费系统资源。用线程池分的时候只需要向线程池提交一个任务就可以了。

啊?如果是线程复用的话,那我用普通的线程池不可以吗,为什么要用 ForkJoinPool?
因为如果用普通线程池的话,大任务的拆分以及小任务结果的归并这些操作的具体细节都需要自己去控制,搞不好还会出现死锁。
举个例子,线程池最大线程数为2,1-100,还是用分治去拆,最小粒度是10。
首先1-100被拆成1-50,51-100分别被两个线程执行,然后1-50,51-100再拆成1-25,26-50,51-75,76-100四个子任务,但此时已经没有多余的线程可以去执行了,所以四个子任务入队阻塞队列等待空闲线程去执行,但此时线程池中的线程又在阻塞等待拆出来子任务的结果。因而产生了死锁,两边互相等待,永远等不到。如下图:
在这里插入图片描述

当然啦,就1-100,每10个数字一组任务这个场景来说,完全可以不用分治思想去实现,直接在main函数提交10个任务,然后依次累加。但这里的1-100只是一个例子,重点在于分治思想的设计和实现,如果抛开分治去讨论就没有意义了。


所以 ForkJoinPool 是工作原理是什么?同样是线程池为什么不会死锁?
工作原理
首先介绍一下基本概念:
ForkJoinTask:任务类 。其fork、join方法对应拆、合任务。常用实现类有RecursiveTask(有返回值),RecursiveAction(无返回值)。
WorkQueue:工作队列。用于存放任务。每个工作线程都有自己的工作队列,所以 ForkJoinPool 存在成员变量 WorkQueue[] workQueues。WorkQueue[] 的容量为2次幂,索引为偶数的存放外部线程提交的任务,索引为奇数的存放内部fork出来的任务。
ForkJoinWorkerThread:工作线程。每个工作线程在处理自身工作队列的同时,会窃取其他工作队列的任务,窃取的位置是底部。

原理大概就是:ForkJoinPool 内部有多个工作队列(对应多个工作线程),提交任务时,会根据一定的规则提交到其中一个队列。在任务执行的过程中如果 fork 了子任务,子任务入队自己工作队列的top,后续当子任务 join 时,如果子任务未被窃取就当前线程直接执行子任务,反之就先处理其他任务等待其完成。

因为处理自身工作队列时是LIFO,而处理其他工作队列时是FIFO,所以工作队列属于双端队列。

为什么即可以多线程执行,又不会像普通线程池一样死锁?
多线程由来:sumbit和fork都会尝试运行一个工作线程。
不会死锁:因为子任务肯定可以得到执行,要么就在自己的工作队列由自身线程执行,要么就被其他线程窃取执行。所以 ForkJoinPool 可以理解为用任务窃取来弥补单线程执行的性能问题。


2、源码

2.1、sumbit干了啥?

先将任务随机入队偶数位置的共享队列,然后创建工作线程和工作队列并运行,而工作线程运行时又会窃取其他队列的任务。所以总能窃取到首次提交到共享队列的任务。
submit

    public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
        return externalSubmit(task);
    }
    
    private <T> ForkJoinTask<T> externalSubmit(ForkJoinTask<T> task) {
        Thread t; ForkJoinWorkerThread w; WorkQueue q;
        if (task == null)
            throw new NullPointerException();
        //如果是内部任务(fork)就尝试直接入队
        if (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) &&
            (w = (ForkJoinWorkerThread)t).pool == this &&
            (q = w.workQueue) != null)
            q.push(task);
        //反之是外部提交的任务执行 externalPush
        else
            externalPush(task);
        return task;
    }

externalPush

    final void externalPush(ForkJoinTask<?> task) {
        int r;
        //随机数
        if ((r = ThreadLocalRandom.getProbe()) == 0) {
            ThreadLocalRandom.localInit();
            r = ThreadLocalRandom.getProbe();
        }
        //自旋
        for (;;) {
            WorkQueue q;
            int md = mode, n;
            WorkQueue[] ws = workQueues;
            //异常情况
            if ((md & SHUTDOWN) != 0 || ws == null || (n = ws.length) <= 0)
                throw new RejectedExecutionException();
            //队列不存在,new Queue
            //这里new的工作队列是偶数位的共享队列,只是用于存放外部提交的任务,所以owner为null。
            else if ((q = ws[(n - 1) & r & SQMASK]) == null) {
                int qid = (r | QUIET) & ~(FIFO | OWNED);
                Object lock = workerNamePrefix;
                ForkJoinTask<?>[] qa =
                    new ForkJoinTask<?>[INITIAL_QUEUE_CAPACITY];
                q = new WorkQueue(this, null);
                q.array = qa;
                q.id = qid;
                q.source = QUIET;
                if (lock != null) {
                    synchronized (lock) {
                        WorkQueue[] vs; int i, vn;
                        if ((vs = workQueues) != null && (vn = vs.length) > 0 &&
                            vs[i = qid & (vn - 1) & SQMASK] == null)
                            vs[i] = q;  // else another thread already installed
                    }
                }
            }
            //队列忙,尝试移动到下一个队列
            else if (!q.tryLockPhase())
                r = ThreadLocalRandom.advanceProbe(r);
            //将任务压入队列,并通知有可用任务
            else {
            	//压入队列
                if (q.lockedPush(task))
                	//通知有可用任务。
                	//主要是确保有足够的工作线程去执行任务。
                	//要么创建新的工作线程。要么唤醒阻塞的工作线程
                    signalWork();
                return;
            }
        }
    }

signalWork

	final void signalWork() {
		//自旋
        for (;;) {
            long c; int sp; WorkQueue[] ws; int i; WorkQueue v;
            //当前有足够多的活跃工作线程
            if ((c = ctl) >= 0L)
                break;
            //工作线程还没满,新建一个工作线程和工作队列并绑定,之后启动工作线程   
            else if ((sp = (int)c) == 0) {
                if ((c & ADD_WORKER) != 0L)
                    tryAddWorker(c);
                break;
            }
            //如果工作队列未启动或已终止,返回
            else if ((ws = workQueues) == null)
                break;
            else if (ws.length <= (i = sp & SMASK))
                break
            else if ((v = ws[i]) == null)
                break;
            //唤醒阻塞线程
            else {
                int np = sp & ~UNSIGNALLED;
                int vp = v.phase;
                long nc = (v.stackPred & SP_MASK) | (UC_MASK & (c + RC_UNIT));
                Thread vt = v.owner;
                if (sp == vp && CTL.compareAndSet(this, c, nc)) {
                    v.phase = np;
                    if (vt != null && v.source < 0)
                    	//unpark
                        LockSupport.unpark(vt);
                    break;
                }
            }
        }
    }

	//尝试创建工作线程
    private void tryAddWorker(long c) {
        do {
            long nc = ((RC_MASK & (c + RC_UNIT)) |
                       (TC_MASK & (c + TC_UNIT)));
            if (ctl == c && CTL.compareAndSet(this, c, nc)) {
                createWorker();
                break;
            }
        } while (((c = ctl) & ADD_WORKER) != 0L && (int)c == 0);
    }
    
   	//创建工作线程
    private boolean createWorker() {
        ForkJoinWorkerThreadFactory fac = factory;
        Throwable ex = null;
        ForkJoinWorkerThread wt = null;
        try {
            if (fac != null && (wt = fac.newThread(this)) != null) {
            	//启动创建的工作线程
                wt.start();
                return true;
            }
        } catch (Throwable rex) {
            ex = rex;
        }
        deregisterWorker(wt, ex);
        return false;
    }
    

*关于ctl,这是一个64位的long类型变量,由4个16位组成,分别是
* RC: 活跃(没有阻塞,即正在扫描或运行任务的)工作线程数 - 目标并行度
* TC: 总工作线程数 - 目标并行度
* SS: Treiber栈顶部阻塞线程的版本计数和状态(Treiber栈:未扫描到任务入栈阻塞等待)
* ID: Treiber栈顶部阻塞线程的poolIndex
ForkJoinPool用到了大量的位运算,比如这个ctl就是,具体我也没去深究,位运算看麻了,这里简单记录了解一下吧…

总的来说 signalWork 可以保证有足够的工作线程在运行(要么新建线程,要么唤醒阻塞线程)。

2.2、工作线程如何运行?怎么窃取?

上面看到提交一个任务会启动一个工作线程,那么工作线程是如何运行的,又是如何窃取任务的?
run

    public void run() {
        if (workQueue.array == null) { // only run once
            Throwable exception = null;
            try {
                onStart();
                //主要逻辑在ForkJpinPool.runWorker方法
                pool.runWorker(workQueue);
            } catch (Throwable ex) {
                exception = ex;
            } finally {
                try {
                    onTermination(exception);
                } catch (Throwable ex) {
                    if (exception == null)
                        exception = ex;
                } finally {
                    pool.deregisterWorker(this, exception);
                }
            }
        }
    }

runWorker

    final void runWorker(WorkQueue w) {
        int r = (w.id ^ ThreadLocalRandom.nextSecondarySeed()) | FIFO;
        w.array = new ForkJoinTask<?>[INITIAL_QUEUE_CAPACITY];
        //自旋
        for (;;) {
            int phase;
            //scan扫描所有工作队列的任务,任务窃取就是在scan窃取的
            if (scan(w, r)) { 
                r ^= r << 13; r ^= r >>> 17; r ^= r << 5; //随机数
            }
            //没扫描到任务就阻塞线程,在阻塞之前更新栈前驱(stackPred )和ctl字段的值
            else if ((phase = w.phase) >= 0) {
                long np = (w.phase = (phase + SS_SEQ) | UNSIGNALLED) & SP_MASK;
                long c, nc;
                do {
                    w.stackPred = (int)(c = ctl);
                    nc = ((c - RC_UNIT) & UC_MASK) | np;
                } while (!CTL.weakCompareAndSet(this, c, nc));
            }
            //阻塞线程
            else {
                int pred = w.stackPred;
                Thread.interrupted();
                w.source = DORMANT;
                long c = ctl;
                int md = mode, rc = (md & SMASK) + (int)(c >> RC_SHIFT);
                if (md < 0)
                    break;
                else if (rc <= 0 && (md & SHUTDOWN) != 0 &&
                         tryTerminate(false, false))
                    break;
                else if (rc <= 0 && pred != 0 && phase == (int)c) {
                    long nc = (UC_MASK & (c - TC_UNIT)) | (SP_MASK & pred);
                    long d = keepAlive + System.currentTimeMillis();
                    LockSupport.parkUntil(this, d);
                    if (ctl == c && 
                        d - System.currentTimeMillis() <= TIMEOUT_SLOP &&
                        CTL.compareAndSet(this, c, nc)) {
                        w.phase = QUIET;
                        break;
                    }
                }
                else if (w.phase < 0)
                    LockSupport.park(this);
                w.source = 0;
            }
        }
    }

stackPred表示在线程池栈当前工作线程的前驱线程的索引,在唤醒线程时用到此属性。

scan
任务窃取就在这个scan方法。

    private boolean scan(WorkQueue w, int r) {
        WorkQueue[] ws; int n;
        if ((ws = workQueues) != null && (n = ws.length) > 0 && w != null) {
        	//扫描所有工作队列
            for (int m = n - 1, j = r & m;;) {
                WorkQueue q; int b;
                //如果工作队列不为空且有任务
                if ((q = ws[j]) != null && q.top != (b = q.base)) {
                    int qid = q.id;
                    ForkJoinTask<?>[] a; int cap, k; ForkJoinTask<?> t;
                    if ((a = q.array) != null && (cap = a.length) > 0) {
                    	//从base窃取任务
                        t = (ForkJoinTask<?>)QA.getAcquire(a, k = (cap - 1) & b);
                        if (q.base == b++ && t != null &&
                            QA.compareAndSet(a, k, t, null)) {
                            q.base = b;
                            w.source = qid;
                            if (q.top - b > 0)
                            	//如果窃取后还有任务,就调用 signalWork 看能否帮忙执行
                                signalWork();
                            //执行窃取的任务
                            w.topLevelExec(t, q,
                                           r & ((n << TOP_BOUND_SHIFT) - 1));
                        }
                    }
                    return true;
                }
                else if (--n > 0)
                	//随机定一个位置后,线性扫描
                    j = (j + 1) & m;
                else
                    break;
            }
        }
        return false;
    }

    final void topLevelExec(ForkJoinTask<?> t, WorkQueue q, int n) {
        if (t != null && q != null) {
            int nstolen = 1;
            //自旋
            for (;;) {
            	//执行任务,进而调用到我们重写的 ForkJoinTask.exec 方法
                t.doExec();
                if (n-- < 0)
                    break;
                //下一个任务优先处理自己工作队列中的任务
                else if ((t = nextLocalTask()) == null) {
                	//如果自己工作队列中没有任务,就从这个队列中再窃取一个任务
                    if ((t = q.poll()) == null)
                    	//如果都没有任务了,就结束这个方法重新scan扫描
                        break;
                    else
                        ++nstolen;
                }
            }
            ForkJoinWorkerThread thread = owner;
            nsteals += nstolen;
            source = 0;
            if (thread != null)
                thread.afterTopLevelExec();
        }
    }

2.3、fork干了啥?

fork的流程就相对简单了,其实就是入队后(如果需要)调用 signalWork 然后等待运行而已。
fork

    public final ForkJoinTask<V> fork() {
        Thread t;
        //如果是ForkJoinWorkerThread,入队工作队列,反之入队common全局队列
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
    
    final void push(ForkJoinTask<?> task) {
        ForkJoinTask<?>[] a;
        int s = top, d, cap, m;
        ForkJoinPool p = pool;
        if ((a = array) != null && (cap = a.length) > 0) {
            QA.setRelease(a, (m = cap - 1) & s, task);
            top = s + 1;
            //size = 0 或 1
            if (((d = s - (int)BASE.getAcquire(this)) & ~1) == 0 &&
                p != null) {
                VarHandle.fullFence();
                //fork可能也会调用到signalWork
                p.signalWork();
            }
            else if (d == m)
            	//扩容任务列表 ForkJoinTask<?>[]
                growArray(false);
        }
    }

java8并行流用到的就是这里的common全局队列,所以java8并行流有个坑就是不同业务(线程)用到的队列是同一个,在某些情况下会相互影响。

2.4、join干了啥?

join则是获取子任务的结果。如果join的时候子任务已有结果直接返回,反之看join的子任务是否还在自己的工作队列上,如果是的话自己运行,如果不是的话就等待结果。
值得注意的是,为提高性能,等待子任务结果时并不是直接阻塞等待,而是边执行其他任务边等待。
并且就算真正进入wait等待也会补偿一个活跃线程,以免无线程可用。补偿逻辑:如果有可以唤醒的线程就唤醒线程,如果线程数未满或者虽然已满但全都在awaitJoin子任务结果就新建线程。

因为如果所有线程都在awaitJoin子任务结果,这个时候就算线程数满了,补偿的时候仍会新建线程。所以在极端情况下ForkJoinPool的总线程数是可能大于参数值的。不过最大不会超过 0x7fff, 超过了就抛异常。

join

    public final V join() {
        int s;
        if (((s = doJoin()) & ABNORMAL) != 0)
            reportException(s);
        return getRawResult();
    }

    private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            //如果已有结果直接返回
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            //如果是ForkJoinWorkerThread线程内部等待
            wt.pool.awaitJoin(w, this, 0L) :
            //反之外部等待
            externalAwaitDone();
    }

awaitJoin

    final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
        int s = 0;
        int seed = ThreadLocalRandom.nextSecondarySeed();
        if (w != null && task != null &&
            (!(task instanceof CountedCompleter) ||
             (s = w.helpCC((CountedCompleter<?>)task, 0, false)) >= 0)) {
            //如果任务还在自身工作队列的话,自己执行任务 
            w.tryRemoveAndExec(task);
            int src = w.source, id = w.id;
            int r = (seed >>> 16) | 1, step = (seed & ~1) | 2;
            s = task.status;
            //s>=0说明还没有结果,继续等待
            while (s >= 0) {
                WorkQueue[] ws;
                int n = (ws = workQueues) == null ? 0 : ws.length, m = n - 1;
                //等待的过程中为提高性能可以扫描其他任务执行
                while (n > 0) {
                    WorkQueue q; int b;
                    if ((q = ws[r & m]) != null && q.source == id &&
                        q.top != (b = q.base)) {
                        ForkJoinTask<?>[] a; int cap, k;
                        int qid = q.id;
                        if ((a = q.array) != null && (cap = a.length) > 0) {
                            ForkJoinTask<?> t = (ForkJoinTask<?>)
                                QA.getAcquire(a, k = (cap - 1) & b);
                            if (q.source == id && q.base == b++ &&
                                t != null && QA.compareAndSet(a, k, t, null)) {
                                q.base = b;
                                w.source = qid;
                                t.doExec();
                                w.source = src;
                            }
                        }
                        break;
                    }
                    else {
                        r += step;
                        --n;
                    }
                }
                //已有结果break
                if ((s = task.status) < 0)
                    break;
                //进入阻塞逻辑
                else if (n == 0) {
                    long ms, ns; int block;
                    if (deadline == 0L)
                        ms = 0L;
                    else if ((ns = deadline - System.nanoTime()) <= 0L)
                        break;
                    else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
                        ms = 1L;
                    //实际阻塞前,tryCompensate补偿一个线程
                    //补偿逻辑:如果有可以唤醒的线程就唤醒线程
                    //         如果线程数未满或者虽然已满但全都在awaitJoin子结果就新建线程
                    if ((block = tryCompensate(w)) != 0) {
                    	//内部调用Object.wait阻塞
                        task.internalWait(ms);
                        CTL.getAndAdd(this, (block > 0) ? RC_UNIT : 0L);
                    }
                    s = task.status;
                }
            }
        }
        return s;
    }
    
    final void internalWait(long timeout) {
        if ((int)STATUS.getAndBitwiseOr(this, SIGNAL) >= 0) {
            synchronized (this) {
                if (status >= 0)
                    try { wait(timeout); } catch (InterruptedException ie) { }
                else
                    notifyAll();
            }
        }
    }

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

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

相关文章

两天写一个电影资讯速览微信小程序(附源码)

简介 基于原生小程序精仿的猫眼电影&#xff0c;仅供学习和参考。 首页效果图 数据请求 url: https://m.maoyan.com/ajax/movieOnInfoList?token,success(res) {const movieList0 _this.formatImgUrl(res.data.movieList)wx.hideLoading()_this.setData({movieIds0: res.…

虚幻中的网络概述一

前置&#xff1a;在学习完turbo强大佬的多人fps之后发觉自己在虚幻网络方面还有许多基础知识不太清楚&#xff0c;结合安宁Ken大佬与虚幻官方文档进行补足。 补充&#xff1a;官方文档中的描述挺好的&#xff0c;自己只算是搬运和将两者结合加强理解。 学习虚幻中的网络先从虚…

基于标签的协同过滤推荐方法研究

&#xff3b;摘要&#xff3d; 2.2标签相似度矩阵 惩罚jaccard相关热度系数

Q4已来,DAO发新生|CyberDAO子DAO种子会议

Q4已来&#xff0c;DAO发新生|CyberDAO子DAO种子会议&#xff0c;2023年10月25日在成都五星级希顿国际酒店隆重召开&#xff0c;本次会议百名DAO精英成员及CyberDAO大中华区运营团队合伙人JR、漫威、安祈出席&#xff01;以蓄势和重塑为主题带领大家乘势而上&#xff0c;创新引…

控制系统典型应用车型 —— 牵引式移动机器人

牵引式移动机器人&#xff08;AGV/AMR&#xff09;&#xff0c;通常由一个牵引车和一个或多个被牵引的车辆组成。牵引车是机器人的核心部分&#xff0c;它具有自主导航和定位功能&#xff0c;可以根据预先设定的路径或地标进行导航&#xff0c;并通过传感器和视觉系统感知周围环…

提高数据研究效率,优化成果分享及复用|ModelWhale 版本更新

秋高气爽、金桂飘香&#xff0c;十月末&#xff0c;我们又迎来了 ModelWhale 新一轮的版本更新。 本次更新中&#xff0c;ModelWhale 主要进行了以下功能迭代&#xff1a; 新增 添加模型服务到知识库&#xff08;专业版✓ 团队版✓&#xff09; 新增 自动生成数据引用信息&am…

MES管理系统解决方案实现生产信息全程追溯

生产制造企业在生产过程中&#xff0c;最令人头疼的问题之一就是产品信息的追溯。当产品出现质量问题时&#xff0c;需要在庞大的数据中寻找出问题批次的产品和同批次的物料&#xff0c;并进行召回处理&#xff0c;这样的工作量十分巨大。为了解决这一问题&#xff0c;许多企业…

Yolov5原理详细解析!一文看懂

引言 Yolo(You Only Look Once)是一种one-stage目标检测算法,即仅需要 “看” 一次就可以识别出图片中物体的class类别和边界框。Yolov5是由Alexey Bochkovskiy等人在YOLO系列算法的基础上进行改进和优化而开发的,使其性能与精度都得到了极大的提升。 Yolov5推出了Yolov5s、…

华为OD机考算法题:计算最大乘积

题目部分 题目计算最大乘积难度易题目说明给定一个元素类型为小写字符串的数组&#xff0c;请计算两个没有相同字符的元素长度乘积的最大值。 如果没有符合条件的两个元素&#xff0c;返回 0。输入描述输入为一个半角逗号分隔的小写字符串的数组&#xff0c;2< 数组长度<…

Painter:使用视觉提示来引导网络推理

文章目录 1. 论文2. 示意图3. 主要贡献4. 代码简化 1. 论文 paper:Images Speak in Images: A Generalist Painter for In-Context Visual Learning github:https://github.com/baaivision/Painter 2. 示意图 3. 主要贡献 在 In-context Learning 中&#xff0c;作为自然语言…

【2018统考真题】给定一个含n(n≥1)个整数的数组,请设计一个在时间上尽可能高 效的算法,找出数组中未出现的最小正整数。

【2018统考真题】给定一个含n(n≥1&#xff09;个整数的数组&#xff0c;请设计一个在时间上尽可能高 效的算法,找出数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正 整数是1;数组{1,2,3}中未出现的最小正整数是4。 代码思路&#xff1a; 题目让你找未出现的最…

2024年北京/上海/广州/深圳PMP®项目管理认证精品班招生简章

PMP认证是Project Management Institute在全球范围内推出的针对评价个人项目管理知识能力的资格认证体系。国内众多企业已把PMP认证定为项目经理人必须取得的重要资质。 【PMP认证收益】 1、能力的提升&#xff08;领导力&#xff0c;执行力&#xff0c;创新能力&#xff0c…

国产 2443A 峰值功率分析仪

2443A 峰值功率分析仪 频率范围覆盖&#xff1a;9kHz至67GHz 产品综述 2443A峰值功率分析仪由峰值功率分析仪主机和系列化峰值功率探头组成&#xff0c;可用于测量和分析微波毫米波脉冲调制信号的多种幅度和时间参数&#xff0c;是表征脉冲调制信号特性的综合性测量与分析仪器。…

【2024秋招】2023-9-16 贝壳后端开发二面

1 自我介绍 2 秒杀系统 2.1 超卖怎么解决 3 redis 3.1 过期策略 3.2 过期算法 4 kafka 4.1 说一说你对kafka的了解 4.2 如何保证事务性消息 4.3 如何保证消息不丢失 4.4 消息队列的两种通信方式 点对点模式 如上图所示&#xff0c;点对点模式通常是基于拉取或者轮询…

Docker实战之一

一、前言 前两天看到一篇文章 再见了 Docker&#xff01;K8S 已成气候&#xff01;&#xff0c;K8S这么高端我们是还没有玩过的&#xff0c;Docker倒是实践过一把&#xff0c;这个系列会简单介绍一下实战的过程&#xff0c;具体背景是因为我们有时需要给门店提供一个相对稳定…

如何在IDEA中配置指定JDK版本?轻松解决!!!

有时候我们在导入项目&#xff0c;如果手动在IDEA中指定jdk版本&#xff0c;往往启动项目会报错误。 因此当我们新引入项目启动不了时可以检查一下自己IDEA中的jdk版本是否正确。 下面以配置jdk版本为11显示步骤&#xff1a; 1、配置 Project Structure 1.1、通过快捷键&qu…

阶乘与双阶乘的相关性质

阶乘与双阶乘的相关性质 双阶乘 基本性质 积分恒等式 极限近似 级数展开 阶乘 斯特林(Stirling)公式 gamma函数

信必优收到中国首批成立的期货公司之一表扬信

近日&#xff0c;信必优收到中国首批成立的期货公司之一的表扬信&#xff0c;客户极力表扬了我司员工卓越的职业操守&#xff0c;充满热情、勤奋努力&#xff1b;在工作中成长迅速&#xff0c;不仅对公司整体开发框架熟练掌握&#xff0c;对公司相关业务也理解深刻&#xff0c;…

进程控制(一):进程终止

文章目录 进程控制&#xff08;一&#xff09;进程终止运行正常退出码 运行异常进程正常/异常总结 进程控制&#xff08;一&#xff09; 在前文中&#xff0c;我们初步了解了进程的概念&#xff0c;以及通过fork函数来创建子进程&#xff0c;并对于为什么运行一个程序&#xf…

虹科分享 | 买车无忧?AR带来全新体验!

文章来源&#xff1a;虹科数字化与AR 阅读原文&#xff1a;https://mp.weixin.qq.com/s/XsUFCTsiI4bkEMBHcGUT7w 新能源汽车的蓬勃发展&#xff0c;推动着汽车行业加速进行数字化变革。据数据显示&#xff0c;全球新能源汽车销售额持续上升&#xff0c;预计到2025年&#xff0…