为什么tomcat要自定义线程池实现?

背景

最近在研究tomcat调优的问题,开发人员做过的最多的tomcat调优想必就是线程池调优了,但是tomcat并没有使用jdk自己的线程池实现,而是自定了了线程池,自己实现了ThreadPoolExecutor类位于org.apache.tomcat.util.threads包下

jdk线程池

首先回顾一下jdk的线程池实现
在这里插入图片描述
提交一个任务时:
1 如果此时线程池中的数量小于corePoolSize,无论线程池中的线程是否处于空闲状态,都会创建新的线程来处理被添加的任务。

2 如果此时线程池中的数量大于等于于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。

3 如果此时线程池中的数量大于等于corePoolSize,且缓冲队列workQueue满,但是线程池中的数量小于maximumPoolSize,则建新线程来处理被添加的任务

4 如果此时线程池中的数量大于corePoolSize,且缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么就需要通过handler所指定的策略来处理此这个任务。

5 线程池中的线程数量大于 corePoolSize时,如果某线程处理完任务后进入空闲状态,空闲时间超过keepAliveTime,该线程将被终止。这样,线程池可以动态的调整池中的线程数到corePoolSize。

重点已经标红处理了:在线程达到corePoolSize个数时,超过的任务是先放在队列里面的

问题:当任务很多又很耗时(比如http请求IO密集型),队列长度怎么设置?过长容易造成任务堆积甚至OOM,最大线程数的设置也将变的没有意义;过短又容易将丢弃任务,tomcat至少要保证请求尽可能交给业务系统去处理

tomcat线程池

在AbstractEndpoint中调用createExecutor方法自定义线程池

public void createExecutor() {internalExecutor = true;TaskQueue taskqueue = new TaskQueue();TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);taskqueue.setParent( (ThreadPoolExecutor) executor);}

那么首先看ThreadPoolExecutor的excute方法

public void execute(Runnable command, long timeout, TimeUnit unit) {submittedCount.incrementAndGet();try {super.execute(command);} catch (RejectedExecutionException rx) {if (super.getQueue() instanceof TaskQueue) {final TaskQueue queue = (TaskQueue)super.getQueue();try {if (!queue.force(command, timeout, unit)) {submittedCount.decrementAndGet();throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));}} catch (InterruptedException x) {submittedCount.decrementAndGet();throw new RejectedExecutionException(x);}} else {submittedCount.decrementAndGet();throw rx;}}}

tomcat中的线程池有一个额外的属性submittedCount,你可以简单的理解为就是一个计数器,每当有一个任务执行在执行时submittedCount就会加1,当任务执行完成后submittedCount就会减1,具体记录这个数字有什么作用,继续往下看就知道了。

核心还在是父类的的excute方法

public void execute(Runnable command) {if (command == null)throw new NullPointerException();/** Proceed in 3 steps:** 1. If fewer than corePoolSize threads are running, try to* start a new thread with the given command as its first* task.  The call to addWorker atomically checks runState and* workerCount, and so prevents false alarms that would add* threads when it shouldn't, by returning false.** 2. If a task can be successfully queued, then we still need* to double-check whether we should have added a thread* (because existing ones died since last checking) or that* the pool shut down since entry into this method. So we* recheck state and if necessary roll back the enqueuing if* stopped, or start a new thread if there are none.** 3. If we cannot queue task, then we try to add a new* thread.  If it fails, we know we are shut down or saturated* and so reject the task.*/int c = ctl.get();if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}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))reject(command);}
  1. 如果当前worker的数量小于核心线程池,调用addWorker
  2. 如果任务能成功入队,并且需要增加一个线程(线程池未关闭并且没有工作线程),调用addWorker
  3. 如果任务入队失败,那么尝试添加一个新线程。如果失败了,拒绝这项任务。

什么是ctl?

我们看到线程池ThreadPoolExecutor内部是通过AtomicInteger类型的 ctl变量来控制运行状态和线程个数;在通过AtomicInteger源码知道器内部就是维护了一个int类型的value值,如下

private volatile int value;

那么通过一个值如何维护状态和个数两个值呢?那么接下来通过底层源码来看大神Doug Lea是如何设计的。

private static final int COUNT_BITS = Integer.SIZE - 3;private static final int CAPACITY   = (1 << COUNT_BITS) - 1;// 取高三位表示以下运行时的状态// runState is stored in the high-order bitsprivate static final int RUNNING     =  -1 << COUNT_BITS;private static final int SHUTDOWN    =   0 << COUNT_BITS;private static final int STOP        =   1 << COUNT_BITS;private static final int TIDYING     =   2 << COUNT_BITS;private static final int TERMINATED  =   3 << COUNT_BITS;/*** 各个值的二进制表示:** 1111 1111 1111 1111 1111 1111 1111 1111 (-1) * 0000 0000 0000 0000 0000 0000 0000 0000 (0) * 0000 0000 0000 0000 0000 0000 0000 0001 (1) * 0000 0000 0000 0000 0000 0000 0000 0010 (2) * 0000 0000 0000 0000 0000 0000 0000 0011 (3)** 【分析】:* 初始容量值,高三位全是0,低29位全是1;后续操作会以此为基础进行操作* CAPACITY                    000 1   1111 1111 1111 1111 1111 1111 1111**              ---------------3位-1位 -28位---* 【前三位,表明状态位,后29位表明线程个数,即 2^29 - 1 基本够用了】** RUNNING(-536870912)         111 0    0000 0000 0000 0000 0000 0000 0000* SHUTDOWN(0)                 000 0    0000 0000 0000 0000 0000 0000 0000* STOP(536870912)             001 0    0000 0000 0000 0000 0000 0000 0000* TIDYING(1073741824)         010 0    0000 0000 0000 0000 0000 0000 0000* TERMINATED(1610612736)      011 0    0000 0000 0000 0000 0000 0000 0000* */

在这里插入图片描述

结论就是:前三位,表明状态位,后29位表明线程个数,即 2^29 - 1 基本够用了

TaskQueue的offer()方法

既然tomcat用了自己的队列,接下来看一下自定义的TaskQueue类

上述源码已经知道,是否调用addWorker方法取决于TaskQueue的offer()方法要返回false结果(暂不考虑入队成功但是worker随后被销毁的情况)

LinkedBlockingQueue的实现

TaskQueue继承于LinkedBlockingQueue,那么先看LinkedBlockingQueue的offer实现

public boolean offer(E e) {if (e == null) throw new NullPointerException();final AtomicInteger count = this.count;if (count.get() == capacity)return false;final int c;final Node<E> node = new Node<E>(e);final ReentrantLock putLock = this.putLock;putLock.lock();try {if (count.get() == capacity)return false;enqueue(node);c = count.getAndIncrement();if (c + 1 < capacity)notFull.signal();} finally {putLock.unlock();}if (c == 0)signalNotEmpty();return true;}

很简单:队列中元素个数达到容量上限,则返回false
这也能解释为什么jdk线程池是队列满了才会继续新增线程至最大线程数

tomcat实现TaskQueue:

首先TaskQueue是继承LinkedBlockingQueue的

public class TaskQueue extends LinkedBlockingQueue<Runnable> 

再看offer方法

public boolean offer(Runnable o) {//we can't do any checksif (parent==null) return super.offer(o);//we are maxed out on threads, simply queue the objectif (parent.getPoolSize() == parent.getMaximumPoolSize()) return super.offer(o);//we have idle threads, just add it to the queueif (parent.getSubmittedCount()<=(parent.getPoolSize())) return super.offer(o);//if we have less threads than maximum force creation of a new threadif (parent.getPoolSize()<parent.getMaximumPoolSize()) return false;//if we reached here, we need to add it to the queuereturn super.offer(o);}

注释解释的很明显了:

  1. 如果当前活跃的线程数等于最大线程数,那么就不能创建线程了,因此直接放入队列中
  2. 如果当前提交的任务数小于等于当前活跃的线程数,表示还有空闲线程,直接添加到队列,让线程去执行即可。此处也终于看到了parent.getSubmittedCount方法,用来获取当前提交的任务数,每当有一个任务执行在执行时submittedCount就会加1,当任务执行完成后submittedCount就会减1
  3. 再校验下当前活跃线程数是否小于最大线程数,如果小于,此时就可以创建新的线程了。
  4. 如果以上都不符合,那就代表既没有空闲线程,又达到了最大线程数,也只能放队列了,但是不需要新建线程

在这里插入图片描述

因此Tomcat的线程池策略是,如果没有空闲线程且线程数大于核心线程配置时:继续增加线程至最大核心数为止

两者对比

tomcat的线程池实现相比jdk实现有以下几点不同:

  1. 队列无限长:高qps时基本不会丢弃任务;但是会有OOM的风险,但是一般单台服务器qps基本不会超过两千
  2. 达到最大线程数之后才会放入队列,低qps时可以快速请求响应不用排队,但是若qps不稳定,会频繁创建销毁线程,对cpu不够友好

行文至此,tomcat为什么要自定义线程池已经讲完了,接下来会补充一下worker的源码分析

worker原理

简介

Worker是ThreadPoolExecutor中的内部类,线程池中的线程,都会被封装成一个Worker类对象,ThreadPoolExecutor维护的其实就是一组Worker对象,其中用集合workers存储这些Worker对象;

Worker类中有两个属性,一个是firstTask,用来保存传入线程池中的任务,一个是thread,是在构造Worker对象的时候,利用ThreadFactory来创建的线程,用来处理线程池队列中的任务;

Worker继承AQS,使用AQS实现独占锁,并且是不可重入的,构造Worker对象的时候,会把锁资源状态设置成-1,因为新增的线程,还没有处理过任务,是不允许被中断的

代码如下

private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{/*** This class will never be serialized, but we provide a* serialVersionUID to suppress a javac warning.*/private static final long serialVersionUID = 6138294804551838833L;/** 这个就是worker持有的线程,也就是线程池中的线程 */    final Thread thread;/** 这个就是提交给线程池的任务 */    Runnable firstTask;/** 每一个线程执行的任务数量的计数器 */  volatile long completedTasks;/*** 我们在调用addWorker方法的时候就会调用这个构造方法,有可能是创建新线程并执行任务,那么firstTask就是传给线程池要执行的任务,如果只是了* 单纯的想创建一个线程,只需要传入null就可以*/Worker(Runnable firstTask) {setState(-1); // inhibit interrupts until runWorkerthis.firstTask = firstTask;// 这个是通过线程工厂类创建一个线程,也就是给线程池创建一个线程this.thread = getThreadFactory().newThread(this); }/** Delegates main run loop to outer runWorker  */public void run() {runWorker(this);}}

worker其实就是一个Runable,其也是需要构造成一个Thread对象,然后调用Thread.start()方法运行的。只不过在worker的run方法中是定义了一个runWoker的方法。这个方法的主要内容从 for 循环不停的从task队列中获取对应的runable的task,然后同步调用这个task的run()方法。其实就是在某个线程中,不停的拿队列中的任务进行执行。

runWorker

上文已经知道线程池添加一个线程是通过调用addWorker方法,在调用addWorker成功后则会执行Worker对象的run方法,进入runWorker方法逻辑

final void runWorker(ThreadPoolExecutor.Worker w) {// 获取当前线程,其实这个当前线程,就是worker对象持有的线程,从线程池中拿到的任务就是由这个线程执行的Thread wt = Thread.currentThread();// 在构造Worker对象的时候,会把一个任务添加进Worker对象// 因此需要把其作为新增线程的第一个任务来执行Runnable task = w.firstTask;// 上面已经将该任务拿出来准备进行执行了(将firstTask取出赋值给task),则需要将该worker对象即线程池中的线程对象持有的任务清空w.firstTask = null;// 将AQS锁资源的状态由-1变成0,运行该线程进行中断 因为在创建的时候将state设为-1了,现在开始执行任务了,也就需要加锁了,所以要把state再重新变为0,这样在后面执行任务的时候才能用来加锁,保证任务在执行过程中不会出现并发异常// 解锁w.unlock();// 用来判断执行任务的过程中,是否出现了异常boolean completedAbruptly = true;try {// 线程池中的线程循环处理线程池中的任务,直到线程池中的所有任务都被处理完后则跳出循环while (task != null || (task = getTask()) != null) {  // 这一步的getTask()就说明Worker一直在轮询的从队列中获取任务,getTask()方法将从队列获取到的任务返回,赋值给task// 给该worker加锁,一个线程只处理一个任务。注意加锁是给worker线程加锁,不是给任务线程加锁,因为worker线程之前一直在轮询地在队列中取任务,但是当执行任务的时候,为了避免执行任务出现异常,就对其加锁w.lock();// 线程池是否是STOP状态// 如果是,则确保当前线程是中断状态// 如果不是,则确保当前线程不是中断状态if ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())// 注意这里中断的是当前线程,也就是worker对象持有的线程wt.interrupt();try {// 扩展使用,在执行任务的run方法之前执行beforeExecute(wt, task);// 记录执行任务过程中,出现的异常Throwable thrown = null;try {// 执行任务的run方法   当前线程环境就是worker对象持有的线程,所以本质就是woker对象在执行task任务的run()方法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 {// 扩展使用,在执行任务的run方法之后执行afterExecute(task, thrown);}} finally {// 执行完任务后,就将任务对象清空task = null;w.completedTasks++; // 该worker已经完成的任务数+1w.unlock();  // 将worker线程地锁释放}}// 正常执行完任务completedAbruptly = false;} finally {// 线程池中所有的任务都处理完后,或者执行任务的过程中出现了异常,就会执行该方法processWorkerExit(w, completedAbruptly);}
}

这个方法主要做几件事

  1. 如果 task 不为空,则开始执行 task
  2. 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则执行该任务
  3. 执行完毕后,通过 while 循环继续 getTask()取任务
  4. 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕

这个方法比较简单,如果忽略状态检测和锁的内容,本质就是如果有第一个任务,就先执行之,之后再从任务队列中取任务来执行,获取任务是通过getTask()来进行的。

到这里也就明白了线程池是怎么复用有限的线程数来执行大量的任务
那么worker是如何获取任务的呢?

核心方法 getTask()

这个方法用来向队列中轮询地尝试获取任务。该方法也是ThreadPoolExecutor中的方法。

这里重要的地方是第二个 if 判断,目的是控制线程池的有效线程数量。

由上文中的分析可以知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,但这时如果超时没有获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁掉,保持线程数量在 corePoolSize 即可。

// 返回任务Runnable
private Runnable getTask() {// timedOut表示 记录上一次从队列中获取任务是否超时boolean timedOut = false; // Did the last poll() time out?// 自旋for (;;) {// 这一部分是判断线程池状态// 获取线程池的状态和线程池中线程数量组成的整形字段,32位// 高3位代表线程池的状态,低29位代表线程池中线程的数量int c = ctl.get();// 获取高3位的值,即线程池的状态int rs = runStateOf(c);// 如果线程池状态不是Running状态,并且 线程也不是SHUTDOWN状态 或者任务队列已空if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {// 则将线程池中的线程数量减1  就是说该线程已经不是运行状态了,所以要这个worker线程也没有用了,直接将该worker去掉。这个是原子操作decrementWorkerCount();//返回一个空任务,因为:// 1:如果任务队列已空,则想返回任务也没有// 2:如果线程池处于STOP或者之上的状态,则线程池不允许再处理任务return null;}// 这一部分是判断线程池有效线程数量// 获取低29位的值,即线程池中线程的数量int wc = workerCountOf(c);// timed是否需要进行超时控制// allowCoreThreadTimeOut默认false// 当线程池中线程的数量没有达到核心线程数量时,获取任务的时候允许超时  如果将allowCoreThreadTimeOut设为true,那也不允许超时// 当线程池中线程的数量超过核心线程数量时,获取任务的时候不允许超时   boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 这个很好理解// wc > maximumPoolSize的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;// timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时// 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减1;// 如果减1失败,则continue返回重试// 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。if ((wc > maximumPoolSize || (timed && timedOut))&& (wc > 1 || workQueue.isEmpty())) {if (compareAndDecrementWorkerCount(c))return null;continue;}// 如果上面都没问题,就可以获取任务了try {// 获取任务// 如果timed = true ,说明需要做超时控制,则根据keepAliveTime设置的时间内,阻塞等待从队列中获取任务// 如果timed = false,说明不需要做超时控制,则阻塞,直到从队列中获取到任务为止Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();// 如果获取到任务,则把任务返回if (r != null)return r;// 执行到这里,说明在允许的时间内,没有获取到任务timedOut = true;} catch (InterruptedException retry) {// 获取任务没有超时,但是出现异常了,将timedOut设置为falsetimedOut = false;}}
}

注意,这里取任务会根据工作线程的数量判断是使用BlockingQueue的poll(timeout, unit)方法还是take()方法。

poll(timeout, unit)方法会在超时时返回null,如果timeout<=0,队列为空时直接返回null。

take()方法会一直阻塞直到取到任务或抛出中断异常。

所以,如果keepAliveTime设置为0,当任务队列为空时,非核心线程取不出来任务,会立即结束其生命周期。

当允许超时控制时:workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 会在keepAliveTime后返回null,返回null的结果是退出完成循环,销毁当前线程;这就说明了keepAliveTime参数的含义,线程最大空闲时间了

提出一个问题,这个keepAliveTime参数是否对核心线程也生效呢,文末会有答案

那么接下来,worker是如何销毁的呢?

核心方法 processWorkerExit()

runWorker 的 while 循环执行完毕以后,在 finally 中会调用 processWorkerExit()方法,来销毁工作线程。该方法就是判断当前线程是需要将其删除还是继续执行任务。该方法也是ThreadPoolExecutor中的方法。

private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {// 如果 completedAbruptly = true ,则线程执行任务的时候出现了异常,需要从线程池中减少一个线程// 如果 completedAbruptly = false,则执行getTask方法的时候已经减1,这里无需在进行减1操作if (completedAbruptly)decrementWorkerCount();// 获取线程池的锁,因为后面是线程池的操作,为了并发安全,需要对线程池加锁final ReentrantLock mainLock = this.mainLock;// 线程池加锁mainLock.lock();try {// 统计该线程池完成的任务数completedTaskCount += w.completedTasks;// 从线程池中移除一个工作线程    works是线程池持有的一个集合  workers.remove(w); // 将没用的worker去掉,也就是当前传入的worker} finally {// 线程池解锁mainLock.unlock();}// 根据线程池的状态,决定是否结束该线程池tryTerminate(); // 钩子方法// 判断线程池是否需要增加线程// 获取线程池的状态int c = ctl.get();// -当线程池是RUNNING或SHUTDOWN状态时// --如果worker是异常结束(即completedAbruptly为false),那么会直接addWorker;// ---如果allowCoreThreadTimeOut = true,并且等待队列有任务,至少保留一个worker;// ---如果allowCoreThreadTimeOut = false,活跃线程数不少于corePoolSizeif (runStateLessThan(c, STOP)) { // 线程池状态小于STOP,就说明当前线程池是RUNNING或SHUTDOWN状态// 如果worker是异常结束的,不进入下面的分支,直接去addWorkerif (!completedAbruptly) {// 根据allowCoreThreadTimeOut的值,来设置线程池中最少的活跃线程数是0还是corePoolSizeint min = allowCoreThreadTimeOut ? 0 : corePoolSize;// 如果等待队列中有任务,要至少保留一个workerif (min == 0 && ! workQueue.isEmpty())// 至少保留一个workermin = 1;// 如果活跃线程数大于等于min,直接返回,不需要再调用addWorker来增加线程池中的线程了if (workerCountOf(c) >= min)return; // replacement not needed}// 增加线程池中的workeraddWorker(null, false);}
}

1.执行decrementWorkerCount方法将线程池中的线程数量减1 ,因为当前worker已经取出去任务了

  1. 如果 completedAbruptly = true ,则代表线程执行任务的时候出现了异常,需要执行
  2. 如果 completedAbruptly = false,则执行getTask方法的时候调用过decrementWorkerCount方法将线程池中的线程数量减1,这里无需在进行减1操作

2.将worker从wokers集合中移除
3.根据线程池的状态,决定是否结束该线程池
4.判断是否再调用addWorker方法

其中 int min = allowCoreThreadTimeOut ? 0 : corePoolSize;根据allowCoreThreadTimeOut的值,来设置线程池中最少的活跃线程数是0还是corePoolSize

  1. 如果worker是异常结束(即completedAbruptly为false),那么会直接addWorker;
  2. 如果allowCoreThreadTimeOut = true,并且等待队列有任务,至少保留一个worker;
  3. 如果allowCoreThreadTimeOut = false,活跃线程数不少于corePoolSize

说明

  1. 如果核心线程出了异常也是会被销毁的,只不过销毁后还会调用addWorker方法增加一个线程
  2. allowCoreThreadTimeOut为true时,min为0,则表明,核心线程在allowCoreThreadTimeOut为true时也是会随着worker的销毁(没有任务可取既空闲了keepAliveTime时间)而销毁,并且不会调用addWorker来增加一个线程

参考:
https://blog.csdn.net/cy973071263/article/details/131484384
https://blog.csdn.net/u014307520/article/details/117787133
https://blog.csdn.net/kkkkk0826/article/details/103405813

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

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

相关文章

入门python

[NOIP2006 普及组] 明明的随机数 题目描述 明明想在学校中请一些同学一起做一项问卷调查&#xff0c;为了实验的客观性&#xff0c;他先用计算机生成了 N N N 个 1 1 1 到 1000 1000 1000 之间的随机整数 ( N ≤ 100 ) (N\leq100) (N≤100)&#xff0c;对于其中重复的数字…

服务器为什么要一直开机?

很多人在选择服务器的时候会想要省钱&#xff0c;然后就想要自己搭建一个服务器&#xff0c;其实服务器呢是需要24小时一直开机的&#xff0c;一般服务器的工作时间是3-5年&#xff0c;它们第一次开机后就会一直到机器故障淘汰才会关机哦&#xff0c;这是为什么呢&#xff1f;今…

TikTok的全球影响:跨文化、跨国界的短视频文化

随着TikTok的崛起&#xff0c;短视频文化正在以前所未有的方式迅速传播&#xff0c;跨足了不同国家和文化的边界。本文将探讨TikTok的全球影响&#xff0c;以及它如何促进了跨文化交流和文化融合。 短视频&#xff1a;跨越语言和文化的沟通工具 TikTok的短视频格式具有独特的跨…

步步为营,如何将GOlang引用库的安全漏洞修干净

文章目录 引场景构建第一步、直接引用的第三方库升级修复策略1.确认是否为直接引用的第三方库2.找到需要升级的版本是否为release版本 第二步、间接引用的第三方库升级修复策略那么问题来了&#xff0c;我们这么间接引用库的对应的直接引用库是哪个呢&#xff1f; &#xff08;…

Vue构建SPA项目实现路由

目录 前言 一、Vue CLI简介 1.什么是Vue CLI 2.Vue CLI的特点 二、SPA项目搭建 1.安装Vue CLI 2.使用脚手架vue-cli来构建项目 ​编辑 3.项目结构说明 4.什么是*.vue文件 三、基于SPA完成路由并嵌套路由 1.基于SPA完成路由 1. 1在src下的components 创建自定义组件…

vue项目升级webpack

vue项目升级webpack 目录 1. vue项目中影响webpack版本的是什么 2.理解package.json中库前缀^和~区别 3.升级webpack4到5操作 1. vue项目中影响webpack版本的是什么 答案是&#xff1a;vue/cli-service版本 2.理解package.json中库前缀^和~区别 x.y.z x代表大版本&#xf…

Spark-3.2.4 高可用集群安装部署详细图文教程

目录 一、Spark 环境搭建-Local 1.1 服务器环境 1.2 基本原理 1.2.1 Local 下的角色分布 1.3 搭建 1.3.1 安装 Anaconda 1.3.1.1 添加国内阿里源 1.3.2 创建 pyspark 环境 1.3.3 安装 spark 1.3.4 添加环境变量 1.3.5 启动 spark 1.3.5.1 bin/pyspark 1.…

化工DCS/SIS/MIS系统时钟同步(NTP服务器)建设

化工DCS/SIS/MIS系统时钟同步&#xff08;NTP服务器&#xff09;建设 化工DCS/SIS/MIS系统时钟同步&#xff08;NTP服务器&#xff09;建设 目前计算机网络中各主机和服务器等网络设备的时间基本处于无序的状态。 随着计算机网络应用的不断涌现&#xff0c;计算机的时间同步问…

vue2中年份季度选择器(需要安装element)

调用 <!--父组件调用--><QuarterCom v-model"quart" clearable default-current/> 组件代码 <template><div><span style"margin-right: 10px">{{ label }}</span><markstyle"position:fixed;top:0;bottom:0…

EPLAN_001#常用功能(一)

一、栅格设置、背景颜色设置 二、插入设备图标&#xff08;快捷键 Insert&#xff09; 按TAB旋转方向 三、 通过左CTRLENTER输入 四、移动属性文本、复制格式 CTRLB 可以移动设备图形中的相关文本&#xff08;或者右键—文本—移动属性文本&#xff09; 很对多个文本的&#xf…

干货 | 汽车行业研发效能提升的挑战与实践案例

在 9 月 15 日的第七届 CSN 大会上&#xff0c;思码逸研发效能专家王艳萍受邀分享了《汽车行业研发效能提升的挑战与实践案例》。演讲包含了思码逸对多家汽车企业服务过程中总结出的行业痛点、解决方案&#xff0c;以及实践案例。 以下为演讲实录&#xff1a; 思码逸与很多知名…

spring的ThreadPoolTaskExecutor装饰器传递调用线程信息给线程池中的线程

概述 需求是想在线程池执行任务的时候&#xff0c;在开始前将调用线程的信息传到子线程中&#xff0c;在子线程完成后&#xff0c;再清除传入的数据。 下面使用了spring的ThreadPoolTaskExecutor来实现这个需求. ThreadPoolTaskExecutor 在jdk中使用的是ThreadPoolExecutor…

[RK3588-Android12] 双HDMI+喇叭Speak同音问题

问题描述 因为上层的音频策略&#xff0c;导致双路HDMI和喇叭&#xff0c;声音会被切换为单路出声音&#xff0c;以下修改将RK3588-Android12 双路HDMI和喇叭播放声音同时出声音。 解决方案&#xff1a; 1.frameworks\base\services\core\java\com\android\server\WiredAccesso…

API接口:概述、设计、应用与未来趋势

一、API接口概述 API&#xff0c;全称应用程序接口&#xff0c;是一种软件程序之间的通信方法。API接口在互联网开发中扮演着重要角色&#xff0c;允许不同的应用程序相互交流和共享数据。API定义了一套标准的通信协议&#xff0c;使得开发人员能够使用特定的函数、方法或协议…

快速排序模拟实现

快速排序&#xff0c;时间复杂度为O&#xff08;NlogN&#xff09;&#xff0c;属于排序中相对快的那一列&#xff0c;以下是快排的模拟实现&#xff1a; 法一&#xff1a;左右指针交换法 void swap(int* x, int* y) {int tmp *x;*x *y;*y tmp; }//交换函数int getmid(int…

Python实现MYSQL蜜罐

1 LOAD DATA INFILE介绍 首先开启一个Mysql&#xff0c;看一下mysql是如何读取主机文件的。 1.1 linux搭建mysql 1&#xff09;docker运行mysql 2&#xff09;启动Mysql docker run -itd…

【GO】网络请求例子

post请求;multipart/form-data类型 // 构建请求参数requestData : map[string]interface{}{"gb": "","code": "","reMemberInfo": map[string]interface{}{"shi": "","…

【开发工具】idea 的全局搜索快捷键(Ctrl+shift+F)失效

文章目录 前言1. 取消 输入法的快捷键&#xff08;推荐使用&#xff09;2.更改 idea的快捷键3. 热键占用总结 前言 当你发现在idea 中看到用于全局搜索的快捷键就是 CtrlshiftF&#xff0c;可是怎么按都不管用的时候&#xff0c;你就不要再执着于自己的操作继续狂点电脑按键了…

Lnmp架构之mysql数据库实战1

1、mysql数据库编译 编译成功 2、mysql数据库初始化 配置数据目录 全局文件修改内容 生成初始化密码并进行初始化设定 3、mysql主从复制 什么是mysql的主从复制&#xff1f; MySQL的主从复制是一种常见的数据库复制技术&#xff0c;用于将一个数据库服务器&#xff08;称为主…

python3读写dbf文件

目录 1 读dbf文件2 写dbf文件 1 读dbf文件 import dbf# 打开dbf文件 table dbf.Table(阿坝藏族羌族自治州_BXF_CHA.dbf)# 打开文件并进行读写操作 table.open(modedbf.READ_WRITE)# 遍历记录并更新字段值 res [] with table:for record in table:res.append(list(record))# …