一文带你了解Java线程池(Executor)-上

分析Java线程池就离不开Executor类,今天就让我们来一起好好看下

除开今天要讲的线程池,我还整理了一些技术资料和面试题集,供大家提升进阶,面试突击,不管你是有跳槽打算还是单纯精进自己,都可以免费领取一份。

面试简历模板到大厂面经汇总,从内部技术资料到互联网高薪必读书单,以及Java面试核心知识点(283页)和Java面试题合集2022年最新版(485页)等等

领取方式在文末!

Executor框架

为了更好地控制多线程,JDK提供了一套Executor框架,可以有效地进行线程控制,其本质上就是一个线程池。

一文带你了解Java线程池(Executor)-上_第1张图片

其中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的使用

ScheduledFuture很简单,它就是在Future基础上还集成了ComparableDelayed的接口。它用于表示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:

线程创建策略如下,通过下面这个流程图可以很好的理解corePoolSizemaximumPoolSize的关系:

一文带你了解Java线程池(Executor)-上_第2张图片

来分析一下这个流程图,当一个任务被提交进来后,首先会比较该线程池运行的线程数量与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和拒绝策略的相关内容我放到下一篇和大家分享,前面说到了为大家准备一份资料,简单介绍下,包含以下内容:

  • Java架构师学习路线图(对标阿里P7级别,更高阶的大佬小弟就不在这献丑了)
  • 模块化学习资源(Java并发编程、分布式缓存的原理及应用、ZooKeeper原理及应用、Netty网络编程原理及应用、Kafka原理及应用、常见的23种经典设计模式、Spring原理及应用、数据结构与算法……)
  • 2022年大厂面试高频知识点整理

资料持续更新中,目前全部都是免费送给大家,如果有需要,尽管拿走,添加我助手领取,备注“CSDN南哥”

你可能感兴趣的:(java,面试,经验分享,架构)