java篇40-java的线程池
一、线程池概念
若不使用线程池,在新创建的线程start()后执行完run()就自动销毁了,造成了资源的浪费。使用线程池可以暂时存储线程。
线程池的主要核心原理:

二、线程池的代码实现

注意:在实际开发中线程池并不会关闭,例如服务器要24小时接收用户的访问请求。
2.1 创建线程池

例如,创建一个固定核心线程数的线程池:
ExecutorService executorService = Executors.newFixedThreadPool(corePoolSize);
其中参数corePoolSize即为核心线程数。
2.2 任务的提交与执行
在Java线程池中,任务的提交与执行有两个主要的方法:submit和execute。这两种方法有一些区别,主要体现在返回值、异常处理和任务包装上。
(一)submit方法
submit方法用于提交实现了Callable接口的任务,它可以返回一个Future对象,通过该对象可以获取任务执行的结果,取消任务等。submit方法还可以接受实现了Runnable接口的任务,但它无法获取任务的执行结果。submit方法在ExecutorService中定义的,并定义了三种重载方式:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
说明:
(1) Future submit(Runnable task, T result);注意,该方法的返回值Future对象使用get方法并不是获取到task的执行结果,而是获得result参数本身,用来表示任务完成。
例如:
// 示例1:使用字符串作为结果标识
ExecutorService executor = Executors.newSingleThreadExecutor();
String taskIdentifier = "Task-001";
Future<String> future = executor.submit(new Runnable() {
@Override
public void run() {
// 执行一些操作,但不返回结果
System.out.println("任务执行中...");
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务完成");
}
}, taskIdentifier);
// 当任务完成后,get() 返回的是传入的 taskIdentifier
String result = future.get(); // 返回 "Task-001"
System.out.println(result); // 输出: Task-001
(2)Future<?> submit(Runnable task) Future.get() 返回 null 只关心任务是否完成,不关心结果
主要特点:
(1)返回一个Future对象,可通过Future的get()方法可以获取到线程执行的返回值,get()方法是同步的,执行get()方法时,如果线程还没执行完,会同步等待,直到线程执行完成。
(2)可以接受Callable和Runnable类型的任务。
(3)执行Runnable和Callable的任务时,run()/call()方法没显式抛出异常。
(二)execute
execute方法用于提交实现了Runnable接口的任务,它没有返回值,因此无法获取任务的执行结果。如果任务执行过程中抛出了异常,线程池会捕获并记录该异常,但无法通过execute方法获知。
public interface Executor {
void execute(Runnable command);
}
建议用submit。
例如,定义实现Runnable接口的类设置任务:

使用线程池:

可以看到不同线程执行不同MyRunnable对象的任务。
而改写代码,增长线程的逐次提交的时间,会发现同一个线程连续执行不同MyRunnable对象的任务:


三、使用ThreadPoolExecutor自定义线程池
3.1 ThreadPoolExecutor线程池的七个核心元素:
(1)核心线程数量
(2)最大线程数量
(3)线程空闲时间(超过核心线程数量的空闲线程在终止前等待新任务的最长时间)、
(4)线程空闲时间(单位)
(5)任务队列(即阻塞队列)
(6)线程工厂(用于创建新线程,可以设置线程名、优先级、守护线程等,便于排查问题。)
(7)拒绝策略(要执行的任务过多时的解决方案)。
例如,自定义一个线程池,核心线程有3个,临时线程有3个,阻塞队列长度为3。当有5个任务提交时,线程池现创建3个线程执行前3个任务,后2个任务在阻塞队列中排队等待;当有8个任务提交时,线程池现创建3个线程执行前3个任务,第4,5,6个任务阻塞队列中排队等待,线程池再创建2个临时线程取处理任务7和任务8(注意任务7,8比阻塞队列种的任务先被执行)。所有,只有当核心线程都在工作,阻塞队列被占满时才会创建临时线程。当有10个任务提交时,线程池现创建3个线程执行前3个任务,第4,5,6个任务阻塞队列中排队等待,线程池再创建3个临时线程取处理任务7、8、9,任务10会触发任务拒绝策略。java中有四个任务的拒绝策略,知道第一种即可。

ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量,能小于0
6,//最大线程数量,不能小于0,大于等于核心线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位,这里为秒
new ArrayBlockingQueue<>(3),//阻塞队列,不能为null
Executors.defaultThreadFactory(),//创建线程工厂,不能为null
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略,不能为null
);
补充线程池大小的选择:
最大并行数:与电脑的CPU型号有关。

上图中的电脑为4核8线程。
4核8线程可以理解为CPU有4个大脑,能同时做4件事情,而Intel采用超线程技术,将4个大脑虚拟成8个,能同时做8件事,此时最大并行数为8。
注意:极少数操作系统不会把所有的内核给一个软件,所以从任务管理器中查看并不一定对,可以利用如下代码查看;

程序分为两种,一种是CPU密集型运算,另一种是I/O密集型运算。若是CPU密集型运算,线程池大小= 最大并行数+1;若是I/O密集型运算(读取本地文件或数据库操作较多),线程池大小= 最大并行数 X 期望 CPU利用率X总时间/CPU计算时间,其中总时间=CPU计算时间+等待时间。
3.2 线程池的5种状态
(1)RUNNING:能接受新提交的任务,也能处理阻塞队列中的任务。
(2)SHUTDOWN:不再接受新提交的任务,但会继续处理阻塞队列中已保存的任务。调用 shutdown() 方法进入此状态。
(3)STOP:不再接受新任务,也不会处理队列中的任务,并会中断正在执行的任务。调用 shutdownNow() 方法进入此状态。
(4)TIDYING:所有任务都已终止,工作线程数为0。进入此状态后会调用 terminated() 钩子方法。
(5)TERMINATED:terminated() 方法执行完成后进入此状态。
3.3 线程池的线程数量的设置
并没有一个通用的标准来设置参数,因此需要结合实际实战经验、业务需求以及服务器资源的状况,灵活而合理地进行参数配置。最终,合适的配置才是最为优越的选择。
当然也有一个简单而广泛适用的公式,可以用于确定线程池中的线程数:
CPU 密集型任务(N+1):
对于消耗主要是CPU资源的任务,可以将线程数设置为N(CPU核心数)+1。额外的一个线程用于防止线程偶发的缺页中断或其他原因导致的任务暂停,防止空闲时间的浪费。一旦任务暂停,多出来的一个线程可以充分利用CPU的空闲时间。
I/O 密集型任务(2N):
对于主要涉及I/O交互的任务,系统会在大部分时间内处理I/O,而在线程处理I/O的时间段内不会占用CPU。因此,在I/O密集型任务中,可以配置更多的线程,具体计算方法是2N。
四、使用实例
4.1 处理大批短期任务
场景:需要并发处理一堆任务,比如下载多个文件。
import java.util.concurrent.*;
public class SimpleExample {
public static void main(String[] args) throws InterruptedException {
// 1. 创建线程池(核心线程2,最大线程4,队列容量2)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2), // 队列只能放2个任务
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 2. 提交10个任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(() -> {
try {
System.out.println("任务" + taskId + " 正在执行,线程:" + Thread.currentThread().getName());
Thread.sleep(2000); // 模拟任务执行时间
System.out.println("任务" + taskId + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("任务" + taskId + " 已提交");
}
// 3. 关闭线程池(不再接受新任务)
executor.shutdown();
// 等待所有任务完成,最多等1分钟
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("所有任务执行完毕");
}
}
注意:submit() 方法需要接收一个 Runnable 或 Callable 对象。,上面使用了lambda表达式,完整写法如下:
executor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println("任务" + taskId + " 正在执行,线程:" + Thread.currentThread().getName());
Thread.sleep(2000); // 模拟任务执行时间
System.out.println("任务" + taskId + " 执行完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
4.2 需要获取返回结果的任务
import java.util.concurrent.*;
public class FutureExample {
public static void main(String[] args) throws Exception {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交带返回值的任务
Future<Integer> future1 = executor.submit(() -> calculate(10));
Future<Integer> future2 = executor.submit(() -> calculate(20));
Future<Integer> future3 = executor.submit(() -> calculate(30));
// 获取结果(会阻塞直到任务完成)
int result1 = future1.get();
int result2 = future2.get();
int result3 = future3.get();
System.out.println("结果1: " + result1);
System.out.println("结果2: " + result2);
System.out.println("结果3: " + result3);
System.out.println("总和: " + (result1 + result2 + result3));
executor.shutdown();
}
private static int calculate(int n) throws InterruptedException {
Thread.sleep(1000); // 模拟计算耗时
return n * n; // 返回平方值
}
}
更多推荐
所有评论(0)