线程池整理

线程池的优势:

  1. 线程复用:线程池中的线程是可以复用的,省去了创建、销毁线程的开销,提高了资源利用率;

  2. 合理利用资源:通过调整线程池大小,让所有处理器尽量保持忙碌,又能防止过多线程产生过多竞争浪费资源;

 

线程池实现原理

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

举例:

LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(5);

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);

运行原理:

通过queue.size()的方法来获取工作队列中的任务数;

刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线程数量10个,后面的任务则根据配置的饱和策略来处理。

 

RejectedExecutionHandler:饱和策略

当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。

JAVA提供了4种策略:

◾AbortPolicy:直接抛出异常

◾CallerRunsPolicy:只用调用所在的线程运行任务

◾DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

◾DiscardPolicy:不处理,丢弃掉。

 

 

Executor接口

在JAVA中,任务执行的主要抽象不是Thread,而是Executor。Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。

所谓Executor框架,其实就是定义了一个接口,我们常用的线程池ThreadPoolExecutor就是对这个接口的一种实现。

常用的线程池主要是ThreadPoolExecutor 和 ScheduledThreadPoolExecutor(定时任务线程池,继承ThreadPoolExecutor)。

 

Executor框架的两级调度模型

在HotSpot VM的模型中,JAVA线程被一对一映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当JAVA线程终止时,对应的操作系统线程也被销毁回收,而操作系统会调度所有线程并将它们分配给可用的CPU。在上层,JAVA程序会将应用分解为多个任务,然后使用应用级的调度器(Executor)将这些任务映射成固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

 

1、从类图上看,Executor接口是异步任务执行框架的基础,该框架能够支持多种不同类型的任务执行策略。

public interface Executor {

    void execute(Runnable command);

}

Executor接口就提供了一个执行方法,任务是Runnbale类型,不支持Callable类型。

2、ExecutorService接口实现了Executor接口,主要提供了关闭线程池和submit方法:

public interface ExecutorService extends Executor {

    List<Runnable> shutdownNow();

    boolean isTerminated();

    <T> Future<T> submit(Callable<T> task);

}

另外该接口有两个重要的实现类:ThreadPoolExecutor与ScheduledThreadPoolExecutor。

其中ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务;而ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行任务,或者定期执行命令。

 

 

Executors与常用线程池

之前使用ThreadPoolExecutor来通过给定不同的参数从而创建自己所需的线程池,但是在后面的工作中不建议这种方式,推荐使用Exectuors工厂方法来创建线程池。

Executors是一个Executor的工厂,有很多定义好的工厂方法,可以帮助懒惰的开发者快速创建一个线程池。

下面是几个常用的工厂方法:

  • newFixedThreadPool 固定长度线程池,每次提交任务都会创建一个新线程,直到线程数量达到指定阈值则不再创建新的;

  • newCachedThreadPool 可缓存线程池(无界线程池),没有工作队列,任务进来就执行,线程数量不够就创建,与前面两个的区别是:空闲的线程会被回收掉,空闲的时间是60s。这个适用于执行很多短期异步的小程序或者负载较轻的服务器。

  • newSingleThreadExecutor 只有一个线程的线程池;

  • newScheduledThreadPool 可以延时或者定时执行任务的线程池。

 

线程池构造参数

  • int corePoolSize : 核心线程数,有新任务来时,如果当前线程小于核心线程,则新建一个线程来执行该任务

  • int maximumPoolSize : 最大线程数,线程池最多拥有的线程数

  • long keepAliveTime : 空闲线程存活时间

  • TimeUnit unit : 空闲线程存活时间的单位

  • BlockingQueue

    workQueue : 存放待执行任务的阻塞队列,新任务来时,若当前线程数>=最大核心线程数,则放到这个队列

  • ThreadFactory threadFactory : 创建新线程的工厂,一般用来给线程取个名字方便排查问题

  • RejectedExecutionHandler handler : 任务被拒绝后的处理器,默认的处理器会直接抛出异常,建议重新实现

 

 

有返回值的提交方式

ThreadPoolExecutor.execute()方法是没有返回值的;

可以使用ThreadPoolExecutor.submit()来提交任务,这个方法会返回一个Future对象,通过这个对象可以知道任务何时被执行完。

submit原理:

submit时用一个FutureTask把用户提交的Callable包装起来,再把FutureTask提交给线程池执行,FutureTask.run运行时会执行Callable中的业务代码,并且过程中FutureTask会维护一个状态标识,根据状态标识,可以知道任务是否执行完成,也可以阻塞到状态为完成获取返回值。

 

Callable、Future、FutureTash详解

Callable与Future是在JAVA的后续版本中引入进来的,Callable类似于Runnable接口,实现Callable接口的类与实现Runnable的类都是可以被线程执行的任务。

三者之间的关系:

◾Callable是Runnable封装的异步运算任务。

◾Future用来保存Callable异步运算的结果,主要是判断任务是否完成、中断任务、获取任务执行结果。

◾FutureTask封装Future的实体类

FutureTask不仅实现了Future接口,还实现了Runnable接口,所以不仅可以将FutureTask当成一个任务交给Executor来执行,还可以通过Thread来创建一个线程。

 

 

关闭线程池

为什么需要关闭线程池?

  1. 如果线程池里的线程一直存活,而且这些线程又不是守护线程,那么会导致虚拟机无法正常退出;

  2. 如果直接粗暴地结束应用,线程池中的任务可能没执行完,业务将处于未知状态;

  3. 线程中有些该释放的资源没有被释放。

怎么关闭线程池?

  1. shutdown 停止接收新任务(继续提交会被拒绝,执行拒绝策略),但已提交的任务会继续执行,全部完成后线程池彻底关闭;

  2. shutdownNow 立即停止线程池,并尝试终止正在进行的线程(通过中断),返回没执行的任务集合;

  3. awaitTermination 阻塞当前线程,直到全部任务执行完,或者等待超时,或者被中断。

由于shutdownNow的终止线程是通过中断,这个方式并不能保证线程会提前停止。(关于中断: 如何处理线程中断)

一般先调用shutdown让线程池停止接客,然后调用awaitTermination等待正在工作的线程完事。

 

 

其他情况

1、举个例子:如果设置了核心线程 < 最大线程数不等(一般都这么设置),但是又设置了一个很大的阻塞队列,那么很可能只有几个核心线程在工作,普通线程一直没机会被创建,因为核心线程满了会优先放到队列里,而不是创建普通线程。

2、如有一种场景中,方法A返回一个数据需要10s,A方法后面的代码运行需要20s,但是这20s的执行过程中,只有后面10s依赖于方法A执行的结果。如果与以往一样采用同步的方式,势必会有10s的时间被浪费,如果采用前面两种组合,则效率会提高:

◾先把A方法的内容放到Callable实现类的call()方法中

◾在主线程中通过线程池执行A任务

◾执行后面方法中10秒不依赖方法A运行结果的代码

◾获取方法A的运行结果,执行后面方法中10秒依赖方法A运行结果的代码

这样代码执行效率一下子就提高了,程序不必卡在A方法处。

 

参考如下:

JAVA线程池原理详解(1)

JAVA线程池原理详解(2)

阻塞队列

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值