java线程(2)---线程池

线程池

在上一篇文章中,我们说到可以使用多线程去实现并发运行的操作,在我们每次需要一个线程时,便去创建一个线程,使用完毕后销毁这个线程。但是如果一个线程只执行一个很短很短的任务时,这样频繁的创建和销毁就显得没那么经济,会大量地消耗系统的资源。这时我们就想:如果某个线程执行完后不被销毁,而是等待执行其他工作呢?这就是线程池的作用了。

线程池可以这样理解:在内存中开辟一块内存空间,里面存放了众多未死亡的线程,线程池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

java中有线程池类图如下:

参考:https://juejin.im/entry/58fada5d570c350058d3aaad
java实现线程池是用ThreadPoolExecutor类来实现的,以下进行详细介绍:

Executor框架接口

Executor框架有三个接口:

Executor:一个运行新任务的简单接口;

Executor接口只有一个execute方法,用来替代通常创建或启动线程的方法。具体启动线程如下:

Thread t = new Thread();
executor.execute(t);

ExecutorService:扩展了Executor接口。添加了一些用来管理执行器生命周期和任务生命周期的方法;提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。如果需要支持即时关闭,也就是shutDownNow()方法,则任务需要正确处理中断。

ScheduledExecutorService:扩展了ExecutorService。支持Future和定期执行任务。扩展ExecutorService接口并增加了schedule方法。调用schedule方法可以在指定的延时后执行一个Runnable或者Callable任务。

ThreadPoolExecutor

继承关系:ThreadPoolExecutor继承了AbstractExecutorService类,AbstractExecutorService是一个抽象类,它实现了ExecutorService接口,ExecutorService又是继承了Executor接口。

hreadPoolExecutor类是线程池中最核心的一个类,首先查看其源代码:
其有4个构造方法:

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

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

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

其中有一些参数:
corePoolSize:核心池的大小(也有地方称为核心线程数量)。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

maximumPoolSize:线程池最大线程数

keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。

unit:参数keepAliveTime的时间单位

workQueue:一个阻塞队列,用来存储等待执行的任务,

threadFactory:线程工厂,主要用来创建线程;

handler:表示当拒绝处理任务时的策略,如丢弃任务并抛出RejectedExecutionException异常、丢弃任务,但是不抛出异常、丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)、调用线程处理该任务。

 

工作流程图如下:

从上图中我们可以看到,线程池提交任务的过程有:
1.首先判断线程池中核心线程池(corePoolSize)所有的线程是否都在执行任务。如果不是,则创建一个新的线程执行所提交的任务。如果线程池中所有的线程都在执行任务,那么进入第二个判断。

2.判断当前的阻塞队列是否已满,如果未满,那么将刚提交的任务放置在阻塞队列当中;如果阻塞队列已满,那么将进入第三个判断。

3.判断当前线程池中所有的线程(maximumPoolSize)是否都在执行任务,如果不是,则创建一个新的线程来执行这个任务;否则,就要交给饱和策略(拒绝策略)来执行。
也就是,线程池在提交执行时使用到了

饱和策略上面已经提过了,包括以下几种:

AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
CallerRunsPolicy:只用调用者所在的线程来执行任务;
DiscardPolicy:不处理直接丢弃掉任务;
DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务。
 

线程池状态

线程池和线程一样,有五种状态如下:
Running:能接受新提交的任务,并且也能处理阻塞队列中的任务;
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。
TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。
进入TERMINATED的条件:
(1)线程池不是RUNNING状态;

(2)线程池状态不是TIDYING状态或TERMINATED状态;

(3)如果线程池状态是SHUTDOWN并且workerQueue为空;

(4)workerCount为0;

(5)设置TIDYING状态成功

ThreadPoolExecutor中的重要方法:

execute()方法:通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法:这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

shutdown()和shutdownNow()方法:关闭线程池。


Execute方法:
源码如下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

判断提交的任务command是否为null,若是null,则抛出空指针异常。
如果线程池中当前线程数不小于核心池大小,那么就会直接进入下面的if语句块了。如果线程池中当前线程数小于核心池大小,则调用函数addIfUnderCorePoolSize判断。addIfUnderCorePoolSize通过名字我们就可以知道是当低于核心线程池大小时执行的方法,源代码如下:

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //创建线程去执行firstTask任务   
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

此时应该是需要创建一个线程去执行我们提交的任务。由于涉及到线程状态的变化,首先要先获得锁(防止其他地方也创建了线程而影响当前线程的数量),并且在锁内判断当前线程数量是否小于核心线程数量。如果是,则调用addThread方法添加线程。addThread源代码如下:

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务   
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量       
        workers.add(w);
        int nt = ++poolSize;     //当前线程数加1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

在addThread方法中,首先使用我们提交的的任务创建了一个Worker对象,然后调用线程工厂threadFactory创建了一个新的线程t,之后将线程t的引用赋值给了Worker对象的成员变量thread,接着通过workers.add(w)将Worker对象添加到工作集当中。

回到前面,当进入到第二个判断条件if (runState == RUNNING && workQueue.offer(command))时,判断:如果当前线程池处于RUNNING状态,则将任务放入任务缓存队列;如果当前线程池不处于RUNNING状态或者任务放入缓存队列失败,则执行addIfUnderMaximumPoolSize方法,如果执行addIfUnderMaximumPoolSize方法失败,则执行reject()方法进行任务拒绝处理;

如果当前线程池处于RUNNING状态且将任务放入缓存队列成功,则继续进行判断:if (runState != RUNNING || poolSize == 0),目的是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown或者shutdownNow方法关闭了线程池的一种应急措施。如果是这样的话,则执行ensureQueuedTaskHandled进行应急处理,从名字可以看出是保证 添加到任务缓存队列中的任务得到处理。

简而言之,整体过程如下:

如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务;

如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中;

如果当前workQueue队列已满的话,则会创建新的线程来执行任务;

如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理。

线程池启动

当创建线程池后,初始时,线程池处于RUNNING状态;

线程池关闭:
​​​​​​
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,中断所有没有正在执行任务的线程。
如果调用了shutdownNow()方法,将线程池置于STOP状态。然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
也就是shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。

任务缓冲队列:

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。

workQueue的类型为BlockingQueue<Runnable>,通常有三种:
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

在了解了线程池的基本原理后,介绍一下四种常用的线程池:
Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。这种缓冲池容量大小为Integer.MAX_VALUE,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程。使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

Executors.newFixedThreadPool(int n):创建一个可重用固定个数的线程池,其中corePoolSize和maximumPoolSize值是相等的,以共享的无界队列方式来运行这些线程。使用的是缓冲队列时LinkedBlockingQueue。

Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行。

Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级....)执行。corePoolSize和maximumPoolSize都设置为1,使用是LinkedBlockingQueue;

配置线程池时可根据任务类型,如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1。如果是IO密集型任务,参考值可以设置为2*NCPU

参考:
https://www.cnblogs.com/dolphin0520/p/3932921.html

https://www.jianshu.com/p/125ccf0046f3

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值