面试官:了解Java中的线程池吗?

Java内置的线程池

线程池:为了节约系统资源,减少创建线程的开销以及更好的管理线程,Java提供了一套Executor框架,封装了对多线程的控制,其体系结构如下图所示:
在这里插入图片描述
Executor本身是一个接口,其代码如下:

public interface Executor {
    
    
    void execute(Runnable command);
}

ExecutorService接口对该接口进行了扩展,增加很多方法

shutdown() shutdowmNow() isShutdown() isTerminated() awaitTermination() submit(Callable) submit(Runnable,T) submit(Runnable) invokeAll()等重载方法

重点关注前五个方法:

  • shutdown(): 调用此方法通知线程池 shutdown,调用此方法后,线程池不再接受新的任务,已经提交的任务不会受到影响,会按照顺序执行完毕。不会阻塞调用此方法的线程。
  • shutdowmNow():立即尝试停止所有正在运行的任务,返回一个待执行的任务列表。不会阻塞调用此方法的线程。该方法除了尽力去尝试停止线程外,没有任何保证,任何响应中断失败的线程可能永远不会停止(如:通过thread.interrupted()中断线程时)。
  • isShutdown():返回一个boolean值,如果已经 shutdown 返回true,反之false。
  • awaitTermination(timeout,timeUnit):阻塞直到所有任务全部完成,或者等待 timeout ,或者在等待timeout期间当前线程抛出InterruptedException
  • isTerminated():返回 true 如果所有的任务已经完成且关闭,否则返回false除非在先前已经调用过shutdown()/shutdownNow()

AbstractExecutorService是一个抽象类,实现了ExecutorService,其子类ThreadPoolExecutor进一步扩展了相关功能,在Java中,贴心的Doug Lea提供了一个工具类供我们去使用ThreadPoolExecutor,在Executors中提供了如下几种线程池
在这里插入图片描述
虽然有这么多的线程池,但都是给ThreadPoolExecutor的构造函数传递不同的参数罢了!

上面所提到的线程池中,需要注意的一个线程池为newScheduledThreadPool(),他的源码如下

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    
    
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

返回的是一个ScheduledThreadPoolExecutor对象,在这个类中我们需要注意这三个方法的使用

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);

schedule()会在给定的时间进行一次调度,后面的两方法会周期性的对任务进行调用,但是还有些许差异,scheduleWithFixedDelay()会在上一次任务执行完毕后等待给定的delay时间后再执行,但是如果代码运行的时长大于delay,则会在运行结束后立即运行。scheduleAtFixedRate()则是在上次任务执行的开始时间之后的period后就执行。

scheduleAtFixedRate()的使用:

代码示例:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
AtomicInteger i = new AtomicInteger(1);
System.out.println("当前时间是:"+LocalDateTime.now());
executorService.scheduleAtFixedRate(() -> {
    
    
    System.out.println(LocalDateTime.now());
    System.out.println("得到" + i + "次执行拉");
    i.getAndIncrement();
    // try {
    
    
    //           Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    
    
    //           e.printStackTrace();
    //        }
}, 3, 2, TimeUnit.SECONDS);

运行结果如下图:
在这里插入图片描述
从上面的运行结果可以知道,基本上每一次调度都是在上一次开始之后的 2s 之后,

注意:如果代码运行的时间超过了等待时间,则上一次调度结束后,立马执行。

打开注释的代码,得到的运行结果如下:
在这里插入图片描述
可以看到,每一次运行的结果时间间隔并不是之前的 2s,而是 3s!!

小结:scheduleAtFixedRate的调度流程

先等待 delay 时间后运行 此时如果代码运行的时间 < period,则下次运行的时间是上一次开始调度的时间的period时间后。 如果代码运行时间 > period,则下次运行的时间是在上一次结束之后立马运行。

scheduleWithFixedDelay()的用法:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
AtomicInteger i = new AtomicInteger(1);
System.out.println("当前时间是:"+LocalDateTime.now());
executorService.scheduleWithFixedDelay(()->{
    
    
    System.out.println("运行开始: "+LocalDateTime.now());
    System.out.println("得到"+i+"执行拉");
    i.getAndIncrement();
    // try {
    
    
    //     Thread.sleep(4000);
    // } catch (InterruptedException e) {
    
    
    //     e.printStackTrace();
    // }
    System.out.println("运行结束: "+ LocalDateTime.now());
},3,2,TimeUnit.SECONDS);

在这里插入图片描述
打开注释,得到如下运行结果:
在这里插入图片描述
从上面两次运行的结果可以看到 scheduleWithFixedDelay()的调度间隔和其代码的运行时间没有关系,相邻的间隔时间固定。

线程池的七大参数

在ThreadPoolExecutor提供了如下的构造方法:

面试官:了解Java中的线程池吗?
2023-06-04 15:51·互联网高级架构师
Java内置的线程池
线程池:为了节约系统资源,减少创建线程的开销以及更好的管理线程,Java提供了一套Executor框架,封装了对多线程的控制,其体系结构如下图所示:


Executor本身是一个接口,其代码如下:

public interface Executor {
    
    
    void execute(Runnable command);
}
ExecutorService接口对该接口进行了扩展,增加很多方法

shutdown() shutdowmNow() isShutdown() isTerminated() awaitTermination() submit(Callable<T>) submit(Runnable,T) submit(Runnable) invokeAll()等重载方法

重点关注前五个方法:

shutdown(): 调用此方法通知线程池 shutdown,调用此方法后,线程池不再接受新的任务,已经提交的任务不会受到影响,会按照顺序执行完毕。不会阻塞调用此方法的线程。
shutdowmNow():立即尝试停止所有正在运行的任务,返回一个待执行的任务列表。不会阻塞调用此方法的线程。该方法除了尽力去尝试停止线程外,没有任何保证,任何响应中断失败的线程可能永远不会停止(如:通过thread.interrupted()中断线程时)isShutdown():返回一个boolean值,如果已经 shutdown 返回true,反之falseawaitTermination(timeout,timeUnit):阻塞直到所有任务全部完成,或者等待 timeout ,或者在等待timeout期间当前线程抛出InterruptedException
isTerminated():返回 true 如果所有的任务已经完成且关闭,否则返回false除非在先前已经调用过shutdown()/shutdownNow()
AbstractExecutorService是一个抽象类,实现了ExecutorService,其子类ThreadPoolExecutor进一步扩展了相关功能,在Java中,贴心的Doug Lea提供了一个工具类供我们去使用ThreadPoolExecutor,在Executors中提供了如下几种线程池

方法名

描述

newCachedThreadPool()

必要时创建新的线程,空闲线程保留60s

newFixedThreadPool()

创建固定数目的线程;空闲线程会一直保留

newWorkStealThreadExecutor()

一种适合fork-join任务的线程池,复杂任务拆分为简单的任务,空闲线程会来帮忙

newSingleThreadExecutor()

只有一个线程的线程池,按顺序执行所提交的任务

newScheduledThreadPool()

用于调度执行的固定线程池

newSingleThreadScheduledExecutor()

用于调度执行的单线程池

虽然有这么多的线程池,但都是给ThreadPoolExecutor的构造函数传递不同的参数罢了!

上面所提到的线程池中,需要注意的一个线程池为newScheduledThreadPool(),他的源码如下

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    
    
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
返回的是一个ScheduledThreadPoolExecutor对象,在这个类中我们需要注意这三个方法的使用

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);

schedule()会在给定的时间进行一次调度,后面的两方法会周期性的对任务进行调用,但是还有些许差异,scheduleWithFixedDelay()会在上一次任务执行完毕后等待给定的delay时间后再执行,但是如果代码运行的时长大于delay,则会在运行结束后立即运行。scheduleAtFixedRate()则是在上次任务执行的开始时间之后的period后就执行。

scheduleAtFixedRate()的使用:

代码示例:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
AtomicInteger i = new AtomicInteger(1);
System.out.println("当前时间是:"+LocalDateTime.now());
executorService.scheduleAtFixedRate(() -> {
    
    
    System.out.println(LocalDateTime.now());
    System.out.println("得到" + i + "次执行拉");
    i.getAndIncrement();
    // try {
    
    
    //           Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    
    
    //           e.printStackTrace();
    //        }
}, 3, 2, TimeUnit.SECONDS);
运行结果如下图:


从上面的运行结果可以知道,基本上每一次调度都是在上一次开始之后的 2s 之后,

注意:如果代码运行的时间超过了等待时间,则上一次调度结束后,立马执行。

打开注释的代码,得到的运行结果如下:


可以看到,每一次运行的结果时间间隔并不是之前的 2s,而是 3s!!

小结:scheduleAtFixedRate的调度流程

先等待 delay 时间后运行 此时如果代码运行的时间 < period,则下次运行的时间是上一次开始调度的时间的period时间后。 如果代码运行时间 > period,则下次运行的时间是在上一次结束之后立马运行。

scheduleWithFixedDelay()的用法:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
AtomicInteger i = new AtomicInteger(1);
System.out.println("当前时间是:"+LocalDateTime.now());
executorService.scheduleWithFixedDelay(()->{
    
    
    System.out.println("运行开始: "+LocalDateTime.now());
    System.out.println("得到"+i+"执行拉");
    i.getAndIncrement();
    // try {
    
    
    //     Thread.sleep(4000);
    // } catch (InterruptedException e) {
    
    
    //     e.printStackTrace();
    // }
    System.out.println("运行结束: "+ LocalDateTime.now());
},3,2,TimeUnit.SECONDS);

打开注释,得到如下运行结果:


从上面两次运行的结果可以看到 scheduleWithFixedDelay()的调度间隔和其代码的运行时间没有关系,相邻的间隔时间固定。

线程池的七大参数
在ThreadPoolExecutor提供了如下的构造方法:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize:核心线程数,即一直存活的线程数量,
  • maximumPoolSize:线程池允许的最大线程数量
  • keepAliveTime:当线程池中的线程数量超过核心线程数量时,空闲线程的最大的存活时间
  • unit:keepAliveTime的时间单位
  • workQueue:工作队列,用于存放未执行的任务的队列
  • threadFactory:线程工厂,创建新线程的地方
  • Handler:拒绝策略,当线程池不接受任务时采取的策略
  • DiscardPolicy:直接丢弃
  • DiscardOldestPolicy:丢弃等待最长时间的任务u
  • AbortPolicy:默认的拒绝策略,抛出RejectedExecutionException异常
  • CallerRunsPolicy:只要线程池不处有shutdown,则将任务交给调用者线程执行,即调用execute()方法的线程,如果处于shutdown,则会被丢弃执行。

在上面的表中,列出了Executors工具类中所提供的创建线程的方法,本质上就是7大参数的不同值。

如newFixedThreadPool ,核心线程等于最大线程则为固定线程

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

在比如newCachedThreadPool(),如果当前线程池有空闲线程可用,则立即执行,如果没有空闲线程,则立即创建新的线程执行,不把他放入工作队列中去。

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),threadFactory);
}

SynchronousQueue是BlockingQueue的一种实现,他没有任何的内部容量,往这个队列中进行插入操作必须等待另一个线程的remove操作,在下一篇并发容器的相关文章中做详细介绍。

线程池的生命周期

在ThreadPoolExecutor类中定义了如下几种线程池的状态

RUNNING:接收新任务和处理队列中的任务 SHUTDOWN:不在接收新的任务,但是会处理等待队列中的任务 STOP:不在接收新任务且不会处理等待队列中的任务,还要中断正在运行的任务 TIDYING:所有任务完成且工作线程数为0,调用terminate()会进入到此状态 TERMINATED:terminate()运行完毕之后会进入这种状态

在这里插入图片描述

线程池的工作原理

在ThreadPoolExecutor这个类中,其execute()方法,展示了线程池的工作原理

源码如下:

public void execute(Runnable command) {
    
    
        if (command == null) // 1
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
    
     // 2
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
    
     //3
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 4
            reject(command);
    }

第一:检查传进来的对象是否为null,如果是则抛出空指针异常。

第二:如果工作线程数小于核心线程数则通过addWoker()将任务交给线程池处理,即立马执行当前的任务

第三:如果工作线程数大于等于核心线程数,则将任务加入到工作队列

第四:如果工作队列已满,则交给线程池处理,如果当前线程数小于最大线程数,则创建新的线程运行任务,反之拒绝该任务

在这里插入图片描述

自定义线程池

在思考如何自定义线程池之前,需要首先回顾下线程池的七大参数:

核心线程数:常驻线程池的线程 最大线程数:线程池的最多的线程容量 存活时间和时间单位:空闲线程的最大存活时间 工作队列:存储还没来得及处理的任务的容器 线程工厂:创建新线程的地方 拒绝策略:说 “No” 的方式

在这7大参数中:核心线程数,最大线程数,以及存活时间和时间单位在我个人看来不是那么重要!毕竟在生产中这些参数在具体场景下都会得到确定,不会有什么特别可以定制的地方,(他好像没那么重要.jpg)

而工作队列的选择和拒绝策略则可以有较多的选择只要是实现了BlockedQueue接口的容器都可以当作工作队列,换句话说就是只要实现了该接口,都可以充当工作队列。同样的,在 jdk 中默认实现的4种拒绝策略,他们都实现了RejectedExecutionHandler接口!而这个接口的作用就是定义哪些不能够被线程池处理的任务,这个接口里面只有一个方法rejectedExecution(Runnable r, ThreadPoolExecutor executor);这个方法的调用时机就是线程池无法接收新的任务时!

线程工厂:ThreadFactory接口中,定义了newThread(Runnable r);方法进行创建新线程。因此只要实现了改接口,也能够根据自己的意愿去创建新线程!

因此在创建自定义线程池的时候,我们可以进行如下选择

实现ThreadFactory:定义创建线程的方式,在Executors中有默认实现,但也可以自己去实现! 实现RejectedExecutionHandler:自定义拒绝策略 实现BlockingQueue:就可以自定义工作队列

除此之外,在Executors工具类中,其创建线程的方式如以下几种:

// Executors
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    
    
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
// ThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    
    
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
// Executors
public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

大多都是根据不同的工作队列,创建了不同特性的线程池!

创建自定义拒绝策略的线程池

参照 netty 的代码,创建如下的拒绝策略:

public class MyRejectHandler implements RejectedExecutionHandler {
    
    
    @Override
     public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    
    
        try {
    
    
            System.out.println("给你机会,你不中用啊");
            final Thread t = new Thread(r, "new thread execute new task");
            t.start();
        } catch (Throwable e) {
    
    
            throw new RejectedExecutionException(
                    "Failed to start a new thread", e);
        }

    }
}

这里创建的线程池是借助了ThreadPoolExecutor, 如果对自己的能力自信,可以自己去实现一个自己的"ThreadPoolExecutor",来给人们一点小小的震撼!

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 
                                                     2, 
                                                     0, 
                                                     TimeUnit.SECONDS, 
                                                     new ArrayBlockingQueue<Runnable>(1), 
                                                     new MyRejectHandler());

for (int i = 0; i < 10; i++) {
    
    
    executor.execute(() -> {
    
    
        System.out.println("new Task submit " + LocalDateTime.now());
    });
    // try {
    
    
    //     Thread.sleep(1000);
    // } catch (InterruptedException e) {
    
    
    //     e.printStackTrace();
    // }
}

先打开注释,放慢任务提交,此时没有触发拒绝策略
在这里插入图片描述注释掉这段线程延时代码,可以得到如下输出,打印了七次给你机会,你不中用啊,这是因为常驻线程为2,可以处理2个任务,工作队列的容量为1,可以保存一个,因此,可以处理3个任务,而通过下面打印的时间,就可以发现在同一时刻,提交了9个任务,因此处理不过来所以打印了 10- 3 = 7次
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Park33/article/details/131046218