线程系列 2 - 并发编程之线程池 ThreadPool 的那些事
1、New Thread 弊端
- 每次 new Thread 新建对象,性能差。
- 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM。
- 缺少更多功能,如更多执行、定期执行、线程中断。
2、线程池的好处
-
降低资源消耗:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。重复利用已创建的线程,可以降低线程创建和销毁造成的消耗。
-
可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
-
提高线程的可管理性:线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息,如已完成任务的数量等。通过线程池可以对线程资源进行统一的分配、监控和调优
-
提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
3、线程池 - ThreadPoolExecutor
3.1、 核心参数
corePoolSize
:核心线程数量。maximumPoolSize
: 线程最大线程数。workQueue
: 阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响。keepAliveTime
: 线程没有任务执行时,最多保持多久时间终止。unit
: keepAliveTime 的时间单位。threadFactory
: 线程工厂,用来创建线程。rejectHandler
: 当拒绝处理任务时的策略。
3.1.1 ThreadFactory (线程工厂)
Executors创建新的线程池时,使用Executors.defaultThreadFactory默认实例,全部位于同一个ThreadGroup(线程组)中,具有相同的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。
package java.util.concurrent;
public interface ThreadFactory {
/**
* 实现该方法可以更改所创建的新线程的名称、线程组、优先级、守护进程状态等。
* 如果newThread()的返回值为null,表示线程工厂未能成功创建线程,线程池可能无法执行任何任务。
*/
Thread newThread(Runnable r);
}
3.1.2 workQueue (任务阻塞队列)
在阻塞队列为空时会阻塞当前线程的元素获取操作。在一个线程从一个空的阻塞队列中获取元素时,线程会被阻塞,直到阻塞队列中有了元素;当队列中有元素后,被阻塞的线程会自动被唤醒(唤醒过程不需要用户程序干预 )。
Java线程池使用BlockingQueue实例暂时接收到的异步任务,BlockingQueue是JUC包的一个超级接口,比较常用的实现类有:
(1)ArrayBlockingQueue
:是一个数组实现的有界阻塞队列,队列中的元素按FIFO排序。该阻塞队列在创建时必须设置大小。
(2)LinkedBlockingQueue
:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。
如果不设置LinkedBlockingQueue的容量(无界队列),当接收的任务数量超出corePoolSize时,则新任务可以被无限制地缓存到该阻塞队列中,直到资源耗尽。newSingleThreadExecutor和newFixedThreadPool使用了LinkedBlockingQueue,并且都没有设置容量(无界队列)。
(3)PriorityBlockingQueue
:是具有优先级的无界队列。
(4)DelayQueue
:这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。newScheduledThreadPool所创建的线程池使用此队列。
(5)SynchronousQueue
:(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。newCachedThreadPool所创建的线程池使用此队列。与前面的队列相比,这个队列比较特殊,它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
3.1.3 RejectHandler (线程池的拒绝策略)
任务被拒绝有两种情况:
(1)线程池已经被关闭。
(2)工作队列已满且maximumPoolSize已满。在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。
任务被拒绝时,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。
RejectedExecutionHandler 是拒绝策略的接口,JUC为该接口提供了以下几种实现:
实现 | 策略 |
---|---|
AbortPolicy (默认) | 如线程池队列满了,新任务会被拒绝,抛出异常:RejectedExecutionException.该异常是非受检异常(运行时异常),很容易忘记捕获。 |
DiscardPolicy | 如线程池队列满了,新任务会直接被丢掉 |
DiscardOldestPolicy | 如队列满了,会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列 |
CallerRunsPolicy | 如队列满了,新任务被添加到线程池时,提交任务线程会自己去执行该任务,不使用线程池中的线程去执行新任务。 |
自定义 | 实现RejectedExecutionHandler接口的rejectedExecution方法. |
自定义拒绝策略示例:
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class ThreadPoolDemo {
// 自定义线程工厂
public static class MyThreadFactory implements ThreadFactory {
static AtomicInteger threadNo = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
String threadName = "MyThreadFactory-" + threadNo.get();
log.info("创建线程,线程名称:" + threadName);
threadNo.incrementAndGet();
//设置线程名称和异步执行目标
Thread thread = new Thread(r, threadName);
//设置为守护线程
thread.setDaemon(true);
return thread;
}
}
// 自定义拒绝策略
public static class MyRejectedPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 一般是对拒绝的任务落库,后续进行补偿等
log.info("{} -> 执行拒绝策略,taskCount = {}", r, executor.getTaskCount());
// 新建一个线程去处理
new Thread(r, "拒绝策略中新建线程").start();
// 或者 sleep 一段时间后再次将任务加入队列
// try {
// TimeUnit.SECONDS.sleep(8);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// // 再次将任务加入队列
// executor.execute(r);
}
}
public static void main(String[] args) throws InterruptedException {
int corePoolSize = 2; //核心线程数
int maximumPoolSize = 4; //最大线程数
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
//阻塞队列任务数
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程工厂
ThreadFactory threadFactory = new MyThreadFactory();
//拒绝和异常处理策略
RejectedExecutionHandler policy = new MyRejectedPolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue, threadFactory, policy);
// 预启动所有核心线程
pool.prestartAllCoreThreads();
// 循环开 10个任务,线程池最大线程数 = 最大核心线程数(4)+阻塞队列数(2) = 6
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (Integer t : list) {
pool.execute(() -> {
try {
log.info("线程名称-> {} ---> 参数 -> {}执行任务中", Thread.currentThread().getName(), t);
// 线程睡眠一会
TimeUnit.SECONDS.sleep(5);
log.info("线程名称-> {} ---> 参数 -> {}任务执行完毕", Thread.currentThread().getName(), t);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//等待10秒
TimeUnit.SECONDS.sleep(15);
log.info("关闭线程池:pool");
pool.shutdown();
}
}
打印结果:
15:42:22.494 [main] INFO com.demo.test.ThreadPoolDemo - 创建线程,线程名称:MyThreadFactory-1
15:42:22.497 [main] INFO com.demo.test.ThreadPoolDemo - 创建线程,线程名称:MyThreadFactory-2
15:42:22.498 [main] INFO com.demo.test.ThreadPoolDemo - 创建线程,线程名称:MyThreadFactory-3
15:42:22.498 [main] INFO com.demo.test.ThreadPoolDemo - 创建线程,线程名称:MyThreadFactory-4
15:42:22.499 [main] INFO com.demo.test.ThreadPoolDemo - com.demo.test.ThreadPoolDemo$$Lambda$21/0x00000008000c6c40@a38d7a3 -> 执行拒绝策略,taskCount = 6
15:42:22.498 [MyThreadFactory-2] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-2 ---> 参数 -> 2执行任务中
15:42:22.499 [MyThreadFactory-3] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-3 ---> 参数 -> 3执行任务中
15:42:22.498 [MyThreadFactory-1] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-1 ---> 参数 -> 1执行任务中
15:42:22.498 [MyThreadFactory-4] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-4 ---> 参数 -> 6执行任务中
15:42:22.500 [main] INFO com.demo.test.ThreadPoolDemo - com.demo.test.ThreadPoolDemo$$Lambda$21/0x00000008000c6c40@77f99a05 -> 执行拒绝策略,taskCount = 6
15:42:22.501 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 7执行任务中
15:42:22.501 [main] INFO com.demo.test.ThreadPoolDemo - com.demo.test.ThreadPoolDemo$$Lambda$21/0x00000008000c6c40@63440df3 -> 执行拒绝策略,taskCount = 6
15:42:22.501 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 8执行任务中
15:42:22.501 [main] INFO com.demo.test.ThreadPoolDemo - com.demo.test.ThreadPoolDemo$$Lambda$21/0x00000008000c6c40@3aeaafa6 -> 执行拒绝策略,taskCount = 6
15:42:22.501 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 9执行任务中
15:42:22.501 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 10执行任务中
15:42:27.500 [MyThreadFactory-2] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-2 ---> 参数 -> 2任务执行完毕
15:42:27.500 [MyThreadFactory-1] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-1 ---> 参数 -> 1任务执行完毕
15:42:27.500 [MyThreadFactory-4] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-4 ---> 参数 -> 6任务执行完毕
15:42:27.500 [MyThreadFactory-3] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-3 ---> 参数 -> 3任务执行完毕
15:42:27.500 [MyThreadFactory-2] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-2 ---> 参数 -> 5执行任务中
15:42:27.500 [MyThreadFactory-1] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-1 ---> 参数 -> 4执行任务中
15:42:27.516 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 9任务执行完毕
15:42:27.516 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 8任务执行完毕
15:42:27.516 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 10任务执行完毕
15:42:27.516 [拒绝策略中新建线程] INFO com.demo.test.ThreadPoolDemo - 线程名称-> 拒绝策略中新建线程 ---> 参数 -> 7任务执行完毕
15:42:32.508 [MyThreadFactory-1] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-1 ---> 参数 -> 4任务执行完毕
15:42:32.508 [MyThreadFactory-2] INFO com.demo.test.ThreadPoolDemo - 线程名称-> MyThreadFactory-2 ---> 参数 -> 5任务执行完毕
15:42:37.505 [main] INFO com.demo.test.ThreadPoolDemo - 关闭线程池:pool
3.2、线程的任务调度流程 (**
)
线程池的任务调度流程(包含接收新任务和执行下一个任务)大致如下:
(1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
(2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略
注意:
(1)核心和最大线程数量、BlockingQueue队列等参数如果配置得不合理,可能会造成异步任务得不到预期的并发执行,造成严重的排队等待现象。
(2)线程池的调度器创建线程的一条重要的规则是:在corePoolSize已满之后,还需要等阻塞队列已满,才会去创建新的线程。
3.3、 核心方法
execute()
: 提交任务,交给线程池执行。submit()
: 提交任务,能够返回执行结果 execute+Future.shutdown()
: 关闭线程池,等待任务都执行完。shutdownNow()
: 关闭线程池,不等待任务执行完。
3.3.1 execute 和 submit
package java.util.concurrent;
public interface Executor {
/**
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
------------------------------------
package java.util.concurrent;
import java.util.Collection;
import java.util.List;
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
}
区别 | execute | submit |
---|---|---|
参数 | Runnable | Callable / Runnable |
返回值 | 无 | Future |
抛出异常 | 否 | 是 |
execute 和 submit 示例
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class ThreadExecuteDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.execute(() -> {
log.info("执行 execute run()方法", Thread.currentThread().getName());
});
pool.execute(() -> {
log.info("执行 execute run()方法 迎接异常", Thread.currentThread().getName());
Integer.valueOf("hello error");
});
Future<String> myCallableFuture = pool.submit(new MyCallable());
try {
// 获取结果 这里容易阻塞
String myCallableResult = myCallableFuture.get();
log.info("myCallableResult ---> {}", myCallableResult);
} catch (InterruptedException e) {
log.error("myCallableResult InterruptedException happened");
e.printStackTrace();
} catch (ExecutionException e) {
log.error("myCallableResult ExecutionException happened");
e.printStackTrace();
}
Future<String> myCallableExceptionFuture = pool.submit(new MyCallableThrowException());
try {
// 获取结果 , 这里容易阻塞 MyCallableThrowException 执行没有主线程快就会阻塞,后面再具体讲Future
String myCallableExceptionFutureResult = myCallableExceptionFuture.get();
log.info("myCallableExceptionFutureResult ---> {}", myCallableExceptionFutureResult);
} catch (InterruptedException e) {
log.error("myCallableExceptionFutureResult InterruptedException happened");
e.printStackTrace();
} catch (ExecutionException e) {
log.error("myCallableExceptionFutureResult ExecutionException happened");
e.printStackTrace();
}
pool.shutdown();
}
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
log.info("线程-> {} --> 执行call()方法", Thread.currentThread().getName());
return "MyCallable you get call result!";
}
}
static class MyCallableThrowException implements Callable<String> {
@Override
public String call() throws Exception {
log.info("线程-> {} --> 执行call()方法", Thread.currentThread().getName());
Integer.valueOf("hello call error");
return "MyCallableThrowException you get call result!";
}
}
}
执行结果:
16:50:24.018 [pool-1-thread-1] INFO com.demo.test.ThreadExecuteDemo - 执行 execute run()方法
16:50:24.018 [pool-1-thread-3] INFO com.demo.test.ThreadExecuteDemo - 线程-> pool-1-thread-3 --> 执行call()方法
16:50:24.018 [pool-1-thread-2] INFO com.demo.test.ThreadExecuteDemo - 执行 execute run()方法 迎接异常
16:50:24.026 [main] INFO com.demo.test.ThreadExecuteDemo - myCallableResult ---> MyCallable you get call result!
16:50:24.028 [pool-1-thread-5] INFO com.demo.test.ThreadExecuteDemo - 线程-> pool-1-thread-5 --> 执行call()方法
16:50:24.028 [main] ERROR com.demo.test.ThreadExecuteDemo - myCallableExceptionFutureResult ExecutionException happened
Exception in thread "pool-1-thread-2" java.lang.NumberFormatException: For input string: "hello error"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.valueOf(Integer.java:983)
at com.demo.test.ThreadExecuteDemo.lambda$main$1(ThreadExecuteDemo.java:17)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
java.util.concurrent.ExecutionException: java.lang.NumberFormatException: For input string: "hello call error"
at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
at com.demo.test.ThreadExecuteDemo.main(ThreadExecuteDemo.java:34)
Caused by: java.lang.NumberFormatException: For input string: "hello call error"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.valueOf(Integer.java:983)
at com.demo.test.ThreadExecuteDemo$MyCallableThrowException.call(ThreadExecuteDemo.java:58)
at com.demo.test.ThreadExecuteDemo$MyCallableThrowException.call(ThreadExecuteDemo.java:54)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Process finished with exit code 0
3.4、 监控方法
- getTaskCount(): 线程池已执行和未执行的任务总数。
- getCompletedTaskCount(): 已完成的任务数量。
- getPoolSize(): 线程池当前的线程数量。
- getActiveCount(): 当前线程池中正在执行任务的线程数量。
package com.icao;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.*;
/**
* @author
* @title: NewCachedThreadPoolExample
* @description: TODO
* @date 2020/4/16 9:30
*/
@Slf4j
public class NewCachedThreadPoolExample {
public static Integer clientCount = 5000;
public static Integer threadCount = 200;
public static List<Integer> list = new Vector<>();
public static void main(String[] args) throws Exception {
// 声明 newCachedThreadPool 线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。
final Semaphore semaphore = new Semaphore(threadCount);
final CountDownLatch countDownLatch = new CountDownLatch(clientCount);
for ( int i = 0; i<clientCount; i++) {
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
add();
/** 线程监控 log - start**/
// 当前线程池中正在执行任务的线程数量
int activeCount =
((ThreadPoolExecutor)newCachedThreadPool).getActiveCount();
log.info("ActiveCount={}",activeCount);
// 线程池已执行和未执行的任务总数
long taskCount =
((ThreadPoolExecutor)newCachedThreadPool).getTaskCount();
log.info("taskCount={}",taskCount);
/** 线程监控 log - end**/
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
log.error("exception", e);
}
// //将计数值减1
countDownLatch.countDown();
}
});
}
countDownLatch.await();
newCachedThreadPool.shutdown();
log.info("list.size() = {}",list.size());
// 线程池当前的线程数量
int poolSize =
((ThreadPoolExecutor)newCachedThreadPool).getPoolSize();
log.info("poolSize={}",poolSize);
// 已完成的任务数量。
long completedTaskCount =
((ThreadPoolExecutor)newCachedThreadPool).getCompletedTaskCount();
log.info("completedTaskCount={}",completedTaskCount);
}
private static void add() {
list.add(0) ;
}
}
3.5、Executors 提供四种线程池
- Executors.newCachedThreadPool
- Executors.newFixedThreadPool
- Executors.newScheduledThreadPool
- Executors.newSingleThreadExecutor
3.5.1 newCachedThreadPool
Executors.newCachedThreadPool :
创建一个可缓存线程池
,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务
。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newCachedThreadPool 线程池特点是:
1、工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE),?这样可灵活的往线程池中添加线程。
2、如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
3、在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
代码模拟高并发:
package com.icao;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.*;
/**
* @author
* @title: NewCachedThreadPoolExample
* @description: TODO
* @date 2020/4/16 9:30
*/
@Slf4j
public class NewCachedThreadPoolExample {
public static Integer clientCount = 5000;
public static Integer threadCount = 200;
public static List<Integer> list = new Vector<>();
public static void main(String[] args) throws Exception {
long startMili=System.currentTimeMillis();// 当前时间对应的毫秒数
log.info("/**开始 "+startMili);
// 声明 newCachedThreadPool 线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
// Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。
final Semaphore semaphore = new Semaphore(threadCount);
// countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
// 是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
final CountDownLatch countDownLatch = new CountDownLatch(clientCount);
for ( int i = 0; i<clientCount; i++) {
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
// 方法 acquire( int permits ) 参数作用,及动态添加 permits 许可数量
// acquire( int permits ) 中的参数可以这么理解, new Semaphore(6) 表示初始化了 6个通路, semaphore.acquire(2) 表示每次线程进入将会占用2个通路,semaphore.release(2) 运行时表示归还2个通路。没有通路,则线程就无法进入代码块。
// 而semaphore.acquire() + semaphore.release() 在运行的时候,其实和 semaphore.acquire(1) + semaphore.release(1) 效果是一样的。
semaphore.acquire();
add();
log.info("CurrentThreadId()={},list.size()={}",
Thread.currentThread().getId(),list.size());
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
log.error("exception", e);
}
// //将计数值减1
countDownLatch.countDown();
}
});
}
// 调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
countDownLatch.await();
newCachedThreadPool.shutdown();
// 这里使用countDownLatch 是为了保证最后执行 log.info("list.size() = {}",list.size());
log.info("list.size() = {}",list.size());
//这里加入需要测试的代码
long endMili=System.currentTimeMillis();//结束时间
log.info("/**结束 s"+endMili);
log.info("/**总耗时为:"+(endMili-startMili)+"毫秒");
// 线程池当前的线程数量
int poolSize =
((ThreadPoolExecutor)newCachedThreadPool).getPoolSize();
log.info("poolSize={}",poolSize);
// 已完成的任务数量。
long completedTaskCount =
((ThreadPoolExecutor)newCachedThreadPool).getCompletedTaskCount();
log.info("completedTaskCount={}",completedTaskCount);
}
private static void add() {
list.add(0) ;
}
}
3.5.2 newFixedThreadPool
Executors.newFixedThreadPool :
创建固定大小(定长)的线程池
,可控制线程最大并发数。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,超出的线程会在队列中等待
。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程
。
3.5.3 newScheduledThreadPool
Executors.newScheduledThreadPool :
创建一个定长线程池,支持定时及周期性任务执行。
3.5.4 newSingleThreadPool
Executors.newSingleThreadExecutor :
创建一个单线程化的线程池
,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
。
4、确定线程池的线程数
虽然使用线程池的好处很多,但是如果其线程数配置得不合理,不仅可能达不到预期效果,反而可能降低应用的性能。确定线程池的线程数,首先需要针对不同类型的异步任务需求,创建不同类型的线程池,并进行针对性的参数配置。
按照任务类型对线程池进行分类
4.1、IO密集型任务
此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率底,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
对于IO密集型任务,一般开CPU核心数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。
4.2、CPU密集型任务
此类任务主要是执行计算任务。由于响应时间很快,所以CPU一直在运行,此类任务CPU的利用率很高。
CPU密集型任务虽然也可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用CPU,CPU密集型任务并行执行的数量应当等于CPU的核心数。比如4核的CPU,开4个线程并行地执行4个CPU密集型任务,此时的效率是最高的。
4.3、混合型任务
此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用、数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。Web服务器的HTTP请求处理操作为此类任务的典型例子。
公式:最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1) * CPU核数.
例子:
比如在Web服务器处理HTTP请求时,假设平均线程CPU运行时间为100毫秒,而线程等待时间(比如包括DB操作、RPC操作、缓存操作等)为900毫秒,如果CPU核数为8,那么根据上面这个公式,估算如下:
(900毫秒 + 100毫秒) / 100毫秒 * 8 = 10 * 8 = 80
5、关闭线程的那些事
-
shutdown
:是JUC提供的一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后线程池的状态转为SHUTDOWN,线程池不会再接收新的任务。 -
shutdownNow
:是JUC提供的一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。 -
awaitTermination
:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法。
(1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
(2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
(4)补充执行 awaitTermination(long timeout,TimeUnit unit) 方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束
5.1、优雅的关闭线程
代码示例:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
public class CloseThreadPoolDemo {
/**
* 优雅的关闭线程池,该方法可以作为工具类方法
*
* @param threadPool
*/
public static void shutdownThreadPoolGracefully(ExecutorService threadPool) {
// 若已经关闭则返回
if (!(threadPool instanceof ExecutorService) || threadPool.isTerminated()) {
return;
}
try {
//拒绝接受新任务
threadPool.shutdown();
} catch (SecurityException e) {
return;
} catch (NullPointerException e) {
return;
}
try {
// 等待60秒,等待线程池中的任务完成执行
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 调用 shutdownNow() 方法取消正在执行的任务
threadPool.shutdownNow();
// 再次等待60秒,如果还未结束,可以再次尝试,或者直接放弃
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
log.info("线程池任务未正常执行结束");
}
}
} catch (InterruptedException ie) {
// 捕获异常,重新调用 shutdownNow() 方法
threadPool.shutdownNow();
}
// 仍然没有关闭,循环关闭1000次,每次等待10毫秒
if (!threadPool.isTerminated()) {
try {
for (int i = 0; i < 1000; i++) {
if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS)) {
break;
}
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
log.error(e.getMessage());
} catch (Throwable e) {
log.error(e.getMessage());
}
}
}
static class MyThread extends Thread {
public MyThread(String theadName) {
super(theadName);
}
@Override
public void run() {
log.info("执行run() start");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("执行run() end");
}
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
pool.execute(new MyThread("MyThread" + i));
}
shutdownThreadPoolGracefully(pool);
for (int i = 5; i < 10; i++) {
pool.execute(new MyThread("MyThread" + i));
}
}
}
执行结果:
17:05:13.511 [pool-1-thread-2] INFO com.demo.test.CloseThreadPoolDemo - 执行run() start
17:05:13.511 [pool-1-thread-3] INFO com.demo.test.CloseThreadPoolDemo - 执行run() start
17:05:13.511 [pool-1-thread-4] INFO com.demo.test.CloseThreadPoolDemo - 执行run() start
17:05:13.511 [pool-1-thread-5] INFO com.demo.test.CloseThreadPoolDemo - 执行run() start
17:05:13.511 [pool-1-thread-1] INFO com.demo.test.CloseThreadPoolDemo - 执行run() start
17:05:18.522 [pool-1-thread-4] INFO com.demo.test.CloseThreadPoolDemo - 执行run() end
17:05:18.522 [pool-1-thread-3] INFO com.demo.test.CloseThreadPoolDemo - 执行run() end
17:05:18.522 [pool-1-thread-2] INFO com.demo.test.CloseThreadPoolDemo - 执行run() end
17:05:18.522 [pool-1-thread-1] INFO com.demo.test.CloseThreadPoolDemo - 执行run() end
17:05:18.522 [pool-1-thread-5] INFO com.demo.test.CloseThreadPoolDemo - 执行run() end
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[MyThread5,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@26a7b76d[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 5]
at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1355)
at com.demo.test.CloseThreadPoolDemo.main(CloseThreadPoolDemo.java:83)
Process finished with exit code 1
线程系列博文:
线程系列 1 - 线程基础
线程系列 3 - 关于 CompletableFuture
线程系列 4 - synchronized 和线程间的通信
线程系列 5 - CAS 和 JUC原子类
线程系列 6 - JUC相关的显示锁
线程系列 7 - JUC高并发容器类
.