线程池
线程是一个重量级的对象,应该避免频繁创建和销毁。
线程池是一种生产者 - 消费者模式,目的:防止频繁创建线程和销毁线程带来的性能开销。
Java 提供的线程池相关的工具类中,最核心的是ThreadPoolExecutor
- corePoolSize:表示线程池保有的最小线程数。
- maximumPoolSize:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
- keepAliveTime & unit:一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
- workQueue:工作队列
- threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
- handler:自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。
- CallerRunsPolicy:提交任务的线程自己去执行该任务。
- AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
- DiscardPolicy:直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走。
使用借助了 Jodd 类库的 ThreadFactoryBuilder 方法来构造一个线程工厂,实现线程池线程的自定义命名
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
2, 5,
5, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").get(),
new RejectedExecutionHandler(){
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
try {
// 队列满了就让放入入任务的线程等待,直到队列没满
while (!e.isShutdown() && e.getQueue().size()==10){
log.info("处理不过来了,暂停1秒...");
TimeUnit.SECONDS.sleep(1);
}
e.getQueue().add(r);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
});
// 创建线程池,不借助工具类,指定线程名字
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 0,
TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10), generateThreadFactory("test-%d"),
new ThreadPoolExecutor.AbortPolicy());
private static ThreadFactory generateThreadFactory(String nameFormat){
AtomicLong count = new AtomicLong(0L);
return (runnable) -> {
Thread thread = new Thread(runnable);
thread.setName(String.format(nameFormat, count.getAndIncrement()));
return thread;
};
}
// 打印线程池状态 ,每秒输出一次线程池的基本内部信息,包括线程数、活跃线程数、完成了多少任务,以及队列中还有多少积压任务等信息
private void printStats(ThreadPoolExecutor threadPool) {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
log.info("=========================");
log.info("Pool Size: {}", threadPool.getPoolSize());
log.info("Active Threads: {}", threadPool.getActiveCount());
log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());
log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
log.info("=========================");
}, 0, 1, TimeUnit.SECONDS);
}
// 允许核心线程回收
threadPool.allowCoreThreadTimeOut(true);
线程池工作原理:
1,将任务提交到队列
2,线程从队列获取任务,执行
3,如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
4,如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
5,如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
6,所以,任务提交时,判断的顺序为线程小于 corePoolSize,创建新的线程来处理,等于corePoolSize,将任务提交到 workQueue(工作队列),工作队列满了, 增加线程到 maximumPoolSize。 工作队列满了,最大线程满了,就执行
注意:
1,任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题。
2,不建议使用 Executors 的最重要的原因是:Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。
3,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止。所以对业务逻辑建议要进行try catch处理。
4,优化:声明线程池后立即调用 prestartAllCoreThreads 方法,来启动所有核心线程;传入 true 给 allowCoreThreadTimeOut 方法,来让线程池在空闲的时候同样回收核心线程。
思考:Java 线程池是先用工作队列来存放来不及处理的任务,满了之后再扩容线程池。当我们的工作队列设置得很大时,最大线程数这个参数显得没有意义,因为队列很难满,或者到满的时候再去扩容线程池已经于事无补了。
如何使线程池,线程数达到corePoolSize时,先增加线程,达到maximumPoolSize,再往队列加入任务呢?
1,Tomcat 线程池对这方面进行了优化可以参考
(1)https://github.com/apache/tomcat/blob/a801409b37294c3f3dd5590453fb9580d7e33af2/java/org/apache/tomcat/util/threads/ThreadPoolExecutor.java
(2)https://segmentfault.com/a/1190000023038647
2,自己写思路(1)重写队列的 offer 方法,造成这个队列已满的假象 (2)达到了最大线程后势必会触发拒绝策略,实现一个自定义的拒绝策略处理程序, 把任务真正插入队列。
线程池关闭:https://www.cnblogs.com/qingquanzi/p/9018627.html
线程中断
当我们调用线程的interrupt方法,它有两个作用:
1、如果此线程处于阻塞状态(比如调用了wait方法,io等待),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。
2、如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自己是否被中断,并做退出操作。
线程池提供了两个关闭方法
shutdownNow:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。将队列里还没有执行的任务放到列表里,返回给调用方。
- shutdownNow方法的执行逻辑:将线程池状态修改为STOP,然后调用线程池里的所有线程的interrupt方法。将队列里还没有执行的任务放到列表里,返回给调用方。
- 因为调用了中断interrupt方法,线程如果处于阻塞状态,就抛出异常,被捕获。由于STOP状态值是大于SHUTDOWN状态,STOP也大于等于STOP,不管任务队列是否为空,都会进入if语句从而返回null,线程退出。
shutdown:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。
- 是将线程池的状态修改为SHUTDOWN状态,然后调用interruptIdleWorkers方法,来中断空闲的线程。
- if (!t.isInterrupted() && w.tryLock()) 运行在w.lock和w.unlock之间的线程将因为加锁失败,而不会被调用interrupt方法,换句话说,就是正在执行线程池里任务的线程不会被中断。
- 不管是被调用了interrupt的线程还是没被调用的线程,什么时候退出呢?,这就要看getTask方法的返回是否为null了。
- 由于线程池被shutdown方法修改为SHUTDOWN状态,SHUTDOWN大于等于SHUTDOWN成立没问题,但是SHUTDOWN不在大于等于STOP状态,所以只有队列为空,getTask方法才会返回null,导致线程退出。
调用完shutdownNow和shuwdown方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。
线程池里的线程在什么情况下才会彻底退出?
1,程序出异常。
2,task = null && getTask()方法返回null, 线程退出
小结:
1,使用shutdownNow,如果线程池里的任务处于阻塞状态,则会导致报错(如果任务里没有捕获InterruptedException异常),如果线程daemon=false , 主线程报错,线程池没有关闭。
2,使用shuwdown方法关闭线程池时,一定要确保任务里不会有永久阻塞等待的逻辑,否则线程池就关闭不了。
3,shutdownNow和shuwdown调用完,线程池并不是立马就关闭了,要想等待线程池关闭,还需调用awaitTermination方法来阻塞等待。
4,注意线程的daemon属性,为true时,主线程执行完,线程池会自动关闭。为false时,主线程执行完,线程池线程仍在,需要手动关闭线程池。
本文暂时没有评论,来添加一个吧(●'◡'●)