【Java】线程池梳理
【Java】线程池梳理
前言
线程池:本质上是一种对象池,用于管理线程资源。在任务执行前,需要从线程池中拿出线程来执行。在任务执行完成之后,需要把线程放回线程池。通过线程的这种反复利用机制,可以有效地避免直接创建线程所带来的坏处。
优点:1、降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存;2、提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行;3、提高线程的可管理性。线程不能无限制地创建,需要进行统一的分配、调优和监控。
缺点:1、频繁的线程创建和销毁会占用更多的CPU和内存;2、频繁的线程创建和销毁会对GC产生比较大的压力;3、线程太多,线程切换带来的开销将不可忽视;4、线程太少,多核CPU得不到充分利用,是一种浪费。
流程
- 判断核心线程池是否已满,如果不是,则创建线程执行任务;
- 如果核心线程池满了,判断队列是否满了,如果队列没满,将任务放在队列中;
- 如果队列满了,则判断线程池是否已满,如果没满,创建线程执行任务;
- 如果线程池也满了,则按照拒绝策略对任务进行处理。
方式
入门级例子
package cn.com.codingce.juc;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println("Thread id is " + Thread.currentThread().getId());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
在这个例子中,首先创建了一个固定长度为5的线程池。然后使用循环的方式往线程池中提交了10个任务,每个任务休眠1秒。在任务休眠之前,将任务所在的线程id进行打印输出。
Thread id is 11
Thread id is 13
Thread id is 12
Thread id is 15
Thread id is 14
Thread id is 11
Thread id is 13
Thread id is 15
Thread id is 14
Thread id is 12
Executors
Executors是一个线程池工厂,提供了很多的工厂方法。
// 创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor();
// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads);
// 创建带缓存的线程池
public static ExecutorService newCachedThreadPool();
// 创建定时调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 创建流式(fork-join)线程池
public static ExecutorService newWorkStealingPool();
newSingleThreadExecutor
创建一个单线程的线程池,若多个任务被提交到此线程池,那么会被缓存到队列(队列长度为Integer.MAX_VALUE ),可保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
private static void createSingleThreadPool() {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(new Date() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
output
Mon Jan 02 11:49:58 CST 2023 pool-1-thread-1 0
Mon Jan 02 11:50:00 CST 2023 pool-1-thread-1 1
Mon Jan 02 11:50:02 CST 2023 pool-1-thread-1 2
Mon Jan 02 11:50:04 CST 2023 pool-1-thread-1 3
Mon Jan 02 11:50:06 CST 2023 pool-1-thread-1 4
Mon Jan 02 11:50:08 CST 2023 pool-1-thread-1 5
Mon Jan 02 11:50:10 CST 2023 pool-1-thread-1 6
Mon Jan 02 11:50:12 CST 2023 pool-1-thread-1 7
Mon Jan 02 11:50:14 CST 2023 pool-1-thread-1 8
Mon Jan 02 11:50:16 CST 2023 pool-1-thread-1 9
因为只有一个线程,所以线程名均相同,且是每隔2秒按顺序输出的。
newFixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。和创建单一线程的线程池类似,只是可以并行处理任务的线程数更多一些。若多个任务被提交到此线程池,会有下面的处理过程。
- 如果线程的数量未达到指定数量,则创建线程来执行任务;
- 如果线程池的数量达到了指定数量,并且有线程是空闲的,则取出空闲线程执行任务;
- 如果没有线程是空闲的,则将任务缓存到队列(队列长度为
Integer.MAX_VALUE)。当线程空闲的时候,按照FIFO的方式进行处理
private static void createFixedThreadPool() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(new Date() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
output
Mon Jan 02 11:49:10 CST 2023 pool-1-thread-2 1
Mon Jan 02 11:49:10 CST 2023 pool-1-thread-1 0
Mon Jan 02 11:49:10 CST 2023 pool-1-thread-3 2
Mon Jan 02 11:49:12 CST 2023 pool-1-thread-2 3
Mon Jan 02 11:49:12 CST 2023 pool-1-thread-3 5
Mon Jan 02 11:49:12 CST 2023 pool-1-thread-1 4
Mon Jan 02 11:49:14 CST 2023 pool-1-thread-1 6
Mon Jan 02 11:49:14 CST 2023 pool-1-thread-2 7
Mon Jan 02 11:49:14 CST 2023 pool-1-thread-3 8
Mon Jan 02 11:49:16 CST 2023 pool-1-thread-2 9
因为线程池大小是固定的,这里设置的是3个线程,所以线程名只有3个。因为线程不足会进入队列等待线程空闲,所以日志间隔2秒输出。
newCachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。这种方式创建的线程池,核心线程池的长度为0,线程池最大长度为Integer.MAX_VALUE。由于本身使用SynchronousQueue作为等待队列的缘故,导致往队列里面每插入一个元素,必须等待另一个线程从这个队列删除一个元素。
private static void createCachedThreadPool() {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(new Date() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
output
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-8 7
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-3 2
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-1 0
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-5 4
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-9 8
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-6 5
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-2 1
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-4 3
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-7 6
Mon Jan 02 11:56:03 CST 2023 pool-1-thread-10 9
因为初始线程池没有线程,而线程不足会不断新建线程,所以线程名都是不一样的。
newScheduledThreadPool
创建一个周期性的线程池,支持定时及周期性执行任务。
private static void createScheduledThreadPool() {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println(new Date() + " 提交任务");
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.schedule(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(new Date() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 3, TimeUnit.SECONDS);
}
}
Output
Mon Jan 02 11:59:19 CST 2023 提交任务
Mon Jan 02 11:59:22 CST 2023 pool-1-thread-1 0
Mon Jan 02 11:59:22 CST 2023 pool-1-thread-2 1
Mon Jan 02 11:59:22 CST 2023 pool-1-thread-3 2
Mon Jan 02 11:59:24 CST 2023 pool-1-thread-2 4
Mon Jan 02 11:59:24 CST 2023 pool-1-thread-1 5
Mon Jan 02 11:59:24 CST 2023 pool-1-thread-3 3
Mon Jan 02 11:59:26 CST 2023 pool-1-thread-2 6
Mon Jan 02 11:59:26 CST 2023 pool-1-thread-1 8
Mon Jan 02 11:59:26 CST 2023 pool-1-thread-3 7
Mon Jan 02 11:59:28 CST 2023 pool-1-thread-3 9
因为设置了延迟3秒,所以提交后3秒才开始执行任务。因为这里设置核心线程数为3个,而线程不足会进入队列等待线程空闲,所以日志间隔2秒输出。
newWorkStealingPool(jdk1.8新增)
创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。
工作窃取概念(Work stealing):工作窃取不是什么 Java 独有的东西,.NET 的 TPL 库早就存在好几年了。所谓工作窃取,指的是闲置的线程去处理本不属于它的任务。每个处理器核,都有一个队列存储着需要完成的任务。对于多核的机器来说,当一个核对应的任务处理完毕后,就可以去帮助其他的核处理任务。
private static void createNewWorkStealingPool() {
ExecutorService forkJoin = Executors.newWorkStealingPool();
forkJoin.execute(() -> {
System.out.println("i====>" + 1 + " " + Thread.currentThread().getId());
});
forkJoin.execute(() -> {
System.out.println("i====>" + 2 + " " + Thread.currentThread().getId());
});
forkJoin.execute(() -> {
System.out.println("i====>" + 3 + " " + Thread.currentThread().getId());
});
forkJoin.execute(() -> {
System.out.println("i====>" + 4 + " " + Thread.currentThread().getId());
});
forkJoin.execute(() -> {
System.out.println("i====>" + 5 + " " + Thread.currentThread().getId());
});
}
output
i====>1 11
i====>2 11
i====>3 12
i====>4 12
i====>5 12
ThreadPoolExecutor
理论上,可以通过Executors来创建线程池,这种方式非常简单。但正是因为简单,所以限制了线程池的功能。比如:无长度限制的队列,可能因为任务堆积导致OOM,这是非常严重的bug,应尽可能地避免。怎么避免?归根结底,还是需要通过更底层的方式来创建线程池。
ThreadPoolExecutor提供了好几个构造方法,但是最底层的构造方法却只有一个。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
这个构造方法有7个参数,逐一来进行分析。
corePoolSize,线程池中的核心线程数;maximumPoolSize,线程池中的最大线程数;keepAliveTime,空闲时间,当线程池数量超过核心线程数时,多余的空闲线程存活的时间,即:这些线程多久被销毁;unit,空闲时间的单位,可以是毫秒、秒、分钟、小时和天,等等;workQueue,等待队列,线程池中的线程数超过核心线程数时,任务将放在等待队列,它是一个BlockingQueue类型的对象;ArrayBlockingQueue,队列是有界的,基于数组实现的阻塞队列;LinkedBlockingQueue,队列可以有界,也可以无界。基于链表实现的阻塞队列;SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将一直处于阻塞状态。该队列也是Executors.newCachedThreadPool()的默认队列;PriorityBlockingQueue,带优先级的无界阻塞队列。
threadFactory,线程工厂,可以使用它来创建一个线程;Executors的实现使用了默认的线程工厂-DefaultThreadFactory。它的实现主要用于创建一个线程,线程的名字为pool-{poolNum}-thread-{threadNum}。handler,拒绝策略,当线程池和等待队列都满了之后,需要通过该对象的回调函数进行回调处理。- AbortPolicy:丢弃任务,抛运行时RejectedExecutionException异常;
- CallerRunsPolicy:在调用者线程执行任务;
- DiscardPolicy:忽视,任务直接丢弃,什么都不会发生;
- DiscardOldestPolicy:从队列中踢出最先进入队列(最后一个执行)的任务(最旧的那个任务),再尝试执行当前任务。
线程池的执行规则如下:1、当线程数小于核心线程数时,创建线程;2、当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列;3、当线程数大于等于核心线程数,且任务队列已满。若线程数小于最大线程数,创建线程。若线程数等于最大线程数,抛出异常,拒绝任务。
private static void createThreadPool() {
ExecutorService executorService = new ThreadPoolExecutor(2, 10, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5, true), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 10; i++) {
final int index = i;
executorService.execute(() -> {
// 获取线程名称,默认格式:pool-1-thread-1
System.out.println(new Date() + " " + Thread.currentThread().getName() + " " + index);
// 等待2秒
try {
sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
output
Mon Jan 02 12:34:11 CST 2023 pool-1-thread-1 0
Mon Jan 02 12:34:11 CST 2023 pool-1-thread-2 1
Mon Jan 02 12:34:11 CST 2023 pool-1-thread-3 7
Mon Jan 02 12:34:11 CST 2023 pool-1-thread-5 9
Mon Jan 02 12:34:11 CST 2023 pool-1-thread-4 8
Mon Jan 02 12:34:13 CST 2023 pool-1-thread-4 2
Mon Jan 02 12:34:13 CST 2023 pool-1-thread-1 3
Mon Jan 02 12:34:13 CST 2023 pool-1-thread-2 4
Mon Jan 02 12:34:13 CST 2023 pool-1-thread-5 5
Mon Jan 02 12:34:13 CST 2023 pool-1-thread-3 6
因为核心线程数为2,队列大小为5,存活时间1分钟,所以流程是第0-1号任务来时,陆续创建2个线程,然后第2-6号任务来时,因为无线程可用,均进入了队列等待,第7-9号任务来时,没有空闲线程,队列也满了,所以陆续又创建了3个线程。所以你会发现7-9号任务反而是先执行的。又因为各任务只需要2秒,而线程存活时间有1分钟,所以线程进行了复用,所以总共只创建了5个线程。
如何正确配置线程池的参数:CPU密集型:corePoolSize = CPU核数 + 1;IO密集型:corePoolSize = CPU核数 * 2。
提交任务的几种方式:往线程池中提交任务,主要有两种方法,execute()和submit()。submit()用于提交一个需要返回果的任务。该方法返回一个Future对象,通过调用这个对象的get()方法,就能获得返回结果。get()方法会一直阻塞,直到返回结果返回。另外,也可以使用它的重载方法get(long timeout, TimeUnit unit),这个方法也会阻塞,但是在超时时间内仍然没有返回结果时,将抛出异常TimeoutException。
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Long> future = executor.submit(() -> {
System.out.println("task is executed");
return System.currentTimeMillis();
});
System.out.println("task execute time is: " + future.get());
}
output
task is executed
task execute time is: 1672634764296
线程池监控:
- ThreadPoolExecutor自带:
long getTaskCount():获取已经执行或正在执行的任务数;long getCompletedTaskCount():获取已经执行的任务数;int getLargestPoolSize():获取线程池曾经创建过的最大线程数,根据这个参数,可以知道线程池是否满过;int getPoolSize():获取线程池线程数;int getActiveCount():获取活跃线程数(正在执行任务的线程数)。
- ThreadPoolExecutor自定义处理:
protected void beforeExecute(Thread t, Runnable r):任务执行前被调用;protected void afterExecute(Runnable r, Throwable t):任务执行后被调用;protected void terminated():线程池结束后被调用。
关闭线程池:1、shutdown()会将线程池状态置为SHUTDOWN,不再接受新的任务,同时会等待线程池中已有的任务执行完成再结束;2、shutdownNow()会将线程池状态置为SHUTDOWN,对所有线程执行interrupt()操作,清空队列,并将队列中的任务返回回来。关闭线程池涉及到两个返回boolean的方法,isShutdown()和isTerminated,分别表示是否关闭和是否终止。
注意
- 尽量使用手动的方式创建线程池,避免使用
Executors工厂类; - 根据场景,合理设置线程池的各个参数,包括线程池数量、队列、线程工厂和拒绝策略;
- 在调线程池
submit()方法的时候,一定要尽量避免任务执行异常被吞掉的问题。
【Java】线程池梳理的更多相关文章
- Java线程池带图详解
线程池作为Java中一个重要的知识点,看了很多文章,在此以Java自带的线程池为例,记录分析一下.本文参考了Java并发编程:线程池的使用.Java线程池---addWorker方法解析.线程池.Th ...
- Java线程池详解
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- java线程池技术(二): 核心ThreadPoolExecutor介绍
版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程池技术属于比较"古老"而又比较基础的技术了,本篇博客主要作用是个人技术梳理,没什么新玩意. 一.Java线程池技术的 ...
- java线程池和中断总结
目录 java线程池和中断总结 一. 线程池的使用 二. java中断机制 中断的处理 三. 线程间通信机制总结 java线程池和中断总结 本系列文是对自己学习多线程和平时使用过程中的知识梳理,不适合 ...
- Java线程池详解(一)
一.线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候不是创建一个线程,而是从线程池里面获取一个可用的线程,然后执行我们的任务.线程池的关键在于它为我们管理了多 ...
- Java线程池原理解读
引言 引用自<阿里巴巴JAVA开发手册> [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销 ...
- Java线程池原理及分析
线程池是很常用的并发框架,几乎所有需要异步和并发处理任务的程序都可用到线程池. 使用线程池的好处如下: 降低资源消耗:可重复利用已创建的线程池,降低创建和销毁带来的消耗: 提高响应速度:任务到达时,可 ...
- Java 线程池框架核心代码分析--转
原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...
- Java线程池使用说明
Java线程池使用说明 转自:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极 ...
- (转载)JAVA线程池管理
平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...
随机推荐
- Chrony时间同步服务
概: 网络时间协议(Network Time Protocol,NTP)是用于网络时间同步的协议.提供NTP时间同步服务的软件有很多,这里采用Chrony软件来实现时间同步 chrony 的优势: ...
- 每日算法3:随机生成五个不同整数,将数字转换为RMB格式
随机生成五个不同整数 点击查看代码 /* 题目解析: 1.采用Math对象的random()方法, 2.将每次生成的数跟之前的数判断相等则此次生成无效i-- */ function randomNum ...
- 【lwip】07-链路层收发以太网数据帧源码分析
目录 前言 7.1 链路层概述 7.2 MAC地址的基本概念 7.3 以太网帧结构 7.4 以太网帧结构 7.5 以太网帧报文数据结构 7.6 发送以太网数据帧 7.7 接收以太网数据帧 7.8 虚拟 ...
- mysql 子查询 联结 组合查询
子查询 SELECT cust_id FROM orders WHERE order_num IN (SELECT order_num FROM orderitems WHERE prod_id='T ...
- C#使用MathNet库来对进行曲线拟合
下面是用来求取一条直线和一条拟合曲线交点的代码 /// <summary> /// 拟合曲线所筛选的点的个数 /// </summary> const int CurveNum ...
- SpringBoot yml配置文件中,logging.level报错
报错 *************************** APPLICATION FAILED TO START *************************** Description: ...
- Kubernetes IPVS和IPTABLES
个人名片: 对人间的热爱与歌颂,可抵岁月冗长 Github:念舒_C.ying CSDN主页️:念舒_C.ying 个人博客 :念舒_C.ying Kubernetes IPVS和IPTABLES ...
- AArch32/AArch64应用程序级内存模型(五)
本文主要为了记录在学习armv8的过程中的一些感悟.由于原文部分章节晦涩难懂,作者参考了网上很多优秀博主的部分章节(可能是直接摘录)并结合自己的理解重新整理了当前这个版本.文中不免有部分章节讲解很浅, ...
- c++ const和指针
const int *p: 定义指针,指向的变量的值不能修改(指向整型常量或普通整型,但值不能通过指针修改) int const *p: 同上 int * const p=&a: 一直指向a, ...
- mysql数据库报错 sql 1452 Cannot add or update a child row:a foreign key constraint fails
其实这句话的意思就是你添加一个值是一个外键,但是这个外键不在关联的数据库中的主键中,这样就导致了添加失败了,解决办法就是添加对应关联数据库的主键的值,不过我要提醒一下!(也就是我采的坑!) 一定要看清 ...