线程池可以说是面试热门问题了,所以是必须要搞懂的东西,但是理顺比较难啊。我勉强试试。
固定线程池
固定数量线程池,就是可以做到每次执行固定个数的线程。比如线程池大小为4.那么每次都有四个线程在运行。
用时间戳记录,会更加直观。
package com.youngthing.interpolation.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 9/4/2022 10:17 PM 创建
*
* @author 花书粉丝
*/
public class ExecutorDemo {
private static final AtomicInteger I = new AtomicInteger();
public static void main(String[] args) {
final ExecutorService executorService = Executors.newFixedThreadPool(4);
final Runnable runnable = () -> {
try {
Thread.sleep(1000);
System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int j = 0; j < 12; j++) {
executorService.execute(runnable);
}
executorService.shutdown();
}
}
打印结果:
i = 0:1662303030
i = 3:1662303030
i = 2:1662303030
i = 1:1662303030
i = 6:1662303031
i = 5:1662303031
i = 4:1662303031
i = 7:1662303031
i = 8:1662303032
i = 9:1662303032
i = 11:1662303032
i = 10:1662303032
可见线程池大小为4的时候,每次就只有四个线程在运行。
newSingleThreadExecutor,这个不过是将大小固定为1的固定线程池而已,不多赘述。
缓存线程池
缓存线程池不能指定线程数量,所以更加灵活。它在任务增加时会多创建一些线程,在任务减少时,会回收一些线程,可以说是一种“弹性计算”。这个回收时间默认是60秒。把上面代码改一改,会发现缓存的线程池是几乎同时执行所有线程的,代码如下:
package com.youngthing.interpolation.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 9/4/2022 10:17 PM 创建
*
* @author 花书粉丝
*/
public class CachedExecutorDemo {
private static final AtomicInteger I = new AtomicInteger();
public static void main(String[] args) {
final ExecutorService executorService = Executors.newCachedThreadPool();
final Runnable runnable = () -> {
try {
Thread.sleep(1000);
System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int j = 0; j < 12; j++) {
executorService.execute(runnable);
}
executorService.shutdown();
}
}
执行结果:
i = 4:1662302891
i = 0:1662302891
i = 8:1662302891
i = 7:1662302891
i = 2:1662302891
i = 11:1662302891
i = 1:1662302891
i = 5:1662302891
i = 9:1662302891
i = 6:1662302891
i = 3:1662302891
i = 10:1662302891
定时线程池
四大线程池介绍了三个,现在这是第四个。这个有点特殊,上述三个都是返回ExecutorService,而newScheduledThreadPool返回的是ScheduledExecutorService,是ExecutorService的子类,提供了一些定时的方法比如延迟方法。同时也支持固定线程池的功能,把上面例子改写,记录下启动时间就知道了:
package com.youngthing.interpolation.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 9/4/2022 10:17 PM 创建
*
* @author 花书粉丝
*/
public class ScheduleExecutorDemo {
private static final AtomicInteger I = new AtomicInteger();
public static void main(String[] args) {
final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4);
final Runnable runnable = () -> {
try {
Thread.sleep(1000);
System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
System.out.println("current:" + System.currentTimeMillis() / 1000);
for (int j = 0; j < 12; j++) {
executorService.schedule(runnable, 2l, TimeUnit.SECONDS);
}
executorService.shutdown();
}
}
因为有点误差,所以我这里记录的延迟了三秒启动,结果如下:
current:1662302723
i = 1:1662302726
i = 3:1662302726
i = 0:1662302726
i = 2:1662302726
i = 4:1662302727
i = 6:1662302727
i = 7:1662302727
i = 5:1662302727
i = 8:1662302728
i = 11:1662302728
i = 9:1662302728
i = 10:1662302728
所以他是固定线程池和定时线程池的混合版本。类似这样的混合版本还有newSingleThreadScheduledExecutor,这个是单线程池和定时线程池的混合版本。
工厂参数
在很多公司的开发规范中,线程池必须自己取名字,这样日志里比较好追踪,其实这个比较简单,因为上述四大线程池,都有个工厂参数,在工厂参数里自己定一个线程名字规则就行了。如以下例子:
package com.youngthing.interpolation.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 9/4/2022 10:17 PM 创建
*
* @author 花书粉丝
*/
public class ExecutorFactoryDemo {
private static final AtomicInteger I = new AtomicInteger();
public static void main(String[] args) {
final ExecutorService executorService = Executors.newFixedThreadPool(4, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
final Thread thread = new Thread(Thread.currentThread().getThreadGroup(), r, "OK-" + I.getAndIncrement());
return thread;
}
});
final Runnable runnable = () -> {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ":" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int j = 0; j < 12; j++) {
executorService.execute(runnable);
}
executorService.shutdown();
}
}
打印结果如下:
OK-2:1662303969
OK-0:1662303969
OK-1:1662303969
OK-3:1662303969
OK-0:1662303970
OK-2:1662303970
OK-1:1662303970
OK-3:1662303970
OK-1:1662303971
OK-3:1662303971
OK-0:1662303971
OK-2:1662303971
从名字上看得一清二楚,就只创建了四个线程,而且线程名字也是我们自定义的名字。
ThreadPoolExecutor
ExecutorService有两个非常重要的实现类,一个是ThreadPoolExecutor,另一个是ForkJoinPool。ForkJoinPool我不会在这篇博客里讲,因为我准备写另外一篇博客去介绍ForkJoinPool。所以我这里只讲ThreadPoolExecutor,这个类在面试时是热门问题,经常被问起,主要问的是参数细节。那我们来看看总共多少个参数吧!ThreadPoolExecutor参数最多的构造器是以下方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
总共7个参数,我将一一介绍。
前两个重要参数是corePoolSize与maximumPoolSize,corePoolSize和maximumPoolSize控制线程池的大小。当线程数量小于corePoolSize时,会创建新线程去执行任务。当队列满的时候,线程数量大于corePoolSize且小于maximumPoolSize时,也会创建新线程去执行任务。当设置的corePoolSize和maximumPoolSize相等时,这时候就是一个固定尺寸的线程池了。当然,这两个参数可以用set方法修改,并不是传入构造器就不能改了。
接下来的两个参数keepAliveTime和unit是活跃时间。在线程数量超过corePoolSize时,溢出的线程,如果剩余的空闲时间超过keepAliveTime,那么将会中止,然后资源得到释放,保持一个较小的线程池。
再看看workQueue这个参数,这里需要注意的是线程拒绝策略。
- 当线程数小于corePoolSize时,创建新线程。
- 当线程数等于corePoolSize时,加入队列。
- 当线程数等于corePoolSize时,且队列满了时,创建新线程。
- 当线程数等于maximumPoolSize时,拒绝新任务。
在实践中有三种队列策略:
- 接力策略,使用SynchronousQueue,这个队列可以理解为长度只能为1的队列,相当于一个小缓冲。
- 无边界策略,使用LinkedBlockingQueue,因为这个阻塞队列不限制大小,但是这个时候maximumPoolSize参数就没有效果了。
- 有边界策略。使用 ArrayBlockingQueue,这是实际生产中最常用的策略。
而threadFactory我就不说了,前一段落说过了。直接说下一个重点,handler。拒绝任务的场景上面说过,在拒绝时会执行拒绝策略,如果时间充裕,可以自己实现一个拒绝策略,但如果时间不充裕,可以有JDK自带的四种拒绝策略:
- 中断策略,实现类为ThreadPoolExecutor.AbortPolicy,在拒绝时抛出异常,这也是默认实现
- 调用者执行策略,实现类为ThreadPoolExecutor.CallerRunsPolicy,由调用者执行溢出的任务。
- 丢弃策略,实现类为ThreadPoolExecutor.DiscardPolicy,直接放弃新加的任务。
- 丢弃最旧任务策略,实现类为ThreadPoolExecutor.DiscardOldestPolicy,直接放弃队列头部的任务,也就是等待最久的任务。