带着面试官畅游Java线程池

简介: java中经常需要用到多线程来处理一些业务,如果单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。

一、为什么使用线程池


     

java中经常需要用到多线程来处理一些业务,如果单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。


java中涉及到线程池的相关类均在jdk1.5开始的 java.util.concurrent(JUC)包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。


二、线程池的优势


     

总体来说,线程池有如下的优势:


(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。


三、线程池的简单使用


   

1、Executors.newCachedThreadPool()

   

cached的池子非常大,可以有很多的线程并发运行。


package threadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo01 {
  public static void main(String[] args) {
    ExecutorService e1 = Executors.newCachedThreadPool();
//    ExecutorService e2 = Executors.newFixedThreadPool(10);
//    ExecutorService e3 = Executors.newSingleThreadExecutor();
    //使用线程
    e1.execute(new Task(1));
  }
}
class Task implements Runnable{
  int i;
  public Task(int i) {
    this.i = i;
  }
  @Override
  public void run() {
    //打印当前线程的名字
    System.out.println(Thread.currentThread().getName() + "-----" + i);
  } 
}

cdc37605d33b47379f03b4e152dbd4cb.png


已经打出了当前线程的名字。


我们将调用线程的方法执行100次,由于run方法执行有时长,线程来不及回池子时就需要再执行,所以需要开启另一个线程。执行100次大概需要开启三十多个线程。


public class ThreadPoolDemo01 {
  public static void main(String[] args) {
  ExecutorService e1 = Executors.newCachedThreadPool();
//  ExecutorService e2 = Executors.newFixedThreadPool(10);
//  ExecutorService e3 = Executors.newSingleThreadExecutor();
  //使用线程
  for(int i=0; i<100; i++) {
    e1.execute(new Task(i));
  }
  }
}
class Task implements Runnable{
  int i;
  public Task(int i) {
  this.i = i;
  }
  @Override
  public void run() {
  //打印当前线程的名字
  System.out.println(Thread.currentThread().getName() + "-----" + i);
  } 
}


22c040b916e249d4b0a123e3ae250119.png


线程池的序号大概是到三十几,说明从线程池中拿了三十多个线程。


我们在run方法中加一个一秒的睡眠,再看看结果。


class Task implements Runnable{
  int i;
  public Task(int i) {
    this.i = i;
  }
  @Override
  public void run() {
    //打印当前线程的名字
    System.out.println(Thread.currentThread().getName() + "-----" + i);
    //睡1s
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  } 
}

77b580b3a55343f68401ab224d88171c.png



可以看到大概使用了100个线程。说明每一次调用都需要用一个新的线程。


2、Executors.newFixedThreadPool()

     

fixed线程池需要指定生成的线程数量。我们在代码中指定生成10个线程。


线程池中只会生成10个线程,如果run方法中睡1秒钟,那么在1s之内只能打印出10个线程名字,我们的代码执行完需要10s。


public class ThreadPoolDemo01 {
  public static void main(String[] args) {
//    ExecutorService e1 = Executors.newCachedThreadPool();
    ExecutorService e2 = Executors.newFixedThreadPool(10);
//    ExecutorService e3 = Executors.newSingleThreadExecutor();
    //使用线程
    for(int i=0; i<100; i++) {
      e2.execute(new Task(i));
    }
  }
}
class Task implements Runnable{
  int i;
  public Task(int i) {
    this.i = i;
  }
  @Override
  public void run() {
    //打印当前线程的名字
    System.out.println(Thread.currentThread().getName() + "-----" + i);
    //睡1s
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  } 
}

线程池中只会生成10个线程,如果run方法中睡1秒钟,那么在1s之内只能打印出10个线程名字,我们的代码执行完需要10s。

35fd607d26534b589c4dd97f7daba399.png


3、Executors.newSingleThreadExecutor()


这个线程池中只有一个线程,可以理解为fixed的单数版。


public class ThreadPoolDemo01 {
  public static void main(String[] args) {
//  ExecutorService e1 = Executors.newCachedThreadPool();
//  ExecutorService e2 = Executors.newFixedThreadPool(10);
  ExecutorService e3 = Executors.newSingleThreadExecutor();
  //使用线程
  for(int i=0; i<100; i++) {
    e3.execute(new Task(i));
  }
  }
}
class Task implements Runnable{
  int i;
  public Task(int i) {
  this.i = i;
  }
  @Override
  public void run() {
  //打印当前线程的名字
  System.out.println(Thread.currentThread().getName() + "-----" + i);
  //睡1s
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  } 
}


1bf89cb8c00c41808b3acd7469b4bc91.png

每次打印出的线程名字都相同,说明自始至终都使用的用一个线程。1s钟只打印1个名字,代码执行完需要100s(需要等1s后线程回到线程池,才能再使用这个线程)。


四、线程池源码分析



public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
可以看到他们都是由ThreadPoolExecutor构造出的方法,那我们再看看ThreadPoolExecutor:
/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    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.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }


1、线程池参数


int corePoolSize     核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务


int maximumPoolSize   最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)


long keepAliveTime   线程的最大存活时间,空闲时间超过keepAliveTime就会被自动终止回收掉,一直回收到剩corePoolSize个


TimeUnit unit       存活时间的单位


BlockingQueue<Runnable> workQueue              阻塞队列


ThreadFactory threadFactory                               线程工厂


RejectedExecutionHandler handler                      拒绝执行时的处理函数


按照下面的代码来执行一下


public class ThreadPoolDemo01 {
  public static void main(String[] args) {
//  ExecutorService e1 = Executors.newCachedThreadPool();
//  ExecutorService e2 = Executors.newFixedThreadPool(10);
//  ExecutorService e3 = Executors.newSingleThreadExecutor();
  ThreadPoolExecutor t = new ThreadPoolExecutor(
            10,   //corePoolSize
            20,   //maximumPoolSize
            10,   //keepAliveTime
            TimeUnit.SECONDS,  //TimeUnit 
            new ArrayBlockingQueue<>(10)  //BlockingQueue
            );
  //使用线程
  for(int i=0; i<100; i++) {
    t.execute(new Task(i));
  }
  }
}
class Task implements Runnable{
  int i;
  public Task(int i) {
  this.i = i;
  }
  @Override
  public void run() {
  //打印当前线程的名字
  System.out.println(Thread.currentThread().getName() + "-----" + i);
  //睡1s
  try {
    Thread.sleep(1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  } 
}

     

在代码中,我们将核心线程数设置为10,最大线程数设置为20,最大存活时间设置为10,单位为秒,阻塞队列的大小设置为10


看一下执行结果:


pool-1-thread-3-----2
pool-1-thread-7-----6
pool-1-thread-1-----0
pool-1-thread-2-----1
pool-1-thread-4-----3
pool-1-thread-5-----4
pool-1-thread-6-----5
pool-1-thread-10-----9
pool-1-thread-9-----8
pool-1-thread-8-----7
pool-1-thread-11-----20
pool-1-thread-12-----21
pool-1-thread-13-----22
pool-1-thread-14-----23
pool-1-thread-15-----24
pool-1-thread-16-----25
pool-1-thread-17-----26
pool-1-thread-18-----27
pool-1-thread-19-----28
pool-1-thread-20-----29
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task threadPool.Task@135fbaa4 rejected from java.util.concurrent.ThreadPoolExecutor@45ee12a7[Running, pool size = 20, active threads = 20, queued tasks = 10, completed tasks = 0]
  at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
  at threadPool.ThreadPoolDemo01.main(ThreadPoolDemo01.java:27)
pool-1-thread-16-----11
pool-1-thread-15-----12
pool-1-thread-13-----15
pool-1-thread-19-----10
pool-1-thread-14-----13
pool-1-thread-8-----18
pool-1-thread-11-----17
pool-1-thread-12-----16
pool-1-thread-18-----14
pool-1-thread-9-----19


2、执行流程分析


由于我们的核心线程数(corePoolSize)设置为10,就会有10个常驻的核心线程去执行


pool-1-thread-3-----2
pool-1-thread-7-----6
pool-1-thread-1-----0
pool-1-thread-2-----1
pool-1-thread-4-----3
pool-1-thread-5-----4
pool-1-thread-6-----5
pool-1-thread-10-----9

     

由于run方法中睡眠了一秒钟,后面进入的任务会进入阻塞队列(blockingQueue)中,当阻塞队列中的十个空间被填满后,创建普通的线程去执行。


pool-1-thread-11-----20
pool-1-thread-12-----21
pool-1-thread-13-----22
pool-1-thread-14-----23
pool-1-thread-15-----24
pool-1-thread-16-----25
pool-1-thread-17-----26
pool-1-thread-18-----27
pool-1-thread-19-----28
pool-1-thread-20-----29

     

由于blockingQueue的大小为10,10个任务进去之后,再进任务就会报拒绝执行(RejectedExecutionException)的错了:


Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task threadPool.Task@135fbaa4 rejected from java.util.concurrent.ThreadPoolExecutor@45ee12a7[Running, pool size = 20, active threads = 20, queued tasks = 10, completed tasks = 0]
  at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
  at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
  at threadPool.ThreadPoolDemo01.main(ThreadPoolDemo01.java:27)

     

1秒之后,线程执行结束之后回到线程池,就可以继续去队列中接受任务。将队列中的十个任务接收完。


核心线程和普通线程是不作区分的,他们没有任何的区别,所以接收任务的时候也是谁先结束谁就去接收。


pool-1-thread-16-----11
pool-1-thread-15-----12
pool-1-thread-13-----15
pool-1-thread-19-----10
pool-1-thread-14-----13
pool-1-thread-8-----18
pool-1-thread-11-----17
pool-1-thread-12-----16
pool-1-thread-18-----14
pool-1-thread-9-----19

     

3、cachedThreadPool()源码分析


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

     

可以看到在cached中,核心线程数为0,线程总数为无穷大,阻塞队列为0,线程存活时间为60s。


这说明cached中没有核心线程,任务也不能进入阻塞队列,那么在一开始就会申请普通线程去执行。而线程存活时间为60s,被复用的次数会非常多,除非线程结束任务后的60s内没有新任务,线程才会被销毁,由于核心线程数为0,所有的线程均会被回收;同时线程数总数位无穷大,可以同时有非常多的线程。


4、newFixedThreadPool()源码分析


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

     

核心线程数和最大线程数一样,都是传入的数值,销毁时间为0。


说明传入的线程都作为核心线程使用,并且使用之后立即销毁。


相关文章
|
1月前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
18天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
15天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
1月前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
112 38
|
22天前
|
存储 缓存 监控
Java中的线程池深度解析####
本文深入探讨了Java并发编程中的核心组件——线程池,从其基本概念、工作原理、核心参数解析到应用场景与最佳实践,全方位剖析了线程池在提升应用性能、资源管理和任务调度方面的重要作用。通过实例演示和性能对比,揭示合理配置线程池对于构建高效Java应用的关键意义。 ####
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
57 4
|
1月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
91 4
|
1月前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
249 2