乐趣区

关于线程池:程序员不得不知的线程池附10道面试题

为什么要用线程池呢?

上面是一段创立线程并运行的代码:

for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(“run thread->” + Thread.currentThread().getName());
userService.updateUser(….);
}).start();
}

咱们想应用这种形式去做异步,或者进步性能,而后将某些耗时操作放入一个新线程去运行。

这种思路是没问题的,然而这段代码是存在问题的,有哪些问题呢?上面咱们就来看看有哪些问题;

  • 创立销毁线程资源耗费;咱们应用线程的目标本是出于效率思考,能够为了创立这些线程却耗费了额定的工夫,资源,对于线程的销毁同样须要系统资源。
  • cpu 资源无限,上述代码创立线程过多,造成有的工作不能即时实现,响应工夫过长。
  • 线程无奈治理,无节制地创立线程对于无限的资源来说仿佛成了“得失相当”的一种作用。

既然咱们下面应用手动创立线程会存在问题,那有解决办法吗?

答案:有的,应用线程池。

线程池介绍

线程池(Thread Pool):把一个或多个线程通过对立的形式进行调度和重复使用的技术,防止了因为线程过多而带来应用上的开销。

线程池有什么长处?

  • 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
  • 进步响应速度。当工作达到时,工作能够不须要等到线程创立就能立刻执行。
  • 进步线程的可管理性。

线程池应用

在 JDK 中 rt.jar 包下 JUC(java.util.concurrent)创立线程池有两种形式:ThreadPoolExecutor 和 Executors,其中 Executors 又能够创立 6 种不同的线程池类型。

ThreadPoolExecutor 的应用

线程池应用代码如下:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolDemo {
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));

public static void main(String[] args) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(“ 田学生您好 ”);
}
});
}
}

以上程序执行后果如下:

田学生您好

外围参数阐明

ThreadPoolExecutor 的构造方法有以下四个:

能够看到最初那个构造方法有 7 个结构参数,其实后面的三个构造方法只是对最初那个办法进行包装,并且后面三个构造方法最终都是调用最初那个构造方法,所以咱们这里就来聊聊最初那个构造方法。

参数解释

corePoolSize

线程池中的外围线程数,默认状况下外围线程始终存活在线程池中,如果将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设为 true,如果线程池始终闲置并超过了 keepAliveTime 所指定的工夫,外围线程就会被终止。

maximumPoolSize

最大线程数,当线程不够时可能创立的最大线程数。

keepAliveTime

线程池的闲置超时工夫,默认状况下对非核心线程失效,如果闲置工夫超过这个工夫,非核心线程就会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设为 true 的时候,外围线程如果超过闲置时长也会被回收。

unit

配合 keepAliveTime 应用,用来标识 keepAliveTime 的工夫单位。

workQueue

线程池中的工作队列,应用 execute() 或 submit() 办法提交的工作都会存储在此队列中。

threadFactory

为线程池提供创立新线程的线程工厂。

rejectedExecutionHandler

线程池工作队列超过最大值之后的回绝策略,RejectedExecutionHandler 是一个接口,外面只有一个 rejectedExecution 办法,可在此办法内增加工作超出最大值的事件处理。ThreadPoolExecutor 也提供了 4 种默认的回绝策略:

  • DiscardPolicy():抛弃掉该工作,不进行解决。
  • DiscardOldestPolicy():抛弃队列里最近的一个工作,并执行当前任务。
  • AbortPolicy():间接抛出 RejectedExecutionException 异样(默认)。
  • CallerRunsPolicy():既不摈弃工作也不抛出异样,间接应用主线程来执行此工作。

蕴含所有参数的应用案例:

public class ThreadPoolExecutorTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
class MyThreadFactory implements ThreadFactory {
private AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
String threadName = “MyThread” + count.addAndGet(1);
t.setName(threadName);
return t;
}
}

运行输入:

main
MyThread1
main
MyThread1
MyThread1
….

这里仅仅是为了演示所有参数自定义,并没有其余用处。

execute() 和 submit()的应用

execute() 和 submit() 都是用来执行线程池的,区别在于 submit() 办法能够接管线程池执行的返回值。

上面别离来看两个办法的具体应用和区别:

// 创立线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue(100));
// execute 应用
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(“ 老田您好 ”);
}
});
// submit 应用
Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(“ 田学生您好 ”);
return “ 返回值 ”;
}
});
System.out.println(future.get());

以上程序执行后果如下:

老田您好
田学生您好
返回值

Executors

Executors 执行器创立线程池很多基本上都是在 ThreadPoolExecutor 构造方法上进行简略的封装,非凡场景依据须要自行创立。能够把 Executors 了解成一个工厂类。Executors 能够创立 6 种不同的线程池类型。

上面对这六个办法进行简要的阐明:

newFixedThreadPool

创立一个数量固定的线程池,超出的工作会在队列中期待闲暇的线程,可用于控制程序的最大并发数。

newCacheThreadPool

短时间内解决大量工作的线程池,会依据工作数量产生对应的线程,并试图缓存线程以便重复使用,如果限度 60 秒没被应用,则会被移除缓存。如果现有线程没有可用的,则创立一个新线程并增加到池中,如果有被应用完然而还没销毁的线程,就复用该线程。终止并从缓存中移除那些已有 60 秒钟未被应用的线程。因而,长时间放弃闲暇的线程池不会应用任何资源。

newScheduledThreadPool

创立一个数量固定的线程池,反对执行定时性或周期性工作。

newWorkStealingPool

Java 8 新增创立线程池的办法,创立时如果不设置任何参数,则以以后机器 CPU 处理器数作为线程个数,此线程池会并行处理工作,不能保障执行程序。

newSingleThreadExecutor

创立一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有工作。如果这个惟一的线程因为异样完结,那么会有一个新的线程来代替它。此线程池保障所有工作的执行程序依照工作的提交程序执行。

newSingleThreadScheduledExecutor

此线程池就是单线程的 newScheduledThreadPool。

线程池如何敞开?

线程池敞开,能够应用 shutdown() 或 shutdownNow() 办法,它们的区别是:

  • shutdown():不会立刻终止线程池,而是要等所有工作队列中的工作都执行完后才会终止。执行完 shutdown 办法之后,线程池就不会再承受新工作了。
  • shutdownNow():执行该办法,线程池的状态立即变成 STOP 状态,并试图进行所有正在执行的线程,不再解决还在池队列中期待的工作,执行此办法会返回未执行的工作。

上面用代码来模仿 shutdown() 之后,给线程池增加工作,代码如下:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadPoolExecutorAllArgsTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 创立线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new MyThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.allowCoreThreadTimeOut(true);
// 提交工作
threadPoolExecutor.execute(() -> {
for (int i = 0; i < 3; i++) {
System.out.println(“ 提交工作 ” + i);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
});
threadPoolExecutor.shutdown();
// 再次提及工作
threadPoolExecutor.execute(() -> {
System.out.println(“ 我想再次提及工作 ”);
});
}
}

以上程序执行后果如下:

提交工作 0
提交工作 1
提交工作 2

能够看出,shutdown() 之后就不会再承受新的工作了,不过之前的工作会被执行实现。

面试题

面试题 1:ThreadPoolExecutor 有哪些罕用的办法?

ThreadPoolExecutor 有如下罕用办法:

  • submit()/execute():执行线程池
  • shutdown()/shutdownNow():终止线程池
  • isShutdown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取外围线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的工作队列
  • allowCoreThreadTimeOut(boolean):设置闲暇时是否回收外围线程

这些办法能够用来终止线程池、线程池监控等。

面试题 2: 说说 submit(和 execute 两个办法有什么区别?

submit() 和 execute() 都是用来执行线程池的,只不过应用 execute() 执行线程池不能有返回办法,而应用 submit() 能够应用 Future 接管线程池执行的返回值。

说说线程池创立须要的那几个外围参数的含意

ThreadPoolExecutor 最多蕴含以下七个参数:

  • corePoolSize:线程池中的外围线程数
  • maximumPoolSize:线程池中最大线程数
  • keepAliveTime:闲置超时工夫
  • unit:keepAliveTime 超时工夫的单位(时 / 分 / 秒等)
  • workQueue:线程池中的工作队列
  • threadFactory:为线程池提供创立新线程的线程工厂
  • rejectedExecutionHandler:线程池工作队列超过最大值之后的回绝策略

面试题 3:shutdownNow() 和 shutdown() 两个办法有什么区别?

shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是,应用 shutdown() 程序不会报错,也不会立刻终止线程,它会期待线程池中的缓存工作执行完之后再退出,执行了 shutdown() 之后就不能给线程池增加新工作了;shutdownNow() 会试图立马进行工作,如果线程池中还有缓存工作正在执行,则会抛出 java.lang.InterruptedException: sleep interrupted 异样。

面试题 6: 理解过线程池的工作原理吗?

当线程池中有工作须要执行时,线程池会判断如果线程数量没有超过外围数量就会新建线程池进行工作执行,如果线程池中的线程数量曾经超过外围线程数,这时候工作就会被放入工作队列中排队期待执行;如果工作队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行工作;如果超过了最大线程数,就会执行拒绝执行策略。

面试题 5: 线程池中外围线程数量大小怎么设置?

「CPU 密集型工作」:比方像加解密,压缩、计算等一系列须要大量消耗 CPU 资源的工作,大部分场景下都是纯 CPU 计算。尽量应用较小的线程池,个别为 CPU 外围数 +1。因为 CPU 密集型工作使得 CPU 使用率很高,若开过多的线程数,会造成 CPU 适度切换。

「IO 密集型工作」:比方像 MySQL 数据库、文件的读写、网络通信等工作,这类工作不会特地耗费 CPU 资源,然而 IO 操作比拟耗时,会占用比拟多工夫。能够应用稍大的线程池,个别为 2 *CPU 外围数。IO 密集型工作 CPU 使用率并不高,因而能够让 CPU 在期待 IO 的时候有其余线程去解决别的工作,充分利用 CPU 工夫。

另外:线程的均匀工作工夫所占比例越高,就须要越少的线程;线程的均匀等待时间所占比例越高,就须要越多的线程;

以上只是理论值,理论我的项目中倡议在本地或者测试环境进行屡次调优,找到绝对现实的值大小。

面试题 7: 线程池为什么须要应用(阻塞)队列?

次要有三点:

  • 因为线程若是无限度的创立,可能会导致内存占用过多而产生 OOM,并且会造成 cpu 适度切换。
  • 创立线程池的耗费较高。

面试题 8: 线程池为什么要应用阻塞队列而不应用非阻塞队列?

阻塞队列能够保障工作队列中没有工作时阻塞获取工作的线程,使得线程进入 wait 状态,开释 cpu 资源。

当队列中有工作时才唤醒对应线程从队列中取出音讯进行执行。

使得在线程不至于始终占用 cpu 资源。

(线程执行完工作后通过循环再次从工作队列中取出工作进行执行,代码片段如下

while (task != null || (task = getTask()) != null) {})。

不必阻塞队列也是能够的,不过实现起来比拟麻烦而已,有好用的为啥不必呢?

面试题 9: 理解线程池状态吗?

通过获取线程池状态,能够判断线程池是否是运行状态、可否增加新的工作以及优雅地敞开线程池等。

  • RUNNING:线程池的初始化状态,能够增加待执行的工作。
  • SHUTDOWN:线程池处于待敞开状态,不接管新工作仅解决曾经接管的工作。
  • STOP:线程池立刻敞开,不接管新的工作,放弃缓存队列中的工作并且中断正在解决的工作。
  • TIDYING:线程池自主整顿状态,调用 terminated() 办法进行线程池整顿。
  • TERMINATED:线程池终止状态。

面试题 10: 晓得线程池中线程复用原理吗?

线程池将线程和工作进行解耦,线程是线程,工作是工作,解脱了之前通过 Thread 创立线程时的一个线程必须对应一个工作的限度。

在线程池中,同一个线程能够从阻塞队列中一直获取新工作来执行,其外围原理在于线程池对 Thread 进行了封装,并不是每次执行工作都会调用 Thread.start() 来创立新线程,而是让每个线程去执行一个“循环工作”,在这个“循环工作”中不停的查看是否有工作须要被执行,如果有则间接执行,也就是调用工作中的 run 办法,将 run 办法当成一个一般的办法执行,通过这种形式将只应用固定的线程就将所有工作的 run 办法串联起来。

总结

本文通过没有应用线程池带来的弊病,Executors 介绍,Executors 的六种办法介绍、如何应用线程池,理解线程池原理,外围参数,以及 10 到线程池面试题。

「胜利不是未来才有的,而是从决定去做的那一刻起,继续累积而成。」

退出移动版