当前位置: 首页 > news >正文

ThreadPool线程池源码解析

ThreadPool线程池源码解析

文章目录

  • 前言
  • 一、基本使用
  • 二、执行流程
  • 三、源码分析
    • ThreadPoolExecutor 中重要属性
    • ThreadPoolExecutor 内部类Worker
    • execute()方法
    • addWorker(command, true)方法
    • runWorker(worker )方法
    • getTask()方法
    • shutdown和shutdownNow
  • 四、线程池异常处理
  • 总结


前言

  1. 如何实现线程复用的?
  2. 先提交的任务一定会先执行吗?
  3. 线程池中的线程如何做到空闲一定时间退出的?
  4. 没有任务时候超过最大存活时间被销毁的是非核心线程?
  5. 在调用shutdown方法关闭线程池的时候,如何判断线程有没有在执行任务?
  6. shutdown和shutdownNow两个方法区别?
  7. 如何处理执行失败的任务?

本觉得自己已经懂线程池了,看到以上几个问题能否都能答上来,还是又觉得自己不懂了,一脸懵逼?如果你能回答上来那么你可以绕过了。

上面前几个问题都是原理概念问题,最后一个是实际使用中必须要面对的问题,当然你可以不面对,我相信这也是觉大多数人的做法,直接忽略,才不管有没有执行成功,我把任务提交到线程池有么有最终执行成功关我毛事。本篇中先对线程池源码进行分析,逐个解答如上问题,并以实际工作案例给出常用的任务失败处理方案。


一、基本使用

在这里插入图片描述
如上是我们再熟悉不过的代码了。

二、执行流程

在这里插入图片描述
如上执行流程我们已经背诵的滚瓜烂熟了。

三、源码分析

在这里插入图片描述
首先看下最基本的类继承关系,我们本篇以方法execute(Runnable)作为切入分析,submit(Runnable)是对这个方法做了下封装,所以我们还是从最基本的开始。

在开始分析前execute(Runnable)方法之前先看下ThreadPoolExecutor相关重要的属性,有个大概印象就行了,遇到不清楚的再回过头来看。

ThreadPoolExecutor 中重要属性

public class ThreadPoolExecutor extends AbstractExecutorService {... ...//高3位:表示当前线程池运行状态 除去高3位之后的低位:表示当前线程池所拥有的线程数量private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// 表示在ctl中,低COUNT_BITS位 用于存放当前线程数量的位private static final int COUNT_BITS = Integer.SIZE - 3;//低COUNT_BITS位 所能表达的最大数值private static final int CAPACITY   = (1 << COUNT_BITS) - 1;//线程池状态://表示可接受新任务,且可执行队列中的任务;private static final int RUNNING    = -1 << COUNT_BITS;//表示不接受新任务,但可执行队列中的任务;private static final int SHUTDOWN   =  0 << COUNT_BITS;//表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务;private static final int STOP       =  1 << COUNT_BITS;// 所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法;private static final int TIDYING    =  2 << COUNT_BITS;//中止状态,已经执行完terminated()钩子方法;private static final int TERMINATED =  3 << COUNT_BITS;private static int runStateOf(int c)     { return c & ~CAPACITY; }private static int workerCountOf(int c)  { return c & CAPACITY; }private static int ctlOf(int rs, int wc) { return rs | wc; }//任务队列,当线程池中的线程达到核心线程数量时,再提交任务 就会直接提交到 workQueueprivate final BlockingQueue<Runnable> workQueue;//线程池全局锁,增加worker 减少 worker 时需要持有mainLock , 修改线程池运行状态时,也需要。private final ReentrantLock mainLock = new ReentrantLock();//线程池中真正存放 worker->thread 的地方。private final HashSet<Worker> workers = new HashSet<Worker>();// 记录线程池生命周期内 线程数最大值private int largestPoolSize;// 记录线程池所完成任务总数private long completedTaskCount;// 创建线程会使用线程工厂private volatile ThreadFactory threadFactory;//拒绝策略private volatile RejectedExecutionHandler handler;//空闲线程存活时间,当allowCoreThreadTimeOut == false 时,会维护核心线程数量内的线程存活,超出部分会被超时。private volatile long keepAliveTime;//控制核心线程数量内的线程 是否可以被回收。true 可以,false不可以。private volatile boolean allowCoreThreadTimeOut;// 核心线程池数量private volatile int corePoolSize;// 线程池最大数量private volatile int maximumPoolSize;// 默认拒绝策略private static final RejectedExecutionHandler defaultHandler =new AbortPolicy();... ...
}

关键代码解析:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

线程池状态具体是存在ctl成员变量中,ctl中不仅存储了线程池的状态还存储了当前线程池中线程数的大小。

//表示可接受新任务,且可执行队列中的任务;
private static final int RUNNING = -1 << COUNT_BITS;
//表示不接受新任务,但可执行队列中的任务;
private static final int SHUTDOWN = 0 << COUNT_BITS;
//表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务;
private static final int STOP = 1 << COUNT_BITS;
// 所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()钩子方法,只会有一个线程执行这个方法;
private static final int TIDYING = 2 << COUNT_BITS;
//中止状态,已经执行完terminated()钩子方法;
private static final int TERMINATED = 3 << COUNT_BITS;

线程池内部有5个常量来代表线程池的五种状态:

  1. RUNNING:线程池创建时就是这个状态,能够接收新任务,以及对已添加的任务进行处理。
  2. SHUTDOWN:调用shutdown方法线程池就会转换成SHUTDOWN状态,此时线程池不再接收新任务,但能继续处理已添加的任务到队列中任务。
  3. STOP:调用shutdownNow方法线程池就会转换成STOP状态,不接收新任务,也不能继续处理已添加的任务到队列中任务,并且会尝试中断正在处理的任务的线程。
  4. TIDYING:SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态。线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池会变为 TIDYING 状态。线程池在 STOP 状态,线程池中执行中任务为空时,线程池会变为 TIDYING 状态。
  5. TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会转变为 TERMINATED 状态。

ThreadPoolExecutor 内部类Worker

线程池中的工作线程以Worker作为体现,Worker也就是线程池中的线程,只是给线程包装了下,Worker中包含工作线程和要执行的任务。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{private static final long serialVersionUID = 6138294804551838833L;// worker内部封装的工作线程final Thread thread;//当worker启动后thread线程会优先执行firstTask,当执行完firstTask后,会到queue中去获取下一个任务。Runnable firstTask;// 记录当前worker所完成的任务数量volatile long completedTasks;Worker(Runnable firstTask) {setState(-1);this.firstTask = firstTask;this.thread = getThreadFactory().newThread(this);}public void run() {runWorker(this);}protected boolean isHeldExclusively() {return getState() != 0;}protected boolean tryAcquire(int unused) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}protected boolean tryRelease(int unused) {setExclusiveOwnerThread(null);setState(0);return true;}public void lock()        { acquire(1); }public boolean tryLock()  { return tryAcquire(1); }public void unlock()      { release(1); }public boolean isLocked() { return isHeldExclusively(); }void interruptIfStarted() {Thread t;if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {try {t.interrupt();} catch (SecurityException ignore) {}}}
}

其中构造函数很重要,firstTask即为我们提交的任务,newThread(this)将自身传入,我们从上面的代码中可以看到Worker implements Runnable,即线程会执行Worker类的run()方法,

	Worker(Runnable firstTask) {setState(-1);this.firstTask = firstTask;this.thread = getThreadFactory().newThread(this);}

run()中会执行外部类的runWorker方法。

   public void run() {runWorker(this);}

execute()方法

public class ThreadPoolExecutor extends AbstractExecutorService {... ...public void execute(Runnable command) {if (command == null)throw new NullPointerException();// 获取ctl的值int c = ctl.get();// 当前线程数小于核心线程池数量,此次提交任务,直接创建一个新的workerif (workerCountOf(c) < corePoolSize) {// addWorker 即为创建线程的过程,会创建worker对象,并且将command作为firstTask// core==true 表示创建的线程为核心线程,false非核心线程if (addWorker(command, true))return;c = ctl.get();}// 执行到这里有几种情况?// 1.当前线程池数量已经达到corePoolSize// 2. addWorker失败// 当前线程池处于running状态,尝试将task放入到workQueue中if (isRunning(c) && workQueue.offer(command)) {// 获取当前ctlint recheck = ctl.get();// !isRunning()成功,代表当你提交到任务队列后,线程池状态被外部线程给修改,例如调用了shutDown(),shutDownNow()// remove成功,提交之后,线程池中的线程还没消费// remove 失败,说明在shutDown或者shutDown之前,就被线程池的线程给处理了if (! isRunning(recheck) && remove(command))reject(command);// 当前线程池是running状态,else if (workerCountOf(recheck) == 0)// 如果当前没有线程,就添加一个线程保证当前至少有一个线程存在addWorker(null, false);}//执行到这里,有几种情况?//1.offer失败:offer失败,需要做什么? 说明当前queue 满了!这个时候如果当前线程数量尚未达到maximumPoolSize的话,会创建新的worker直接执行command//2.当前线程池是非running状态:线程池状态为非running状态,这个时候因为 command != null addWorker 一定是返回false。else if (!addWorker(command, false))reject(command);}... ...
}

execute方法的执行流程大致可以分为以下几步:

  • 工作线程数量小于核心数量,创建核心线程;
  • 达到核心数量,进入任务队列;
  • 任务队列满了,创建非核心线程;
  • 达到最大数量,执行拒绝策略;

execute方法中逻辑还是很清晰的。
首先我们看第一个场景,当前线程数小于核心线程数,直接创建一个新的worker。
也就是execute()方法中的这块逻辑。

 if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}

addWorker(command, true)方法

//firstTask 可以为null,如线程池刚启动初始化核心线程,worker自动到queue中获取任务,如果不是null,则worker优先执行firstTask
//core 如果为true表示创建的为核心线程 false表示为非核心线程
private boolean addWorker(Runnable firstTask, boolean core) {// 自旋:判断当前线程池状态是否允许创建线程的事情retry:for (;;) {// 获取当前ctl值int c = ctl.get();// 获取当前线程池运行状态int rs = runStateOf(c);// 判断当前线程池是否允许添加线程if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;// 内部自旋:获取创建线程令牌的过程for (;;) {//判断当前线程是否超过限制,超过限制就无法创建线程int wc = workerCountOf(c);if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;// 通过cas将线程数量加1,能够成功加1相当于申请到创建线程的令牌if (compareAndIncrementWorkerCount(c))break retry;c = ctl.get();// 判断当前线程状态是否发生变化if (runStateOf(c) != rs)continue retry;// else CAS failed due to workerCount change; retry inner loop}}boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {// 创建workw = new Worker(firstTask);//将新创建的work节点的线程 赋值给tfinal Thread t = w.thread;if (t != null) {final ReentrantLock mainLock = this.mainLock;//持有全局锁,可能会阻塞,直到获取成功为止,同一时刻操纵 线程池内部相关的操作,都必须持锁。mainLock.lock();try {//获取最新线程池运行状态保存到rs中int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();// 将创建的work添加到线程池中workers.add(w);// 获取最新当前线程池线程数量int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {// 释放锁mainLock.unlock();}if (workerAdded) {// 添加work成功后,将创建的线程启动t.start();workerStarted = true;}}} finally {// 启动失败if (! workerStarted)// 释放令牌// 将当前worker清理出workers集合addWorkerFailed(w);}return workerStarted;
}

runWorker(worker )方法

从上面的addWorker方法可以看到创建了一个work添加到线程池中,然后调用start()方法,也就是调用Worker中的run方法,run方法中调用runWorker方法。

final void runWorker(Worker w) {// 工作线程Thread wt = Thread.currentThread();// 任务Runnable task = w.firstTask;w.firstTask = null;// 释放锁w.unlock(); // allow interruptsboolean completedAbruptly = true;try {// 取任务,如果有第一个任务,这里先执行第一个任务// getTask:取任务while (task != null || (task = getTask()) != null) {// 加锁,是因为当调用shutDown方法它会判断当前是否加锁,加锁就会跳过它接着执行下一个任务w.lock();// 检查线程池状态if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {// 钩子方法,方便子类在任务执行前做一些处理beforeExecute(wt, task);Throwable thrown = null;try {//真正任务执行的地方,task可能是FutureTask 也可能是 普通的Runnable接口实现类。//如果前面是通过submit()提交的 runnable/callable 会被封装成 FutureTask。task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {// 钩子方法,方便子类在任务执行后做一些处理afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}
}

关键代码解析:

while (task != null || (task = getTask()) != null)

在我们线程池刚启动还没有创建核心线程时候,提交任务到进来就会创建核心线程,创建完核心线程,调用run方法,执行提交进来的任务,这时候task也就是我们提交过来的任务,所以不为空进入循环体内,执行任务。执行完成之后又会进入上面的判断逻辑,这时候task==null了,所以进入getTask()方法从任务队列中获取任务来执行,此处也就是实现了线程复用。

w.lock();

在执行任务前要加锁,此处要结合shutDown方法来分析了,在shutDown中尝试w.tryLock()获取锁来判断线程是否在执行任务,

getTask()方法

private Runnable getTask() {// 是否超时boolean timedOut = false;// 自旋for (;;) {int c = ctl.get();// 线程池状态int rs = runStateOf(c);//当前程池状态是SHUTDOWN的时候会把队列中的任务执行完直到队列为空// 线程池状态是stop时退出if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {decrementWorkerCount();return null;}// 获取工作线程数量int wc = workerCountOf(c);// 是否允许超时,有两种情况:// 1. 是允许核心线程数超时,这种就是说所有的线程都可能超时// 2. 是工作线程数大于了核心数量,这种肯定是允许超时的// 注意,非核心线程是一定允许超时的,这里的超时其实是指取任务超时boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;if ((wc > maximumPoolSize || (timed && timedOut))&& (wc > 1 || workQueue.isEmpty())) {if (compareAndDecrementWorkerCount(c))return null;continue;}try {// 真正取任务的地方// 默认情况,只有当工作线程数量大于核心线程数量时,才会调用poll方法触发超时调用Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();if (r != null)// 取到任务就返回return r;timedOut = true;} catch (InterruptedException retry) {timedOut = false;}}
}

关键代码解析:

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();

BlockingQueue:
1)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null
2)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止

当允许核心线程超时,或者当前线程数超过最大线程数时采用poll(time)方法获取任务,获取不到timedOut = true,在下一个for循环中就将此线程杀死了,这么来看,被杀死的线程不一定是非核心线程。

当当前线程小于核心线程数使用take()方法会一直阻塞直到取到任务或抛出中断异常。

上面我们就分析完成了第一场景:当前线程数未达到核心线程数时候创建核心线程,创建完成核心线程后就处理我们提交的任务,处理完成后这个线程就去从阻塞队列阻塞获取任务去了。

下面我们通过一组图展现这个流程:
线程池刚创建出来是什么样子呢(未启动创建核心线程),如下图:
在这里插入图片描述
刚创建出来的线程池中只有一个构造时传入的阻塞队列而已,此时里面并没有的任何线程。
当有线程通过execute方法提交了一个任务,会发生什么呢?
提交任务的时候,其实会去进行任务的处理首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数corePoolSize。如果小于,那么就直接通过ThreadFactory创建一个线程来执行这个任务,如下图:
在这里插入图片描述
当任务执行完之后,线程不会退出,而是会去从阻塞队列中获取任务,如下图:
在这里插入图片描述
如上就是我们的第一个业务场景,核心线程为满提交任务时候的处理流程。从execute()方法中如下代码可以看到,即使线程池中有核心线程没有“闲着”,没有在处理任务,在没有达到核心线程数时提交过来的任务都是新建线程来处理的。

   if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}

首先我们看第二个场景,当前线程数等于核心线程数,不再创建线程,而是将提交的任务提交到任务队列,也就是execute()方法中的这块逻辑。

  if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}

代码很简单,其实当我们分析完第一个场景后线程池的提交任务源码核心主逻辑基本就已经分析完了,还是以图分析。
如果线程池里的线程数不再小于核心线程数呢?那么此时就会尝试将任务放入阻塞队列中,入队成功之后,如下图:
在这里插入图片描述
这样在阻塞的线程就可以获取到任务了。
但是,随着任务越来越多,队列已经满了,任务放入失败了,那怎么办呢?
接下来就到我们的第三个场景了,当前线程达到核心线程数,并且任务队列已经满,那么就要创建非核心线程了,也就是如下代码:

else if (!addWorker(command, false))

在这里插入图片描述
从这里可以发现,就算队列中有任务,新创建的线程还是优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行
第三个场景也就分析完了。
但是不幸的事发生了,线程数已经达到了最大线程数量,那么此时会怎么办呢?
下面分析第四个场景,队列已经满,并且已经达到最大线程数。
在这里插入图片描述
RejectedExecutionHandler的实现JDK自带的默认有4种AbortPolicy:

  1. 丢弃任务,抛出运行时异常CallerRunsPolicy:
  2. 由提交任务的线程来执行任务DiscardPolicy:
  3. 丢弃这个任务,但是不抛异常DiscardOldestPolicy:
  4. 从队列中剔除最先进入队列的任务,然后再次提交任务

shutdown和shutdownNow

线程池提供了shutdown和shutdownNow两个方法来关闭线程池。
shutdown方法:

    public void shutdown() {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();advanceRunState(SHUTDOWN);interruptIdleWorkers();onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();}tryTerminate();}

就是将线程池的状态修改为SHUTDOWN,然后尝试打断空闲的线程(如何判断空闲,上面在说Worker时候说过),也就是在阻塞等待任务的线程。
shutdownNow方法:

    public List<Runnable> shutdownNow() {List<Runnable> tasks;final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {checkShutdownAccess();advanceRunState(STOP);interruptWorkers();tasks = drainQueue();} finally {mainLock.unlock();}tryTerminate();return tasks;}

就是将线程池的状态修改为STOP,然后尝试打断所有的线程,从阻塞队列中移除剩余的任务,这也是为什么shutdownNow不能执行剩余任务的原因。

四、线程池异常处理

在这里插入图片描述
这段使用方式我相信好多人觉得是没问题的。我们特意在run()方法中构造一个异常,然鹅控制台并没有输出异常,这下知道问题了吧。
在这里插入图片描述
我们经常在系统优化时候使用线程池,将不重要的逻辑拆出来使用线程池异步执行,以降低业务接口响应时间,在task线程run()方法中一顿操作猛如虎,总之宗旨也是提交到了线程池就行了,我也不关心执行结果,不出问题还好,出了问题找都找不到,我们采用submit提交的任务,run()中如果不把我们处理业务逻辑try…catch了出问题都没痕迹。

在这里插入图片描述
如上是在工作中遇到的实际生产问题,为了提交接口响应将非核心逻辑采用异步执行,构建了个task编写了一坨处理逻辑放入run()方法中,也没做任何异常逻辑处理,然后run()方法中在操作数据库入表时候,由于数据库字段长度限制导致入表失败,查询日志么有任何报错异常,这让人怎么找问题?本来很简单的一个问题,结果找了很久,程序员就是互相挖坑的职业。
提交到线程池的任务,在执行编写的run()方法失败的时候怎么处理?对于多数人来说真的是不管…
那么如何处理呢?
(1)在提交的任务中将异常捕获并处理,不抛给线程池。
(2)异常抛给线程池,但是我们要及时处理抛出的异常。
在这里插入图片描述
第一种思路把业务逻辑都trycatch起来,缺点:所有的不同任务类型都要trycatch,增加了代码量,代码丑陋。

第二种思路又有多种实现方式

  1. 自定义线程池:自定义线程池,继承ThreadPoolExecutor并复写其afterExecute(Runnable r, Throwable t)方法。

  2. 实现Thread.UncaughtExceptionHandler接口:实现Thread.UncaughtExceptionHandler接口,实现void uncaughtException(Thread t, Throwable e);方法,并将该handler传递给线程池的ThreadFactory。

上面方式针对的都是通过execute(xx)的方式提交任务,如果你提交任务用的是submit()方法,应该使用下面的方式

  1. 采用Future模式:如果提交任务的时候使用的方法是submit,那么该方法将返回一个Future对象,所有的异常以及处理结果都可以通过future对象获取。 采用Future模式,将返回结果以及异常放到Future中,在Future中处理。

总结

拙技蒙斧正,不胜雀跃。

相关文章:

ThreadPool线程池源码解析

ThreadPool线程池源码解析 文章目录前言一、基本使用二、执行流程三、源码分析ThreadPoolExecutor 中重要属性ThreadPoolExecutor 内部类Workerexecute&#xff08;&#xff09;方法addWorker(command, true)方法runWorker(worker )方法getTask()方法shutdown和shutdownNow四、…...

Python中 5个非常有用的单行代码

Python中 5个非常有用的单行代码1. 什么是单行代码&#xff1f;2. 了解和使用单行代码的好处3. 5个单行代码示例参考作为开发人员&#xff0c;你想提高生产力吗&#xff1f;掌握这 5 个 Python 一行代码&#xff0c;你就能写出简洁的代码。 这篇博客将介绍5个非常有用的初学者可…...

蓝牙模块各种工作模式

摘要&#xff1a;本文主要归纳总结蓝牙模块的不同工作模式&#xff0c;通过蓝牙模块不同的工作模式了解其扮演不同角色时工作的一个基本原理&#xff0c;为更深入的研究蓝牙模块底层的工作机制和技术方案进行铺垫。 1、主设备工作模式 主设备是能够搜索别人并主动建立连接的一…...

修剪灌木[蓝桥杯2022初赛]

题目描述 爱丽丝要完成一项修剪灌木的工作。 有 N 棵灌木整齐的从左到右排成一排。 爱丽丝在每天傍晚会修剪一棵灌木&#xff0c;让灌木的高度变为 0 厘米。 爱丽丝修剪灌木的顺序是从最左侧的灌木开始&#xff0c;每天向右修剪一棵灌木。 当修剪了最右侧的灌木后&#xff0c;…...

Python的知识点运用-1(日期转换)

问&#xff1a;如何将 星期一, 三月 13, 2023转换成2023-03-13看到这个问题&#xff0c;你的第一反应是什么&#xff1f;&#xff1f;&#xff1f;反正我是懵逼的。不过后面一想&#xff0c;时间模块可以。在这个问题后面&#xff0c;群友又问了一个问题&#xff0c;如何在本地…...

原理图制图规范详细说明

1、1 原理图必须使用公司统一原理图库 在原理图设计中&#xff0c;必须采用公司统一原理图库&#xff0c;以保证设计的一致性和打包后封装、料单等结果的一致性。不使用公司统一原理图库造成的连接、封装错误个人承担责任。 注意使cds.lib中的路径指向库服务器eda-svr1的路径…...

【Unity小知识】Editor编写常用方法汇总

汇总一些Unity Editor开发的常用方法和实现方式&#xff0c;会持续更新。 添加自定义菜单栏方法 using UnityEngine; using UnityEditor;public class EditorTools : EditorWindow {[MenuItem("EditorTools/自定义的编辑器方法")]public static void CustomEditroFu…...

【数据仓库-4】-- 提取、转换、装载(ETL)

1.数据抽取 1.1 逻辑抽取 1.1.1 全量数据抽取 一般发生在我们初始化时,需要一次性将源库(业务系统)的所有数据抽取到数据仓库的ODS层。 比如将一个全表导出数据文件或者查询源表所有数据的SQL语句, 都是全量抽取的例子。 select * from user.person; 1.1.2 增量数据抽取 对…...

【DBC专题】-12-不同类型报文(应用/诊断/网关/测量标定)在DBC中配置,以及在Autosar各模块间的信号数据流向

点击返回「Autosar从入门到精通-实战篇」总目录 案例背景(共18页精讲)&#xff1a;该篇博文将告诉您&#xff1a; 1)Autosar中&#xff0c;不同类型报文(App应用&#xff0c;UDS/OBD诊断&#xff0c;NM网络管理报文&#xff0c;XCP测量标定)的信号数据流向&#xff1b; 2)CAN …...

【Linux】进程的基础概念 进程的相关操作 进程的状态

进程一、进程的基本知识1、基本概念2、进程的描述 —— PCB3、task_ struct内容分类二、进程的相关操作1、在Linux下查看进程2、通过系统调用在代码中获取进程标示符3、如何创建子进程4、关于fork()的一些深度理解三、进程的状态Linux中的进程的状态四、僵尸进程与孤儿进程僵尸…...

【小猫爪】AUTOSAR学习笔记06-Communication Stack之ComM模块

【小猫爪】AUTOSAR学习笔记06-Communication Stack之ComM模块前言1 ComM简介2 ComM功能介绍2.1 PNC 状态管理2.2 Channel状态管理2.3 通信禁止功能2.4 不同类型的NM2.5 User、PNC 与 Channel 的映射2.6 状态保存END前言 因为一个偶然的机会让我接触到了AUTOSAR&#xff0c;所以…...

BP插件开发(JAVA)

本文会包括BP插件开发流程及打包&#xff0c;API&#xff0c;javaswing&#xff08;UI&#xff09;部分的内容。阅读完本文后&#xff0c;读者将初步具有开发BP插件的能力。1 开始开发我们使用IDEA作为开发工具&#xff08;使用其他IDE也绰绰有余&#xff09;。引入依赖包&…...

【Zookeeper】介绍与配置

目录 概述 工作机制 特点 数据结构 应用场景 统一配置管理 统一集群管理 ​编辑 服务器动态上下线 软负载均衡 下载 启动 启动客户端 配置参数 集群配置 配置服务器编号 配置zoo.cfg 分发zoo.cfg配置文件 选举机制 第一次启动 非第一次启动 集群启动停止脚本…...

chrome快捷键

Ctrl T&#xff1a;打开新标签页。Ctrl W&#xff1a;关闭当前标签页。Ctrl Shift T&#xff1a;重新打开最近关闭的标签页。Ctrl Tab&#xff1a;在打开的标签页之间切换。Ctrl Shift Tab&#xff1a;在打开的标签页之间反向切换。Ctrl N&#xff1a;打开新窗口。Ctrl…...

手搓string类

手搓string类 文章目录手搓string类string的成员一.构造&#xff0c;拷贝构造&#xff0c;赋值重载和析构1.构造函数2.拷贝构造3.swap问题4.赋值重载5.析构函数二.简单接口1.c_str2.size(有效字符长度)3.capacity&#xff08;有效字符容量&#xff09;4.operator[]5.迭代器和范…...

小白学Pytorch系列--Torch API (7)

小白学Pytorch系列–Torch API (7) Comparison Ops allclose 此函数检查输入和其他是否满足条件&#xff1a; >>> torch.allclose(torch.tensor([10000., 1e-07]), torch.tensor([10000.1, 1e-08])) False >>> torch.allclose(torch.tensor([10000., 1e-…...

函数(上)——“Python”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是Python的函数呀&#xff0c;下面&#xff0c;就让我们进入函数的世界吧 首先可以选择性地看一下小雅兰很久之前写的C语言函数章节的知识&#xff1a; 函数——“C”_认真学习的小雅兰.的博客-CSDN博客 函数递归&#xf…...

ChatGPT说:如何利用ChatGPT变现?躺着赚钱不是梦。

您好&#xff0c;我是码农飞哥&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通 &#x1f601; 2. 毕业设计专栏&…...

4.网络爬虫—Post请求(实战演示)

网络爬虫—Post请求实战演示POST请求GET请求POST请求和GET请求的区别获取二进制数据爬[百度官网](https://www.baidu.com/)logo实战发送post请求百度翻译实战使用session发送请求模拟登录17k小说网常见问题前言&#xff1a; &#x1f4dd;​&#x1f4dd;​此专栏文章是专门针对…...

【视频文稿】车载Android应用开发与分析 - 开发系统应用

本期视频地址&#xff1a;https://www.bilibili.com/video/BV1NY411z7TK/ 前言 Hello&#xff0c;大家好&#xff0c;我是林栩。 开发车载应用&#xff0c;其实主要都是在Android系统中编写各种系统应用&#xff0c;所以上期视频先介绍了Android系统源码的下载和编译流程&…...

Scala流程控制

目录 单分支 双分支 多分支 for 循环控制 循环守卫 循环步长 循环嵌套 循环返回值 While 和 do..While 循环控制 While循环控制 do..While 循环控制 循环中断 单分支 if (条件表达式) {执行代码块 }var age StdIn.readShort()if (age < 18){println("童年&quo…...

人脸活体检测系统(Python+YOLOv5深度学习模型+清新界面)

摘要&#xff1a;人脸活体检测系统利用视觉方法检测人脸活体对象&#xff0c;区分常见虚假人脸&#xff0c;以便后续人脸识别&#xff0c;提供系统界面记录活体与虚假人脸检测结果。本文详细介绍基于YOLOv5深度学习技术的人脸活体检测系统&#xff0c;在介绍算法原理的同时&…...

prometheus03-如何导出prometheus指标

Prometheus是一个开源的监控系统和时间序列数据库&#xff0c;用于收集和存储服务的指标数据。要导出Prometheus指标&#xff0c;你需要使用或实现一个Prometheus Exporter。以下是一个简单的指南&#xff0c;分为三个主要步骤&#xff1a; 选择或实现Prometheus Exporter Pr…...

Linux驱动开发——串口设备驱动

Linux驱动开发——串口设备驱动 一、串口简介 串口全称叫做串行接口&#xff0c;通常也叫做 COM 接口&#xff0c;串行接口指的是数据一个一个的顺序传输&#xff0c;通信线路简单。使用两条线即可实现双向通信&#xff0c;一条用于发送&#xff0c;一条用于接收。串口通信距…...

LeetCode--缺失的第一个正数(41)和 接雨水(42)

目录 缺失的第一个正数 接雨水 0ms&#xff0c;100% 代码 缺失的第一个正数 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/first-missing-positive 题目&#xff1a;给你一个未排序的整数数组 nums &#xff0c;请…...

java源码阅读---ReentrantLock源码解析

ReentrantLock源码解读 在讲ReentrantLock之前我们先看一下Lock接口里的方法 Lock接口中的方法 lock()方法 void lock(); //直接加锁,如果加锁失败什么也不返回lockInterruptibly()方法 void lockInterruptibly() throws InterruptedException;lockInterruptibly()方法能够…...

OpenCv + Qt5.12.2 文字识别

OpenCv Qt5.12.2 文字检测与文本识别 前言 ​ 好久没有进行一些相关的更新的了&#xff0c;去年一共更新了四篇&#xff0c;最近一直在做音视频相关的直播服务&#xff0c;又是重新学习积攒经验的一个过程。去年疫情也比较严重&#xff0c;等到解封&#xff0c;又一直很忙&a…...

网络作业1【计算机网络】

网络作业1【计算机网络】前言推荐网络作业1一. 单选题&#xff08;共7题&#xff0c;58.1分&#xff09;二. 多选题&#xff08;共1题&#xff0c;8.3分&#xff09;三. 判断题&#xff08;共4题&#xff0c;33.6分&#xff09;最后前言 2023-3-13 20:11:42 以下内容源自《计…...

常见背包问题

一.前言若你想学习或正在学习动态规划&#xff0c;背包问题一定是你需要了解的一种题型&#xff0c;并且大多数人最初都是从背包问题入坑进而打开动态规划这一大门。背包问题分为多种&#xff0c;你可以先掌握最常见的主要是三类&#xff1a;01背包、完全背包、多重背包二.分析…...

【python】python编译器以及安装

✅作者简介&#xff1a;一名在读大二学生&#xff0c;希望大家多多支持 &#x1f525;系列专栏&#xff1a;python &#x1f4ac;个人主页&#xff1a;小园园子的CSDN博客 python编译器以及安装一、编译器与解释器详细内容Python解释器种类Python的运行机制二、python环境搭建p…...