Java并发(二)线程池详解

一、前言

程序的所有业务逻辑都是跑在线程上的,每个进程都由一个主线程和N个子线程组成,每个线程都要消耗系统资源,如果不停地创建线程,那么系统资源最终会耗尽,会出现卡顿卡死的情况,这时候就需要对线程进行管理,控制线程的数量、线程重用等方案就出来,避免过度地消耗系统资源,同时也提高了运行效率。那么如何对线程进行管理呢?这就要涉及到Java的池化技术,我们来研究一下什么是池化技术。

1、池化技术

池化技术简单点来说,就是提前申请好大量的系统资源,以备不时之需。池化技术能够减少资源对象的创建次数,提高程序的性能,特别是在高并发下这种提高更加明显。使用池化技术缓存的资源对象有如下共同特点:1,对象创建时间长;2,对象创建需要大量资源;3,对象创建后可被重复使用。

在编程领域,比较典型的池化技术有:线程池、连接池、内存池、对象池等。

2、线程池

线程池的优势:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
  4. 提供更强大的功能,延时定时线程池。

二、基础线程池 ThreadPoolExecutor

除了基础线程池,Java还提供了Executors静态工厂,可通过Executors快速构建出特定功能的线程池。

1、ThreadPoolExecutor 线程池参数

 public ThreadPoolExecutor(int corePoolSize,
                               int maximumPoolSize,
                               long keepAliveTime,
                               TimeUnit unit,
                               BlockingQueue<Runnable> workQueue,
                               RejectedExecutionHandler handler) 
  • corePoolSize:线程池中的核心线程数;
  • maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:线程池中非核心线程闲置超时时长(准确来说应该是没有任务执行时的回收时间,后面会分析);
    • 一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

    • 如果设置allowCoreThreadTimeOut(boolean value),则也会作用于核心线程

  • TimeUnit:时间单位。可选的单位有分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS) 等;
  • workQueue:任务的阻塞队列,缓存将要执行的Runnable任务,由各线程轮询该任务队列获取任务执行。可以选择以下几个阻塞队列。
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出)
      排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:线程创建的工厂。可以进行一些属性设置,比如线程名,优先级等等,有默认实现。
  • RejectedExecutionHandler:任务拒绝策略,当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最老的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
    • 也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

2、ThreadPoolExecutor 线程池使用示例

import java.util.concurrent.*; 
 
public class Test { 
    public static void main(String[] args) throws Exception { 
        ExecutorService executorService = new ThreadPoolExecutor(1, 1, 
                60L, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(10)); 
 
        executorService.execute(new Runnable() { 
            @Override 
            public void run() { 
                System.out.println("开启线程"); 
            } 
        }); 
 
        executorService.shutdown(); //关闭线程池
    } 
}

3、线程池ThreadPoolExecutor的构造

在这里插入图片描述

3.1、Executor接口

线程池的顶级接口。定义了方法execute(Runnable),该方法接收一个Runnable实例,用来执行一个任务,该任务即是一个实现Runnable接口的类。

此服务方法无返回值,原因是因为实现Runnable接口的类的run方法是无返回(void)的。

常用方法 : void execute(execute)

作用 : 启动并执行线程任务

3.2、ExecutorService接口

继承自Executor接口,提供了更多的方法调用,例如优雅关闭方法shutdown,有返回值的submit。

1、ExecutorService生命周期

运行 - Running 、关闭 - shuttingdown、终止 - terminated

Running : 线程池正在执行中,活动状态。创建后即进入此状态

shuttingdown : 优雅关闭,线程池正在关闭中。不再接收新的线程任务,已有的任务(正在处理的 + 队列中阻塞的),处理完毕后,关闭线程池。

调用shutdown()方法,即进入此状态

terminated : 线程池已关闭。

2、submit方法

有返回值,Future类型。重载了方法,submit(Runnable)不需要提供返回值。submit(Callable)、submit(Runnable,T)可以提供线程执行后的结果返回值。

submit 返回一个 Future 对象,我们可以调用其 get 方法获取任务执行的结果。代码很简单,就是将 Runnable 包装成 FutureTask 而已。

FutureTask 实现了 RunnableFuture 接口,RunnableFuture 继承自Runnable。执行任务时会调用 FutureTask 的 run 方法,run 方法中执行真正的任务代码,执行完后调用 set 方法设置结果。

如果任务执行完毕,get 方法会直接返回结果,如果没有,get 方法会阻塞并等待结果。
set 方法中设置结果后会取消阻塞,使 get 方法返回结果。

3、Future

线程执行完毕结果。获取线程执行结果是通过get()方法获取。get()无参,阻塞等待线程执行结束。

get(long timeout, TimeUnit unit)有参,阻塞等待固定时长,超时未获取,则抛出异常。

4、Callable

类似Runnable的一个线程接口。其中的对应run的方法是call方法。此接口提供了线程执行完毕返回值。

3.3、ThreadPoolExecutor类
3.3.1、Worker

Worker是线程在线程池中的包装类。一个 Worker 代表一个线程。线程池用一个 HashSet 管理这些线程。
需要注意的是,Worker 本身并不区分核心线程和非核心线程,核心线程只是概念模型上的叫法,特性是依靠对线程数量的判断来实现的 Worker 特性如下:

继承自 AQS,本身实现了一个最简单的不公平的不可重入锁。
构造方法传入 Runnable,代表第一个执行的任务,可以为空。构造方法中新建一个线程。
实现了 Runnable 接口,在新建线程时传入 this。因此线程启动时,会执行 Worker 本身的 run 方法。
run 方法调用了 ThreadPoolExecutor 的 runWorker 方法,负责实际执行任务。

3.3.2、execute() 方法的执行机制

在这里插入图片描述

  1. 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。
  2. 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。
  3. 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。
  4. 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

注意:当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。

任务队列是一个阻塞队列,线程执行完任务后会去队列取任务来执行,如果队列为空,线程就会阻塞,直到取到任务。

新任务如何添加进队列?

线程池使用 addWorker(Runnable firstTask, boolean core) 方法新建线程,第一个参数代表要执行的任务,线程会将这个任务执行完毕后再从队列取任务执行。第二参数是核心线程的标志,它并不是 Worker 本身的属性,在这里只用来判断工作线程数量是否超标。

任务如何执行?

在 addWorker 方法中,线程会被启动。新建线程时,Worker 将自身传入,所以线程启动后会执行 Worker 的 run 方法,这个方法调用了 ThreadPoolExecutor 的 runWorker 方法执行任务,runWorker 中会循环取任务执行,执行逻辑如下:

如果 firstTask 不为空,先执行 firstTask,执行完毕后置空;
firstTask 为空后调用 getTask() 从队列中取任务执行;
一直执行到没有任务后,退出 while 循环
调用 processWorkerExit() 方法,将 Worker 移除出 HashSet,此时线程执行完毕,也不再被引用,会自动销毁。

线程如何销毁?超时机制如何实现?

在 runWorker 方法中 getTask 方法返回 null 之后会导致线程执行完毕,被移除出 HashSet,从而被系统销毁。 线程的超时机制也是在这个方法实现的,借助于 BlockingQueue 的 poll 和 take 方法。

poll 方法可以设置一个超时时间,当队列为空时,在此时间内阻塞等待,超时后返回 null
take 方法在队列为空时直接抛出异常
超时机制实现原理如下:

当 allowCoreThreadTimeOut 为 true,所有线程都会超时,全部调用 poll 方法,传入 keepAliveTime 参数。
当 allowCoreThreadTimeOut 为 false 时,如果工作线程数量大于核心线程数,将此线程当作非核心线程处理,调用 poll 方法
当 allowCoreThreadTimeOut 为 false 且工作线程数量小于等于核心线程数时,将此线程当作核心线程处理,调用 take 方法,队列为空时抛出异常,进入下一次循环。如果队列一直为空,核心线程会一直在此循环等待任务进行处理。

三、Executors静态工厂类

Executors静态工厂本质上是对ThreadPoolExecutor 的进一步封装,将其封装成对应不同功能的线程池,下图是他们的关系:
在这里插入图片描述
通过Executors静态工厂可以构建出newFiexedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool等常用线程池,他们分别有不同的功能和应用场景,让开发者能够实现快速开发,使用现成的轮子,因为是应对特定功能和使用场景封装的,所以也就失去了灵活性,如果这几个现成池不能满足需求,只能用基础线程池ThreadPoolExecutor 。

1、newFiexedThreadPool

newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
for(int i=0;i<5;i++) {
  final int index = i;
    fixedThreadPool.execute(new Runnable() {
    @Override
    public void run() {
      try {
        System.out.println(Thread.currentThread().getName() + ", " + index);
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  });
}
//控制台信息
pool-1-thread-1,0
pool-1-thread-2,1
pool-1-thread-3,2
pool-1-thread-4,3
pool-1-thread-1,4

2、newCachedThreadPool

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例如下:

ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
  final int index = i;
  try {
    Thread.sleep(index * 1000);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  executorService.execute(new Runnable() {
    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + "," +index);
    }
  });
}
//控制台信息
pool-1-thread-1,0
pool-1-thread-1,1
pool-1-thread-1,2
pool-1-thread-1,3
pool-1-thread-1,4

3、newSingleThreadExecutor

newSingleThreadExecutor创建一个单线程化的线程池,只会用工作线程来执行任务,保证顺序,示例如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i=0;i<10;i++) {
  final int index = i;
  singleThreadExecutor.execute(new Runnable() {
    @Override
    public void run() {
      try {
         System.out.println(Thread.currentThread().getName() + "," + index);
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  });
}

4、newScheduledThreadPool

newScheduledThreadPool 创建一个定长线程池,支持周期和定时任务示例如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
System.out.println("before:" + System.currentTimeMillis()/1000);
scheduledThreadPool.schedule(new Runnable() {
  @Override
  public void run() {
    System.out.println("延迟3秒执行的哦 :" + System.currentTimeMillis()/1000);
  }
}, 3, TimeUnit.SECONDS);
System.out.println("after :" +System.currentTimeMillis()/1000);
//控制台信息
before:1518012703
after :1518012703
延迟3秒执行的哦 :1518012706
System.out.println("before:" + System.currentTimeMillis()/1000);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
  @Override
  public void run() {
    System.out.println("延迟1秒之后,3秒执行一次:" +System.currentTimeMillis()/1000);
  }
}, 1, 3, TimeUnit.SECONDS);
System.out.println("after :" +System.currentTimeMillis()/1000);

控制台消息
before:1518013024
after :1518013024
延迟1秒之后,3秒执行一次:1518013025
延迟1秒之后,3秒执行一次:1518013028
延迟1秒之后,3秒执行一次:1518013031

5、注意事项

在ThreadPoolExecutor中,可通过workQueue设置阻塞队列的长度,Executors虽然提供了快速便捷的线程池创建方式,但是由于其不提供可设置的阻塞队列值,所以Executors创建出来的线程池阻塞队列采用默认Integer.MAX_VALUE,如果创建大量的线程,可能会导致OOM。所以在高并发的场景中不推荐使用Executors创建线程池。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值