Java 线程池的基本介绍和使用
在 Java 中,可以利用线程做很多事情,创建多个线程来高效的完成任务,例如 Tomcat 用多个线程来接收和处理请求,我们也可以创建多个线程来批量处理数据,原本串行执行的任务变成并行执行,充分利用 CPU 多个核的性能。
我们可以用这样的方式创建线程执行并发任务:
for (int i = 0; i < 任务数量; i++) {
Thread thread = new Thread(任务);
thread.start();
}
这样子确实能够并发完成任务,但是也带了问题:创建的线程数量不可控制,一个任务就创建一个线程,当任务量庞大的时候,会带来极大的内存开销,反复创建、销毁线程。
通过使用线程池的方式,就可以集中管理线程资源,线程池能够给我们带来如下优点:
- 复用线程。利用一定数量的线程,反复执行任务,而不用频繁的创建、销毁线程。
- 控制了资源的总量,合理利用 CPU 和内存。由于复用线程,CPU 和 内存相较于创建多个线程来说占用更低。
- 统一管理资源,统一停止线程。相同任务类型的线程被统一管理起来,能够在某些情况统一的停止这些任务。
⭐ 在实际开发中,如果需要创建超过五个线程执行类似的任务,就可以考虑使用线程池了。
🚀 创建线程池
我们已经知道了线程池的优点和强大了,现在再介绍一下线程池要怎么去创建,怎么去使用。
🛫 线程池创建线程的规则
首先,线程池有几个核心属性,分别是:
corePoolSize
,核心线程数量,线程池的线程数量会维持在这个数字上maximumPoolSize
,最大线程数量,创建的线程数量不会超过这个数字keepAliveTime
,线程存活时间,超过核心线程数的线程,如果空闲时间超过指定时间,就会被回收任务队列
,用于接收、存储待执行的任务,当线程空闲下来时,会从任务队列中取出任务并执行- 直接交接队列(SynchronousQueue),队列大小为零,新任务直接开始运行,不会等待
- 有界队列(ArrayBlockQueue),任务队列是有限的
- 无界队列(LinkedBlockQueue),任务队列是无限的,理论上可以添加任意数量的任务
线程池创建线程的规则是这样的:
- 如果当前线程数<核心线程数当前线程数 < 核心线程数当前线程数<核心线程数,则接受新任务就创建新线程
- 如果核心线程数≤当前线程数<最大线程数核心线程数 \le 当前线程数 < 最大线程数核心线程数≤当前线程数<最大线程数,则接收新任务时,将新任务加入到任务队列中
- 如果任务队列满了,并且 当前线程数<最大线程数当前线程数 < 最大线程数当前线程数<最大线程数,则创建新的线程执行任务
- 如果任务队列满了,并且 当前线程数=最大线程数当前线程数 = 最大线程数当前线程数=最大线程数,那么拒绝该任务并抛出拒绝任务异常
RejectedExecutionException
🛫 使用 Executors 创建线程池
上文已经提到了线程池的几个核心属性以及创建线程的规则,其实这些核心属性是创建线程池对象的参数,主要来自于ThreadPoolExecutor
类的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
我们可以利用这个方法来创建一个线程池:
public static void main(String[] args) {
// 创建一个线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
// 创建 10 个任务,打印线程名
for (int i = 0; i < 10; i++) {
// ThreadPoolExecutor 的 execute 方法可以接收并执行任务
// 接收 Runnable 接口实现类,Runnable 是一个方法接口,因此可以使用 Lambda 表达式
pool.execute(()-> {
System.out.println(Thread.currentThread().getName());
});
}
}
输出结果:
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-3
pool-1-thread-2
pool-1-thread-5
pool-1-thread-1
阿里手册里面推荐我们给一个线程池指定一个线程工厂,从而另线程池创建的每个线程的名字都有意义。在使用这个方法创建线程的时候,可以这样创建,以指定具体的线程名:
// 创建一个线程工厂,实现接口 ThreadFactory
public class MyThreadPoolFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger(1);
private final String prefixName;
public MyThreadPoolFactory(String prefixName) {
this.prefixName = prefixName;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefixName + threadNum.getAndIncrement());
return t;
}
}
// 修改上面的例子,创建线程池的语句修改为:
ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), new MyThreadPoolFactory("我的线程"));
// 再次运行查看结果
运行结果:
我的线程2
我的线程5
我的线程3
我的线程1
我的线程3
我的线程5
我的线程2
我的线程4
我的线程3
我的线程1
也可以参考Java线程池中设置线程名称三种方式 - 屠城校尉杜 - 博客园 (cnblogs.com),用现有的工具类创建。
我们也可以使用 Executor
类快速创建各种类型的线程池,可以创建如下类型的线程池:
线程类型 | 说明 |
---|---|
固定数量的线程池 | 创建一个线程池,其核心线程数和最大线程数都相同 |
单线程线程池 | 创建一个线程池,其最多只有一个线程 |
缓存线程池 | 任务队列使用直接交接队列 最大线程数为整形最大值 |
定时任务线程池 | 可以定时执行任务或者周期执行任务 |
我们来看看 Executor 的用法,首先是看看固定数量的线程池的创建方法:
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("固定线程池"));
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
// 运行结果如下:
固定线程池3
固定线程池5
固定线程池5
固定线程池4
固定线程池1
固定线程池2
固定线程池1
固定线程池4
固定线程池3
固定线程池5
// 方法实际调用情况,还是用到了我们上面提到的方法:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
此外,其他类型的线程池创建方法也大同小异,具体可以看看下面的Executors
的相关方法,使用起来还是很简单的。
// 创建一个缓存线程池
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
// 创建一个单线程线程池
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
// 创建一个定时任务线程池,这里其实还是调用了 new ThreadPoolExecutor(),可以进源码查看,任务队列使用延时队列
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
// ScheduledThreadPoolExecutor 的 schedule 方法可以指定任务在一定时间后开始运行,运行结束后任务结束
// scheduleAtFixedRate 方法则可以让一个任务以指定的周期运行,运行结束后会在指定时间后继续运行
🛫 推荐的方法
阿里的Java开发手册是推荐我们使用 ThreadPoolExecutor
来创建线程池,并且指定线程工厂从而让每个线程都有自己的具体名字的。因此,在实际开发过程中,我们可以尽量使用 ThreadPoolExecutor
创建线程池。
🛫 指定合适的线程数量
创建线程池指定合适的线程数量对于应用的性能也很有影响,线程数量分配少了,则性能有限,线程数量分配多了,则浪费了资源。但是要如何确定线程需要的数量呢?
- 如果是CPU密集型的应用,例如加密、哈希计算、大量计算的任务,可以考虑使用 1-2 被CPU核心数量的线程数。
- 如果是IO密集型的应用,例如需要反复读写磁盘的任务,则可以指定尽可能多倍于CPU核心数的线程数,因为大多数后线程其实是在等待磁盘 IO 的,可以让其他线程使用资源。 还有一条经验法则,可以利用这条公式设置线程数:线程数=CPU核心数×(1+平均等待时间平均工作时间)线程数=CPU核心数 \times (1 + \frac{平均等待时间}{平均工作时间})线程数=CPU核心数×(1+平均工作时间平均等待时间) 总而言之,线程数的设置没有一个通用的规则,而是根据具体的应用场景不断调试出合适的数量。
🚀 停止线程池
在使用线程池的过程中,我们也并不是想要让线程池一直运行下去的,有时候可能根据业务需要,例如项目需要紧急停止、用户需要暂停等等,我们需要让一个正在运行并且正在执行任务的线程池停止下来。 那么,具体可以怎么停止一个线程池呢?可以看看下面的代码:
public class ThreadPoolDemo implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("演示停止线程池"));
for (int i = 0; i < 50; i++) {
pool.submit(new ThreadPoolDemo());
}
// 查看线程池是否处于 shutdown 状态
System.out.println("线程池 shutdown:" + pool.isShutdown());
System.out.println("停止线程池");
pool.shutdown();
// 线程池已经进入 shutdown 状态,但是应用还在运行,线程池中的任务还在执行
System.out.println("线程池 shutdown:" + pool.isShutdown());
// 提交新任务会报异常
//pool.submit(new ThreadPoolDemo());
// 如果想要立即停止线程池,可以使用 shutdownNow,会返回线程池中未执行完毕的任务列表
// List<Runnable> runnables = pool.shutdownNow();
// 查看线程池是否已经终止
System.out.println("线程池 terminated:" + pool.isTerminated());
// 等待线程池中的任务运行完毕,主线程阻塞指定时间
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("线程池 terminated:" + pool.isTerminated());
}
}
// 打印结果:
线程池 shutdown:false
停止线程池
线程池 shutdown:true
线程池 terminated:false
线程池 terminated:true
上面的例子已经演示了如何停止一个正在运行的线程池,现在总结一下上面相关方法的区别:
方法 | 说明 |
---|---|
shutdown | 停止线程池,线程池并不会马上停止。拒绝接受新任务,如果提交新任务会抛出异常 |
shutdownNow | 立即停止线程池,正在执行任务的线程会收到中断通知。返回任务队列中的任务 |
isShutdown | 线程池是否收到了 shutdown 指令 |
isTerminated | 线程池是否已经停止 |
awaitTerminated | 调用该方法的线程阻塞住,等待指定时间,运行结果如下:如果所有任务执行完毕,返回 true。 超过了等待时间,返回false。 当前线程被中断,抛出中断异常 |
🛫 为线程池指定拒绝策略
线程池在以下这些情况会拒绝任务:
- 线程池的状态是 shutdown,此时提交任务会执行拒绝策略
- 线程池的任务队列已满,并且线程数量已经到达最大线程数,此时提交任务会执行拒绝策略
可以指定的拒绝策略如下,具体可以查看接口RejectedExecutionHandler
的实现类:
策略 | 说明 |
---|---|
AbortPolicy | 拒绝任务并直接抛出异常,默认策略 |
DiscardPolicy | 默默丢弃任务,不抛出异常 |
CallerRunsPolicy | 拒绝任务,并让提交任务的线程运行被拒绝的任务 |
DiscardOldestPolicy | 丢弃最早提交的任务,然后重新尝试接收该任务 |
可以在 ThreadPoolExecutor
的这个构造方法中指定拒绝策略:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
// 例如 new ThreadPoolExecutor(2,5,1,...,new ThreadPoolExecutor.CallerRunsPolicy());
🚀 线程池的状态
从上文中也可以看出,线程池有着各种状态,或者可以提交并执行任务,或者不再接受任务但是仍然执行运行中的任务,或者直接中断执行的任务…… 下面是线程池的各种状态:
状态 | 说明 |
---|---|
RUNNING | 线程池创建后的状态,接收并执行任务 |
SHUTDOWN | 拒绝接收新任务,等待任务执行完成,shutdown() 执行后的状态 |
STOP | 拒绝接收新任务,中断执行中的任务,返回任务队列中的任务,shutdownNow() 执行后的状态 |
TIDYING | 任务已经停止,没有工作线程,执行线程池的 terminated() 方法 |
TERMINATED | 线程池已经停止 |
转载自:https://juejin.cn/post/7213733567620218917