剑指JUC原理-13.线程池

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

    • ThreadPoolExecutor
      • 线程池状态
      • 构造方法
      • newFixedThreadPool
      • newCachedThreadPool
      • newSingleThreadExecutor
      • 提交任务
        • Future机制原理
          • 回顾Runnable和Callable
          • Future机制原理
          • Future源码解析
      • 关闭线程池
        • shutdown流程
        • shutdownNow流程
        • 问题说明
          • 线程是如何中断的
          • 为什么修改线程池状态为shutdown以后线程池就不能接收新任务了
            • execute方法
            • addWorker方法
        • 源码比较
          • shutdown源码
          • shutdownNow源码
      • **异步模式之工作线程**
        • 定义
        • 饥饿
        • 创建多少线程池合适
          • CPU 密集型运算
          • I/O 密集型运算
      • 任务调度线程池
      • 正确处理执行任务异常
        • 方法1:主动捉异常
        • 方法2:使用 Future
      • 任务调度线程池模拟定时执行任务
      • Tomcat线程池
    • Fork/Join
      • 概念
      • 使用

ThreadPoolExecutor

在这里插入图片描述

线程池状态

ThreadPoolExecutor 使用 int 的高 3 位(最高位1代表的是负数)来表示线程池状态,低 29 位表示线程数量

状态名高三位接收新任务处理阻塞队列任务说明
RUNNING111YY
SHUTDOWN000NY不会接收新任务,但会处理阻塞队列剩余任务
STOP001NN会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING010任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED011终结状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN(调用了线程池的SHUTDOWN) > RUNNING(创建出来的初始状态)

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作
进行赋值

// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

构造方法

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目(核心线程 + 救急线程数)
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

工作方式:

在这里插入图片描述

线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。

如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线
程来救急。(救急线程和核心线程的最大区别就是,它有生存时间,执行完了,过一段时间,没有新任务了,就销毁掉了,下次高峰期来了,才会再有)

如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它
著名框架也提供了实现

  • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
  • CallerRunsPolicy 让调用者运行任务
  • DiscardPolicy 放弃本次任务
  • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

在这里插入图片描述

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。

其它优秀框架的实现(总结整理)

  • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
    便定位问题
  • Netty 的实现,是创建一个新线程来执行任务
  • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
  • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。

newFixedThreadPool

固定大小的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

评价 适用于任务量已知,相对耗时的任务

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

特点

1.核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着

  • 全部都是救急线程(60s 后可以回收)
  • 救急线程可以无限创建

2.队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交
货)

SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {try {log.debug("putting {} ", 1);integers.put(1);log.debug("{} putted...", 1);log.debug("putting...{} ", 2);integers.put(2);log.debug("{} putted...", 2);} catch (InterruptedException e) {e.printStackTrace();}
},"t1").start();sleep(1);new Thread(() -> {try {log.debug("taking {}", 1);integers.take();} catch (InterruptedException e) {e.printStackTrace();}
},"t2").start();sleep(1);new Thread(() -> {try {log.debug("taking {}", 2);integers.take();} catch (InterruptedException e) {e.printStackTrace();}
},"t3").start();

输出

11:48:15.500 c.TestSynchronousQueue [t1] - putting 1 
11:48:16.500 c.TestSynchronousQueue [t2] - taking 1 
11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted... 
11:48:16.500 c.TestSynchronousQueue [t1] - putting...2 
11:48:17.502 c.TestSynchronousQueue [t3] - taking 2 
11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted... 

评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

使用场景:

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程
也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

提交任务

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException;// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException;// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService pool = Executors.newFixedThreadPool(1);}private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException {String result = pool.invokeAny(Arrays.asList(() -> {log.debug("begin 1");Thread.sleep(1000);log.debug("end 1");return "1";},() -> {log.debug("begin 2");Thread.sleep(500);log.debug("end 2");return "2";},() -> {log.debug("begin 3");Thread.sleep(2000);log.debug("end 3");return "3";}));log.debug("{}", result);}private static void method2(ExecutorService pool) throws InterruptedException {List<Future<String>> futures = pool.invokeAll(Arrays.asList(() -> {log.debug("begin");Thread.sleep(1000);return "1";},() -> {log.debug("begin");Thread.sleep(500);return "2";},() -> {log.debug("begin");Thread.sleep(2000);return "3";}));futures.forEach( f ->  {try {log.debug("{}", f.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}});}private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {Future<String> future = pool.submit(() -> {log.debug("running");Thread.sleep(1000);return "ok";});log.debug("{}", future.get());}

通过以上的例子,可以获取到对应的结果,过程不做赘述,下面主要介绍Future为什么能获取到 线程的结果并返回,至于其它几个返回值,大同小异!

Future机制原理
回顾Runnable和Callable
  • Callable定义了call()方法,Runnale定义了run()方法。

  • call()方法可以抛出异常,run()方法无法抛出异常。

  • Callable有返回值,是泛型的,创建的时候传递进去,执行结束后返回。

  • Callable执行任务的时候可以通过FutureTask得到任务执行的状态。

Callable的call方法实际执行在Runnable的run方法中。Runnable实例对象需要Thread包装启动,Callable先通过FutureTask(本质还是Runnable)包装,再给Thread包装执行。

Future机制原理

Future就是对于具体的Runnable或Callable任务的执行结果进行取消,查询是否完成,获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

JDK内置的Future主要使用了Callable接口和FutureTask类。下面对源码进行解析:

Callable接口:

@FunctionalInterface
public interface Callable<V> {/*** Computes a result, or throws an exception if unable to do so.** @return computed result* @throws Exception if unable to compute a result*/V call() throws Exception;
}

如何使用Callable:

一般情况下都是配合ExecutorService来使用的,在ExecutorService接口中声明了三个submit方法,用来生成Future对象,参数为Callable实例或Runnable实例。

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

继承关系:

public class FutureTask<V> implements RunnableFuture<V> {}public interface RunnableFuture<V> extends Runnable, Future<V> { void run();}

关系明确: FutureTask类实现了RunnableFuture接口,RunnableFuture接口又继承了Runnable接口和Future接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,拥有Future接口提供的各种方法。

流程跑通: 通常把任务定义Callable接口的call方法内部,返回值为泛型,再生成一个FutureTask的对象,FutureTask构造方法内部参数封装着Callable实例,然后把这个对象当作一个Runnable,作为参数传递给Thread包装执行。

Future源码解析

Future接口提供的方法:

public interface Future<V> {//取消任务。参数:是否立即中断任务执行,或者等等任务结束boolean cancel(boolean mayInterruptIfRunning);//任务是否已经取消,若已取消,返回trueboolean isCancelled();//任务是否已经完成。包括任务正常完成、抛出异常或被取消,都返回trueboolean isDone();/*会一直阻塞等待任务执行结束,获得V类型的结果。InterruptedException: 线程被中断异常, ExecutionException: 任务执行异常,如果任务被取消,还会抛出CancellationException*/V get() throws InterruptedException, ExecutionException;/*参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException*/V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask源码解析:

构造方法:

public FutureTask(Callable<V> callable) {if (callable == null)throw new NullPointerException();this.callable = callable;//状态为NEWthis.state = NEW;       // ensure visibility of callable}public FutureTask(Runnable runnable, V result) {this.callable = Executors.callable(runnable, result);this.state = NEW;       // ensure visibility of callable}

实际上,Callable = Runnable + result,看Executors.callable(runnable,result)的实现:

public static <T> Callable<T> callable(Runnable task, T result) {if (task == null)throw new NullPointerException();//new了一个RunnableAdapter,返回Callable,说明RunnableAdapter实现了Callablereturn new RunnableAdapter<T>(task, result);}

利用RunnableAdapter适配器实现了将Runnable转为Callable,继续看RunnableAdapter类:

static final class RunnableAdapter<T> implements Callable<T> {final Runnable task;final T result;RunnableAdapter(Runnable task, T result) {this.task = task;this.result = result;}public T call() {//Runnable task执行了run()task.run();//返回了T resultreturn result;}}

本质上还是Runnable去执行run方法,只是增加了result常量来接受返回的结果而已。

FutureTask的run方法:

public void run() {/*compareAndSwapObject(this, runnerOffset,]null, Thread.currentThread()))其中第一个参数为需要改变的对象,第二个为偏移量,第三个参数为期待的值,第四个为更新后的值。*/if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))return;try {Callable<V> c = callable;if (c != null && state == NEW) {V result;boolean ran;try {//call()方法是由FutureTask调用的,说明call()不是异步执行的result = c.call();ran = true;} catch (Throwable ex) {result = null;ran = false;//设置异常setException(ex);}if (ran)set(result);}} finally {// runner must be non-null until state is settled to// prevent concurrent calls to run()runner = null;// state must be re-read after nulling runner to prevent// leaked interruptsint s = state;//判断是否被中断if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}}

set方法:

protected void set(V v) {// NEW -> COMPLETINGif (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {//返回结果,也包括异常outcome = v;//COMPLETING -> NORMALUNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state//唤醒等待的线程finishCompletion();}}

将返回值和状态赋值回去。

report方法:

private V report(int s) throws ExecutionException {Object x = outcome;if (s == NORMAL)return (V)x;if (s >= CANCELLED)throw new CancellationException();throw new ExecutionException((Throwable)x);}

get方法:

public V get() throws InterruptedException, ExecutionException {int s = state;//是否是未完成状态,是则等待if (s <= COMPLETING)//等待过程s = awaitDone(false, 0L);return report(s);}/*** @throws CancellationException {@inheritDoc}*/public V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException {if (unit == null)throw new NullPointerException();int s = state;if (s <= COMPLETING &&(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)throw new TimeoutException();return report(s);}

run方法解析完成,使用get方法获取返回结果。

总结:Future只实现了异步,而没有实现回调,主线程get时会阻塞,可以轮询以便获取异步调用是否完成。

关闭线程池

shutdown流程
  • 修改线程池状态为SHUTDOWN

  • 不再接收新提交的任务

  • 中断线程池中空闲的线程

  • 第③步只是中断了空闲的线程,但正在执行的任务以及线程池任务队列中的任务会继续执行完毕

shutdownNow流程
  • 修改线程池状态为STOP

  • 不再接收任务提交

  • 尝试中断线程池中所有的线程(包括正在执行的线程)

  • 返回正在等待执行的任务列表 List<Runnable>

此时线程池中等待队列中的任务不会被执行,正在执行的任务也可能被终止(为什么是可能呢?因为如果正常执行的任务如果不响应中断,那么就不会被终止,直到任务执行完毕)

问题说明
线程是如何中断的

中断线程池中的线程的方法是通过调用 Thread.interrupt()方法来实现的,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt() 方法是无法中断当前的线程的(因为sleep、condition、await这些是响应中断的)。所以,shutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候是能立即退出的。

为什么修改线程池状态为shutdown以后线程池就不能接收新任务了

在向线程池提交任务的时候,会先检查线程池状态, 线程池状态为非关闭(或停止)时才能提交任务,这里已经将线程池状态修改为shutdown了,自然就不能接受新的任务提交了,可参考execute(Runnable command)逻辑和 addWorker逻辑)

execute方法
public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();// 【 1 】、worker数量比核心线程数小,直接创建worker执行任务if (workerCountOf(c) < corePoolSize) {if (addWorker(command, true))return;c = ctl.get();}// 【 2 】、worker数量超过核心线程数,任务直接进入队列。这里进入队列前先判断了线程池状态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);}//【 3 】、 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务(即:线程阻塞队列满了但线程池中的线程数没达到最大线程数,// 则新开启一个线程去执行该任务)。// 这儿有3点需要注意:// 1. 线程池不是运行状态时,addWorker内部会判断线程池状态// 2. addWorker第2个参数表示是否创建核心线程// 3. addWorker返回false,则说明任务执行失败,需要执行reject操作else if (!addWorker(command, false))reject(command);
}
addWorker方法
// 新建一个worker
w = new Worker(firstTask);
int rs = runStateOf(ctl.get());// 这儿需要重新检查线程池状态,即线程池状态为shutdown以后不能再提交任务了
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {// 添加到工作线程集合中workers.add(w);int s = workers.size();// 更新largestPoolSize变量的值if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;
}
源码比较
shutdown源码
public void shutdown() {final ReentrantLock mainLock = this.mainLock;// 获取线程池的锁mainLock.lock();try {// 检查关闭进入许可checkShutdownAccess();// 将线程池状态改为SHUTDOWNadvanceRunState(SHUTDOWN);// 【中断线程池中空闲线程】,注意和下面的shutdownNow方法中的进行对比interruptIdleWorkers();// 留给定时任务线程池的钩子方法,这里没有实现,在定时任务线程池中有实现onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();}// ①、如果线程池状态为正在运行 或 已经是 TIDYING 状态以上了 或者  线程池状态为shutdown但是等待队列中还有任务,// 那么这个方法什么都不做,直接返回// ②、如何线程池中还有未被中断的线程,则这里会再次去中断他(并且利用中断传播 从等待队列中删除等待的worker)// ③、如果线程池的状态是shutdown,并且等待队列中已经没有任务了,那么此时会把线程池状态转换为 TIDYING,// 并唤醒所有调用awaitTermination()等待线程池关闭的线程tryTerminate();
}
shutdownNow源码
public List<Runnable> shutdownNow() {List<Runnable> tasks;// 获取线程池的锁final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// 检查关闭进入许可checkShutdownAccess();// 将线程池状态改为STOPadvanceRunState(STOP);// 【中断所有线程】interruptWorkers();// 获取任务队列里未完成任务tasks = drainQueue();} finally {mainLock.unlock();}// ①、如果线程池状态为正在运行 或 已经是 TIDYING 状态以上了 或者  线程池状态为shutdown但是等待队列中还有任务,// 那么这个方法什么都不做,直接返回// ②、如何线程池中还有未被中断的线程,则这里会再次去中断他(并且利用中断传播 从等待队列中删除等待的worker)// ③、如果线程池的状态是shutdown,并且等待队列中已经没有任务了,那么此时会把线程池状态转换为 TIDYING,// 并唤醒所有调用awaitTermination()等待线程池关闭的线程tryTerminate();return tasks;
}

通过比较shutdown源码和shutdownNow的源码我们可以发现,这两个方法最大的不同在于中断线程的地方:

  • 首先,需要再次明确的一点是,中断线程并不是立即把这个线程停止,而是把这个线程的【中断状态】设置为true,表示有其他线程来中断过这个线程。

  • shutdown方法调用interruptIdleWorkers()方法中断的只是线程池中空闲的线程,那么也就说明线程池中正在工作的线程没有被中断,所以说正在工作的线程会继续执行完毕,并且正在工作的线程也会去任务队列中将已经提交的任务取出来并执行。

  • shutdownNow方法调用的是interruptWorkers()方法,该方法会逐一遍历线程池中的每一个线程(包括空闲线程和正在工作的线程)并且去中断他,所以说当调用shutdownNow方法的时候,所有线程都会被中断,等待队列中的任务不会被执行,正在执行的任务也可能会中断退出(为什么是可能而不是一定?因为如果正在执行的线程不响应中断,那么他就会继续运行)

异步模式之工作线程

定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现
就是线程池,也体现了经典设计模式中的享元模式。

例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那
么成本就太高了

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成
服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工

饥饿

固定大小线程池会有饥饿现象

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作。客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待、后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
public class TestDeadLock {static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");static Random RANDOM = new Random();static String cooking() {return MENU.get(RANDOM.nextInt(MENU.size()));}public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(2);executorService.execute(() -> {log.debug("处理点餐...");Future<String> f = executorService.submit(() -> {log.debug("做菜");return cooking();});try {log.debug("上菜: {}", f.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}});//        executorService.execute(() -> {
//            log.debug("处理点餐...");
//            Future<String> f = executorService.submit(() -> {
//                log.debug("做菜");
//                return cooking();
//            });
//            try {
//                log.debug("上菜: {}", f.get());
//            } catch (InterruptedException | ExecutionException e) {
//                e.printStackTrace();
//            }
//        });}
}

输出

17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜
17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅

当注释取消后,可能的输出

17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐... 
17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 

解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程
池,例如:

public class TestDeadLock {static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");static Random RANDOM = new Random();static String cooking() {return MENU.get(RANDOM.nextInt(MENU.size()));}public static void main(String[] args) {ExecutorService waiterPool = Executors.newFixedThreadPool(1);ExecutorService cookPool = Executors.newFixedThreadPool(1);waiterPool.execute(() -> {log.debug("处理点餐...");Future<String> f = cookPool.submit(() -> {log.debug("做菜");return cooking();});try {log.debug("上菜: {}", f.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}});waiterPool.execute(() -> {log.debug("处理点餐...");Future<String> f = cookPool.submit(() -> {log.debug("做菜");return cooking();});try {log.debug("上菜: {}", f.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}});}
}

输出

17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 
17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 
17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁
创建多少线程池合适
  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存
CPU 密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因
导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程
RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

经验公式如下

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 50% = 8

例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 10% = 40

任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,
由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个
任务的延迟或异常都将会影响到之后的任务。

public static void main(String[] args) {Timer timer = new Timer();TimerTask task1 = new TimerTask() {@Overridepublic void run() {log.debug("task 1");sleep(2);}};TimerTask task2 = new TimerTask() {@Overridepublic void run() {log.debug("task 2");}};// 使用 timer 添加两个任务,希望它们都在 1s 后执行// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行timer.schedule(task1, 1000);timer.schedule(task2, 1000);}

输出

20:46:09.444 c.TestTimer [main] - start... 
20:46:10.447 c.TestTimer [Timer-0] - task 1 
20:46:12.448 c.TestTimer [Timer-0] - task 2 

使用 ScheduledExecutorService 改写:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {System.out.println("任务1,执行时间:" + new Date());try { Thread.sleep(2000); } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);

输出

任务1,执行时间:Thu Jan 03 12:45:17 CST 2019 
任务2,执行时间:Thu Jan 03 12:45:17 CST 2019 

异常的情况

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);// 添加两个任务,希望它们都在 1s 后执行executor.schedule(() -> {System.out.println("任务1,执行时间:" + new Date());int i = 0 / 10;try { Thread.sleep(2000); } catch (InterruptedException e) { }}, 1000, TimeUnit.MILLISECONDS);executor.schedule(() -> {System.out.println("任务2,执行时间:" + new Date());}, 1000, TimeUnit.MILLISECONDS);

可以看到,有异常的情况,输出并不受影响

任务2,执行时间:Mon Nov 06 16:59:26 CST 2023
任务1,执行时间:Mon Nov 06 16:59:26 CST 2023

timer中如果一个线程出现了异常,那么剩下的任务就不执行了,而ScheduledExecutorService出现了异常,那么后续任务还是会执行的

scheduleAtFixedRate 例子

以固定的速率去执行任务

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {log.debug("running...");
}, 1, 1, TimeUnit.SECONDS);
参数:任务对象、延时时间、执行间隔、时间单位

输出

21:45:43.167 c.TestTimer [main] - start... 
21:45:44.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:45.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:46.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:47.215 c.TestTimer [pool-1-thread-1] - running... 

scheduleAtFixedRate 例子(任务执行时间超过了间隔时间):

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {log.debug("running...");sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s

21:44:30.311 c.TestTimer [main] - start... 
21:44:31.360 c.TestTimer [pool-1-thread-1] - running... 
21:44:33.361 c.TestTimer [pool-1-thread-1] - running... 
21:44:35.362 c.TestTimer [pool-1-thread-1] - running... 
21:44:37.362 c.TestTimer [pool-1-thread-1] - running... 

scheduleWithFixedDelay 例子:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {log.debug("running...");sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s

21:40:55.078 c.TestTimer [main] - start... 
21:40:56.140 c.TestTimer [pool-1-thread-1] - running... 
21:40:59.143 c.TestTimer [pool-1-thread-1] - running... 
21:41:02.145 c.TestTimer [pool-1-thread-1] - running... 
21:41:05.147 c.TestTimer [pool-1-thread-1] - running... 

评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线
程也不会被释放。用来执行延迟或反复执行的任务

正确处理执行任务异常

方法1:主动捉异常
ExecutorService pool = Executors.newFixedThreadPool(1);pool.submit(() -> {try {log.debug("task1");int i = 1 / 0;} catch (Exception e) {log.error("error:", e);}});

输出

21:59:04.558 c.TestTimer [pool-1-thread-1] - task1 
21:59:04.562 c.TestTimer [pool-1-thread-1] - error: 
java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 
方法2:使用 Future
ExecutorService pool = Executors.newFixedThreadPool(1);Future<Boolean> f = pool.submit(() -> {log.debug("task1");int i = 1 / 0;return true;});log.debug("result:{}", f.get());

输出

21:54:58.208 c.TestTimer [pool-1-thread-1] - task1 
Exception in thread "main" java.util.concurrent.ExecutionException: 
java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.util.concurrent.FutureTask.get(FutureTask.java:192) at cn.itcast.n8.TestTimer.main(TestTimer.java:31) 
Caused by: java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 

任务调度线程池模拟定时执行任务

如何让每周四 18:00:00 定时执行任务?

// 获得当前时间LocalDateTime now = LocalDateTime.now();// 获取本周四 18:00:00.000LocalDateTime thursday =now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);// 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000if(now.compareTo(thursday) >= 0) {thursday = thursday.plusWeeks(1);}// 计算时间差,即延时执行时间long initialDelay = Duration.between(now, thursday).toMillis();// 计算间隔时间,即 1 周的毫秒值long oneWeek = 7 * 24 * 3600 * 1000;ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);System.out.println("开始时间:" + new Date());executor.scheduleAtFixedRate(() -> {System.out.println("执行时间:" + new Date());}, initialDelay, oneWeek, TimeUnit.MILLISECONDS);

Tomcat线程池

Tomcat 在哪里用到了线程池呢?

在这里插入图片描述

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】,也是tomcat实现高并发的保障

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

如果总线程数达到 maximumPoolSize,这时不会立刻抛 RejectedExecutionException 异常,而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常。

tomcat 7 部分源码(后续版本在该模块改动并不大)

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("Queue capacity is full.");}} catch (InterruptedException x) {submittedCount.decrementAndGet();Thread.interrupted();throw new RejectedExecutionException(x);}} else {submittedCount.decrementAndGet();throw rx;}}}

一旦超过了最大核心数了,父类的实现就会抛执行异常。

子类使用 try catch捕获到

首先拿到线程池中的阻塞队列,尝试将这个任务再次加入到队列中,如果这次成功了就不抛异常了。

TaskQueue.java

public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {if ( parent.isShutdown() )throw new RejectedExecutionException("Executor not running, can't force a command into the queue");return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected}

Fork/Join

概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型
运算

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计
算,如归并排序、斐波那契数列、都可以用分治思想进行求解

Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运
算效率

Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

class AddTask1 extends RecursiveTask<Integer> {int n;public AddTask1(int n) {this.n = n;}@Overridepublic String toString() {return "{" + n + '}';}@Overrideprotected Integer compute() {// 如果 n 已经为 1,可以求得结果了if (n == 1) {log.debug("join() {}", n);return n;}// 将任务进行拆分(fork)AddTask1 t1 = new AddTask1(n - 1);t1.fork();log.debug("fork() {} + {}", n, t1);// 合并(join)结果int result = n + t1.join();log.debug("join() {} + {} = {}", n, t1, result);return result;}
}

然后提交给 ForkJoinPool 来执行

public static void main(String[] args) {ForkJoinPool pool = new ForkJoinPool(4);System.out.println(pool.invoke(new AddTask1(5)));
}

结果

[ForkJoinPool-1-worker-0] - fork() 2 + {1} 
[ForkJoinPool-1-worker-1] - fork() 5 + {4} 
[ForkJoinPool-1-worker-0] - join() 1 
[ForkJoinPool-1-worker-0] - join() 2 + {1} = 3 
[ForkJoinPool-1-worker-2] - fork() 4 + {3} 
[ForkJoinPool-1-worker-3] - fork() 3 + {2} 
[ForkJoinPool-1-worker-3] - join() 3 + {2} = 6 
[ForkJoinPool-1-worker-2] - join() 4 + {3} = 10 
[ForkJoinPool-1-worker-1] - join() 5 + {4} = 15 
15 

用图来表示

在这里插入图片描述

改进

class AddTask3 extends RecursiveTask<Integer> {int begin;int end;public AddTask3(int begin, int end) {this.begin = begin;this.end = end;}@Overridepublic String toString() {return "{" + begin + "," + end + '}';}@Overrideprotected Integer compute() {// 5, 5if (begin == end) {log.debug("join() {}", begin);return begin;}// 4, 5if (end - begin == 1) {log.debug("join() {} + {} = {}", begin, end, end + begin);return end + begin;}// 1 5int mid = (end + begin) / 2; // 3AddTask3 t1 = new AddTask3(begin, mid); // 1,3t1.fork();AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5t2.fork();log.debug("fork() {} + {} = ?", t1, t2);int result = t1.join() + t2.join();log.debug("join() {} + {} = {}", t1, t2, result);return result;}
}

然后提交给 ForkJoinPool 来执行

public static void main(String[] args) {ForkJoinPool pool = new ForkJoinPool(4);System.out.println(pool.invoke(new AddTask3(1, 5)));
}

结果

[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3 
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9 
[ForkJoinPool-1-worker-0] - join() 3 
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ? 
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ? 
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6 
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15 
15

用图来表示

在这里插入图片描述

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

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

相关文章

从NetSuite Payment Link杂谈财务自动化、数字化转型

最近在进行信息化的理论学习&#xff0c;让我有机会跳开软件功能&#xff0c;用更加宏大的视野&#xff0c;来审视我们在哪里&#xff0c;我们要到哪去。 在过去20多年&#xff0c;我们的财务软件经历了电算化、网络化、目前处于自动化、智能化阶段。从NetSuite这几年的功能发…

【Effective Modern C++】条款2:理解auto类型推导

条款2&#xff1a;理解auto类型推导 条款1中&#xff0c;模板类型推导的函数模板形如&#xff1a; template<typename T> void f(ParamType param);当变量采用auto声明时&#xff0c;auto扮演了模板中的T这个角色&#xff0c;而变量的类型扮演的是ParamType的角色。 条…

Django实战项目-学习任务系统-查询列表分页显示

接着上期代码框架&#xff0c;6个主要功能基本实现&#xff0c;剩下的就是细节点的完善优化了。 接着优化查询列表分页显示功能&#xff0c;有很多菜单功能都有查询列表显示页面情况&#xff0c;如果数据量多&#xff0c;不分页显示的话&#xff0c;页面展示效果就不太好。 本…

Spring AOP 简介

一、Spring AOP AOP 是一种思想&#xff0c;而 Spring AOP 是一个框架&#xff0c;提供了一种对 AOP 思想的实现。 1、什么是 AOP&#xff1f; AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff1a;是一种编程思想&#xff0c;表示面向切面编程。指的是对某…

python栈_简单算术表达式_加减乘除

# 从左到右遍历中缀表达式中的每个数字和符号&#xff0c;若是数字就输出&#xff0c;即成为后缀表达式的一部分&#xff1b;若是符号则要分为两种情况&#xff1a; # (1)是括号时&#xff0c;如果是左括号&#xff0c;直接将左括号入栈&#xff0c;如果是右括号则栈顶元素依次…

插件_日期_lunar-calendar公历农历转换

现在存在某需求&#xff0c;需要将公历、农历日期进行相互转换&#xff0c;在此借助lunar-calendar插件完成。 下载 [1] 通过npm安装 npm install lunar-calendar[2]通过文件方式引入 <script type"text/javascript" src"lib/LunarCalendar.min.js">…

React 项目结构小结

React 项目结构小结 简单的记录一下目前 React 项目用的依赖和实现 摸索了大半年了大概构建一套用起来还算轻松的体系……&#xff1f;基本上应该是说可以应对大部分的项目了 使用的依赖 目前项目还在 refactoring 的阶段&#xff0c;所以乱得很&#xff0c;这里是新建一个…

有什么好用的CRM客户管理系统?推荐这5大高口碑的CRM系统!

有什么好用的CRM客户管理系统&#xff1f;推荐这5大高口碑的CRM系统&#xff01; 好用的CRM客户管理系统&#xff1a; ①需要进行精细化管理 ②需要专业的管理工具 ③最好能够做到和erp系统和oa系统的集成 授人以鱼不如授人以渔&#xff0c;在给题主推荐crm系统之前&#…

高数笔记06:无穷级数

图源&#xff1a;文心一言 时间比较紧张&#xff0c;仅导图~~&#x1f95d;&#x1f95d; 第1版&#xff1a;查资料、画导图~&#x1f9e9;&#x1f9e9; 参考资料&#xff1a;《高等数学 基础篇》武忠祥 &#x1f433;目录 &#x1f433;常数项级数 &#x1f40b;概要 &…

C++基础——对于C语言缺点的补充(2)

上篇文章中说到&#xff0c;为了解决C语言会出现人为定义的函数和库函数出现重定义的错误&#xff0c;C引入了一个新的概念&#xff0c;即命名空间&#xff0c;通过认为定义命名空间&#xff0c;来解决上述问题。 在本篇文章中&#xff0c;将继续介绍C相对于C语言不足来进行的补…

Harbor企业级Registry基础镜像仓库的详细安装使用教程(保姆级)

Harbor Docker 官方提供的私有仓库 registry&#xff0c;用起来虽然简单 &#xff0c;但在管理的功能上存在不足。 Harbor是vmware一个用于存储和分发Docker镜像的企业级Registry服务器&#xff0c;harbor使用的是官方的docker registry(v2命名是distribution)服务去完成。 ha…

数据结构之堆的实现(图解➕源代码)

一、堆的定义 首先明确堆是一种特殊的完全二叉树&#xff0c;分为大根堆和小根堆&#xff0c;接下来我们就分别介绍一下这两种不同的堆。 1.1 大根堆&#xff08;简称&#xff1a;大堆&#xff09; 在大堆里面&#xff1a;父节点的值 ≥ 孩子节点的值 我们的兄弟节点没有限制&…

5 ip的分配

如上一节所述&#xff0c;需要和其他设备通信&#xff0c;那么需要先配置ip. 1、如何配置ip 1.可以使用 ifconfig&#xff0c;也可以使用 ip addr 2.设置好了以后&#xff0c;用这两个命令&#xff0c;将网卡 up 一下&#xff0c;就可以了 //---------------------------- 使…

AI大模型时代网络安全攻防对抗升级,瑞数信息变革“下一代应用与数据安全”

AI与大模型技术加速普及&#xff0c;安全领域也在以创新视角聚焦下一代应用安全WAAP变革&#xff0c;拓展新一代数据安全领域。近日瑞数信息重磅发布了瑞数全新API扫描器、API安全审计、数据安全检测与应急响应系统及分布式数据库备份系统四大新品。此次发布在延续瑞数信息Bot自…

freertos简单串口

先来完善一下FreeRTOSConfig.h这个配置文件 /*FreeRTOS V9.0.0 - Copyright (C) 2016 Real Time Engineers Ltd.All rights reservedVISIT http://www.FreeRTOS.org TO ENSURE YOU ARE USING THE LATEST VERSION.This file is part of the FreeRTOS distribution.FreeRTOS is …

通信原理板块——时域均衡

微信公众号上线&#xff0c;搜索公众号小灰灰的FPGA,关注可获取相关源码&#xff0c;定期更新有关FPGA的项目以及开源项目源码&#xff0c;包括但不限于各类检测芯片驱动、低速接口驱动、高速接口驱动、数据信号处理、图像处理以及AXI总线等 1、均衡器 为了减小码间串扰的影响…

放卷开环张力控制(伺服转矩模式应用)

收放卷张力开环闭环控制算法,请参考下面文章链接: PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-CSDN博客文章浏览阅读4k次,点赞3次,收藏3次。里工业控制张力控制无处不在,也衍生出很多张力控制专用控制器,磁粉制动器等,本篇博客主要讨论PLC的张力控制相关应用和算…

Scala和Play WS库编写的爬虫程序

使用Scala和Play WS库编写的爬虫程序&#xff0c;该程序将爬取网页内容&#xff1a; import play.api.libs.ws._ import scala.concurrent.ExecutionContext.Implicits.global ​ object BaiduCrawler {def main(args: Array[String]): Unit {val url ""val proxy…

06.Oracle数据备份与恢复

Oracle数据备份与恢复 一、通过RMAN方式备份二、使用emp/imp和expdb/impdb工具进行备份和恢复三、使用Data guard进行备份与恢复 一、通过RMAN方式备份 通过 RMAN&#xff08;Oracle 数据库备份和恢复管理器&#xff09;方式备份 Oracle 数据库&#xff0c;可以使用以下步骤&a…

【微服务】mysql + elasticsearch数据双写设计与实现

目录 一、前言 二、为什么使用mysqles双写 2.1 单用mysql的问题 2.2 为什么不直接使用es 2.2.1 非关系型表达 2.2.2 不支持事务 2.2.3 多字段将造成性能低下 三、mysqles双写方案设计要点 3.1 全新设计 VS 中途调整架构 3.2 全表映射 VS 关键字段存储 3.2.1 最大程度…