【多线程与并发】线程池的问题

什么是线程池?线程池的工作原理和使用线程池的好处?

一个线程池管理了一组工作线程,同时它还包括了一个用于放置等待执行任务的任务队列(阻塞队列)。

默认情况下,在创建了线程池后,线程池中的线程数为0。当任务提交给线程池之后的处理策略如下:

  1. 如果此时线程池中的数量小于corePoolSize(核心池的大小),即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(也就是每来一个任务,就要创建一个线程来执行任务)
  2. 如果此时线程池中的数量大于等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列,则该任务会等待空闲线程将其取出去执行。
  3. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue已满,并且线程池中的数量小于maximumPoolSize(线程池最大线程数),建立新的线程来处理被添加的任务。
  4. 如果此时线程池中的数量大于等于corePoolSize,缓冲队列workQueue已满,并且线程池中数量等于maximumPoolSize,那么通过RejectedExcuptionHandler所指定的策略(任务拒绝策略)来处理任务。
    也就是处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
  5. 特别注意,在corePoolSize和maximumPoolSize之间的线程数会被自动释放。在线程中线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize。这样,线程池可以动态的调整池中的线程数。

                               

                                             线程池的主要处理流程图

使用线程池的好处:

1)通过重复利用已创建的线程,减少在创建和销毁线程上所花的时间以及系统资源的开销。
2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即执行。
3)提高线程的可管理性。使用线程池可以对线程进行统一的分配和监控。
4)如果不使用线程池,有可能造成系统创建大量线程而导致消耗完成系统内存。

 

对于原理,有几个接口和类值得我们关注:
Executor接口
Executors类
ExecutorService接口
AbstractExecutorService抽象类
ThreadPoolExecutor类

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;

然后ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法:

然后ThreadPoolExecutor继承了类AbstractExecutorService。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

 

Executor接口

public interface Executor {
    void execute(Runnable command);
}

Executor接口只有一个方法execute(),并且需要传入一个Runnable类型的参数。那么它的作用自然是具体的执行参数传入的任务。

ExecutorService接口

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
}

Executors类

它主要用来创建线程池。

Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
Executors.newFixedThreadPool(int n); //创建容量为1的缓冲池
Executors.newCachedThreadPool();     //创建容量为1的缓冲池
Integer.MAX_VALUE(无界线程池)

下面是这三个静态方法的具体实现:

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

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

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

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

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

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

ThreadPoolExecutor类

在ThreadPoolExecutor类中提供了四个构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);

不过在Java doc中,并不提倡这样创建,而是使用Executors类中提供的几个静态方法来创建线程池。

下面解释一下构造器中各个参数的含义:

corePoolSize:核心池的大小。默认情况下,在创建了线程池之后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池最大线程数,它表示在线程池中最多能够创建多少个线程。
keepAliveTime:默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的参数不大于corePoolSize,即当线程池中的线程池大于corePoolSize时,如果一个线程空闲时间达到keepAliveTime,则会终止,直到线程池中的线程池数不超过corePoolSize。
unit:参数keepAliveTime的时间单位。
workQueue:一个阻塞队列中,任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue<Runnable>,通常可以取出下面三种类型:

  1. ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小:
  2. LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列的大小,则默认为Integer.MAX_VALUE;
  3. synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。 在某次添加元素后必须等待其他线程取走后才能继续添加。

ThreadFactory:线程工厂,主要用来创建线程。
handler:表示当拒绝处理任务时的策略,有以下四种取值

  1. AbortPolicy:直接抛出异常(默认的)
  2. DiscardPolicy:直接丢弃任务
  3. DiscardOldestPolicy:丢弃队列中最旧(对头)的任务,并执行当前任务
  4. CallerRunsPolicy:不用线程池中的线程执行,用调用者所在线程执行。

在ThreadPoolExecutor类中有几个非常重要的方法:
execute()、submib()、shutdown()、shutdownNow()

execute和submit的区别:
submit有返回值,execute没有返回值。所以说可以根据任务有无返回值选择对应的方法。
submit方便异常的处理。如果任务可能会抛出异常,而希望外面的调用者能够感知这些异常,那么界需要调用submit方法,通过捕获Future.get抛出的异常。

shutdown()和shutdownNow()的区别:
shutdown()和shutdownNow()是用来关闭线程池的。
shutdown方法:此方法执行后不得向线程池再提交任务,如果有空闲线程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结束,然后销毁所有线程。
shutdownNow方法:此方法执行后不得向线程池再提交任务,如果有空闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入List<Runnbale>容器,作为返回值。取消正在执行的线程(实际上仅仅是设置正在执行线程的中断标志位,调用线程的interrupt方法来中断线程)。

 

线程池的注意事项

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。

1)线程池的大小。

多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU的有效利用率和系统的整体性能。

2)并发错误。

多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。

3)线程泄露

这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄露现象。

 

简单线程池的设计

一个典型的线程池,应该包括如下几个部分:

  1. 线程管理器(ThreadPool),用于启动,停用,管理线程池
  2. 工作线程(WorkThread),线程池中的线程
  3. 请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
  4. 请求队列(RequestQueue),用于存放和提取请求
  5. 结果队列(ResultQueue),用于存储请求执行后返回的结果

线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue)添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请求队列(RequestQueue),只要有请求存在,则会提出请求,进行执行。然后,线程池管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调用结果处理函数。

不难发现,这个系统的核心资源在于请求队列和结果队列,工作线程通过轮询RequestQueue获得任务,主线程通过查看结果队列,获得执行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计,以防止因为不断轮询而导致过多的CPU开销。

 

线程池工作模型

 

如何合理的配置Java线程池?如CPU密集型的任务,基本线程池应该配置多大?
IO密集型的任务,基本线程池应该配置多大?
用有界队列好还是无界队列好?
任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?

1)配置线程池时,CPU密集型任务可以少线程数,大概和机器的CPU核数相当,可以使得每个线程都在执行任务。

2)IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,2*CPU核数。

3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些可能会有爆发性增长的情况下使用无界队列。

4)任务非常多时,使用非阻塞队列使用CAS操作替代锁可以获得好的吞吐量。synchronousQueue吞吐率最高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值