1.new Thread的弊端
在引入Executor之前,我们创建一个线程使用new Thread来创建一个线程去执行,如下所示
RunnableImpl runnable = new RunnableImpl();
Thread thread = new Thread(runnable);
thread.start();
这种方式的弊端很多:
1>每次要new一个线程,创建一个线程的开销很大;
2>线程缺乏统一的管理,可能会无线创建线程,之间相互竞争,会导致系统崩溃
3>不支持定时执行,延时执行任务的功能;
Executor可以很好的解决这些问题;
2.Executor框架之间的常用类和接口
Executor框架之间的常用类和接口如图所示:
1>Executor接口
就一个接口,执行给定的Runnable task;
2>ExecutorService
继承了Executor接口,同时提供了更多的功能,线程的关闭,终止提交等;submit方法可以接受Callable的参数也可以接受Runable的参数,同时返回一个Future,也就是任务的执行结果;
3>ThreadPoolExecutor类
ThreadPoolExecutor是一个具体的类,用来构造线程池对象,其他的一些构造线程池都是在这个构造方法的基础上来构造的,具体可以看下它的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:核心线程数,核心线程数会一直存活,即使有空闲线程;
maximumPoolSize:最大线程数。当线程数>=核心线程数时,且任务队列已满,线程会创建新的线程来处理;
当线程数>=最大线程数,且任务队列已满,线程池会拒绝任务而抛出异常;
keepAliveTime:线程的空闲时间。当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数=核心线程数;
unit:时间的单位;
workQueue:存放线程的队列;
线程池的执行过程:1.首先从线程池中获取可用线程执行任务,如果没有可用线程,则使用ThreadFactory来创建线程,直到线程数达到corePoolSize;需要注意的是,如果线程池中线程的数量小于小于核心线程数,即使有空闲线程也是会创建线程的;2.线程数达到核心线程数,且线程队列没有满时,新的线程放到线程的队列里面,直到队列放不了更多的任务;3.当任务队列已满时,且线程数目没有达到最大线程数,创建线程,直到线程数达到最大线程限制;4.线程数达到最大线程数以后新的任务会被拒绝,调用RejectedExecutionHandler 进行处理;
3.几个创建线程池的方法
创建线程主要通过Executors,这是主要的一些方法
常用的方法介绍下:
newFixedThreadPool(int nThreads)
创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
newWorkStealingPool()
创建持有足够线程的线程池来支持给定的并行级别,并通过使用多个队列,减少竞争,它需要穿一个并行级别的参数,如果不传,则被设定为默认的CPU数量。
newSingleThreadExecutor()
该方法返回一个固定数量的线程池
该方法的线程始终不变,当有一个任务提交时,若线程池空闲,则立即执行,若没有,则会被暂缓在一个任务队列只能怪等待有空闲的线程去执行。
newCachedThreadPool()
返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若有空闲的线程则执行任务,若无任务则不创建线程,并且每一个空闲线程会在60秒后自动回收。
newScheduledThreadPool(int corePoolSize)
返回一个SchededExecutorService对象,但该线程池可以设置线程的数量,支持定时及周期性任务执行。
newSingleThreadScheduledExecutor()
创建一个单例线程池,定期或延时执行任务。
1>newFixedThreadPool
创建一个固定数量的线程池,如下所示
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数和最大线程数是一样的,阻塞队列使用的是无界队列LinkedBlockingQueue;线程超过数量后,会放在队列里面,demo如下
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 8; i++) {
int finalI = i + 1;
pool.submit(() -> {
try {
System.out.println("任务"+ finalI +":开始等待2秒,时间:"+LocalTime.now()+",当前线程名:"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("任务"+ finalI +":结束等待2秒,时间:"+ LocalTime.now()+",当前线程名:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
}
执行结果如下所示,可以看到任务1,2,3,4先执行,5.6.7.8先放到队列里面,等1.2.3.4执行完成后,任务5,6,7,8再开始执行;线程池的线程是重复利用的;5.6.7.8重用了,1.2.3.4的线程;
任务4:开始等待2秒,时间:18:09:03.140,当前线程名:pool-1-thread-4
任务2:开始等待2秒,时间:18:09:03.140,当前线程名:pool-1-thread-2
任务1:开始等待2秒,时间:18:09:03.140,当前线程名:pool-1-thread-1
任务3:开始等待2秒,时间:18:09:03.140,当前线程名:pool-1-thread-3
任务2:结束等待2秒,时间:18:09:05.141,当前线程名:pool-1-thread-2
任务5:开始等待2秒,时间:18:09:05.141,当前线程名:pool-1-thread-2
任务1:结束等待2秒,时间:18:09:05.141,当前线程名:pool-1-thread-1
任务6:开始等待2秒,时间:18:09:05.141,当前线程名:pool-1-thread-1
任务4:结束等待2秒,时间:18:09:05.141,当前线程名:pool-1-thread-4
任务7:开始等待2秒,时间:18:09:05.142,当前线程名:pool-1-thread-4
任务3:结束等待2秒,时间:18:09:05.142,当前线程名:pool-1-thread-3
任务8:开始等待2秒,时间:18:09:05.142,当前线程名:pool-1-thread-3
任务5:结束等待2秒,时间:18:09:07.141,当前线程名:pool-1-thread-2
任务6:结束等待2秒,时间:18:09:07.141,当前线程名:pool-1-thread-1
任务7:结束等待2秒,时间:18:09:07.142,当前线程名:pool-1-thread-4
任务8:结束等待2秒,时间:18:09:07.142,当前线程名:pool-1-thread-3
2>newCachedThreadPool
源码如图所示,核心线程数是0;最大线程数是Integer的最MAX_VALUE,存活时间是60;队列使用SynchronousQueue队列;
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
有新的线程任务就会去创建一个线程;当线程空闲时间超过60s之后就会回收;demo
public static void main(String[] args) throws Exception{
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; i++) {
int finalI = i + 1;
pool.submit(() -> {
try {
System.out.println("任务"+ finalI +":开始等待60秒,时间:"+LocalTime.now()+",当前线程名:"+Thread.currentThread().getName());
Thread.sleep(60000);
System.out.println("任务"+ finalI +":结束等待60秒,时间:"+LocalTime.now()+",当前线程名:"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//睡眠10秒
Thread.sleep(10000);
}
pool.shutdown();
}
执行结果:
任务1:开始等待60秒,时间:18:36:01.170,当前线程名:pool-1-thread-1
任务2:开始等待60秒,时间:18:36:11.116,当前线程名:pool-1-thread-2
任务3:开始等待60秒,时间:18:36:21.116,当前线程名:pool-1-thread-3
任务4:开始等待60秒,时间:18:36:31.117,当前线程名:pool-1-thread-4
任务5:开始等待60秒,时间:18:36:41.117,当前线程名:pool-1-thread-5
任务6:开始等待60秒,时间:18:36:51.118,当前线程名:pool-1-thread-6
任务7:开始等待60秒,时间:18:37:01.119,当前线程名:pool-1-thread-7
任务1:结束等待60秒,时间:18:37:01.172,当前线程名:pool-1-thread-1
任务2:结束等待60秒,时间:18:37:11.116,当前线程名:pool-1-thread-2
任务8:开始等待60秒,时间:18:37:11.119,当前线程名:pool-1-thread-2
任务3:结束等待60秒,时间:18:37:21.117,当前线程名:pool-1-thread-3
任务4:结束等待60秒,时间:18:37:31.117,当前线程名:pool-1-thread-4
任务5:结束等待60秒,时间:18:37:41.118,当前线程名:pool-1-thread-5
任务6:结束等待60秒,时间:18:37:51.118,当前线程名:pool-1-thread-6
任务7:结束等待60秒,时间:18:38:01.119,当前线程名:pool-1-thread-7
任务8:结束等待60秒,时间:18:38:11.120,当前线程名:pool-1-thread-2
这里可以看出,任务8开始的时候,因为任务2已经结束了,所以直接用了任务2的线程;
3>newScheduledThreadPool
这个线程池主要用来延迟执行任务或者定期执行任务;代码如下
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
这里调用的是父类的构造函数,ScheduledThreadPoolExecutor的父类是ThreadPoolExecutor,所以返回的也是ThreadPoolExecutor对象。核心线程数是传入的参数corePoolSize,线程最大值是Integer的MAX_VALUE,存活时间时0,时间单位是纳秒,队列是DelayedWorkQueue。
返回的结果是一个ScheduledExecutorService,该类主要有两个方法
public interface ScheduledExecutorService extends ExecutorService {
//delay延迟时间,unit延迟单位,只执行1次,在经过delay延迟时间之后开始执行
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
//首次执行时间时然后在initialDelay之后,然后在initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,
long period,
TimeUnit unit);
//首次执行时间时然后在initialDelay之后,然后延迟delay时间执行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
4.最后解释下为啥submit的参数是Runnable和Callable都可以?
具体的实现是在AbstractExecutorService类中,具体代码如下
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
里面都是构造了一个RunnableFuture的类,我们来看下构造的方法
通过Runnable来构造的时候使用了Executors.callable方法,来看下具体的代码
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
这里使用了适配器模式
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
5.使用CompletionService获取多线程的返回值
在java 5的多线程中,可以使用callable接口来实现具有返回值的线程(ps:runnable的任务是没有返回值);自己写代码的话,可以通过维护一个Collection来存放提交任务的返回结果Future对象;然后在主线程中遍历这个Collection并调用Future的get方法获取到线程的返回值;demo:
public class ThreadPoolTest4 {
private final int POOL_SIZE = 5;
private final int TOTAL_TASK = 20;
class MyThread implements Callable<String> {
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public String call() {
int sleepTime = new Random().nextInt(1000);
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回给调用者的值
String str = name + " sleep time:" + sleepTime;
System.out.println(name + " finished...");
return str;
}
}
// 方法一,自己写集合来实现获取线程池中任务的返回结果
public void testByQueue() throws Exception {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
BlockingQueue<Future<String>> queue = new LinkedBlockingQueue<Future<String>>();
// 向里面扔任务
for (int i = 0; i < TOTAL_TASK; i++) {
Future<String> future = pool.submit(new MyThread("Thread" + i));
queue.add(future);
}
// 检查线程池任务执行结果
for (int i = 0; i < TOTAL_TASK; i++) {
System.out.println("method1:" + queue.take().get());
}
// 关闭线程池
pool.shutdown();
}
// 方法二,通过CompletionService来实现获取线程池中任务的返回结果
public void testByCompetion() throws Exception {
// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(POOL_SIZE);
CompletionService<String> cService = new ExecutorCompletionService<String>(pool);
// 向里面扔任务
for (int i = 0; i < TOTAL_TASK; i++) {
cService.submit(new MyThread("Thread" + i));
}
// 检查线程池任务执行结果
for (int i = 0; i < TOTAL_TASK; i++) {
Future<String> future = cService.take();
System.out.println("method2:" + future.get());
}
// 关闭线程池
pool.shutdown();
}
public static void main(String[] args) throws Exception {
ThreadPoolTest4 threadPoolTest4 = new ThreadPoolTest4();
threadPoolTest4.testByQueue();
threadPoolTest4.testByCompetion();
}
两种方式:一种是通过自己的一个collection来保存线程的执行结果,之后遍历获取结果,但是这时候遍历的顺序的时候,是按照放入的顺序来的,也就是说,collection的第一个结果并不一定是先完成的;第二种方式是使用CompletionService,然后通过take方法来获取,这种方式会保证 先完成的线程执行结果先获取到;如果这时候需要有其他的操作,这种方式就是比较节省时间的;
6.线程的拒绝策略
这个也是面试的时候经常问到的问题,默认的提供了四种方式,所有的拒绝策略需要实现接口RejectedExecutionHandler
默认提供的拒绝策略有AbortPolicy,DiscardPolicy,DiscardOldestPolicy,CallerRunsPolicy;进行下说明
1>AbortPolicy
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
是Java线程池默认的拒绝策略,不执行此任务,直接抛出一个异常;
2>DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
可以看到,方法体是空的,所以也就是不做任何的处理,当前任务不执行;
3>DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
从队列里面抛弃最前面的一个任务,然后执行当前的任务;
4>CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
在调用execute的线程里面执行此command,会阻塞入口。
参考文章:
https://www.tuicool.com/articles/QF7Jr2V
https://blog.csdn.net/king_kgh/article/details/76022136
https://www.cnblogs.com/E-star/p/4882154.html
https://blog.csdn.net/cbjcry/article/details/70154897