Java线程池(一)—— 核心概念及使用方法

1,106 阅读6分钟

Java线程池简介及核心概念

一、简介

Java提供的管理线程的工具类(线程管理API)

主要功能:线程调度、复用;控制线程数量。

好处:节省频繁创建线程导致的性能开销。

二、如何使用

  1. 使用JDK自带的线程池

    // 使用JDK中自带的线程池
    ExecutorService service = Executors.newSingleThreadExecutor();
    service.execute(()->{
        // 要执行的逻辑代码
    });
    

    重点:熟悉系统自带的几种线程池的核心参数及其应用场景(有助于理解自定义线程池)。

  2. 使用自定义线程池

    // 自定义线程池
    ExecutorService customService = new ThreadPoolExecutor(
            0,			     //核心池大小
            Integer.MAX_VALUE,	     //线程池最大容量
            60L,		             //非核心线程池存活时间
            TimeUnit.SECONDS,	     //时间单位
            new SynchronousQueue<>(),    //阻塞队列,用于存放任务
            runnable -> {
                // 线程工厂,常用来给线程池中的线程命名
                Thread thread = new Thread(runnable, "my Thread: ");
                return thread;
            },
            (runnable, threadPoolExecutor) -> {
                // 自定义拒绝策略,当阻塞队列中任务的数量 + 非核心线程数量 > 线程池的最大容量时
                // 会使用该策略拒绝用户的提交请求
            }
    );
    // 提交任务
    customService.execute(()->{
            // 要执行的逻辑代码
    });
    

    重点:熟悉自定义线程池的核心参数及其意义。

三、基础概念

3.1 核心池大小(corePoolSize)

核心池大小是创建线程池的一项重要指标,当一个新的任务被提交到线程池后,如果此时线程池中运行的线程数 < 核心池大小,那么会直接创建一个核心线程运行该任务。

3.2 线程池最大容量(maximumPoolSize)

线程池最大容量是指当前线程池能容纳的最大线程数,也就是同一时间线程池内最大可以有maximumPoolSize个线程执行任务。当核心线程数 + 非核心线程数 > 线程池最大容量时会触发拒绝策略。

3.3 阻塞队列(BlockQueue)

阻塞队列用于存放用户提交到线程池的任务,在生产者-消费者模型中阻塞队列充当的角色是用于将生产者产生的数据转交到消费者手上,由于阻塞队列可以保证线程安全所以用户可以不用过度关心线程安全问题。

特点:

阻塞队列就像它的名字一样,其最大的特点是阻塞,当队列为空时如果消费者依然发出take请求,那么将会阻塞该次请求,知道队列中插入元素时才会解除阻塞状态;同理,当队列已经满了,生产者产生数据需要放入阻塞队列中时,阻塞队列依然会阻塞这次请求,直到队列有空闲的位置才会解除阻塞状态。

阻塞队列put.png

图3.1 阻塞队列put

阻塞队列take.png

图3.2 阻塞队列take

分类:

有界队列:用户传入队列大小,一旦超过该大小队列将会阻塞即将到来的put请求。

无界队列:容纳元素一般设置为当前能承载的最大值,是一个非常大的数,可以近似看作无界。

四、JDK中的线程池

4.1 SingleThreadExecutor(单线程线程池)

定义:

new ThreadPoolExecutor(
  1, 				// 核心池大小
  1, 				// 线程池最大容量
  0L, 				// 非核心线程存活时间
  TimeUnit.MILLISECONDS, 	// 时间单位
  new LinkedBlockingQueue()     // 阻塞队列
  threadFactory);	        // 线程工厂

SingleThreadExecutor中有且只有一个核心线程,提交的所有任务都需要通过这个核心线程来执行,运行时是单线程串行执行任务。

过程:创建一个任务1并提交到线程池中运行,线程池此时为空,会创建一个核心线程来运行这个任务;此时再创建一个任务2提交到线程池中运行,此时核心线程被占用,会将任务2添加到阻塞队列中等待执行。

4.2 FixedThreadPool(定长线程池)

定义:

new ThreadPoolExecutor(
  nThreads, 			// 核心池大小-用户自定义
  nThreads, 			// 线程池最大容量-用户自定义
  0L, 				// 非核心线程存活时间
  TimeUnit.MILLISECONDS, 	// 时间单位
  new LinkedBlockingQueue(), 	// 阻塞队列
  threadFactory);		// 线程工厂

FixedThreadPool是一个定长线程池,长度由用户决定,从初始化参数可以看出来,定长线程池中只有核心线程。

过程:只要当前提交任务时核心线程数量没满就创建核心线程并立即执行任务,否则就加入阻塞队列中等待执行。

4.3 CachedThreadPool(缓存线程池)

定义:

new ThreadPoolExecutor(
  0, 			    // 核心池大小
  2147483647, 		    // 线程池最大容量
  60L, 			    // 非核心线程存活时间
  TimeUnit.SECONDS,         // 时间单位
  new SynchronousQueue(),   // 阻塞队列
  threadFactory);	    // 线程工厂

缓存线程池中没有核心线程,只有存活时间为60秒的非核心线程,也就是说60秒内可以对该非核心线程进行复用。

过程:第一个任务被提交到线程池时会被添加到阻塞队列中,并等待线程执行这个任务,如果当前有空闲的线程那么会使用该线程执行这个任务,如果没有空闲的线程那么就新建一个非核心线程运行这个任务,当第一个任务被取出后第二个提交的任务才能被加入队列;值得一提的是,SynchronousQueue这个阻塞队列的实现方式,SynchronousQueue不缓存任务,也就是说当任务被添加进来后必须被立即取出,否则会阻塞后面任务添加进阻塞队列的流程。

缓存线程池的阻塞队列不缓存任何任务-.-~

4.4 ScheduledThreadPool(延时线程池)

定义:

new ThreadPoolExecutor(
  corePoolSize, 					// 核心池大小
  2147483647, 						// 线程池最大容量
  10L, 							// 非核心线程存活时间
  TimeUnit.MILLISECONDS, 				// 时间单位
  new ScheduledThreadPoolExecutor.DelayedWorkQueue(),   // 阻塞队列
  threadFactory, 					// 线程工厂
  handler);						// 拒绝策略

延时线程池中主要进行定时和周期性任务。

五、总结

  1. 线程池是什么?
    是一套线程管理API,集合了对线程的创建、调度和复用。

  2. 为什么要用线程池?
    线程池最大的优势在于线程管理和线程复用,都知道创建线程是一个代价及其高昂的操作,因为涉及到和操作系统进行交互。
    试想一下,如果使用new Thread()创建100,000个线程会发生什么?极大的概率会产生OOM。但是使用线程池就不会了,这就是线程复用的最大优势,资源的重复利用。

  3. 线程池的核心参数?

    1. 核心池大小:当线程池内可以运行的任务 < 核心池大小时,线程池会立即创建一个核心线程运行该任务,即使任务执行完毕,核心线程的资源也不会被回收。
    2. 线程池最大容量:顾名思义,就是线程池中能容纳的最大任务数,当核心线程数 + 非核心线程数 > 线程池最大容量时会触发拒绝策略。
    3. 非核心线程存活时间:当任务执行完成后,非核心线程存活的时间,在存活时间内可以复用该线程,如果在规定时间内没有任务要复用非核心线程,那么该线程的资源将被回收。
    4. 时间单位:非核心线程存活时间的时间单位。
    5. 阻塞队列:充当消息传递者的身份,生产者提交任务时实际上是添加到阻塞队列中;消费者获取任务时实际上也是从阻塞队列中获取。阻塞队列最大的特点是阻塞,当阻塞队列已满,此时生产者提交的操作将被阻塞,直到阻塞队列有空闲时才会解除阻塞状态;同理,当阻塞队列为空,此时消费者获取的操作将被阻塞,直到阻塞队列有任务被提交时才会解除阻塞状态。
    6. 线程工厂:创建线程的工厂,可以自定义工厂提供一些命名的线程。
    7. 拒绝策略:当核心线程数 + 非核心线程数 > 线程池最大容量时会触发拒绝策略。