Java线程池与多线程详解
文章目录
- Java线程池与多线程详解
- 一、前言
- 二、创建线程的几种方式
- 1、继承Thread类创建线程
- 2、实现Runnable接口创建线程
- 3、实现Callable接口创建线程
- 4、通过线程池创建线程
- 三、线程池的执行流程以及常用函数
- 3.1 、线程的执行流程
- 3.2、线程睡眠(sleep)
- 3.3、线程等待(wait)
- 3.4、sleep()方法和wait()方法的区别
- 3.5、为什么wait(),notify()方法要和synchronized一起使用
- 3.6、线程让步(yield())
- 3.7、线程中断(interrupt)
- 3.8、线程强制执行(join)
- 3.9、线程的关闭
- 3.10、线程死锁
- 3.10.1、如何查看线程死锁
- 3.10.2、如何避免死锁
- 四、Java创建线程池的五种方式
- 4.1、ewSingleThreadPool
- 4.2、newFixedThreadPool
- 4.3、newCachedThreadPool
- 4.4、newScheduledThreadPool
- 4.5、newWorkStealingPool
- 五、ThreadPoolExecutor 类介绍
- 5.1、线程池的7大核心参数详解
- 1、corePoolSize :核心线程数
- 2、maximumPoolSize:最大线程数
- 3、keepAliveTime :存活时间,线程没有任务执行时最多保持多久时间会终止
- 4、unit:时间单位
- 5、workQueue: 阻塞队列
- 6、ThreadFactory:线程工厂
- 7、handler:拒绝策略
- 7.1、RejectedExecutionHandler接口
- 8、补充参数:allowCoreThreadTimeOut
- 5.2、Excutor 和 Submit的区别
- 5.3、线程池的参数设置
- 六、关闭线程池
- 七、为什么《阿里巴巴Java开发手册》上要禁止使用Executors来创建线程池
- 7.1、Executors各个方法的弊端
- 八、Spring已经实现的线程池
- 九、在SpringBoot中使用线程池
- 9.1、第一种方式
- 9.2、第二种方式
- 9.3、第三种方式
- 十、总结
一、前言
Java中经常需要使用多线程来处理一些业务,我们非常不建议单纯的使用继承Thread类或者实现Runnable接口、Callable接口的方式来创建线程,线程不断的创建,不断的关闭就会过度的消耗CPU资源、线程上下切换(频繁的切换线程,可能会导致系统崩溃)等问题,同时创建过多的线程也会引发资源耗尽的风险(栈溢出问题),因此引入线程池是比较合理的解决方案,方便线程任务的管理,Java中涉及到线程池相关的类均在java.util.concurrent包中,涉及到核心接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
线程池的优势:
- 降低CPU资源的消耗:使得线程可以重复利用,不需要在频繁的创建线程和销毁线程上浪费资源
- 提高响应速度:任务到达时,线程可以不用创建就能执行
- 线程的可管理性:线程是稀缺资源,如果无限制的创建就会严重影响系统效率,线程池可以对线程进行管理、监控、调优
二、创建线程的几种方式
1、继承Thread类创建线程
参考上一篇博文:
2、实现Runnable接口创建线程
参考上一篇博文:
3、实现Callable接口创建线程
参考上一篇博文:
4、通过线程池创建线程
- Java提供了构建线程池的方式(通过Executors.newFixedThreadPool(int) 创建指定数量的线程池)但是阿里编码规范中不允许使用这种方式创建线程,原因是这种方式对线程的控制粒度比较低,通过查看源码可以看到,这中方式创建的线程只允许你指定线程数量,而不能控制核心线程数,最大线程数,拒绝策略等
//这是newFixedThreadPool的源码,可以看到他返回的是Java自己构建的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 推荐使用自己手动创建线程池的方式(就是自己 new 一个ThreadPoolExecutor(…))
三、线程池的执行流程以及常用函数
3.1 、线程的执行流程
- 当线程池中的线程数量小于核心线程数时,会一直创建线程直到线程数量等于核心线程数
- 当线程池中的线程数量 等于核心线程数时,新加入的任务会被放到任务队列等待执行
- 当任务队列已满,又有新任务时,会创建新线程直到线程数量等于最大线程数
- 当线程数量等于最大线程数,且队列任务已满时,新加入的任务就会被拒绝
3.2、线程睡眠(sleep)
//源码
public static void sleep(long millis) throws InterruptedException // 普通函数:设置休眠时间的毫秒数
public static void sleep(long millis,int nanos) throws InterruptedException // 普通函数:设置休眠毫秒数和纳秒数
sleep()是Thread类的一个静态方法,,由Thread.sleep()调用实现,作用是在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),使当前线程从运行状态进入到阻塞状态,sleep()方法会指定休眠时间,线程的休眠时间大于或等于该休眠时间时,该线程会被唤醒,此时它会从 阻塞状态变成 就绪状态然后等待CPU的调度执行。
//使用案例-睡眠一秒钟,模拟网络演示
public void testSonThread(){
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
}
}
3.3、线程等待(wait)
//源码
public final void wait() throws InterruptedException {
wait(0);
}
wait()是Object类的一个方法,wait()方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,wait()方法是让当前正在执行的线程进入等待状态,无限期的等待,直到被唤醒(通过 notify()和notifyAll()方法唤醒)为止。wait()方法、必须要与synchronized一起使用,也就是说wait()和notify()方法是针对获取了object锁的对象进行操作,从语法的角度来说,Obj.wait()、Obj.notify()必须在synchronized(Obj).{…}语句块中,从功能上来说wait()就是在线程获取对象锁后,主动释放对象锁,同时让线程休眠,直到有其它线程调用对象的notify()唤醒该线程,才能继续获得对象锁,并继续执行。相应的notify()方法就是对对象锁的唤醒操作,但是有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized{}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予对象锁,唤醒线程继续执行,这样就提供了线程间同步,唤醒的操作。
3.4、sleep()方法和wait()方法的区别
Thread.sleep()与Object.wait()二者都是可以暂停当前线程,释放CPU的控制权,主要区别在于Obiect.wait()在释放CPU的同时,释放了对象锁的控制。
- sleep()是Thread类中的方法,而wait()是Obiect类中的方法
- 最主要的是sleep()方法没有释放对象锁,而wait()方法释放了锁,使得其它线程可以使用同步控制块或者方法
- 使用范围:wait、notify和notifyAll只能在同步控制方法或者同步代码块中使用,一旦一个对象调用了wait()方法,必须要采用notify()或者notifyAll()方法唤醒该线程,而sleep()方法在任何地方都能使用
- 相同点:sleep()方法必须捕获异常,wait()、notify()、notifyAll()同样也需要捕获异常
3.5、为什么wait(),notify()方法要和synchronized一起使用
因为wait()方法是通知当前线程等待并释放对象锁,notify()方法是通知等待此对象锁的线程重新获得对象锁,然后如果没有获得对象锁,wait方法和notify方法是没有意义的,即必须先 获得对象锁,才能对对象锁进行操作,于是才必须把notify()和wait()方法写到synchronized方法或者是synchronized代码块中。
3.6、线程让步(yield())
yield()的作用是让步,它也是Thread类的一个静态方法,它也可以让当前正在执行的线程暂停,但是并不会阻塞线程,他能让当前线程由 运行状态进入到 就绪状态,从而让其它具有相同优先级的等待线程获得CPU的使用权,但是并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得CPU的执行权,也可能当前线程又进入到运行状态继续执行
3.7、线程中断(interrupt)
//源码
public boolean isInterrupted() //普通函数:判断线程是否被中断
public void interrupt() //普通函数:中断线程执行
interrupt方法定义在Thread类中,由Thread.interrupt()调用实现,该方法将设置该线程的中断标志位,即设置为true,中断的结果线程死亡、还是等待新的任务或者是继续运行至下一步,取决于程序本身,线程会时不时的检测这个中断标志位,以判断线程是否被中断,它并不像stop方法那样会中断一个正在运行的线程。interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,需要用户自己去监视线程的状态并做处理,支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。
3.8、线程强制执行(join)
// Thread类方法
public final void join() throws InterruptedException //普通函数:强制执行
join()方法定义在Thread类中,由Thread.join()调用实现,作用是允许其它线程加入到当前线程中,当某个线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才能继续执行。可以简单的理解为插队,10个人排队买票,突然有个人插队,后面的人得等刚刚插入进来得人买完,才能继续买票。子线程加入到主线程并阻塞了主线程,子线程执行完毕后才恢复主线程的运行。
3.9、线程的关闭
Java终止正在运行的线程主要有以下三种方式
- 使用中断标志位
通过设置一个boolean类型的变量,使用While循环执行线程,当flag标志为true时一直执行线程内容,当flag标志为false时跳出循环不再执行线程内容,此时run()方法执行完毕,线程正常关闭!
public class TicketThread extends Thread {
//定义买票的总数
private static int ticket = 100;
//定义循环终止标记
boolean flag = true;
private ReentrantLock lock = new ReentrantLock();
public void run() {
//确保有票就卖出
while (flag) {
if (ticket <= 0) {
flag = false;
System.out.println("没有票了,售罄!");
return;
}
//使用Synchronized确保每次只有一个线程进入
synchronized (TicketThread.class) {
//lock.lock();
//try {
if (ticket > 0) {
//便于观察线程执行过程,这里使线程休眠一下
try {
Thread.sleep(500);//模拟买票所需要的时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票!");
ticket--;
}
}
}
}
public static void main(String[] args) {
TicketThread thread = new TicketThread();
new Thread(thread, "窗口1").start();
new Thread(thread, "窗口2").start();
new Thread(thread, "窗口3").start();
}
}
- 使用interrupt方法中断线程
要中断一个正在运行中的线程,可调用线程类(Thread)对象的实例方法:interrupt()方法,interrupt()方法并不能真正的停止线程,而是在当前线程打了一个停止的标记,如果在中断时:
- 线程正处于非阻塞状态,则将中断标志设置为true,则此后,一旦线程调用了wait、join、sleep方法中的一种,立马就会抛出InterruptedException异常,且中断标志被清楚,重新设置为false。可以理解为,调用线程的interrupt方法,其本质就是设置该线程的中断标志,将中断标志设置为true,并根据线程的状态决定是否抛出异常。
- 如果此线程正处于阻塞状态(比如调用了wait、sleep、join等方法),则会立即退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获异常处理,然后让线程退出
public static void main(String[] args) throws InterruptedException {
// 创建可中断的线程实例
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("thread 执行步骤1:线程即将进入休眠状态");
try {
// 休眠 1s
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("thread 线程接收到中断指令,执行中断操作");
// 中断当前线程的任务执行
break;
}
System.out.println("thread 执行步骤2:线程执行了任务");
}
});
thread.start(); // 启动线程
// 休眠 100ms,等待 thread 线程运行起来
Thread.sleep(100);
System.out.println("主线程:试图终止线程 thread");
// 修改中断标识符,中断线程
thread.interrupt();
}
执行结果
- 使用stop方法强行终止线程(不推荐使用)
stop方法是@Deprecated注解修饰过的方法,也就是不推荐使用的过期方法,因为stop方法会直接停止线程,这样就没有给线程足够的时间来处理停止前的工作,就会造成数据不完成的问题,因此不建议使用。
3.10、线程死锁
- 当第一个线程拥有A对象锁标记,并等待B对象锁 标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记,就会产生死锁
- 一个线程可以同时拥有多个对象锁的标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
死锁的概念:当多个线程因竞争系统资源或相互通信而处于半永久阻塞状态时,若无外力作用,这些线程都将无法向前推进,这些线程将无限期的等待此组线程中的某个线程占用的,自己永远无法得到的资源,这种现象称为死锁。
通过以下按钮演示死锁的发生:假如两个人A和B再桌子上同时吃饭,桌子上只有一双筷子,当一个人拥有两根筷子的是hi才能吃
/**
*锁对象(筷子)
*/
public class Chopsticks {
private String color;//颜色
private Integer sum;//数量
private Integer length;//长度
private Integer width;//宽度
}
package com.study.threadStudy.service;
import com.study.threadStudy.entity.Chopsticks;
/**
* 测试死锁问题
*/
public class TestChopSticks {
public static void main(String[] args) {
//创建两个筷子对象
Chopsticks chopsticksA=new Chopsticks();
Chopsticks chopsticksB=new Chopsticks();
//A
Runnable A=new Runnable() {
@Override
public void run() {
//持有第一根筷子
synchronized (chopsticksA){
System.out.println("A拿到了一根筷子");
//持有第二根筷子
synchronized (chopsticksB){
System.out.println("A拿到了第二根筷子,一双筷子拿到了,开始吃饭");
}
}
}
};
//B
Runnable B=new Runnable() {
@Override
public void run() {
//持有第一根筷子
synchronized (chopsticksB){
System.out.println("B拿到了一根筷子");
//持有第二根筷子
synchronized (chopsticksA){
System.out.println("B 拿到了第二根筷子,一双筷子拿到了,开始吃饭");
}
}
}
};
//开启A和B,开始抢筷子
new Thread(A).start();
new Thread(B).start();
}
}
//运行结果
A拿到了一根筷子
B拿到了一根筷子
此时A和B各自持有一双筷子,并且都在等待对方持有的另一根筷子,导致两个人都吃不了饭。可以通过sleep()方式使其中一个线程休眠一会,A(B)吃完B(A)再吃,或者把A(B)同步代码块中的锁换一下位置,一开始两个人都抢同一根筷子,没抢到的等另一个吃完饭再吃。
3.10.1、如何查看线程死锁
可以通过这个工具(位于jdk安装路径bin目录)去排查,现在介绍一下
可以看到两个死锁
3.10.2、如何避免死锁
- 同一个代码块,不要同时持有两个对象锁
- 同一个方法只能被一个线程使用,不要嵌套锁,独木桥每次只能通过一个人
- 如果业务场景需要一次锁定多个资源对象,可以定义锁的先后顺序,例如通过sleep()方法,打破死锁的互相占有
四、Java创建线程池的五种方式
首先,我们的线程池类型一共有四种 newSingleThreadPool、newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool,在JDK1.8时又加入了一种:newWorkStealingPool,所以一共是五种
线程池类型 | 解释 |
newSingleThreadPool | 创建单核心的线程池 |
newFixedThreadPool | 创建指定数量的线程池,可控制线程的最大并发数 |
newCachedThreadPool | 创建一个自动增长的线程池 |
newScheduledThreadPool | 创建一个按照计划规定执行的线程池 |
newWorkStealingPool | 创建一个具有抢占式操作的线程池 |
4.1、ewSingleThreadPool
//创建一个单核心的线程池
ExecutorService service=Executors.newSingleThreadPool();
//源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
创建一个单线程化的线程池子,他只会用唯一的工作线程来执行任务,保证所有的任务按照指定的顺序和优先级执行。SingleTHreadPool的意义在于统一所有外界任务在一个线程中,使得这些任务之间不需要处理线程同步的问题。
4.2、newFixedThreadPool
//这里就是创建一个线程池,里面有十个线程
ExecutorService service=Executors.newFixedThreadPool(10);
//newFixedThreadPool的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建指定数量的线程池,可控制线程的最大并发数,超出数量的线程会在队列中等待,它是一种线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池关闭,当所有的线程都处于运行状态时,新的任务将会处于队列等待状态,直到有线程空闲出来,等待状态中的任务才会被执行。FixedThreadPool只有核心线程,且核心线程都不会被回收,这意味着它可以更快的响应外界的请求
4.3、newCachedThreadPool
//创建一个自动增长的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如果没有可回收,则新建线程。由Executors的newCachedThreadPool方法创建的线程池不存在核心线程,只存在数量不定的核心线程,而且其数量最大值为Integer.MAX_VALUE,当线程池中的线程都处于活跃状态时(全满)线程池就会创建新的线程来处理新的任务,线程池中的空闲线程都有超时机制,默认超时时间为60s,超过60s的空闲线程就会被回收,和FixedThreadPool不同的是,CachedThreadPool的任务队列其实相当于是一个空的集合,这将导致任何进来的任务都将会被执行,因为在这种场景下SynchronousQueue是不能插入任务的,SynchronousQueue是一个特殊的队列,在很多情况下可以理解为一个无法存储元素的队列,当CachedThreadPool的特性看,这类线程比较适合执行大量耗时任务较小的任务,当整个线程池都处于闲置状态时,线程池中的线程都会因为超时而被停止回收,几乎是不占任何系统资源的。
4.4、newScheduledThreadPool
//创建一个按照计划规定执行任务的线程池
ExecutorService executorService = Executors.newScheduledThreadPool(2);
//源码
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
通过Executors.newScheduledThreadPool方式创建的线程池,核心线程数量固定,而非核心线程数量是没有限制的,并且当核心线程闲置时他才会被立即回收,ScheduledThreadPool这类线程池主要用于执行定时任务和具有固定时期的重复任务。
4.5、newWorkStealingPool
//创建一个具有抢占式操作的线程池,也称之为 任务窃取式线程池
ExecutorService executorService = Executors.newWorkStealingPool();
//源码
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
newWOrkStealingPool时Java1.8新添加的线程池,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序,和另外四种不同的是,它用的是ForkJoinPool,使用ForkJoinPool的好处是,把一个任务拆分成多个小任务分发到多个线程上去执行,这些小任务都执行完成后,再将结果合并。newWorkStealingPool中每一个线程都有自己的队列,当线程发现自己的队列没有任务了,就会到别的线程的队列中获取任务执行,可以简单的理解为窃取。
**使用场景:**能够合理的使用CPU进行对任务操作(并行操作),适合使用在很耗时的任务中。底层用的ForkJoinPool 来实现的。 ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。
五、ThreadPoolExecutor 类介绍
ThreadPoolExecutor是线程池最核心的一个类,它为线程池提供了四个构造方法,我们来看一下其中最原始的一个构造方法,其余三个都是由它衍生而来
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;
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
5.1、线程池的7大核心参数详解
可以看到这个构造方法有7个参数,这7个参数直接影响到线程池的效果,以下进行详细解析:
1、corePoolSize :核心线程数
表示当前线程池的核心线程数,当初始化线程池时会创建核心线程进入等待状态,即使他是空闲的,核心线程也不会被回收摧毁,从而降低了任务一来就要创建新线程的时间和性能开销。设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。
2、maximumPoolSize:最大线程数
表示当前线程池的 最大线程数量,意味着核心线程都被用完了,那就只能创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务。
3、keepAliveTime :存活时间,线程没有任务执行时最多保持多久时间会终止
表示非核心线程的存活时间,除了核心线程以外,那些被新创建出来的线程可以存活多久,意味着这些新的线程一旦完成任务,而后面都是空闲状态时,就会在设定的时间到达后被回收摧毁。
4、unit:时间单位
存活时间单位,它们取值有以下几种
单位名称 | 取值 |
TimeUnit.DAYS; | 天 |
TimeUnit.HOURS; | 小时 |
TimeUnit.MINUTES; | 分钟 |
TimeUnit.SECONDS; | 秒 |
TimeUnit.MILLISECONDS; | 毫秒 |
TimeUnit.MICROSECONDS; | 微妙 |
TimeUnit.NANOSECONDS; | 纳秒 |
5、workQueue: 阻塞队列
表示任务的 阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那些还未被执行的任务就会进入队列中排队,可以分为有界、无界、同步移交三种队列类型,当核心线程数都在被使用时,新提交的任务就会被存储到阻塞队列中,BlockingQueue(任务阻塞队列)的实现类主要有以下几种:
- ArrayBlockingQueue:基于数组的先进先出队列,有界(创建队列时指定队列的最大容量)
- LinkedBlockingQueue:基于链表的先进先出队列,无界与有界队列相比,除非系统资源耗尽,否则不存在任务入队失败的情况
- synchronousQueue:没有容量,总是将新的任务提交给线程执行,如果没有空闲线程则尝试创建新的线程,如果线程数大于最大线程数(maximumPoolSize),则执行拒绝策略
- PriorityBlockingQueue:一个具有优先级的无界阻塞队列,总是具有最高优先级的任务先执行
6、ThreadFactory:线程工厂
表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
7、handler:拒绝策略
7.1、RejectedExecutionHandler接口
//源码
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
- RejectedExecutionHandler接口里面就只有一个方法,当要创建的线程属于大于线程池的最大线程数时,新的任务就会被拒绝,就会调用这个接口里的这个方法
- 我们可以自己实现这个接口,实现对这些超出数量的任务进行处理
表示 拒绝策略(也叫 饱和策略),当队列已满且工作线程大于等于线程池的最大线程数(包括核心线程)时,就会采用拒绝策略去拒绝新进来的任务,以下是几种常见的拒绝策略:
- AbortPolicy:直接抛出RejectedExecutionException(拒绝执行异常),默认的拒绝策略
//查看源码可知它的逻辑就是直接抛出异常
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- DiscardPolicy:什么也不做,直接忽略
//查看源码,这个处理策略就是什么都不做的
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy:丢弃执行任务队列中最老的任务,尝试为当前提交的任务腾出位置
//查看源码发现当任务拒绝添加时会抛弃队列中最旧的任务,再把这个新的任务添加进去
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
- CallerRunsPolicy:直接由提交任务着执行这个任务
//查看源码,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回调到调用者去执行
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
8、补充参数:allowCoreThreadTimeOut
该属性用来控制是否允许核心线程超时退出,默认值为false,如果线程池的大小已经达到了核心线程数,不管有没有任务执行,线程池都会保证这些核心线程处于存活状态,可以直到该属性是用来控制核心线程的
ps:通过corePoolsize和maximumPoolSize来控制如何新增线程,通过allowCoreThreadTimeOut和keepAliveTime,控制如何销毁线程
5.2、Excutor 和 Submit的区别
- Excutor用于提交不需要返回值的任务,线程在执行完后既不会返回结果,也不会抛出异常
- Submit则是用于提交需要返回值的任务,在线程执行完Submit提交的任务后,会返回一个 future 对象,这个 future 对象可以用来判断任务是否执行成功,并且可以用 future 的get()方法来获取其返回值
5.3、线程池的参数设置
- 计算密集型(CPU密集型)的任务比较消耗CPU,所以一般核心线程数设置的大小等于或者略微大于CPU的核数,一般为N+1(N代表CPU的核数)
- 磁盘密集型(IO密集型)的任务主要时间消耗在IO等待上,CPU压力并不大,所以线程数一般设置较大,例如多线程访问数据库,数据库有128个表,可以直接考虑使用128个线程,IO密集型一般为2*N(N为CPU的核数)
- 设置参数时需要注意以下两点:
- corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;
- workQueue设置不当容易导致OOM(Out Of Memory);
六、关闭线程池
- shutdownNow():立即关闭线程池(暴力),正在执行中的以及队列中的任务会全部中断,同时该方法会返回被中断的队列中的任务列表
- shutdown():平滑关闭线程池,正在执行中的以及队列中的任务能够执行完成,后续进来的任务会被执行拒绝策略
- isTerminated():当正在执行的以及队列中的任务全部都执行(清空)完后就会返回true
七、为什么《阿里巴巴Java开发手册》上要禁止使用Executors来创建线程池
Executors创建出来的线程池使用的都是无界队列,而使用无界队列会带来很多弊端,最重要的就是,它可以无限保存任务,因此很有可能会造成OOM(内存溢出)异常,同时这种方式创建的线程池对线程的控制粒度比较低,比如我们无法控制上述的线程池中最重要的7个核心参数。
7.1、Executors各个方法的弊端
- newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM内存溢出
- newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大是:Integer.MAX_VALUE,可能会创建数量非常多的线程,导致OOM内存溢出
- @Async是Spring的注解,默认异步配置的是SimpleAsyncTaskExecutor,该线程池默认是来一个任务创建一个线程池,若在系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。
八、Spring已经实现的线程池
- SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程
- SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
- ConcurrentTaskExecutor:Executor 的适配类,不推荐使用。如果 ThreadPoolTaskExecutor 不满足要求时,才用考虑使用这个类。
- SimpleThreadPoolTaskExecutor:是 Quartz 的 SimpleThreadPool 的类。线程池同时被 quartz 和非 quartz 使用,才需要使用此类。
- ThreadPoolTaskExecutor :最常使用,推荐。其实质是对 java.util.concurrent.ThreadPoolExecutor 的包装。
九、在SpringBoot中使用线程池
9.1、第一种方式
我们自定义一个线程池,然后使用依赖注入,注入到需要使用到线程池的类中,然后调用exector方法并new 一个Runnable接口并重写run方法来开启多线程处理
- 新增线程池配置类
package com.study.threadStudy.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@Slf4j
public class ThreadPoolConfig {
@Bean(value = "threadExecutor")
public ThreadPoolTaskExecutor executorService() {
log.info("start threadpool");
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//获取运行机器的CPU核数,作为核心线程数量
Integer availibleNum = Runtime.getRuntime().availableProcessors();
//配置-核心线程数
executor.setCorePoolSize(availibleNum);
//配置-最大线程数 线程数*2
executor.setMaxPoolSize(availibleNum * 2);
//配置-队列容量,队列大小
executor.setQueueCapacity(9999);
//配置-非核心线程存活时间
executor.setKeepAliveSeconds(60);
//配置-线程池中线程的名称前缀
executor.setThreadNamePrefix("test_jiaoThread");
//配置-拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
}
- 在业务层使用@Resource注解注入线程池,并开启子线程执行一个循环输出,为了效果的明显,我这里子线程延时一秒钟
package com.study.threadStudy.service;
import com.study.threadStudy.config.ThreadPoolConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Service
@Slf4j
public class ThreadPoolService {
//这里如果你使用@Autowired注解注入可能会报错哦,你自己百度一下两个注解的区别,这里不做讲解了
@Resource(name = "threadExecutor")
private ThreadPoolTaskExecutor taskThreadExector;
public void testThread() {
log.info("主线程开始执行-------");
taskThreadExector.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "我是子线程" + i);
}
}
});
//主线程
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + "我是主线程" + i);
}
}
}
- 新建Controller,这里只是为了方便测试,你也可以按照你自己的想法来触发这个多线程
package com.study.threadStudy.controller;
import com.study.threadStudy.service.ThreadPoolService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class ThreadPoolController {
@Autowired
ThreadPoolService threadPoolService;
@RequestMapping("/threadPool")
public String test() {
threadPoolService.testThread();
return "测试成功";
}
}
- 浏览器访问一下地址,查看结果
http://localhost:9000/test/threadPool
- 查看结果,发现主线程的方法体已经执行完了,但是子线程的方法体还在执行,说明成功开启了异步执行
// ........
http-nio-9000-exec-1我是主线程46
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
test_jiaoThread1我是子线程5
test_jiaoThread1我是子线程0
test_jiaoThread1我是子线程1
test_jiaoThread1我是子线程2
test_jiaoThread1我是子线程3
// ........
9.2、第二种方式
使用Spring框架提供的@Async注解(并重写默认的线程池),实现创建线程池并开启多线程,但是需要注意的是@Async注解的使用有一些小坑哦,使用这个注解要么你就重写默认的线程池,要么你就使用name属性指定你自定义的线程池,至于原因,上面第六条已经说的很清楚了。
- 新增配置类,注意添加@EnableAsync注解开启异步处理,并实现 AsyncConfigurer接口,重写默认的线程池
package com.study.threadStudy.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolSyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
//获取运行机器的CPU核数作为最大核心线程数
Integer availibleNum=Runtime.getRuntime().availableProcessors();
//配置-核心线程数
executor.setCorePoolSize(availibleNum);
//配置-最大线程数
executor.setMaxPoolSize(availibleNum*2);
//配置-队列的最大容量
executor.setQueueCapacity(9999);
//配置-非核心线程最大存活时间
executor.setKeepAliveSeconds(60);
//配置-线程名称
executor.setThreadNamePrefix("SyncThreadJiao");
//配置-饱和策略、拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//初始化线程池,这里不初始化,下面调用子线程的时候会报错的
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){
return null;
}
}
- 在业务层新建两个方法,一个用于执行主线程逻辑,一个用于处理子线程逻辑,重点是在子线程逻辑方法上增加@Async注解
package com.study.threadStudy.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class ThreadSyncService {
public void testThread(){
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName()+"我是主线程"+i);
}
//不要在这里调用下面的子线程方法,不然你会发现他还是走的主线程
}
@Async
public void testSonThread(){
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
}
}
}
- controller层中新增一个方法访问这个Service,记住先把上面的Service注入进去先
package com.study.threadStudy.controller;
import com.study.threadStudy.service.ThreadPoolService;
import com.study.threadStudy.service.ThreadSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class ThreadPoolController {
@Autowired
ThreadPoolService threadPoolService;
@Autowired
ThreadSyncService threadSyncService;
@RequestMapping("/threadPool")
public String test() {
threadPoolService.testThread();
return "测试成功";
}
@RequestMapping("/threadSync")
public String testSyncThread() {
threadSyncService.testThread();
//调用子线程方法
threadSyncService.testSonThread();
//调用子线程方法
threadSyncService.testSonThread();
return "测试成功";
}
}
- 查看结果,成功执行了异步,效果和第一种方式是一样的
//........
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
2022-05-30 16:31:26.563 INFO 14464 --- [nio-9000-exec-1] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService
SyncThread_Jiao1我是子线程0
SyncThread_Jiao1我是子线程1
SyncThread_Jiao1我是子线程2
SyncThread_Jiao1我是子线程3
//........
9.3、第三种方式
使用@Async注解,使用自定义线程池,这种方式的逻辑和上面第二种方式基本上是一样的,不一样的地方在于
- 配置类中不是通过实现 AsyncConfigurer接口,而是自定义了一个线程池,重点是会声明这个线程池的名称
package com.study.threadStudy.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@EnableAsync
@Configuration
@Slf4j
public class ThreadPoolCustomConfig {
@Bean(value = "threadCustomExecutor")
public Executor executorService() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//获取运行机器的CPU核数,作为核心线程数量
Integer availibleNum = Runtime.getRuntime().availableProcessors();
//配置-核心线程数
executor.setCorePoolSize(availibleNum);
//配置-最大线程数 线程数*2
executor.setMaxPoolSize(availibleNum * 2);
//配置-队列容量,队列大小
executor.setQueueCapacity(9999);
//配置-非核心线程存活时间
executor.setKeepAliveSeconds(60);
//配置-线程池中线程的名称前缀
executor.setThreadNamePrefix("customExecutor_jiao");
//配置-拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//初始化线程池
executor.initialize();
return executor;
}
}
- 在使用的时候声明@Async注解名称的时候,添加上你的自定义线程池的名称
@Async("threadCustomExecutor")
public void testSonThread(){
for (int i = 0; i < 50; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
}
}
- 执行结果
//.......
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
customExecutor_jiao1我是子线程0
customExecutor_jiao1我是子线程1
customExecutor_jiao1我是子线程2
//.......
十、总结
Java多线程的问题很复杂,小小的学习总结,可能不够完美和充分,有问题的地方欢迎指导补充。