java多线程系列-Executors框架

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值