线程池
文章目录
线程的创建与销毁非常耗资源,在我们的生产与消费者模型中,若有成千上万的消费者和生产者,我们就不宜用for循环来创建那么多线程,如果是单核CPU,则会很快速的去切换,很消耗资源;
对用大量的线程创建,我们则要用到线程池,把任务交给几个已经准备好的线程去执行;
0.线程池优点(⭐)
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:使用线程池可以统一进行线程分配、调度和监控。
1.线程池各种接口框架:
Executor:执行器 是一个顶层接口 ,只有一个方法:void execute(Runnable command);
这个方法用来接受任务,这个类只有这一个方法所以过于抽象;
ExecutorService:这也是个接口,继承了Executor,这里面包含了许多抽象方法;
AbstractExecutorService:这是个抽象类 ,实现了ExecutorService这个接口,这里面实现了ExecutorService这个接口中的所有抽象方法
ThreadPoolExecutor:真正实现的类(这是一个普通类),继承了AbstractExecutorService这个类;
2.线程池的执行原理
对于我们而言:我们只需向线程池提交任务即可,我们来看看线程池是如何运行的:(⭐)
对应如图,当我们向线程池中提交任务时,执行的顺序如图1、2、3、4依次执行,即:
首先将任务提交到核心线程中,若已满,则提交到任务队列中,若任务队列也已满,则创建新的线程(已不是核心线程)来执行,若总线程已达到最大,则执行第四步,根据你设置的处理方法来处理;
3.如何创建一个线程池
这是一个ThreadPoolExecutor的构造器:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
-
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
-
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列, 可以选择以下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
-
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
-
keepAliveTime(线程活动保持时间):线程池的非核心线程(⭐)空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
- 官方的解释:就是线程数大于corePoolSize时空闲线程存活的最大时间 ;
- 所以这个参数只能回收超出核心线程后的线程,API还提供了一个方法:allowCoreThreadTimeOut(boolean)当参数设置为true时,核心线程空闲超时后也会被回收;
-
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。
-
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
(这四种都是ThreadPoolExecutor类中的静态内部类)- DiscardPolicy:不处理,丢弃掉;
-
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务;
-
AbortPolicy:直接抛出异常。(默认采用此策略) ;
-
CallerRunsPolicy:只用调用者所在线程来运行任务;
4.向线程池中提交任务
1.用execute()方法提交,没有返回值
例子:
public class Test {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new
ThreadPoolExecutor(5,10,1L,
TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(20),
new ThreadPoolExecutor.DiscardPolicy());
while(true) {
Thread.sleep(500);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+LocalDateTime.now());
}
});
}
}
}
输出:
pool-1-thread-1 2019-04-27T09:46:48.835
pool-1-thread-2 2019-04-27T09:46:49.264
pool-1-thread-3 2019-04-27T09:46:49.764
pool-1-thread-4 2019-04-27T09:46:50.265
pool-1-thread-5 2019-04-27T09:46:50.766
pool-1-thread-1 2019-04-27T09:46:51.265
pool-1-thread-2 2019-04-27T09:46:51.767
pool-1-thread-3 2019-04-27T09:46:52.267
pool-1-thread-4 2019-04-27T09:46:52.768
pool-1-thread-5 2019-04-27T09:46:53.268
…
从输出可见,线程数并没有达到最大线程数(10个),这是因为我们所需要执行的方法很简单,核心线程足够应付,所以队列中一直没有满,就不会创建核心线程外的线程;
这个例子的最大问题就是命名问题,这是系统的默认命名,可以看到并不适合我们来管理,所以要自己命名的话,需要用到另外一种构造方法;
public class Test {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new
ThreadPoolExecutor(5, 10, 1L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20),
new ThreadFactory() {
//原子变量
private final AtomicInteger id = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("Thread-Hander-DateAnalysis-"+id.getAndIncrement());
//1.设置名字
//2.设置优先级
//3.设置是否为守护线程
return thread;
}
},
new ThreadPoolExecutor.DiscardPolicy());
while(true) {
Thread.sleep(500);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+LocalDateTime.now());
}
});
}
}
}
输出:
Thread-Hander-DateAnalysis-1 2019-04-27T10:01:36.543
Thread-Hander-DateAnalysis-2 2019-04-27T10:01:36.823
Thread-Hander-DateAnalysis-3 2019-04-27T10:01:37.325
Thread-Hander-DateAnalysis-4 2019-04-27T10:01:37.824
Thread-Hander-DateAnalysis-5 2019-04-27T10:01:38.325
Thread-Hander-DateAnalysis-1 2019-04-27T10:01:38.825
Thread-Hander-DateAnalysis-2 2019-04-27T10:01:39.326
Thread-Hander-DateAnalysis-3 2019-04-27T10:01:39.827
Thread-Hander-DateAnalysis-4 2019-04-27T10:01:40.327
Thread-Hander-DateAnalysis-5 2019-04-27T10:01:40.828
…
2.用submit()方法提交任务,带有返回值
public class Test {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new
ThreadPoolExecutor(5, 10, 1L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20),
new ThreadFactory() {
//原子变量
private final AtomicInteger id = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("Thread-Hander-DateAnalysis-"+id.getAndIncrement());
//1.设置名字
//2.设置优先级
//3.设置是否为守护线程
return thread;
}
},
new ThreadPoolExecutor.DiscardPolicy());
while(true) {
Thread.sleep(500);
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() {
return Thread.currentThread().getName()+" "+LocalDateTime.now().toString();
}
});
String str = future.get();
System.out.println(str);
}
}
}
输出:
Thread-Hander-DateAnalysis-1 2019-04-27T10:50:04.904
Thread-Hander-DateAnalysis-2 2019-04-27T10:50:04.904
Thread-Hander-DateAnalysis-3 2019-04-27T10:50:04.905
Thread-Hander-DateAnalysis-4 2019-04-27T10:50:04.905
Thread-Hander-DateAnalysis-5 2019-04-27T10:50:04.905
Thread-Hander-DateAnalysis-1 2019-04-27T10:50:04.905
Thread-Hander-DateAnalysis-2 2019-04-27T10:50:04.906
Thread-Hander-DateAnalysis-3 2019-04-27T10:50:04.906
Thread-Hander-DateAnalysis-4 2019-04-27T10:50:04.906
对于
这种方法的问题是,我们每次提交任务,要等上一个任务结束后获取返回值后再提交,这相当于同步处理了,这样效率比较低下;
对上面的代码稍作改进,将每次任务的返回值收集再一个容器里面,等最后再将这个容器里的内容输出,这样才达到了异步处理的效果;
代码如下:
public class Test {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new
ThreadPoolExecutor(5, 10, 1L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20),
new ThreadFactory() {
//原子变量
private final AtomicInteger id = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("Thread-Hander-DateAnalysis-"+id.getAndIncrement());
//1.设置名字
//2.设置优先级
//3.设置是否为守护线程
return thread;
}
},
new ThreadPoolExecutor.DiscardPolicy());
List<Future<String>> list = new ArrayList<>();
for(int i=0; i<10; i++) {
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() {
return Thread.currentThread().getName()+" "+LocalDateTime.now().toString();
}
});
list.add(future);
}
//把任务提交完再等结果
for(Future future:list) {
System.out.println(future.get());
}
}
}
结果:
Thread-Hander-DateAnalysis-1 2019-04-27T10:47:58.181
Thread-Hander-DateAnalysis-2 2019-04-27T10:47:58.181
Thread-Hander-DateAnalysis-3 2019-04-27T10:47:58.181
Thread-Hander-DateAnalysis-4 2019-04-27T10:47:58.181
Thread-Hander-DateAnalysis-5 2019-04-27T10:47:58.181
Thread-Hander-DateAnalysis-5 2019-04-27T10:47:58.182
Thread-Hander-DateAnalysis-4 2019-04-27T10:47:58.182
Thread-Hander-DateAnalysis-1 2019-04-27T10:47:58.182
Thread-Hander-DateAnalysis-5 2019-04-27T10:47:58.182
Thread-Hander-DateAnalysis-4 2019-04-27T10:47:58.182
5.线程池的关闭(⭐)
可以通过调用线程池的shutdown或shutdownNow方法 来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别:(⭐)
-
shutdown():
public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(SHUTDOWN); interruptIdleWorkers(); onShutdown(); // hook for ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } tryTerminate(); }
方法首先加锁,其次先检查系统安装状态。接着就会将线程池状态变为 SHUTDOWN,在这之后线程池不再接受提交的新任务。此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认情况下将会使用 ThreadPoolExecutor.AbortPolicy,抛出RejectedExecutionException 异常。
interruptIdleWorkers方法只会中断空闲的线程,不会中断正在执行任务的的线程。空闲的线程将会阻塞在线程池的阻塞队列上。 -
shutdownNow():
public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); interruptWorkers(); tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; }
执行该方法,线程池的状态立刻变成 STOP 状态,并中断所有的线程(正在执行的和空闲的),不再处理还在池队列中等待的任务,执行此方法会返回工作队列中所有未完成的任务。
注意中断线程并不代表线程立刻结束,这里需要线程主动配合线程中断响应;
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true 。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法;
例:
public class Test {
public static void main(String[] args) throws Exception{
ThreadPoolExecutor executor = new
ThreadPoolExecutor(5, 10, 1L,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(20),
new ThreadFactory() {
//原子变量
private final AtomicInteger id = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("Thread-Hander-DateAnalysis-"+id.getAndIncrement());
//1.设置名字
//2.设置优先级
//3.设置是否为守护线程
return thread;
}
},
new ThreadPoolExecutor.DiscardPolicy());
List<Future<String>> list = new ArrayList<>();
for(int i=0; i<10; i++) {
Future<String> future = executor.submit(new Callable<String>() {
@Override
public String call() {
return Thread.currentThread().getName()+" "+LocalDateTime.now().toString();
}
});
list.add(future);
}
//把任务提交完再等结果
for(Future future:list) {
System.out.println(future.get());
}
if(!executor.isShutdown()) {
executor.shutdown();
//线程池完全关闭了才会返回true(isTerminated方法)
if(executor.isTerminated()) {
System.out.println("terminated");
}
if(executor.isTerminating()) {
System.out.println("terminating");
}
}
}
}
输出:
Thread-Hander-DateAnalysis-1 2019-04-27T11:04:34.592
Thread-Hander-DateAnalysis-2 2019-04-27T11:04:34.592
Thread-Hander-DateAnalysis-3 2019-04-27T11:04:34.593
Thread-Hander-DateAnalysis-4 2019-04-27T11:04:34.592
Thread-Hander-DateAnalysis-5 2019-04-27T11:04:34.592
Thread-Hander-DateAnalysis-2 2019-04-27T11:04:34.593
Thread-Hander-DateAnalysis-1 2019-04-27T11:04:34.593
Thread-Hander-DateAnalysis-2 2019-04-27T11:04:34.593
Thread-Hander-DateAnalysis-2 2019-04-27T11:04:34.593
Thread-Hander-DateAnalysis-1 2019-04-27T11:04:34.593
terminatingProcess finished with exit code 0
可以看到,shutdown方法执行后不是立马关闭所有线程; 先要执行完队列里的任务;
以下代码会输出什么:
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 10,
10L, TimeUnit.SECONDS, new LinkedBlockingQueue());
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("I:" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadPoolExecutor.shutdownNow();
System.out.println("Java");
}
输出:
Java
java.lang.InterruptedException: sleep interrupted
I:0、I:1、 at java.lang.Thread.sleep(Native Method)
at com.yy.lock.CallableExer$1.run(CallableExer.java:21)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
I:2、I:3、I:4、
Process finished with exit code 0
题目解析:因为程序中使用了 shutdownNow()
会导致程序执行一次之后报错,抛出 sleep interrupted
异常,又因为本身有 try/catch,所以程序会继续执行打印 ;
优雅的关闭线程池(⭐)
上面讲述了两种关闭线程池的方法,那么我们应该如何正确的关闭线程池呢?
这里先认识一下awaitTerminnation方法;
shutdown和shutdowmNow方法都不会主动等待执行任务的结束,如果要等到线程池任务执行结束,需要调用awaitTerminnation这个方法主动等待任务调用结束;
使用案例:
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello");
}
});
executor.shutdown();
while(!executor.awaitTermination(1,TimeUnit.SECONDS)) {
System.out.println("还未关闭");
}
}
还未关闭 (每秒打印一次)
还未关闭
还未关闭
还未关闭
hello
优雅关闭线程池典例代码: (✌)
threadPool.shutdown();
//还可以加个循环设定最大重试次数
try { //这个try块是自己加的,并不是这里有受查异常
if(!threadPool.awaitTermination(60,TimeUnit.SECONDS)) {
//调用了shutdown后还可以继续调用shutdownNow
threadPool.shutdownNow();
if(!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("线程池任务未正常执行结束!!!");
}
}
}catch (InterruptedException e) {
threadPool.shutdownNow();
}
6.合理配置线程池
我们在创建线程池的时候,应该如何设置参数比较合理呢,我们可以依赖下面4种性质来合理分配参数:
任务性质(类型):
- CPU密集型任务(执行速度快)
- IO密集型任务(有阻塞,执行速度较慢)
- 混合型任务
任务优先级:
- 高
- 中
- 低
任务的执行时间:
- 长
- 中
- 短
任务的依赖性: 是否依赖其他系统资源,如数据库连接;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MjUEJR2D-1573101381530)(C:\Users\Laptop\AppData\Local\Temp\1556335123647.png)]
拥有上面分析后,我们就可以对应来设置参数:
性质不同的任务可以用不同规模的线程池分开处理。
- CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。
- 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。
- 混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
注意:如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越
长,那么线程数应该设置得越大,这样才能更好地利用CPU。
7. 典型问题
1.ThreadPoolExecutor 有哪些常用的方法?
- submit()/execute():执行线程池
- shutdown()/shutdownNow():终止线程池
- isShutdown():判断线程是否终止
- getActiveCount():正在运行的线程数
- getCorePoolSize():获取核心线程数
- getMaximumPoolSize():获取最大线程数
- getQueue():获取线程池中的任务队列
- allowCoreThreadTimeOut(boolean):设置空闲时是否回收核心线程
3.在 ThreadPool 中 submit() 和 execute() 有什么区别?
submit() 和 execute() 都是用来执行线程池的,只不过使用 execute() 执行线程池不能有返回方法,而使用 submit() 可以使用 Future 接收线程池执行的返回值。
submit() 方法源码(JDK 8)如下:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
execute() 源码(JDK 8)如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//..... 其他
}
4.说一下 ThreadPoolExecutor 都需要哪些参数?
ThreadPoolExecutor 最多包含以下七个参数:
- corePoolSize:线程池中的核心线程数
- maximumPoolSize:线程池中最大线程数
- keepAliveTime:闲置超时时间
- unit:keepAliveTime 超时时间的单位(时/分/秒等)
- workQueue:线程池中的任务队列
- threadFactory:为线程池提供创建新线程的线程工厂
- rejectedExecutionHandler:线程池任务队列超过最大值之后的拒绝策略
5.在线程池中 shutdownNow() 和 shutdown() 有什么区别?
对于这个问题,上面已经讲的很细了,这里再补充以下:
答:shutdownNow() 和 shutdown() 都是用来终止线程池的,它们的区别是:
- 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出;
- 执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务;
- 它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出⭐,它可能必须要等待所有正在执行的任务都执行完成了才能退出;
6.说一说线程池的工作原理?
答:当线程池中有任务需要执行时,线程池会判断如果线程数量没有超过核心数量就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列超过最大队列数,并且线程池没有达到最大线程数,就会新建线程来执行任务;如果超过了最大线程数,就会执行拒绝执行策略。
7.以下线程名称被打印了几次?⭐
public static void main(String[] args) throws Exception {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
4L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2),
new ThreadPoolExecutor.DiscardPolicy());
//该方法表示核心线程在超过等待时间后也会进行回收
//没有这个方法,上面的超时参数只是针对于非核心线程而言,核心线程是不会回收的;
threadPool.allowCoreThreadTimeOut(true);
for (int i = 0; i < 5; i++) {
System.out.println("hello");
threadPool.execute(new Runnable() {
@Override
public void run() {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
输出:
hello
hello
hello
hello
hello //上面这几行在一瞬间内就输出了
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1 //上面3行每隔2秒输出一个Process finished with exit code 0 //最后一行到结束,等待了6秒(2+4)
答:线程名被打印了 3 次。
题目解析:线程池第 1 次执行任务时,会新创建任务并执行;第 2 次执行任务时,因为没有空闲线程所以会把任务放入队列;第 3 次同样把任务放入队列,因为队列最多可以放两条数据,所以第 4 次之后的执行都会被舍弃(任务队列大小为2,且没有定义拒绝策略),于是就打印了 3 次线程名称。
- 注意:任务提交的循环会在一瞬间执行完,提交到线程池并不是提交一个去执行一个,而是先全部提交再去执行;
8. 线程池的线程是如何做到复用的?
线程池中的线程在循环中尝试取任务执行,这一步会被阻塞,如果设置了allowCoreThreadTimeOut为true,则线程池中的所有线程都会在keepAliveTime时间超时后还未取到任务而退出。或者线程池已经STOP,那么所有线程都会被中断,然后退出。
线程池中的线程执⾏任务分两种情况,如下:
- 在execute()⽅法中创建⼀个线程时,会让这个线程执⾏当前任务;
- 这个线程执⾏完当前任务后,会反复从BlockingQueue获取任务来执行 ;
9. 线程池是如何做到高效并发的?(高阶,结合底层源码)
看整个线程池的工作流程,有以下几个需要特别关注的并发点:
-
线程池状态和工作线程数量的变更,这个由一个AtomicInteger变量 ctl来解决原子性问题;
- 高3位表示线程池的状态(running、shutdown、stop、tidying、terminated),低29位表示当前线程数;
- 由于原子变量是无锁技术,所以在线程池添加线程时使用锁的范围可以减小一些,先无锁增加ctl(采用CAS),后面的具体细节再使用线程池定义的全局锁(mainLock,是一个ReetrentLock),这样就缩小了锁的范围;
-
向工作Worker容器workers中添加新的Worker的时候。这个线程池本身已经加锁了;
-
工作线程Worker从等待队列中取任务的时候。这个由工作队列本身来保证线程安全,比如LinkedBlockingQueue等。
-
如果当前运⾏的线程少于corePoolSize,则创建新线程来执⾏任务,执⾏这⼀步骤需要
获取全局锁;
-
如果运⾏的线程等于或多于corePoolSize,则将任务加⼊BlockingQueue,这一步不需要获取全局锁;
-
如果⽆法将任务加⼊BlockingQueue(队列已满),则创建新的线程来处理任务,执⾏
这⼀步骤也需要获取全局锁;
而线程池工作时绝大多数时间处于第二个阶段,多个线程同时使用这个线程池就不会产生太大的并发效率问题,因为第二个阶段不需要多个线程去竞争这个线程池的锁;
-
7.Executor框架
线程池的创建分为两种方式:
- ThreadPoolExecutor (上面所介绍的就是)
- Executors
这里就来谈谈Executors如何创建线程池,以及如何使用和原理;
Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。
通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor (这个是他们底层的实现类),这三种类型各有特点,可以满足不同类型的开发;
1.FixedThreadPool(创建固定大小的线程池)
在Executors中,代码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点
- 可以看到,它的核心线程和总线程大小都设置为了同一个值(我们所规定的值),所以这是一个固定大小的线程池;当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize ;
- maximumPoolSize、keepAliveTime都是无效参数 ;
- 使用无界队列不会拒绝任何任务,除非使用shutdown或shutdownNow方法使线程停止;
应用场景
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场合,适用于负载比较重的服务器。
2.SingleThreadPoolExecutor
顾名思义,这个线程池为单个线程而准备
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
代码测试:
public class ThreadTest {
static int i;
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for(i=0; i<3; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
for(int j=0; j<3; j++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" Ivalue: "+i+" Jvalue: "+j);
}
}
});
}
executorService.shutdown();
}
}
输出:(⭐)
pool-1-thread-1 Ivalue: 3 Jvalue: 0
pool-1-thread-1 Ivalue: 3 Jvalue: 1
pool-1-thread-1 Ivalue: 3 Jvalue: 2
pool-1-thread-1 Ivalue: 3 Jvalue: 0
pool-1-thread-1 Ivalue: 3 Jvalue: 1
pool-1-thread-1 Ivalue: 3 Jvalue: 2
pool-1-thread-1 Ivalue: 3 Jvalue: 0
pool-1-thread-1 Ivalue: 3 Jvalue: 1
pool-1-thread-1 Ivalue: 3 Jvalue: 2Process finished with exit code 0
来看这个输出,首先可以观察到这里只有一个线程来执行任务,观察Ivalue,即i的值,一开始就为3,这说明了我们的submit方法提交任务时是先将所有的任务提交完了再来执行run方法的,这验证了我们前面所讲的;
应用场景
SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
3. CachedThreadPool
先来看它的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- CachedThreadPool的corePoolSize被设置为0,即corePool为空;
- maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。
- 这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。
- CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的 。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源;
使用场景
CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器。
4. ScheduledThreadPoolExecutor
顾名思义,这是一个可以实现周期性定时调用任务的线程池;