【多线程】认识线程池
(1)为什么使用线程池(线程池的优点)
(1)优点介绍
池化技术在很多地方都有使用,例如:线程池、数据库连接池、Http连接池等等。每次创建线程都是很耗时间的,所以使用池化技术把创建的线程放进线程池,需要使用的时候不再重新创建而是到线程池中去找一个空闲的线程,用完以后再把线程还回线程池,这样就可以减少每次获取资源的消耗,提高对资源的利用率。线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。
什么时候使用线程池:
(1)单个任务处理时间比较短
(2)需要处理的任务数量很大
总结如下:
- 降低资源消耗:通过重复使用已经创建的线程来降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,任务可以不需要等线程创建,直接从池中获取线程,立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
(2)代码实例
(1)使用单线程处理
public static void singleThread() throws InterruptedException {
Long startTime = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
/*new Thread(() ->{
list.add(random.nextInt());
}).start();*/
Thread thread = new Thread() {
public void run() {
list.add(random.nextInt());
}
};
thread.start();
thread.join();
}
System.out.println("时间:"+(System.currentTimeMillis()-startTime));
System.out.println("大小:"+list.size());
}
运行时间结果:
(2)使用线程池来处理
public static void threadPool() throws InterruptedException {
Long startTime = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("时间:"+(System.currentTimeMillis()-startTime));
System.out.println("大小:"+list.size());
}
运行时间结果:
(3)结果分析
使用单线程的时候,耗时8秒多,而使用线程池的时候,耗时仅0.1秒。为什么使用线程池的性能快这么多?那就是频繁创建和切换线程的时候
ULT
KLT
上下文切换
(2)认识Executor框架
(2.1)基本介绍Executor
在Java5之后引进,通过Executor来启动线程,比使用Thread的start方法更好。好处体现在更加容易管理、效率更高、更重要的是可以避免this逃逸。
什么是this逃逸?
this逃逸就是指在构造函数返回之前其他线程就持有该对象的引用,调用还没有构造完全的对象的方法会报错。
Executor矿建不仅仅提供了线程池的管理,还提供了线程工厂、队列和拒绝策略等,Executor框架让并发编程变得更加简单了。
(2.2)Executor框架结构(主要由三大部分组成)
(1)任务(Runnable/Callable)
执行任务需要实现的Runnable接口或Callable接口,Runnable接口和Callable接口的实现类都可以被ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行
Runnable和Callable的区别?
Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换
(2)任务的执行(Executor)
任务执行机制的核心接口 Executor,还有继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。
关注 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。
(3)异步计算的结果(Future)
Future接口和它的实现类FutureTask 类都可以代表异步计算的结果。
当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)
(2.3)Executor框架的使用流程和示意图
- 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象
- 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。
- 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
- 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
(2.4)常见的线程池实现方式
- Executors.newCachedThreadPool():无限线程池。
- Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
- Executors.newSingleThreadExecutor():创建单个线程的线程池。
实际上还是利用 ThreadPoolExecutor 类实现的
(3)ThreadPoolExecutor 类简介(重要)
线程池实现类 ThreadPoolExecutor 是 Executor 框架最核心的类(Executor是线程池的执行接口,ThreadPoolExecutor是其实现类)
(3.1)ThreadPoolExecutor 类分析
ThreadPoolExecutor 类中提供的四个构造方法。主要看最长的那个,其余三个都是在这个构造方法的基础上产生。
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//1-线程池的核心线程数量
int maximumPoolSize,//2-线程池的最大线程数
long keepAliveTime,//3-当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//4-时间单位
BlockingQueue<Runnable> workQueue,//5-任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//6-线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//7-拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
以下总结很重要,需要认真记忆:
(1)ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,线程就会被存放在队列中
(2)ThreadPoolExecutor其他常见参数:
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁
- unit : keepAliveTime 参数的时间单位
- threadFactory :executor 创建新线程的时候会用到
- handler :饱和策略。关于饱和策略下面单独介绍一下
(3)ThreadPoolExecutor 饱和策略定义:
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
- ThreadPoolExecutor.AbortPolicy(拒绝策略):抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy(延迟策略):调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy(丢弃新任务策略): 不处理新任务,直接丢弃掉
- ThreadPoolExecutor.DiscardOldestPolicy(丢弃最早未处理): 此策略将丢弃最早的未处理的任务请求。
(4)饱和策略的案例
Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)
(3.2)推荐使用ThreadPoolExecutor 构造函数创建线程池
在《阿里巴巴Java开发手册》里明确指出线程线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。主要是因为使用线程池可以减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,可能会造成系统创建大量同类线程而导致消耗完内存或者“过渡切换”的问题。
此外,《阿里巴巴Java开发手册》中强制要求线程池不能使用Executors去创建,而是通过ThreadPoolExecutor构造函数的方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。
为什么不直接用Executors,原因如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
线程池的实现方法主要有两种:
(1)通过ThreadPoolExecutor构造函数实现(推荐)
(2)通过 Executor 框架的工具类 Executors 来实现 我们可以创建三种类型的 ThreadPoolExecutor:
FixedThreadPool(不推荐)
SingleThreadExecutor(不推荐)
CachedThreadPool(不推荐)
(4)ThreadPoolExecutor 使用示例(重要)
(4.1)示例代码:Runnable+ThreadPoolExecutor
(4.1.1)首先创建一个 Runnable 接口的实现类,当然也可以是 Callable 接口
import java.util.Date;
/**
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
* @author shuang.kou
*/
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
(4.1.2)编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建一个线程,创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable,把创建好的线程放进线程池
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
通常我们都是使用:threadPool.execute(new Job()); 来提交一个任务到线程池中,所以核心的逻辑就是 execute() 函数了。
(4.1.3)总结一下
- corePoolSize: 核心线程数为 5
- maximumPoolSize :最大线程数 10
- keepAliveTime : 等待时间为 1L
- unit: 等待时间的单位为 TimeUnit.SECONDS
- workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100
- handler:饱和策略为 CallerRunsPolicy
(4.2)线程池中线程的状态
- RUNNING:运行状态,指可以接受任务执行队列里的任务
- SHUTDOWN:指调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕才会关闭线程
- STOP:指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务,不等队列中任务执行完
- TIDYING:所有任务都执行完毕,在调用 shutdown() / shutdownNow() 中都会尝试更新为这个状态
- TERMINATED:终止状态,当执行 terminated() 后会更新为这个状态
下图为线程池的状态转换过程:
(4.3)execute() 方法是如何处理的
(1)基本流程描述
- 获取当前线程池的状态
- 当前线程数量小于 coreSize(核心线程数) 时创建一个新的核心线程运行
- 如果当前线程处于运行状态,并且写入阻塞队列成功
- 双重检查,再次获取线程池状态;如果线程池状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略
- 如果当前线程池为空就新创建一个线程并执行
- 如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略
(2)方法源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// 当前线程数量小于 coreSize 时创建一个新的核心线程运行
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果线程池是在运行状态,并且等待队列可以存放任务
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果非运行状态
if (! isRunning(recheck) && remove(command))
// 执行拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
// 放入等待队列
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
(3)执行结构图
线程复用就体现在processWorkerExit
(4.4)优雅的关闭线程池
无非就是两个方法 shutdown()/shutdownNow(),但他们有着重要的区别:
- shutdown() 执行后停止接受新任务,会把队列的任务执行完毕
- shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop
通常使用以下方式关闭线程池:
long start = System.currentTimeMillis();
for (int i = 0; i <= 5; i++) {
pool.execute(new Job());
}
pool.shutdown();
while (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
LOGGER.info("线程还在执行。。。");
}
long end = System.currentTimeMillis();
LOGGER.info("一共处理了【{}】", (end - start));
pool.awaitTermination(1, TimeUnit.SECONDS) 会每隔一秒钟检查一次是否执行完毕(状态为 TERMINATED),当从 while 循环退出时就表明线程池已经完全终止了。
(4.5)线程池原理分析
线程池每次会同时执行5个任务,这5个任务执行完以后,剩余的5个任务才会被执行
execute方法的源码如下:
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static int workerCountOf(int c) {
return c & CAPACITY;
}
//任务队列
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
(1)当向线程池提交一个任务之后,线程池是如何处理这个任务呢?看一下线程池的主要处理流程,流程图如下:
(2)流程如下:
- 线程池判断核心线程池里的线程是否都在执行任务,如果不是,就创建一个新的工作线程来执行任务,如果核心线程池里的线程都在执行任务,就进入下一个流程
- 线程池判断工作队列是否已经满了,如果工作队列没有满,就把新提交的任务存储在这个工作队列里,如果工作队列满了,就进入下一个流程
- 线程池判断线程池的线程是否都处在工作状态,如果没有,就创建一个新的工作线程来执行任务,如果已经满了,就交给饱和策略来处理这个任务
(3)ThreadPoolExecutor执行execute()方法的示意图
(4)ThreadPoolExecutor执行execute方法分下面4种情况
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
(4.6)几个常见的对比
(1)Runnable 和 Callable 的区别
Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换
(2)execute() 和 submit()的区别
- execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
- submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
(3)shutdown() 和 shutdownNow() 的区别
- shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕
- shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List
(4)isTerminated() 和 isShutdown() 的区别
- isShutDown 当调用 shutdown() 方法后返回为 true
- isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
(5)几种常见的线程池
(1)FixedThreadPool(不推荐)
FixedThreadPool 被称为可重用固定线程数的线程池
分析一下:
- 如果当前运行的线程数小于 corePoolSize,也就是说还没达到线程池最大容纳量,这时候来一个新的任务,就创建新的线程来执行任务
- 如果当前运行的线程数等于 corePoolSize,这时候来一个新任务,就会把任务放到等待队列 LinkedBlockingQueue 中等待线程
- 线程池中的此案成执行完自己的任务后,会在循环中反复的从等待队列 LinkedBlockingQueue 中提取等待的任务来执行
不推荐使用FixedThreadPool的原因:
FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
- 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
- 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
(2)SingleThreadExecutor(不推荐)
SingleThreadExecutor 是只有一个线程的线程池,新创建的 SingleThreadExecutor 的 corePoolSize 和 maximumPoolSize 都被设置为 1,其他参数和 FixedThreadPool 相同
分析一下:
- 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
- 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
- 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;
为什么不推荐使用SingleThreadExecutor:
SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM
(3)CachedThreadPool(不推荐)
CachedThreadPool 是一个会根据需要创建新线程的线程池,CachedThreadPool 的corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
分析一下:
- 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
- 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;
为什么不推荐使用 CachedThreadPool:
CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM
(4)ScheduledThreadPoolExecutor(基本不会用到)
(6)比较不同线程池的代码实例(重要!!!)
(1)使用ExecutorService创建不同线程池的简单介绍
(2)代码
创建三种不同的线程池,然后分别去执行业务代码,输出线程名称对应处理数据的关系。
public class TestThread05 {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService1 = Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
executorService1.execute(new MyTask(i));
// executorService2.execute(new MyTask(i));
// executorService3.execute(new MyTask(i));
}
}
}
class MyTask implements Runnable {
int i = 0;
public MyTask(int i) {
this.i=i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--"+i);
try {
//模拟业务逻辑
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(3)执行结果
(1)newCachedThreadPool(快)
但是占用CPU也最多,出现CPU占用100%的情况,可能出现OOM内存溢出的情况。
(2)newFixedThreadPool(慢)
(3)newSingleThreadExecutor(最慢)
(4)结果分析
(1)认识newCachedThreadPool等方法的内部
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
(2)ThreadPoolExecutor构造方法各种参数的含义
(3)newCachedThreadPool参数和执行过程(快)
(1)参数详情
(1)核心线程数为0
(2)非核心线程数为MAX_VALUE
MAX_VALUE的值为0x7fffffff,也就是2的31次方-1
换算成10进制值为上亿
(3)队列为SynchronousQueue
SynchronousQueue是BlockingQueue的一种,所以SynchronousQueue是线程安全的。SynchronousQueue和其他的BlockingQueue不同的是SynchronousQueue的capacity是0。即SynchronousQueue不存储任何元素。
也就是说SynchronousQueue的每一次insert操作,必须等待其他线性的remove操作。而每一个remove操作也必须等待其他线程的insert操作。
其本身是没有容量大小,比如我放一个数据到队列中,我是不能够立马返回的,我必须等待别人把我放进去的数据消费掉了,才能够返回。
(2)执行的过程
(1)往线程池中放入task,创建一个非核心线程
(2)后面每放入一个task,都会创建一个对应的非核心线程来处理
(3)如果业务处理耗时较快的话,就会出现线程复用
(4)newFixedThreadPool参数和执行过程(慢)
(1)参数详情
(1)核心线程数为构造器传参赋值
(2)非核心线程数为0
其实MaximumPoolSize的意思不是非核心线程数,而是所有线程数,如果核心线程数为nThreads,而全部线程数也为nThreads,那么剩下的非核心线程数也就是0了
(3)队列为LinkedBlockingQueue列表队列
队列的长度为MAX_VALUE,意味着无限长
(2)执行的过程
(1) 往线程池中放入task
例如参数的核心线程数为10个,全部任务数为100个,那么前10个任务放进线程池时直接使用核心线程处理。后面90个任务放进线程池且没有空闲核心线程的时候,就会放进等待队列,等待有空闲核心线程了才会被处理
所以看执行的结果能看到,始终是10个线程在重复往返处理所有的任务
(5)newSingleThreadExecutor参数和执行过程(最慢)
(1)参数详情
(1)核心线程数默认为1
(2)最大总线程数也为1,则非核心线程数为0
(3)队列为LinkedBlockingQueue列表队列
队列的长度为MAX_VALUE,意味着无限长
(2)执行的过程
线程池中只有1个核心线程在处理任务,其余任务在等待队列中等待
查看处理结果也能看出来,只有一个核心线程在处理
(7)实际使用线程池的自定义设置参数方式
上述几种创建线程池的方式虽然方便,但是在实际开发的时候不能适用大部分的场景,那么就需要我们根据场景的实际情况来手动设置合适的参数。
(1)代码实例
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,0L,TimeUnit.MICROSECONDS,
new ArrayBlockingQueue<Runnable>(10));
for (int i = 0; i < 100; i++) {
threadPoolExecutor.execute(new MyTask(i));
}
}
(2)运行结果
(1)提交优先级:1-10被核心线程处理,11-20进入等待队列,21-30被非核心线程处理。
(2)执行优先级:1-10先被处理,21-30再被处理,11-20在等待队列中等到核心线程空闲了最后处理。
(3)结果分析
上述线程池额参数:核心线程数10,最大线程数20(非核心线程10个),等待队列长度10。
看上面的执行结果可以看出,前10个任务放入线程池被10个核心线程处理,再放入10个任务进入等待队列等待,再放入10个任务被10个非核心线程处理。所以30个线程之后就出现异常了,因为触发了等待队列的拒绝策略。
(4)自定义参数的规则
线程池最容易出坑的地方,就是线程参数设置不合理。比如核心线程设置多少合理,最大线程池设置多少合理等等。当然,这块不是乱设置的,需要结合具体业务。
比如线程池如何调优,如何确认最佳线程数?
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。
(8)Springboot使用线程池
(6.1)代码示例
既然用了 SpringBoot ,那自然得发挥 Spring 的特性,所以需要 Spring 来帮我们管理线程池:
@Configuration
public class TreadPoolConfig {
/**
* 消费队列线程
* @return
*/
@Bean(value = "consumerQueueThreadPool")
public ExecutorService buildConsumerQueueThreadPool(){
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("consumer-queue-thread-%d").build();
ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
return pool ;
}
}
使用时:
@Resource(name = "consumerQueueThreadPool")
private ExecutorService consumerQueueThreadPool;
@Override
public void execute() {
//消费队列
for (int i = 0; i < 5; i++) {
consumerQueueThreadPool.execute(new ConsumerQueueThread());
}
}
其实也挺简单,就是创建了一个线程池的 bean,在使用时直接从 Spring 中取出即可。
(6.2)监控线程池
可利用Springboot的 actuator 组件来做线程池的监控,对线程池的监控可以知道自己任务执行的状况、效率等。
(6.3)线程池隔离
如果我们很多业务都依赖于同一个线程池,当某个业务因为各种不可控的原因消耗可所有的此案成,导致线程池全部占满,则其他的业务就不能正常运转了,这样的后果会很严重。
例如Tomcat接受请求的线程池,假设其中一些响应特别慢,线程资源得不到回收释放,线程池慢慢被占满,最坏的情况就是整个应用都不能提供服务。
所以我们需要对线程池进行隔离,隔离的方式通常是按照业务进行划分的,比如下单的任务用一个线程池,获取数据的任务用另一个线程池,这样即使其中一个出现问题把线程池耗尽,也不会影响其他的任务运行。
(6.3.1)hystrix隔离
Hystrix 已经帮我们实现了隔离,首先需要定义两个线程池,分别用于执行订单、处理用户
/**
* Function:订单服务
*
* @author crossoverJie
* Date: 2018/7/28 16:43
* @since JDK 1.8
*/
public class CommandOrder extends HystrixCommand<String> {
private final static Logger LOGGER = LoggerFactory.getLogger(CommandOrder.class);
private String orderName;
public CommandOrder(String orderName) {
super(Setter.withGroupKey(
//服务分组
HystrixCommandGroupKey.Factory.asKey("OrderGroup"))
//线程分组
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("OrderPool"))
//线程池配置
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10)
.withKeepAliveTimeMinutes(5)
.withMaxQueueSize(10)
.withQueueSizeRejectionThreshold(10000))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
)
;
this.orderName = orderName;
}
@Override
public String run() throws Exception {
LOGGER.info("orderName=[{}]", orderName);
TimeUnit.MILLISECONDS.sleep(100);
return "OrderName=" + orderName;
}
}
/**
* Function:用户服务
*
* @author crossoverJie
* Date: 2018/7/28 16:43
* @since JDK 1.8
*/
public class CommandUser extends HystrixCommand<String> {
private final static Logger LOGGER = LoggerFactory.getLogger(CommandUser.class);
private String userName;
public CommandUser(String userName) {
super(Setter.withGroupKey(
//服务分组
HystrixCommandGroupKey.Factory.asKey("UserGroup"))
//线程分组
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("UserPool"))
//线程池配置
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10)
.withKeepAliveTimeMinutes(5)
.withMaxQueueSize(10)
.withQueueSizeRejectionThreshold(10000))
//线程池隔离
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
)
;
this.userName = userName;
}
@Override
public String run() throws Exception {
LOGGER.info("userName=[{}]", userName);
TimeUnit.MILLISECONDS.sleep(100);
return "userName=" + userName;
}
}
模拟运行;
public static void main(String[] args) throws Exception {
CommandOrder commandPhone = new CommandOrder("手机");
CommandOrder command = new CommandOrder("电视");
//阻塞方式执行
String execute = commandPhone.execute();
LOGGER.info("execute=[{}]", execute);
//异步非阻塞方式
Future<String> queue = command.queue();
String value = queue.get(200, TimeUnit.MILLISECONDS);
LOGGER.info("value=[{}]", value);
CommandUser commandUser = new CommandUser("张三");
String name = commandUser.execute();
LOGGER.info("name=[{}]", name);
}