在传统的多线程编程中,我们通常会为每个任务创建一个线程来执行。但是,频繁地创建和销毁线程会带来一定的开销,同时也会导致系统资源的浪费。线程池的出现解决了这个问题。
线程池是一种预先创建好一定数量的线程,并将任务提交给这些线程执行的机制。通过重复利用已创建的线程,线程池减少了线程创建和销毁的开销,提高了系统的并发效率。
1.减少任务执行时间:在做统计的时候用得比较多,当统计维度或者统计次数比较多时,可以通过任务拆分,将每个子任务交个独立线程去执行,这样减少结算最终结果时间;
2.提高用户体验度:在用户发起请求时,在计算完成用户获取的结果时,还要处理其他任务时,其他任务就可以交给线程池去执行。比如我填好了个人信息,保存成功了,但是我们的后台任务可能还要去生成文书之类的任务,如果等到这些任务都执行完成,用户就会一直等待提交结果,体验不太好。此时后台任务交给线程池去执行,提高用户请求的响应速度。
Java 提供了 ThreadPoolExecutor
类来实现线程池。
ThreadPoolExecutor 是 ExecutorService 接口的一个具体实现,可以通过它来创建和管理线程池。它支持核心线程数、最大线程数、任务队列、线程存活时间等多项配置。
必须了解
)说明:ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
RejectedExecutionHandler handler)
corePoolSize
:线程池的核心线程数,即线程池中保持活动状态的最小线程数。
maximumPoolSize
:线程池的最大线程数,即线程池中允许存在的最大线程数。队列达到最大时开启最大线程数
keepAliveTime
:非核心线程的空闲存活时间,当线程池中的线程数超过核心线程数时,空闲的非核心线程在经过一段时间后会被回收。
unit
:空闲存活时间的时间单位。
workQueue
:当线程池中的线程数达到核心线程数
时,新的任务会被放入任务队列中。
handler
:当线程池中的线程数已达到最大线程数且任务队列已满时,线程池会根据配置的拒绝策略来处理新的任务。Java 提供了几种内置的拒绝策略,如抛出异常、丢弃任务等(默认AbortPolicy丢弃任务
)
tips:jdk线程池是队列达到最大时,才会开始启用 最大线程参数 maximumPoolSize 配置,没有达到最大队列都是核心线程投入使用执行
private static final ThreadPoolExecutor executors =
new ThreadPoolExecutor(
2,
5,
1,
TimeUnit.DAYS,
new ArrayBlockingQueue<Runnable>(10),
new ThreadPoolExecutor.AbortPolicy()
);
public static void main(String[] args) {
for (int i=0;i<13;i++){
executors.execute(new Runnable() {
@Override
public void run() {
System.out.println("Thread name:{} start"+Thread.currentThread().getName());
System.out.println("active count:{} end"+executors.getActiveCount());
System.out.println("queue size:{}"+executors.getQueue().size());}
});
}
}
在使用线程池时,我们需要注意以下几点:
1 适当配置线程池大小
线程池的大小需要根据任务的类型和系统的资源情况进行合理配置。如果线程池过大,会造成资源浪费;如果线程池过小,则无法充分利用系统资源。
2 选择合适的任务队列
选择合适的任务队列可以避免任务丢失或者过多任务阻塞导致系统负载过高。根据任务的特性和需求选择适当的任务队列类型,如有界队列(ArrayBlockingQueue
)或无界队列(LinkedBlockingQueue
)等。
3 处理异常
在编写多线程任务时,需要适当处理异常,避免异常在线程池中被吞掉。可以通过 try-catch
块来捕获异常,并进行适当的处理或日志记录。
4 及时关闭线程池
在不需要线程池时,应该及时关闭线程池以释放资源。可以通过调用 shutdown()
或 shutdownNow()
方法来关闭线程池。前者会等待所有任务执行完成后关闭,后者会立即停止所有任务并关闭线程池。
(当然开发中关闭线程池的场景还是比较少的,很多时候线程池都是统一配置,一直运行
)
线程池提供了几种不同的方法来提交任务并执行,具体取决于任务的类型和需求。下面是几种常见的提交任务的方式:
execute() 方法(如上面演示): execute()
方法用于提交实现了 Runnable
接口的任务。线程池会从任务队列中取出任务,并在空闲的线程中执行。
executor.execute(new Runnable() {
@Override
public void run() {
// 任务执行的代码
}
});
submit() 方法: submit()
方法用于提交实现了 Callable
接口的任务,并返回一个 Future
对象,可以通过该对象获取任务的执行结果。
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// 任务执行的代码
return "任务执行结果";
}
});
// 获取任务执行结果
String result = future.get();
invokeAll()
方法用于同时提交多个 Callable
任务,并返回一个包含 Future
对象的列表。调用该方法会阻塞,直到所有任务都执行完成。List<Callable<String>> tasks = new ArrayList<>();
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
// 任务1执行的代码
return "任务1执行结果";
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
// 任务2执行的代码
return "任务2执行结果";
}
});
List<Future<String>> futures = executor.invokeAll(tasks);
// 遍历获取任务执行结果
for (Future<String> future : futures) {
String result = future.get();
// 处理任务执行结果
}
invokeAny()
方法用于同时提交多个 Callable
任务,并返回其中一个任务的执行结果。调用该方法会阻塞,直到有一个任务完成。List<Callable<String>> tasks = new ArrayList<>();
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
// 任务1执行的代码
return "任务1执行结果";
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
// 任务2执行的代码
return "任务2执行结果";
}
});
String result = executor.invokeAny(tasks);
// 处理任务执行结果
通过以上不同的方法,可以根据任务的特性和需求选择合适的方式来提交任务给线程池执行。无论是提交单个任务、批量提交任务还是获取任务执行结果,线程池提供了灵活且方便的方法来管理和执行多线程任务。