分析Java线程池就离不开Executor
类,今天就让我们来一起好好看下
除开今天要讲的线程池,我还整理了一些技术资料和面试题集,供大家提升进阶,面试突击,不管你是有跳槽打算还是单纯精进自己,都可以免费领取一份。
从面试简历模板到大厂面经汇总,从大厂内部技术资料到互联网高薪必读书单,以及Java面试核心知识点(283页)和Java面试题合集2022年最新版(485页)等等
领取方式在文末!
为了更好地控制多线程,JDK提供了一套Executor框架,可以有效地进行线程控制,其本质上就是一个线程池。
其中ThreadPoolExecutor
表示一个线程池。Executors
类则扮演着线程池工厂的角色,通过Executors
可以取得一个拥有特定功能的线程池。从上图可知,ThreadPoolExecutor
类实现了Executor
接口,因此,通过这个接口,任何Runnable
对象都可以被ThreadPoolExecutor线程池调度。
Executor框架提供了各种类型的线程池,主要有以下工厂方法:
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
以上工厂方法分别返回具有不同工作特性的线程池。这些线程池工厂方法的具体说明如下:
newFixedThreadPool
: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂时存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务newSingleThreadExecutor
: 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务newCachedThreadPool
: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用newSingleThreadScheduledExecutor
: 该方法返回一个ScheduledExecutorService
对象,线程池大小为1。ScheduledExecutorService
接口在ExecutorService
接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。newScheduledThreadPool
: 该方法也返回一个ScheduledExecutorService
对象,但该线程池可以指定线程数量一个值得注意的方法是newScheduledThreadPool()
。它返回一个ScheduledExecutorService
对象,可以根据时间对线程进行调度。它的一些主要方法如下:
public ScheduledFuture> schedule(Runnable command,long delay,TimeUnit unit)
public ScheduledFuture> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit)
public ScheduledFuture> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit)
ScheduledExecutorService
起到了计划任务的作用,它会在指定的时间,对任务进行调度。
方法schedule()
会在给定时间,对任务进行一次调度。方法scheduleAtFixedRate()
和scheduleWithFixedDelay()
会对任务进行周期性的调度,但是两者有一点区别: 对于FixedRate
方式来说,任务调度的频率是一定的。它是以上一个任务开始执行时间为起点,之后的period
时间,调度下一次任务;而FixDelay
则是在上一个任务结束后,再经过delay时间进行任务调度。
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
//如果前面的任务没有完成,则调度也不会启动
ses.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(new Date().toLocaleString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2,TimeUnit.SECONDS);
output:
2017-8-28 21:46:49
2017-8-28 21:46:51
2017-8-28 21:46:53
2017-8-28 21:46:55
2017-8-28 21:46:57
2017-8-28 21:46:59
上述输出的单位是秒,可以看到,时间间隔是2秒。如果任务的执行时间改为8秒,会有怎么样的打印
2017-8-28 21:48:54
2017-8-28 21:49:02
2017-8-28 21:49:10
2017-8-28 21:49:18
2017-8-28 21:49:26
2017-8-28 21:49:34
可以发现,周期不再是2秒,而是变成了8秒。可知,如果周期太短,那么任务就会在上一个任务结束后,立即被调用。如果改成scheduleWithFixedDelay,并且周期为2秒,任务耗时8秒,那么任务的时间间隔为10秒。
2017-8-28 21:52:20
2017-8-28 21:52:30
2017-8-28 21:52:40
2017-8-28 21:52:50
如果任务本身抛出了异常,那么后续的所有执行都会被中断,因此,做好异常处理就非常重要。
ScheduledFuture
很简单,它就是在Future
基础上还集成了Comparable
和Delayed
的接口。它用于表示ScheduledExecutorService
中提交了任务的返回结果。我们通过Delayed
的接口getDelay()
方法知道该任务还有多久才被执行。
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
ScheduledFuture sf = service.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("job start");
return "ok";
}
},5, TimeUnit.SECONDS);
TimeUnit.SECONDS.sleep(2);
System.out.println("delay:"+sf.getDelay(TimeUnit.SECONDS));
if(Math.random()>0.5){
System.out.println("and then cancel the job");
sf.cancel(false);//mayInterruptIfRunning : false
}else{
System.out.println("do not cancel,wait for result:");
System.out.println(sf.get());
service.shutdown();
}
可以通过cancel
来取消一个任务,或者通过get()
方法来返回任务的结果(Callable
支持,Runnable
返回null)
对于上面锁列出的线程池,虽然看起来有着完全不同的功能特点,但其内部实现均使用了ThreadPoolExecutor
实现,下面给出了这三个线程池的实现方式:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
由以上线程池的实现代码可以看到,它们都只是ThreadPoolExecutor
类的封装,看一下ThreadPoolExecutor
最重要的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数含义如下:
corePoolSize
指定了线程池中的最小工作线程数量
maximumPoolSize
指定了线程池中的最大线程数量
keepAliveTime
当线程池线程数量超过corePoolSize
时,多余的空闲线程的存活时间
unit
keepAliveTime的单位
workQueue
任务队列,被提交但尚未被执行的任务
threadFactory
线程工厂,用于创建线程
handler
拒绝策略。当任务太多来不及处理,如何拒绝任务
线程创建策略如下,通过下面这个流程图可以很好的理解corePoolSize
和maximumPoolSize
的关系:
来分析一下这个流程图,当一个任务被提交进来后,首先会比较该线程池运行的线程数量与corePoolSize
,如果小于(哪怕池中有空闲线程)则实例化一个新线程(来处理这个任务);否则尝试入队,若入队失败(offer
方法返回false
),说明队满,则判断是否小于maximumPoolSize
,若小于则新建临时线程;否则执行拒绝策略。
我们可以通过一个实例来验证下这个过程:
import java.util.concurrent.*;
public class ThreadPoolTest {
private static class MyTask implements Runnable {
private String name;
public MyTask(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start handle " + this);
try {
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName() + " finished " + this);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//传入了有限队列,大小为5 默认的拒绝策略为抛弃
ExecutorService pool = new ThreadPoolExecutor(2, 4,
0L, TimeUnit.MILLISECONDS,
new TaskQueue(5));
//10个任务
for (int i = 1; i <= 10; i++) {
MyTask task = new MyTask("Task-" + i);
try {
pool.execute(task);
} catch (RejectedExecutionException e) {
System.out.println(task + " was rejected.");
}
}
//关闭线程池,它会等待已提交的任务执行完毕
pool.shutdown();
}
/**
* 继承了LinkedBlockingQueue,增加了打印信息
*/
private static class TaskQueue extends LinkedBlockingQueue {
public TaskQueue() {
super();
}
public TaskQueue(int capacity) {
super(capacity);
}
@Override
public boolean offer(Runnable runnable) {
boolean result = super.offer(runnable);
System.out.println(runnable + " enqueue " + (result ? " success" : "failed."));
return result;
}
@Override
public Runnable take() throws InterruptedException {
Runnable task = super.take();
System.out.println(task + " was finishd and removed.");
return task;
}
}
}
输出如下:
Task-3 enqueue success
Task-4 enqueue success
Task-5 enqueue success
Task-6 enqueue success
Task-7 enqueue success
Task-8 enqueue failed.
Task-9 enqueue failed.
Task-10 enqueue failed.
Task-10 was rejected. //被拒接
pool-1-thread-1 start handle Task-1
pool-1-thread-2 start handle Task-2
pool-1-thread-3 start handle Task-8
pool-1-thread-4 start handle Task-9
pool-1-thread-1 finished Task-1
pool-1-thread-1 start handle Task-3
pool-1-thread-2 finished Task-2
pool-1-thread-2 start handle Task-4
pool-1-thread-3 finished Task-8
pool-1-thread-3 start handle Task-5
pool-1-thread-4 finished Task-9
pool-1-thread-4 start handle Task-6
pool-1-thread-1 finished Task-3
pool-1-thread-1 start handle Task-7
pool-1-thread-2 finished Task-4
pool-1-thread-3 finished Task-5
pool-1-thread-4 finished Task-6
pool-1-thread-1 finished Task-7
我们自己实现了一个有界队列,增加了一些打印信息便于理解。构造了一个核心线程数为2,最大线程数为4的线程池。同时,它的有界队列大小为5。也就是说最多能同时运行4个线程,有5个任务在队列中保存,若此时再有任务进来,转而执行拒绝策略。
从上面的输出可以看出,Task-1、Task-2
直接被处理,接着Task-3、Task-4、Task-5、Task-6、Task-7
入队,然后Task-8、Task-9
入队失败,但是此时运行的线程数为2,小于最大的值4,因此这两个任务被新建的临时线程处理;接着Task-10
入队失败,同时运行的线程数达到最大值,执行拒绝策略。
还有workQueue和拒绝策略的相关内容我放到下一篇和大家分享,前面说到了为大家准备一份资料,简单介绍下,包含以下内容:
资料持续更新中,目前全部都是免费送给大家,如果有需要,尽管拿走,添加我助手领取,备注“CSDN南哥”