本文代码示例已放入github:请点击我
快速导航------>src.main.java.yq.Thread.MyThreadPool
什么是线程池?
答:线程池就相当于是线程的管理者,他会帮我们去创建线程,回收线程。
使用线程池的好处是什么?
答:使用线程池的好处有一下几点
1.会提高效应效率,因为线程池中存在空闲线程,可直接进行执行,就省去了线程创建时间
2.降低资源消耗,创建线程和销毁线程都会消耗系统资源,而线程池里存在空闲线程,而且线程也会复用,不会立即销毁。
3.提高线程的可管理性,前面我们在 -- java并发编程之多线程基础 -- 中就说过了,线程是双刃剑,需要合理利用,如果线程 数超过一定数量,CPU会经不起消耗,这样反而会导致性能下降,所以我们需要合理创建线程。而线程池内部实现了队列,线程池将会多出来的任务存入到队列里面,那么这样就不会存在同时创建大量的线程了。而且线程池可以控制空闲的线程的存活时间,如果空闲线程在指定时间没用执行那么就会被销毁。如果想了解队列--------> java并发编程之队列
线程池的使用场景?
答:在会频繁创建线程,销毁线程,以及大量并发的时候都可以使用我们的线程池进行管理线程。
怎么使用线程池?
首先我们看看下面的这两个类:
- ThreadPoolExecutor:自定义线程池,可以自己根据需求创建线程池。
- Executors:JDK已经实现好了的几种线程池方案,返回值是ExecutorService接口,ThreadPoolExecutor也实现该接口。
1:ThreadPoolExecutor
这就是ThreadPoolExecutor的几个构造函数,那么该构造函数有什么用呢?我们挑最多的来解释把。
名称 | 作用 |
corePoolSize | 核心线程数,如果当前线程池中核心线程数<corePoolSize,那么有新的任务就会创建一个新的线程 |
maximumPoolSize | 最大线程数,允许存在最大线程数量,如果超过该数量就执行拒绝策略 |
keepAliveTime | 空闲回收时间,当线程池中的核心线程数大于corePoolSize的时候,超过这个时间,那么该线程会被终止 |
unit | 时间单位,设置keepAliveTime的时间单位 |
workQueue | 保存多出来的任务队列(阻塞,当任务数量超过corePoolSize就是保存到workQueue里面。 |
threadFactory | 可以指定线程创建策略,创建线程的策略,大多数情况使用默认即可 |
handler | 拒绝策略,当任务数量大于workQueue+maximumPoolSize的时候执行的决绝策略,大多数情况使用默认即可。 |
另外在介绍几个线程池常用的方法:
方法 | 作用 |
void shutdown(); | 关闭线程池,就算有线程没用执行完毕,也会进行关闭线程 |
boolean isShutdown(); | 判断线程池是否关闭 |
<T> Future<T> submit(Callable<T> task); | 执行任务的方法 ,这里只是举例,当然参数类型还可以是 Runnable 类型 ,该方法有返回值,可以抛出异常 |
void execute(Runnable command); | 没用返回值的执行任务的方法,不可以抛出异常,只能是Runnable类型 |
好了对于线程池我们也基本理解了,接下来我们就是用代码更深入理解ThreadPoolExecutor:
/**
* 第一个例子
* 实现思路:使用CyclicBarrier模拟高并发情况。一次当线程数达到30才进行执行
* 。但是我们的线程池设置的最大线程数和队列数量是不够30的 所以肯定会发生拒绝策略。
*/
static final class MyThreadPoolExecutor01 extends Thread {
//屏障 用于模拟多线程并发访问
private CyclicBarrier cyclicBarrier;
private ThreadPoolExecutor threadPoolExecutor;
public MyThreadPoolExecutor01(CyclicBarrier cyclicBarrier,ThreadPoolExecutor threadPoolExecutor) {
this.cyclicBarrier = cyclicBarrier;
this.threadPoolExecutor = threadPoolExecutor;
}
@Override
public void run() {
System.out.println("我创建成功了-----------》"+Thread.currentThread().getName());
try {
//阻塞,只有30个线程就绪之后才会被放行
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
threadPoolExecutor.execute(new RunThread());
}
public static void main(String[] args) {
//达到30和线程之后 统一放开
CyclicBarrier cyclicBarrier = new CyclicBarrier(30);
//创建一个 核心线程数为4 最大线程数为8,队列最大容量为8 自定义拒绝策略
ThreadPoolExecutor threadPoolExecutor1 = new ThreadPoolExecutor(4,
8, 3L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(8),new MyHandler());
//理论上 我们的这个线程池可以同时运行最大16个线程
while (true){
//为了防止跑得太快 我们阻塞一下生产的速度
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
new MyThreadPoolExecutor01(cyclicBarrier,threadPoolExecutor1).start();
}
}
/**
* 这是线程池的拒绝策略,
* 当任务数量大于 maximumPoolSize + workQueue 队列数量的时候的拒绝策略
*/
static final class MyHandler implements RejectedExecutionHandler{
//拒绝的时候发生的策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString()+"被我们的线程池拒绝了");
}
}
//该类是用来运行的线程
static final class RunThread implements Runnable{
@Override
public void run() {
System.out.println("我是线程---->"+Thread.currentThread().getName());
}
}
}
运行结果:
从上面的例子可以看到,当我们创建线程到30的时候,我们的线程池开始运行了,但是我们的线程只执行到了八,就执行了拒绝策略,这是为什么呢?因为我们最大线程数是设置的8,队列数量设置的也是8,也就是我们一共能接下16个任务,但是我们却使用CyclicBarrier让30个线程同时并行执行,那么16后面的线程肯定会被拒绝。
必读:
上面的例子,我们使用的是execute进行执行,这个方法是没用返回值的,如果想要线程执行有返回值,请使用submit进行执行任务,任务类就需要继承Callable<T>接口,T是返回类型,使用了submit之后会返回一个Future对象,使用Future对象的get方法就可以获取到返回值,但是这个返回值不是即时的,给你的值,并不代表就是真实的执行结果,因为我们都知道,多线程是在并行进行执行的,当在主线程中开一个线程进行执行的时候,马上就会执行下一步,如果这个时候我们的子线程还没有执行完毕,你就把结果拿着跑了,这样肯定是不合理的,另外get方法可以设置时间参数,如果指定时间没用运行完毕拿到真实执结果,那么就会抛出异常。下面的例子就是使用submit方式进行获取返回值,供参考。
@PostMapping(value = "/api/clothAward")
public Object clothAward(@RequestBody JackpotParam jackpotParam){
CountDownLatch countDownLatch = new CountDownLatch(1);
try{
Optional<MyOrder> myOrder = myOrderRepo.findById(jackpotParam.getMyOrderId());
Assert.isTrue(myOrder.isPresent(),"该订单已经参与布奖,可以执行修改布奖操作");
//使用线程池进行调用线程执行
Future<Object> submit = threadPoolExecutor.submit(new JackPotCallable(jackpotParam, boxService, jackpotService, productService, false, getUserId()));
//调用拿到结果的方法
Object object = getObject(countDownLatch, submit);
//阻塞主线程,只有拿到真实结果才允许放行,不然会导致返回客户端结果不一致
countDownLatch.await();
return object;
}catch (Exception e){
countDownLatch.countDown();
throw new DIYException("布奖失败:"+e.getMessage());
}
}
//拿到执行结果的方法
public Object getObject(CountDownLatch countDownLatch,Future<Object> submit){
Object result = null;
while (true){
try {
//25秒没用拿到执行结果,那么会抛出异常
result = submit.get(25,TimeUnit.SECONDS);
if(result != null){
countDownLatch.countDown();
break;
}
} catch (Exception e) {
throw new DIYException("获取布奖结果超时:"+e.getMessage());
}finally {
countDownLatch.countDown();
}
}
return result;
}
上面的例子就是拿到执行结果的方法,但是意义不大,因为这样就是个单线程了,主线程进行阻塞了,只是一个例子,举例说明怎么获取执行结果。
好了自定义线程池 ThreadPoolExecutor 就到这里了,有人说线程池,每次都要自己创建,那我忘记哪些参数怎么配置怎么办。
JDK为我们封装好了几个线程池,我们可以直接拿来使用即可。
2:Executors
该类的方法都是静态方法,可以直接进行调用
在介绍了ThreadPoolExecutor之后,这里我们就不过多使用代码演示,因为基本上使用方法都是一样的。我们介绍Executors的常用几种方法
newCachedThreadPool:一个最大线程数接近无限大的一个线程池,空余线程回收时间为1分钟,如果使用这个基本上就不会发生我们上面自定义实现的 ThreadPoolExecutor 出现拒绝策略,因为我们最大线程数才8,而这个线程池为 Integer.MAX_VALUE 这么大
newFixedThreadPool:该线程池创建时要指定最大线程数,他的最大线程数和核心线程数都是一样的,并且如果有空余线程会立马被回收。但是他有一个LinkedBlockingQueue队列,基本上也算是一个无限大的一个线程池了。
newScheduledThreadPool:核心线程数为指定的线程数,但是最大线程数也是Integer.MAX_VALUE,并且支持定时执行。
newSingleThreadExecutor:改线程的核心线程数和最大线程数都是1,空余线程会被立马回收,但是队列是LinkedBlockingQueue。
这里我我们就使用一下 newScheduledThreadPool 可定时指定的线程池
/**
* 例子二 Executors
* 因为其他几个都是一样的 我们只演示 newScheduledThreadPool 定时执行
*/
static final class MyThreadPoolExecutor02 extends Thread{
public static void main(String[] args) {
//该线程池 核心线程数 0 最大 基本上接近无限大 为Integer.MAX_VALUE 空闲回收时间为60秒
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
while (true){
//三秒执行一次 其余的任务会被缓存到 队列里面
scheduledExecutorService.schedule(new RunThread(),3,TimeUnit.SECONDS);
}
}
//该类是用来运行的线程
static final class RunThread implements Runnable{
@Override
public void run() {
System.out.println("我是线程---->"+Thread.currentThread().getName());
}
}
}
至于这个演示的结果,我们就不展示了。他的意思就是,三秒会执行一次,然后其余的任务会保存到队列里面。
好了,到了这里我们的的线程池就讲解完毕了,如果有什么不对的地方请指出,谢谢大家阅读
另外线程不能胡乱创建,配置线程数量可分为两种:CPU密集型,和IO密集型号,这里就不过多解释。
本文代码示例已放入github:请点击我
快速导航------>src.main.java.yq.Thread.MyThreadPool