概述
为什么会有线程池?线程池的产生背景?
线程是一种系统资源,每创建一个新的线程都要占用一定的内存(分配栈内存);
倘若在高并发的情况下一下子来了很多的请求任务,要是为每一个任务都创建一个新的线程,这对内存的占用是相当大的,还有可能出现栈内存溢出,这是一个问题。
而且并不是线程越多越好,CPU也就那几个,一下子创建这么多线程CPU也忙不过来,这就导致一部分线程由于获取不到CPU时间片而进行阻塞,引起上下文切换。上下文切换地越频繁对程序性能的影响越大,尤其是高并发下。
所以基于以上两个原因,我们应该充分利用已有的线程,充分发挥它们的作用,而不是每次都创建新线程(享元模式的体现),所以线程池就出现了。
- 池化技术有很多, 比如线程池、数据库连接池、HTTP连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
- 《Java 并发编程的艺术》 提到的来说一下使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。(创建的线程,实际最后要和操作系统的线程做映射,很消耗资源)
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 .
ThreadPoolExecutor
那我们应该如何创建一个线程池呢 ? Java中已经提供了创建线程池的一个接口:Executor
而我们一般使用它的子类:ThreadPoolExecutor。
线程池状态
- ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
- 从数字上比较(第一位是符号位),
TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
线程池状态和线程池中线程的数量 由一个原子整型ctl来共同表示
- 这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值 同时更改两个属性的值。
七参构造方法
ThreadPoolExecutor最全的构造方法
从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。
- corePoolSize参数一: 线程池核心线程数
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。
- maximumPoolSize参数二:线程池最大线程数
- maximumPoolSize - corePoolSize = 救急线程数
- 注意 : 救急线程在没有空闲的核心线程和任务队列满了的情况才使用救急线程
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
- keepAliveTime参数三:救急线程?(空闲还是所有线程都是如此)?空闲时的最大生存时间 (核心线程可以一直运行)
unit参数四:存活时间单位 (针对救急线程),是keepAliveTime的计量单位
workQueue参数五:阻塞队列或工作队列(存放任务) 。新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
- 有界阻塞队列 ArrayBlockingQueue基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
- 无界阻塞队列 LinkedBlockingQueue基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
- 最多只有一个同步元素的 SynchronousQueue一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
- 优先队列 PriorityBlockingQueue具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
- threadFactory参数六:线程工厂(也给线程取名字),创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
- handler参数七:拒绝策略。当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
虽然jdk提供了中拒绝策略,但是很多第三方框架都不使用jdk自带的拒绝策略,而是选择了在自己的需求上进行扩展,比如以下实现:
- Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便位问题
- Netty的实现,是创建一个新线程来执行任务
- ActiveMQ的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 ,在等待的过程中有新线程空闲,就可以完成添加任务。
- PinPoint的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略。
工作方式
jdk中线程划分为核心线程与救急线程,它们都是懒加载方式,只有在调用的时候才会创建。当一个任务传给线程池以后,可能有以下几种可能
- 核心线程有空闲时将任务分配给一个核心线程来执行
- 核心线程都在执行任务,将任务放到阻塞队列workQueue中等待被执行
- 阻塞队列满了,使用救急线程来执行任务(有界队列才会使用救急线程)
- 救急线程用完以后,超过生存时间(keepAliveTime)后会被释放
- 任务总数大于了最大线程数(maximumPoolSize)与阻塞队列容量的最大值(workQueue.capacity),就会使用拒接策略(即救急线程都用上了,已经接收不了那么多的任务了,拒绝接收之后的任务)
- 最大线程数=核心线程数+救急线程数
- 救急线程跟核心线程的区别是救急线程有生存时间,在救急线程执行完任务之后,过一段时间也没有新任务就会停止执行;而核心线程执行完任务之后仍然会保留在线程池中;
线程池工厂方法
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。与ThreadPoolExecutor有关的工厂方法 如下:
1、newFixedThreadPool()
- 传入两个参数创建一个固定大小的线程池
- 核心线程数:nThreads
- 线程工厂:threadFactory
- 这个是Executors类提供的工厂方法来创建线程池!Executors 是Executor 框架的工具类!
内部实现
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
从它的内部实现中我们可以看出该方法有如下特点:
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
因为ThreadPoolExecutor构造方法的七个参数中,前两个代表核心线程数以及最大线程数,这里的newFixedThreadPool方法中核心线程数以及最大线程数都是nThreads,所以它的核心线程数 == 最大线程数。
阻塞队列是无界的,可以放任意数量的任务
因为它在创建队列的时候使用了new LinkedBlockingQueue< Runnable>() ,并没有指定队列的大小,所以是无限大的(会扩容)
- 使用场景:适用于任务量已知,相对耗时的任务
使用示例:
public class TestFixedThreadPool {
public static void main(String[] args) {
// 自定义线程工厂
ThreadFactory factory = new ThreadFactory() {
AtomicInteger atomicInteger = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "myThread_" + atomicInteger.getAndIncrement());
}
};
// 创建核心线程数量为2的线程池
// 通过 ThreadFactory可以给线程添加名字
ExecutorService executorService = Executors.newFixedThreadPool(2, factory);
// 任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println("this is fixedThreadPool");
}
};
executorService.execute(runnable);
}
}
因为它使用的是核心线程执行任务,所以在执行完任务之后仍然留在线程池中;
2、newCachedThreadPool
创建一个带缓冲功能的线程池,内部实现也是调用了ThreadPoolExecutor的构造方法实现
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
同样我们可以知道如下特点:
这个线程池没有核心线程,最大的线程数为Integer.MAX_VALUE,所有创建的线程都是救急线程 (可以无限创建,不超过整数范围),空闲时生存时间为60秒
- 阻塞队列使用的是SynchronousQueue同步队列,SynchronousQueue是一种特殊的队列
- 没有容量,没有线程来取任务的时候取是任务放不进去的()
- 只有当线程取任务时,才会将任务放入该阻塞队列中(类似一手交钱一手交货)
- 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。
- 适合任务数比较密集,但每个任务执行时间较短的情况
3、newSingleThreadExecutor
该线程池内只有一个线程;
使用场景是:public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
希望多个任务排队执行。把线程数固定为 1,当任务数多于 1 时,任务会放入无界队列进行排队。 就算所有的任务执行完毕,这唯一的线程也不会被释放。
跟普通的单个线程的区别:
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施;而newSingleThreadExecutor线程池还会新建一个线程取代之前被终止的线程,保证池的正常工作,即Executors.newSingleThreadExecutor()创建的线程池中线程个数始终为1,不能修改。
和创建固定大小线程池传参为1的Executors.newFixedThreadPool(1) 的线程池区别:
相同点是:Executors.newFixedThreadPool(1) 初始时为1,而且它也有当该线程因为任务执行失败而终止时,会创建一个新线程取代之前被终止的线程,保证池的正常工作。
区别点是:固定线程池是直接返回一个ThreadPoolExecutor 构造方法创建的对象,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改核心线程数之类的属性。
而单线程线程池并不是直接返回,而是在外层进行了包装,使用到了装饰器模式FinalizableDelegatedExecutorService,只对外暴露了 ExecutorService 接口,因此不能通过返回的对象调用 ThreadPoolExecutor 中特有的方法,只能调用ExecutorService 接口中的方法
执行/提交任务 execute/submit
接下来看一下线程池中有关提交任务的方法;如下所示#
//1 执行任务
void execute(Runnable command);
//2 提交任务 task,用返回值 Future 获得任务执行结果,Future的原理就是利用我们之前讲到的保护性暂停模式来接受返回结果的,主线程可以执行 FutureTask.get()方法来等待任务执行完成
<T> Future<T> submit(Callable<T> task);
//3 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
//4 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException;
//5 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
//6 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
1、execute()方法
Future的原理就是利用我们之前讲到的保护性暂停模式来接受返回结果的,主线程可以执行 FutureTask.get()方法来等待任务执行完成。
演示:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 通过submit执行Callable中的call方法
// 通过Future来捕获返回值
Future<String> future = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "hello submit";
}
});
// 主线程中可以使用future查看捕获的返回值
System.out.println(future.get());
}
因为这里的Callable接口中只有一个接口方法,所以我们可以用拉姆达表达式来简化它。
3、两个invokeAll方法
4、两个invokeAny方法
提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,区别是是否带超时时间。
关闭线程池相关方法
shutdown()方法
- 将线程池的状态改为 SHUTDOWN
-
shutdownNow()
将线程池的状态改为 STOP
- 不再接受新任务,也不会在执行阻塞队列中的任务
- 会将阻塞队列中未执行的任务返回给调用者
- 并用 interrupt 的方式中断正在执行的任务
其他
```java // 不在 RUNNING 状态的线程池,此方法就返回 true boolean isShutdown();
// 线程池状态是否是 TERMINATED boolean isTerminated();
// 调用 shutdown 后,由于调用使线程结束线程的方法是异步的并不会等待所有任务运行结束就返回,因此如果它想在线程池 TERMINATED 后做些其它事情,可以利用此方法等待 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
<a name="43d1b3ee"></a>
### 异步模式之工作线程模式
工作线程模式多线程设计模式之一;
<a name="f8252817"></a>
#### **定义**
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务,也可以将其归类为**分工模式,**它的典型实现就是线程池,也体现了经典设计模式中的**享元模式。**<br />例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)
`**注意:**` **不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率**
- 例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工
<a name="62185e98"></a>
#### 什么是饥饿
这里的饥饿是指线程数量不足导致的饥饿;<br />而固定大小的线程池就会有饥饿现象产生:<br />两个工人是**同一个线程池中的两个线程**,他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
- 客人点餐:必须先点完餐,等菜做好,才能上菜,假定在此期间处理点餐的工人必须等待
- 后厨做菜:没啥说的,做就是了
比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好;<br />但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了(两个工人都可以点餐以及做菜),这时没人做饭了,线程数量不足了,就产生了饥饿现象;
**这个不是死锁!!!**而是由于线程数量不足导致程序无法执行。<br />**解决方法**可以增加线程池的大小,**不过不是根本解决方案,** **还是前面提到的,不同的任务类型,采用不同的线程池 。**
@Slf4j(topic = “guizy.TestDeadLock”) public class TestStarvation {
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();
}
});
}
}
<a name="7cce3541"></a>
#### `线程池中线程设置多少为好?`
**线程池过小会导致程序不能充分地利用系统资源、容易导致饥饿,过大会导致更多的线程上下文切换,占用更多内存**。<br />下面是两种不同类型的应用程序创建线程数量的原则:
<a name="9aab2968"></a>
##### CPU 密集型运算
应用程序主要用来做数据分析以及计算,这个时候线程的数量通常采用 cpu 核数 + 1 的大小,就能够实现最优的 CPU 利用率,+1 是保证当线程由于**页缺失故障(操作系统)或其它原因导致暂停时**,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费。
<a name="2bcf56a4"></a>
##### I/O 密集型运算
CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,这个时候可以利用多线程提高CPU它的利用率。<br />**经验公式如下**:<br />`线程数 =核数*期望CPU利用率*总时间(CPU计算时间+等待时间) / CPU 计算时间`<br />时间占比就可以利用一些分析以及监控工具来得到。<br />例如 4 核 CPU, 计算时间是 50%(使用CPU时间) ,其它等待时间是 50%,期望 cpu 被 100% 利用,则套用公式有<br />4 _ 100% _ 100% / 50% = 8<br />例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式<br />4 _ 100% _ 100% / 10% = 40
<a name="e4ba7a32"></a>
#### 任务调度线程池 ScheduledExecutorService (`重点`)
- 在『任务调度线程池』功能加入之前,可以使用`java.util.Timer`来实现定时功能,Timer 的优点在于简单易用,**但由于所有任务都是由**`**同一个线程**`**来调度,因此所有任务都是**`**串行**`**执行的**,同一时间只能有一个任务在执行,**前一个任务的延迟或异常都将会影响到之后的任务。**
- 我们可以用任务调度线程池实现定时执行任务以及反复执行任务的效果。
**使用Timer**
@Slf4j(topic = “guizy.TestTimer”) public class TestTimer { public static void main(String[] args) { Timer timer = new Timer(); TimerTask task1 = new TimerTask() { @Override public void run() { log.debug(“task 1”); Sleeper.sleep(2); } };
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
// 使用timer添加两个任务, 希望他们都在1s后执行
// 由于timer内只有一个线程来执行队列中的任务, 所以task2必须等待task1执行完成才能执行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
} 结果 08:21:17.548 guizy.TestTimer [Timer-0] - task 1 08:21:19.553 guizy.TestTimer [Timer-0] - task 2
<a name="d6b8eef6"></a>
##### **使用 ScheduledExecutorService 任务调度器**
public class TestTimer { public static void main(String[] args) { //Scheduled表示有调度功能的线程池 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); //如果线程数为1还是会串行执行 //schedule方法参数:第二个参数是延迟的时间 executor.schedule(() -> System.out.println(“任务1, 执行时间:” + new Date()), 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> System.out.println("任务2, 执行时间:" + new Date()), 1000, TimeUnit.MILLISECONDS);
}
} //即使其中的任务出现异常也不会影响其他任务的正常执行 任务1, 执行时间:Sun Jan 03 08:53:54 CST 2021 任务2, 执行时间:Sun Jan 03 08:53:54 CST 2021
- **ScheduledExecutorService 中 scheduleAtFixedRate方法的使用**
- 可以使用该方法定时执行任务
public class TestTimer { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); log.debug(“start….”); // 延迟1s后, 按1s的速率打印running executor.scheduleAtFixedRate(() -> log.debug(“running”), 1, 1, TimeUnit.SECONDS); } }
08:51:59.930 guizy.TestTimer [main] - start…. 08:52:01.050 guizy.TestTimer [pool-1-thread-1] - running 08:52:02.049 guizy.TestTimer [pool-1-thread-1] - running 08:52:03.045 guizy.TestTimer [pool-1-thread-1] - running 08:52:04.046 guizy.TestTimer [pool-1-thread-1] - running 08:52:05.045 guizy.TestTimer [pool-1-thread-1] - running 08:52:06.047 guizy.TestTimer [pool-1-thread-1] - running
public class TestTimer { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); log.debug(“start….”); // 延迟1s后, 按1s的速率打印running executor.scheduleAtFixedRate(() -> { log.debug(“running”); Sleeper.sleep(2); }, 1, 1, TimeUnit.SECONDS); } }
// 睡眠时间 > 速率, 按睡眠时间打印 08:54:58.567 guizy.TestTimer [main] - start…. 08:54:59.675 guizy.TestTimer [pool-1-thread-1] - running 08:55:01.684 guizy.TestTimer [pool-1-thread-1] - running 08:55:03.685 guizy.TestTimer [pool-1-thread-1] - running 08:55:05.690 guizy.TestTimer [pool-1-thread-1] - running
- ScheduledExecutorService 中scheduleWithFixedDelay方法的使用
- 睡眠时间 + 速率时间, 为打印的间隔时间
public class TestTimer { public static void main(String[] args) { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); log.debug(“start….”); // 延迟1s后, 按1s的速率打印running executor.scheduleWithFixedDelay(() -> { log.debug(“running”); Sleeper.sleep(2); }, 1, 1, TimeUnit.SECONDS); } }
08:56:22.581 guizy.TestTimer [main] - start…. 08:56:23.674 guizy.TestTimer [pool-1-thread-1] - running 08:56:26.679 guizy.TestTimer [pool-1-thread-1] - running 08:56:29.680 guizy.TestTimer [pool-1-thread-1] - running 08:56:32.689 guizy.TestTimer [pool-1-thread-1] - running
- 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务。
问: 如何让每周四 18:00:00 定时执行任务?
public class TestSchedule {
// 如何让每周四 18:00:00 定时执行任务?
public static void main(String[] args) {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
// 获取周四时间
LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
// 如果 当前时间 > 本周周四,必须找到下周周四
if(now.compareTo(time) > 0) {
time = time.plusWeeks(1);
}
System.out.println(time);
// initailDelay 代表当前时间和周四的时间差
// period 一周的间隔时间
long initailDelay = Duration.between(now, time).toMillis();
long period = 1000 * 60 * 60 * 24 * 7;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(() -> {
System.out.println("running...");
}, initailDelay, period, TimeUnit.MILLISECONDS);
}
}
<a name="GXFm8"></a>
##### 正确处理执行任务异常
- 可以发现,如果线程池中的线程执行任务时,**如果任务抛出了异常,默认是中断执行该任务而不是抛出异常或者打印异常信息。**
解决方法1:**主动捉异常**
ExecutorService pool = Executors.newFixedThreadPool(1); pool.submit(() -> { try { log.debug(“task1”); int i = 1 / 0; } catch (Exception e) { log.error(“error:”, e); } });
方法2:**使用 Future,错误信息都被封装进submit方法的返回方法中!**
ExecutorService pool = Executors.newFixedThreadPool(1);
Future