运维开发网

每日六道java新手入门面试题,通往自由的道路--线程池

运维开发网 https://www.qedev.com 2021-07-01 10:15 出处:网络 作者: 太子爷哪吒
目录1. 你知道线程池吗?为什么需要使用线程池?小结:2. 你知道有多少种创建线程池的方式3. 线程池的五种状态你有了解吗?4. 你知道ThreadPoolExecutor的构造方法和参数吗5. 你可以说下线程池的执行过程原理吗6. 能
目录
  • 1. 你知道线程池吗?为什么需要使用线程池?
    • 小结:
  • 2. 你知道有多少种创建线程池的方式
    • 3. 线程池的五种状态你有了解吗?
      • 4. 你知道ThreadPoolExecutor的构造方法和参数吗
        • 5. 你可以说下线程池的执行过程原理吗
          • 6. 能否写一个简单线程池的demo?
            • 总结

              1. 你知道线程池吗?为什么需要使用线程池?

              在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。

              而在java中, JVM 中每创建和销毁线程就需要资源和时间的损耗了,线程中也是存在上下文切换,这需要一定的开销,并且线程的创建并不是越多越好,而如果创建的线程数太多,上下文切换的频率就变高,可能使得多线程带来的好处抵不过线程切换带来的开销,就有点得不偿失了。

              那我们需要如何管控好线程呢?

              所以我们可以创建一个容器把线程数缓存在容器了,以便给他人使用,并且无需再自行创建和销毁线程。

              每日六道java新手入门面试题,通往自由的道路--线程池

              小结:

              线程池就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁。

              使用线程池的好处

              • 降低了资源的消耗,重用存在的线程,减少线程的创建和销毁的资源损耗。
              • 提高了响应速度,无需等待创建和销毁的时间,一旦任务到达的时候,即可通过线程池的线程执行。
              • 提高了线程的管控性,线程是稀缺的资源,如果无限创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

              2. 你知道有多少种创建线程池的方式

              JDK1.5以后提供一个Executors工具类 ,里面提供一些静态工厂方法,生成一些常用的线程池。

              newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

              那我们来看看底层的方法和实现过程:

              底层:

              public static ExecutorService newCachedThreadPool() {
                  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                http://www.cppcns.com          编程客栈      60L, TimeUnit.SECONDS,
                                                new SynchronousQueue<Runnable>());
              }
              

              我们实现的步骤:

              public class ThreadPoolDemo {
                  public static void main(String[] args) {
                      threadPoolTest();
                  }
                  private static void threadPoolTest() {
                      // 1. 使用工厂类获取线程池对象
                      ExecutorService executorService = Executors.newCachedThreadPool();
                      // 2. 提交任务
                      for (int i = 1; i < 8; i++) {
                          executorService.submit(new MyRunnable(i));
                      }
                  }
              }
              // 我们的任务类
              class MyRunnable implements Runnable {
                  private int id;
                  public MyRunnable(int id) {
                      this.id = id;
                  }
                  @Override
                  public void run() {
                      // 打印是哪个线程的名称。
                      System.out.println(Thread.currentThread().getName() + "执行了任务" + id);
                  }
              }
              

              可以得到的结果:

              每日六道java新手入门面试题,通往自由的道路--线程池

              可以发现,线程池的开启是一直运行的状态,而如果你想结束的话,可以使用一个shutdown方法即 executorService.shutdown(); 每次任务都会创建多一个线程出来了。

              我们可以看下newCacheThreadPool的运行流程如下:

              • 提交任务进线程池。
              • 因为corePoolSize为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE。
              • 尝试将任务添加到SynchronousQueue队列。
              • 如果SynchronousQueue入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue拉取任务并在当前线程执行。
              • 如果SynchronousQueue已有任务在等待,入列操作将会阻塞。

              当需要执行很多短时间的任务时,newCacheThreadPool的线程复用率比较高, 会显著的提高性能。而且线程60s后会回收,意味着即使没有任务进来,newCacheThreadPool并不会占用很多资源。

              newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

              还是一样看下底层和代码实现过程吧:

              底层:

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

              代码实现过程:

              public static void main(String[] args) {
                  // threadPoolTest();
                  threadPoolTest2();
              }
              private static void threadPoolTest2() {
                  // 1. 使用工厂类获取线程池对象
                  ExecutorService executorService = Executors.newFixedThreadPool(3);
                  // 2. 提交任务
                  for (int i = 1; i < 8; i++) {
                      executorService.submit(new MyRunnable(i));
                  }
              }
              

              得到的结果:

              pool-1-thread-2执行了任务2

              pool-1-thread-1执行了任务1

              pool-1-thread-3执行了任务3

              pool-1-thread-3执行了任务6

              pool-1-thread-1执行了任务5

              pool-1-thread-2执行了任务4

              pool-1-thread-3执行了任务7

              我们可以发现,创建线程数量就是我们指定3,核心线程数量和总线程数量相等,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程。因为LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,故如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。

              与newCacheThreadPool的区别

              • 因为 corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。
              • 在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收。 CachedThreadPool会在60s后收回。
              • 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多
              • 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。

              ​newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行

              还是一样看下底层和代码实现过程吧:

              底层

              public static ExecutorService newSingleThreadExecutor() {
                  return new FinalizableDelegatedExecutorService
                      (new ThreadPoolExecutor(1, 1,
                                              0L, TimeUnit.MILLISECONDS,
                                              new LinkedBlockingQueue<Runnable>()));
              }
              

              代码实现过程

              public static void main(String[] args) {
                  // threadPoolTest();
                  // threadPoolTest2();
                  threadPoolTest3();
              }
              private static void threadPoolTest3() {
                  // 1. 使用工厂类获取线程池对象
                  ExecutorService executorService = Executors.newSingleThreadExecutor();
                  // 2. 提交任务
                  for (int i = 1; i < 8; i++) {
                      executorService.submit(new MyRunnable(i));
                  }
              }
              

              得到的结果:

              pool-1-thread-1执行了任务1

              pool-1-thread-1执行了任务2

              编程客栈pool-1-thread-1执行了任务3

              pool-1-thread-1执行了任务4

              pool-1-thread-1执行了任务5

              pool-1-thread-1执行了任务6

              pool-1-thread-1执行了任务7

              可以发现,只创建了一个线程,有且仅有一个核心线程( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。

              newScheduledThreadpool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

              还是一样看下底层和代码实现过程吧:

              底层

              public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
                  return new ScheduledThreadPoolExecutor(corePoolSize);
              }
              public ScheduledThreadPoolExecutor(int corePoolSize) {
                  super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                        new DelayedWorkQueue());
              }
              

              代码实现

              public static void main(String[] args) {
                  //        threadPoolTest();
                  //        threadPoolTest2();
                  //        threadPoolTest3();
                  threadPoolTest4();
              }
              private static void threadPoolTest4() {
                  // 1. 使用工厂类获取线程池对象
                  ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
                  // 2. 每个任务延迟两秒执行
                  for (int i = 1; i < 8; i++) {
                      scheduledExecutorService.schedule(new MyRunnable(i), 2, TimeUnit.SECONDS);
                  }
                  System.out.println("看是不是我先执行了!");
              }
              

              可以看到的结果:

              看是不是我先执行了!

              pool-1-thread-1执行了任务1

              pool-1-thread-1执行了任务4

              pool-1-thread-2执行了任务2

              pool-1-thread-3执行了任务3

              pool-1-thread-2执行了任务6

              pool-1-thread-1执行了任务5

              pool-1-thread-3执行了任务7

              我们可以发现,线程池只创建我们指定的线程数,并且返回的是一个继承了ExecutorService的ScheduledExecutorService的接口。它给我们提供一些延迟的方法:

              public ScheduledFuture<?> schedule(Runnable command,
                                                     long delay, TimeUnit unit);
              延迟时间单位是unit,时间数是delay,任务是Runnable类型的command。
              public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                                      long delay, TimeUnit unit);
              而这个方法是上面方法的重载,不一样的是任务是Callable类型的
              

              3. 线程池的五种状态你有了解吗?

              线程池它有以下五种状态:

              每日六道java新手入门面试题,通往自由的道路--线程池

              具体有:

              • RUNNING:这是最正常的状态,能正常接受新的任务,正常处理等待队列中的任务。
              • SHUTDOWN:不接受新的任务提交,但是会继续处理正在执行的业务并且也会处理阻塞队列中的任务。
              • STOP:不接受新的任务提交,不再处理等待队列中的任务,并且中断正在执行任务的线程。
              • TIDYING:所有的任务都执行完毕或销毁了,当前活动线程数为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()进入终止状态。
              • TERMINATED:线程池彻底终止,即terminated()方法结束后,线程池的状态就会变成这个。

              4. 你知道ThreadPoolExecutor的构造方法和参数吗

              我们先来看看它的构造方法有哪些:

              // 五个参数的构造函数
              public ThreadPoolExecutor(int corePoolSize,
                                        int maximumPoolSize,
                                        long keepAliveTime,
                                        TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue) {...}
              // 六个参数的构造函数
              public ThreadPoolExecutor(int corePoolSize,
                                        int maximumPoolSize,
                                        long keepAliveTime,
                                        TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue,
                                        ThreadFactory threadFactory) {...}
              // 六个参数的构造函数
              public ThreadPoolExecutor(int corePoolSize,
                                        int maximumPoolSize,
                                        long keepAliveTime,
                                        TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue,
                                        RejectedExecutionHandler handler) {...}
              // 七个参数的构造函数
              public ThreadPoolExecutor(int corePoolSize,
                                        int maximumPoolSize,
                                        long keepAliveTime,
                                        TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue,
                                        ThreadFactory threadFactory,
                                        RejectedExecutionHandler handler) {...}
              

              我们再来详解下构造方法中涉及的7个参数,其中最重要5个参数就是第一个构造方法中的。

              • int corePoolSize:该线程池中核心线程数量

              核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干,而非核心线程(临时工)如果长时间的闲置,就会被销毁。但是如果将

              allowCoreThreadTimeOut设置为true时,核心线程也是会被超时回收。

              • int maximumPoolSize:该线程池中允许存在的工作线程的最大数量。

              该值相当于核心线程数量 + 非核心线程数量。

              • long keepAliveTime:非核心线程闲置超时时长。

              非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。

              • TimeUnit unit:keepAliveTime的时间单位。

              TimeUnit是一个枚举类型 ,包括以下属性:

               NANOSECONDS : 1微毫秒

              MICROSECONDS : 1微秒

              MILLISECONDS : 1毫秒

              SECONDS : 秒

              MINUTES : 分

              HOURS : 小时

              DAYS : 天

              • BlockingQueue workQueue:阻塞队列,维护着等待执行的Runnable任务对象。

              当新任务来的时候,会先判断当前运行线程数量是否达到了核心线程数,如果达到了,就会被存放在阻塞队列中排队等待执行。

              每日六道java新手入门面试题,通往自由的道路--线程池

              常用的几个阻塞队列:

              1.ArrayBlockingQueue

              数组阻塞队列,底层数据结构是数组,需要指定队列的大小。

              2.SynchronousQueue

              同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。

              3.DelayQueue

              延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。

              4.LinkedBlockingQueue

              链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。

              还有两个非必须的参数:

              • ThreadFactory threadFactory

              创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。

              • RejectedExecutionHandler handler

              拒绝处理策略,在线程数量大于最大线程数后就会采用拒绝处理策略,四种拒绝处理的策略为 :

              • ThreadPoolExecutor.AbortPolicy默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
              • ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常。
              • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
              • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

              5. 你可以说下线程池的执行过程原理吗

              昨天MyGirl跟我讲了一下她去银行办理业务的一个场景:

              1. 首先MyGirl(任务A)先去银行(线程池)办理业务,她发现她来早了,现在银行才刚开门,柜台窗口服务员还没过来(相当于线程池中的初始线程为0),此时银行经理看到MyGirl来了,就安排她去一号柜台窗口并安排了1号正式工作人员来接待她。
              2. 在MyGirl的业务还没办完时,一个不知名的路人甲(任务B)出现了,他也是要来银行办业务,于是银行经理安排他去二号柜台并安排了2号正式工作人员。假设该银行的柜台窗口就只有两个(核心线程数量2)。
              3. 紧接着,在所有人业务都还没做完的情况,持续来个三个不知名的路人乙丙丁,他们也是要来办业务的,但是由于柜台满了,安排了他们去旁边的银行大厅的座位上(阻塞队列,这里假设大小为3)等候并给了对应顺序的号码,说等前面两个人办理完后,按顺序叫号你们呦,请注意听。
              4. 过一会,一www.cppcns.com个路人戊也想来银行办理业务,而经理看到柜台满了,座位满了,只能安排了一个临时工(非核心线程,这里假设最大线程为3,即非核心为1)手持pad设备并给路人戊去办理业务。
              5. 而此时,一个路人戌过来办理业务,而经理看到柜台满了,座位满了,临时工也安排满了(最大线程数+阻塞队列都满了),无奈经理只能掏出一本《如何接待超出最大限度的手册》,选择拒接接待路人戌通知他,过会再来吧您嘞,这里已经超负荷啦!
              6. 最后,相继所有人的业务都办完了,现在也没人再来办业务,并且临时工的空闲时间也超过了1小时以上了(最大空闲时间默认60秒),经理让临时工都先下班回家了(销毁线程)。
              7. 但是一个银行要保证正常的运行,只能让正式员工继续上班,不得提早下班。

              每日六道java新手入门面试题,通往自由的道路--线程池

              而实际上线程的流程原理跟这个一样,我们来看下处理任务的核心方法execute,它的源码大概是什么样子的呢,当然我们也可以看源码中的注释,里面也写的很清楚。这里具体讲下思路。

              public void execute(Runnable command) {
                  if (command == null)
                      throw new NullPointerException();  
                  // 1. 获取ctl,ctl是记录着线程池状态和线程数。
                  int c = ctl.get();
                  // 2. 判断当前线程数小于corePoolSize核心线程,则调用addWorker创建核心线程执行任务
                  if (workerCountOf(c) < corePoolSize) {
                     if (addWorker(command, true))
                         return;
                     // 创建线程失败,需要重新获取clt的状态和线程数。
                     c = ctl.get();
                  }
                  // 3. 如果不小于corePoolSize,进入下面的方法。
                  // 判断线程池是否运行状态并且运行线程数大于corePoolSize,将任务添加到workQueue队列。
                  if (isRunning(c) && workQueue.offer(command)) {
                      int recheck = ctl.get();
                      // 3.1 再次检查线程池是否运行状态。
                      // 如果isRunning返回false(状态检查),则remove这个任务,然后执行拒绝策略。
                      if (! isRunning(recheck) && remove(command))
                          reject(command);
                          // 3.2 线程池处于running状态,但是没有线程,则创建线程加入到线程池中
                      else if (workerCountOf(recheck) == 0)
                          addWorker(null, false);
                  }
                  // 4. 如果放入workQueue失败,则创建非核心线程执行任务,
                  // 如果这时创建非核心线程失败(当前线程总数不小于maximumPoolSize时),就会执行拒绝策略。
                  else if (!addWorker(command, false))
                       reject(command);
              }
              

              我们可以大概看下思路图:

              先解释下ctl

              每日六道java新手入门面试题,通往自由的道路--线程池

              变量ctl定义为AtomicInteger,记录了“线程池中的任务数量”和“线程池的状态”两个信息。以高三位记录着线程池的状态和低29位记录线程池中的任务数量。

              RUNNING : 111

              SHUTDOWN : 000

              STOP : 001

              TIDYING : 010

              TERMINATED : 011

              最后总结一下执行过程:

              • 任务到达时,会先判断核心线程是否满了,不满则调用addWorker方法创建核心线程执行任务。
              • 然后会判断下线程池中的线程数 < 核心线程,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到核心线程总数)。此步骤会开启锁mainLock.lock();。
              • 而在线程池中的线程数 >= 核心线程时,新来的线程任务会进入任务阻塞队列中等待,然后空闲的核心线程会依次去阻塞队列中取任务来执行。
              • 当阻塞队列满了,说明这个时候任务很多了,此时就需要一些非核心线程临时工来执行这些任务了。于是会创建非核心线程去执行这个任务。
              • 最后当阻塞队列满了, 且总线程数达到了maximumPoolSize,则会采取拒绝策略进行处理。
              • 当非核心线程取任务的时间达到keepAliveTime还没有取到任务即空闲时间,就会回收非核心线程。

              6. 能否写一个简单线程池的demo?

              这里还是直接用简单的ThreadPoolExecutor创建吧,等后续写线程池相关文章,再详细写自己创建的线程池吧。

              我们先创建一个任务类Task:

              /**
               * 自定义任务类
               */
              public class Task implements Runnable{
                  private int id;
                  public Task(int id) {
                      this.id = id;
                  }
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName() + "即将执行的任务是" + id + "任务");
                      try {
                          Thread.sleep(300);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println(Thread.currentThread().getName() + "执行完成的任务是" + id + "任务");
                  }
              }
              

              测试代码

              public class ThreadPoolExecutorDemo {
                  private static final int CORE_POOL_SIZE = 3;
                  private static final int MAX_POOL_SIZE = 5;
                  private static final int QUEUE_CAPACITY = 10;
                  private static final Long KEEP_ALIVE_TIME = 1l;
              
                  public static void main(String[] args) {
                      //通过ThreadPoolExecutor构造函数自定义参数创建
                      qhrAbWThreadPoolExecutor executor = new ThreadPoolExecutor(
                              CORE_POOL_SIZE,
                              MAX_POOL_SIZE,
                              KEEP_ALIVE_TIME,
                              TimeUnit.SECONDS,
                              new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                              new ThreadPoolExecutor.CallerRunsPolicy());
                      for (int i = 0; i < 10; i++) {
                          Task task = new Task( i);
                          //执行Runnable
                          executor.execute(task);
                      }
                      //终止线程池
                      executor.shutdown();
                      while (!executor.isTerminated()) {
                      }
                      System.out.println("线程已经全部执行完");
                  }
              }
              

              得到的结果

              pool-1-thread-1即将执行的任务是0任务

              pool-1-thread-3即将执行的任务是2任务

              pool-1-thread-2即将执行的任务是1任务

              pool-1-thread-1执行完成的任务是0任务

              pool-1-thread-3执行完成的任务是2任务

              pool-1-thread-1即将执行的任务是3任务

              pool-1-thread-3即将执行的任务是4任务

              pool-1-thread-2执行完成的任务是1任务

              pool-1-thread-2即将执行的任务是5任务

              pool-1-thread-3执行完成的任务是4任务

              pool-1-thread-1执行完成的任务是3任务

              pool-1-thread-3即将执行的任务是6任务

              pool-1-thread-1即将执行的任务是7任务

              pool-1-thread-2执行完成的任务是5任务

              pool-1-thread-2即将执行的任务是8任务

              pool-1-thread-3执行完成的任务是6任务

              pool-1-thread-1执行完成的任务是7任务

              pool-1-thread-3即将执行的任务是9任务

              pool-1-thread-2执行完成的任务是8任务

              pool-1-thread-3执行完成的任务是9任务

              线程已经全部执行完

              总结

              这篇文章就到这里了,如果这篇文章对你也有所帮助,希望您能多多关注我们的更多内容!

              0

              精彩评论

              暂无评论...
              验证码 换一张
              取 消