「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」
前言
- 关于作者:励志不秃头的一个CURD的Java农民工,想挑战看看自己能完成多少天的更文挑战
- 关于文章:在工作中经常会用到多线程开发,面试中频率有比较高,下面我们来聊聊线程,其中,线程池个人觉得比较重要
创建线程的方式
- 继承Thead类,重写run方法
- 通过Runnable接口
- 通过Callable接口创建,异步,可返回结果
- 通过线程池创建
- ForkJoinPool 线程池
继承Thead类
class MyThread extends Thread {
@Override
public void run() {
//逻辑执行代码
System.out.println("XXX");
}
}
- Java中类是单继承的,如果继承了Thread了,该类就不能再有其他的直接父类了
- 从操作上分析,继承方式更简单,获取线程名字也简单
- 调用 start 方法后, 会等待 JVM 为当前线程分配到 CPU 时间片会调用 run 方法执行。
实现Runnable接口
// 定义线程
class MyRunner implements Runnable {
@Override
public void run() {
//逻辑执行代码
System.out.println("XXX");
}
}
// 线程执行
Thread thread = new Thread(new MyRunner());
thrad.start();
- Java中类可以多实现接口,此时该类还可以继承其他类,并且还可以实现其他接口(设计上,更优雅)
- 从操作上分析,实现方式稍微复杂点,获取线程名字也比较复杂,得使用Thread.currentThread()来获取当前线程的引用
- 从多线程共享同一个资源上分析,实现方式可以做到(是否共享同一个资源)
通过Callable接口创建
class MyCallable implements Callable {
@Override
public String call() throws Exception {
return "XXX";
}
}
FutureTask futureTask = new FutureTask<>(new MyCallable());
futureTask.run();
通过线程池创建
ThreadPoolExecutor,创建线程池的核心工具类,创建线程池后,通过execute或者 submit方法提交线程执行任务,进入线程池中。等待线程池为分配资源执行或指定的拒绝策略拒绝线程。
ForkJoinPool
ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。两个抽象子类:RecursiveAction 和 RecursiveTask。
- RecursiveTask:代表有返回值的任务;
- RecursiveAction:代表没有返回值的任务;
线程的状态有哪些?(线程的生命周期)
线程池
线程池的核心参数
- corePoolSize:核心线程数,核心线程会一直存活,即使没有任务需要执行
- 当线程数小于核心线程数时(还未满,就会一直增),即使有线程空闲,线程池也会优先创建新线程处理
- 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
- maxPoolSize:最大线程数
- 当线程数>corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务,直到线程数量达到maxPoolSize
- 当线程数已经=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
- queueCapacity:任务队列容量(阻塞队列)
- 当核心线程数达到最大时,新任务会放在队列中排队等待执行
- keepAliveTime:线程空闲时间
- 当线程空闲时间达到keepAliveTime时,线程会被销毁,直到线程数量=corePoolSize
- 如果allowCoreThreadTimeout=true,则会直到线程数量=0(这个特性需要注意)
- allowCoreThreadTimeout:允许核心线程超时(如上,会影响keepAliveTime哦)
- rejectedExecutionHandler:任务拒绝处理器(用户可以自定义拒绝后的处理方式)
rejectedExecutionHandler拒绝处理任务
两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,且任务队列已满时,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务(并不是立马停止,而是执行完再停止)。
若拒绝后,此时,线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值是AbortPolicy,会抛出异常
hreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy:丢弃任务,抛运行时异常
- CallerRunsPolicy:执行任务(这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功);如果执行器已关闭,则丢弃.
- DiscardPolicy:对拒绝任务直接无声抛弃,没有异常信息
- DiscardOldestPolicy: 对拒绝任务不抛弃,而是抛弃队列里面等待最久的(队列头部的任务将被删除)一个线程,然后把拒绝任务加到队列(Queue是先进先出的任务调度算法,具体策略会咋下面有分析);(如果再次失败,则重复此过程)
- 实现RejectedExecutionHandler接口,可自定义处理器(可以自己实现然后set进去)
private class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// 核心改造点,由blockingqueue的offer改成put阻塞方法
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadPoolExecutor处理任务的顺序、原理
一个任务通过 execute(Runnable) 方法被添加到线程池,任务就是一个 Runnable 类型的对象,任务的执行方法就是 Runnable 类型对象的 run() 方法。
当一个任务通过 execute(Runnable) 方法欲添加到线程池时,线程池采用的策略如下(即添加任务的策略):
- 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的数量等于 corePoolSize ,但是缓冲队列 workQueue 未满,那么任务被放入缓冲队列。
- 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量小于maximumPoolSize ,建新的线程来处理被添加的任务。
- 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler 所指定的策略来处理此任务。
简要概括如下:
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满
- 若线程数小于最大线程数,创建线程
- 若线程数等于最大线程数,抛出异常,拒绝任务
为什么不允许使用Executors去创建
在Executors 类里面还有几个方法:newFixedThreadPool(),newCachedThreadPool() 等几个方法,实际上也是间接调用了ThreadPoolExocutor,不过是传的不同的构造参数。
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool: 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
如何停止线程池里的子线程 **
在内存中存储线程的id,通过shell命令去kill
- thread run的第一秒,默认直接get线程id值,然后写到concurrentHashMap中
- 任何时候要终止线程的时候,使用java调用shell命令:kill -9 processId
好了,以上就是本篇文章的全部内容,多线程在工作中使用还是要多注意下细节的,出了bug排查起来也会比一般的要复杂多。我是新生代农民工L_Denny,我们下篇文章见。