线程池的使用
发布日期:2021-09-25 11:48:25 浏览次数:3 分类:技术文章

本文共 14065 字,大约阅读时间需要 46 分钟。

多线程软件设计方法确实可以最大限度地发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能,但是,若不加控制和管理随意使用线程,对系统的性能反而会产生不利影响

首先,线程的创建和关闭依然需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间的情况;其次,线程本身也要占用内存空间的,大量的线程会抢占宝贵的内存资源,如果处理不当,可能会导致Out of Memory异常,即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间

线程池中,总有那么几个活跃线程,当你使用线程时,可以从池子随便拿一个空闲线程,当完成工作时,并不着急关闭线程,而是将这个线程退回到池子,简而言之,使用线程池后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向池子归还线程

在开发中,合理使用线程池能带来3个好处:

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

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

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

1.JDK线程池

在JDK并发包java.util.concurrent中,ThreadPoolExecutor通常使用工厂类Executors来创建,Executors可以创建3种类型的ThreadPoolExecutor,如下;ScheduledThreadPoolExecutor也使用工厂类Executors来创建,且可创建两种类型:

//	返回一个固定线程数量的线程池,当有一个新的任务提交,线程池中若有空闲线程则立即执行//	若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务//适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较中的服务器	 public static ExecutorService newFixedThreadPool(int nThreads)//	 返回一个只有一个线程的线程池,若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中//	 待线程空闲,按先入先出的顺序执行队列中的任务//适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景	 public static ExecutorService newSingleThreadExecutor()//	 返回一个可根据实际情况调整线程数量的线程池//适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器	 public static ExecutorService newCachedThreadPool()//	 返回一个ScheduledExecutorService对象,线程池大小为1//	 扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行或周期性地执行某个任务	 public static ScheduledExecutorService newSingleThreadScheduledExecutor()//	 返回ScheduledExecutorService对象,但该线程池可以指定线程数量//适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的场景	 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

举一个简单例子,展示线程池的使用

public class Test {	public static class MyTask implements Runnable{		@Override		public void run() {			// TODO Auto-generated method stub			System.out.println(System.currentTimeMillis()+":Thread ID:"			+Thread.currentThread().getId());			try{				Thread.sleep(1000);			}catch(InterruptedException e){				e.printStackTrace();			}		}			}	public static void main(String[] args) {		// TODO Auto-generated method stub		MyTask task=new MyTask();//		创建固定大小的线程池,有5个线程		ExecutorService es=Executors.newFixedThreadPool(5);//		依次向线程池提交了10个任务,每个任务会将自己的执行时间和执行这个线程的ID打印出来,安排每个任务执行1秒		for(int i=0;i<10;i++){			es.submit(task);		}	}

输出,10个任务分成2批进行,并且时间相差1秒(很奇怪,毫秒为单位的话,运行差为什么不直等于1000)

1531795042751:Thread ID:111531795042751:Thread ID:141531795042751:Thread ID:121531795042751:Thread ID:131531795042751:Thread ID:101531795043752:Thread ID:111531795043752:Thread ID:141531795043752:Thread ID:131531795043752:Thread ID:121531795043752:Thread ID:10

分析,上述不同方式创建的线程池有完全不同的功能,但是其内部均是使用了ThreadPoolExecutor实现,先看下ThreadPoolExecutor类

public class ThreadPoolExecutor extends AbstractExecutorService {

其主要构造函数

public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue
workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
  • corePoolSize,核心池的大小,当提交一个任务到线程池,线程池会创建一个线程来执行任务,即使其它空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小就不再创建
  • maximumPoolSize,线程池中的最大线程数,表示线程池中最多能创建多少个线程,如果队列满了,并且已经创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务,如果使用了无界队列该参数则没效果
  • keepAliveTime,表示线程没有任务执行时最多保持多久时间会终止,默认,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize
  • unit,参数keepAliveTime的时间单位
  • workQueue,一个阻塞队列,用来存储等待执行的任务
  • threadFactory,线程工厂,用来创建线程
  • handler,当拒绝处理任务时的策略

由ThreadPoolExecutor类可知,其实继承了 AbstractExecutorService类的,而AbstractExecutorService类是一个抽象类,实现了ExecutorService接口:

public abstract class AbstractExecutorService implements ExecutorService {

而ExecutorService接口继承了Executor接口:

public interface ExecutorService extends Executor {

由此可见,Executor、ExecutorService、AbstractExecutorService、ThreadPoolExecutor之间的关系

Executor是一个顶层接口,在它里面只声明了一个方法:

void execute(Runnable command);

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

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

ThreadPoolExecutor继承了抽象类AbstractExecutorService,其中有几个重要方法提及一下:execute、submit提交任务,shutdown和shutdownNow关闭线程池

Executor框架主要由3大部分组成如下:

  • 任务,包括被执行任务需要实现的接口:Runnable或Callable
  • 任务的执行,包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口
  • 异步计算的结果,包括接口Future和实现Future接口的FutureTask类

类和接口的简单介绍:

  • Executor,接口,是Executor框架的基础,将任务的提交和任务的执行分离
  • ThreadPoolExecutor,线程池的核心实现类,用来执行被提交的任务
  • ScheduledThreadPoolExecutor,实现类,可以在给定的延迟后执行命令,或者定期执行命令
  • Future接口和其实现类FutureTask,代表异步计算结果
  • Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行

2.线程池状态

当创建线程池,初始时,线程池处于RUNNING 状态;如果调用shutdown方法,线程池处于SHUTDOWN状态,此时线程池不能够接收新的任务,它会等待所有任务执行完毕;如果调用shutdownNow,则线程池处于STOP 状态,此时线程池不能够接收新的任务,并会尝试终止正在执行的任务;当线程池处于SHUTDOWN或者STOP,并且所有工作线程已经销毁,任务缓存队列已经清空或者执行结束后,线程池被设置为TERMINATED

private static final int COUNT_BITS = Integer.SIZE - 3;    // runState is stored in the high-order bits    private static final int RUNNING    = -1 << COUNT_BITS;    private static final int SHUTDOWN   =  0 << COUNT_BITS;    private static final int STOP       =  1 << COUNT_BITS;    private static final int TIDYING    =  2 << COUNT_BITS;    private static final int TERMINATED =  3 << COUNT_BITS;

3.任务缓存队列

BlockingQueue<Runnable> workQueue可以使用以下几种:

  • 直接提交的队列,SynchronousQueue,没有容量,提交的任务不会被真实保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果新的线程数量已经达到最大值,则执行拒绝策略
  • 有界的任务队列,ArrayBlockingQueue,若有新的任务要执行,如果线程池的实际线程数小于corePoolSize,则优先创建新的线程,若大于corePoolSize,将新任务加入等待队列。若等待队列已满无法加入,在总线程数不大于maximumPoolSize时创建新的线程执行任务,否则执行拒绝策略。队列按照先进先出算法处理任务
  • 无界的任务队列,LinkedBlockingQueue,与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。队列按照先进先出算法处理任务
  • 优先任务队列,PriorityBlockingQueue,可以控制任务执行的先后顺序,是一个特殊的无界队列

4.拒绝策略

JDK内置的拒绝策略如下:

  • AbortPolicy,直接抛出异常,阻止系统正常工作
  • DiscardPolicy,默默丢弃无法处理的任务,不予任何处理
  • DiscardOldestPolicy,丢弃队列里最近的一个任务,并执行当前任务
  • CallerRunsPolicy,只用调用者所在线程来运行任务

以上策略均实现了RejectedExecutionHandler接口,若以上策略仍不满足,则可自己扩展

5.对1中所介绍的线程池详解下

(1)newFixedThreadPool

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

其corePoolSize和maximumPoolSize都被设置为指定参数nThreads,当线程池中的线程数大于corePoolSize,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止,这里设置为0L,即多余的空闲线程会被立即终止

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列,其容量为Integer.MAX_VALUE,有如下特点:

  • 1.当线程池中的线程数达到corePoolSize,新任务在无界队列中等待,so线程池中的线程数不超过maximumPoolSize
  • 2.由于1,maximumPoolSize是一个无效参数
  • 3.由于1和2,keepAliveTime是一个无效参数
  • 4.由于使用无界队列,运行中的FixedThreadPool不会拒绝任务

(2)newSingleThreadExecutor

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

(3)newCachedThreadPool

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

其中corePoolSize为0,maximumPoolSize是无界的,且空闲线程等待新任务的最长时间为60s,使用没有容量的SynchronousQueue作为线程池的工作队列,但是maximumPoolSize是无界的,意味着,如果主线程提交任务的速度高于maximumPoolSize中线程处理任务的速度,CachedThreadPool会不断创建新线程,极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源

6.核心调度

在ThreadPoolExecutor中,最核心的任务提交方法是execute,虽然通过submit也可以提交任务,但其实际上是通过调用execute来实现的。execute实际上是Executor中声明的方法,可以向线程池提交一个任务,交由线程池去执行:

public void execute(Runnable command) {        if (command == null)            throw new NullPointerException();        int c = ctl.get();        if (workerCountOf(c) < corePoolSize) {            if (addWorker(command, true))                return;            c = ctl.get();        }        if (isRunning(c) && workQueue.offer(command)) {            int recheck = ctl.get();            if (! isRunning(recheck) && remove(command))                reject(command);            else if (workerCountOf(recheck) == 0)                addWorker(null, false);        }        else if (!addWorker(command, false))            reject(command);    }

代码第6行的workerCountOf(c)函数取得了当前线程池的线程总数,当线程总数小于corePoolSize时,会通过addWorker方法直接调度执行,否则,在第11行代码处workQueue.offer进入等待队列,如果进入等待队列失败(比如有界队列达到了上限),则会执行第18行,将任务交给线程池,如果当前线程数已经达到maximumPoolSize,则提交失败,执行第18行的拒绝策略

execute用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

submit用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并可以通过future的get方法获取返回值,其中get()方法会阻塞当前线程直到任务完成,而get(long timeout,TimeUnit unit)阻塞当前一段时间后立即返回,这时候有可能任务没有完成

7.自定义线程创建

以上已经对线程池有了一定介绍,但是,线程池中的线程是从哪里来的?

ThreadFactory是一个接口,只有一个方法,用来创建线程:

public interface ThreadFactory {    /**     * Constructs a new {@code Thread}.  Implementations may also initialize     * priority, name, daemon status, {@code ThreadGroup}, etc.     *     * @param r a runnable to be executed by new thread instance     * @return constructed thread, or {@code null} if the request to     *         create a thread is rejected     */    Thread newThread(Runnable r);}

自定义线程可以帮助我们做不少事情,比如,我们可以跟踪线程池究竟在何时创建了多少线程,也可以自定义线程的名称、组以及优先级等信息,还可以将其设置为守护线程。。。

eg,使用自定义的ThreadFactory,一方面记录了线程的创建,另一方面将所有的线程都设置为守护线程,这样当主线程退出后,将会强制销毁线程池:

public class Test {	public static class MyTask implements Runnable{		@Override		public void run() {			// TODO Auto-generated method stub			System.out.println(System.currentTimeMillis()+":Thread ID:"			+Thread.currentThread().getId());			try{				Thread.sleep(1000);			}catch(InterruptedException e){				e.printStackTrace();			}		}			}	public static void main(String[] args) throws InterruptedException {		// TODO Auto-generated method stub		MyTask task=new MyTask();		ExecutorService es=new ThreadPoolExecutor(5,5,0L,TimeUnit.MILLISECONDS,				new SynchronousQueue
(), new ThreadFactory(){ @Override public Thread newThread(Runnable r) { // TODO Auto-generated method stub Thread t=new Thread(r); t.setDaemon(true); System.out.println("creat "+t); return t; } }); for(int i=0;i<5;i++){ es.submit(task); } Thread.sleep(2000); }}

8.扩展线程池

ThreadPoolExecutor提供了beforeExecute、afterExecute和terminated对线程池进行控制,在默认的ThreadPoolExecutor中,这三个方法都是空方法,在实际应用中,可以对其进行扩展来实现对线程池运行状态的跟踪,输出一些有用的调试信息,以帮助系统故障判断

eg,在这个线程池的扩展中,将记录每一个任务的执行日志:

package executor;import java.util.concurrent.ExecutorService;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class Test {	public static class MyTask implements Runnable{		public String name;		public MyTask(String name){			this.name=name;		}		@Override		public void run() {			// TODO Auto-generated method stub			System.out.println("正在执行:Thread Id"+Thread.currentThread().getId()+",Task name="+ name);			try{				Thread.sleep(100);			}catch(InterruptedException e){				e.printStackTrace();			}		}			}	public static void main(String[] args) throws InterruptedException {		// TODO Auto-generated method stub		ExecutorService es=new ThreadPoolExecutor(5,5,0L,TimeUnit.MILLISECONDS,				new LinkedBlockingQueue
()){ @Override protected void beforeExecute(Thread t, Runnable r) { // TODO Auto-generated method stub System.out.println("准备执行"+((MyTask)r).name); } @Override protected void afterExecute(Runnable r, Throwable t) { // TODO Auto-generated method stub System.out.println("执行完成"+((MyTask)r).name); } @Override protected void terminated() { // TODO Auto-generated method stub System.out.println("线程池退出"); } }; for(int i=0;i<5;i++){ MyTask task=new MyTask("TASK-"+i); es.execute(task); Thread.sleep(10); } es.shutdown(); }}

输出,可以看到,所有任务的执行前、执行后的时间点以及任务的名字都可以捕获:

准备执行TASK-0正在执行:Thread Id10,Task name=TASK-0准备执行TASK-1正在执行:Thread Id11,Task name=TASK-1准备执行TASK-2正在执行:Thread Id12,Task name=TASK-2准备执行TASK-3正在执行:Thread Id13,Task name=TASK-3准备执行TASK-4正在执行:Thread Id14,Task name=TASK-4执行完成TASK-0执行完成TASK-1执行完成TASK-2执行完成TASK-3执行完成TASK-4线程池退出

9.线程池的关闭

可以通过shutdown或shutdownNow方法关闭线程池,原理是:遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,so无法响应中断的任务可能永远无法终止,两个方法存在一定区别

shutdown()并不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止;只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

public void shutdown() {        final ReentrantLock mainLock = this.mainLock;        mainLock.lock();        try {            checkShutdownAccess();            advanceRunState(SHUTDOWN);            interruptIdleWorkers();            onShutdown(); // hook for ScheduledThreadPoolExecutor        } finally {            mainLock.unlock();        }        tryTerminate();    }

shutdownNow()会立即终止线程池,首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

public List
shutdownNow() { List
tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); advanceRunState(STOP); interruptWorkers(); tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); return tasks; }

只要调用了这两个关闭方法中任意一个,isShutdown就会放回true,当所有的任务都已经关闭,才表示线程池关闭成功,这时调用isTerminated方法会返回true

10.合理地配置线程池

ThreadPoolExecuto提供了动态调整线程池容量大小的办法:

  • public void setCorePoolSize(int corePoolSize),设置核心池大小
  • public void setMaximumPoolSize(int maximumPoolSize),设置线程池最大能创建的线程数目大小

线程池的大小对系统的性能有一定的影响,一般来说,确定线程池的大小需要考虑CPU数量、内存大小等因素,在《Java Concurrency in Practice》中给出了一个估算线程池大小的计算公式:

Ncpu=CPU的数量Ucpu=目标CPU的使用率,0≤Ucpu≤1W/C=等待时间与计算时间的比率为保证处理器达到期望的使用率,最优的池的大小等于:Nthreads= Ncpu *Ucpu *(1+ W/C)

要想合理配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:

  • 任务的性质:CPU密集型任务、IO密集型任务、混合型任务
  • 任务的优先级:高、中、低
  • 任务的执行时间:长、中、端
  • 任务的依赖性:是否依赖其他系统资源,eg,数据库连接

可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数

性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池;由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu,混合型任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务

11.线程池的监控

如果在系统中大量使用线程池,则由必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题:

  • taskCount,线程池需要执行的任务数量
  • completedTaskCount,线程池在运行过程中已经完成的任务数量,小于等于taskCount
  • largestPoolSize,线程池里曾经创建过的最大线程数,通过这个数据可以知道线程池是否曾经满过
  • getPoolSize,线程池的线程数量,如果线程池不销毁的话,线程池的线程不会自动销毁,so该数值大小只增不减
  • getActiveCount,获得活动的线程数

转载地址:https://blog.csdn.net/Autumn03/article/details/81075105 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:线程通信简单实例
下一篇:Spring学习-AOP

发表评论

最新留言

不错!
[***.144.177.141]2024年03月18日 12时39分57秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章