线程池ThreadPoolExecutor底层原理源码分析

线程池执行任务的具体流程是怎样的

ThreadPoolExecutor中提供了两种执行任务的方法:

  • void execute(Runnable command)
  • Future<?> submit(Runnable task)

实际上submit中最终还是调用的execute()方法,只不过会返回一个Future对象,用来获取任务执行结果:

public Future<?> submit(Runnable task) {
    
    
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

execute(Runnable command)方法执行时会分为三步:
在这里插入图片描述

注意:

  • 提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。
  • ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行。

线程池的五种状态是如何流转的

线程池有五种状态

  • RUNNING:会接收新任务并且会处理队列中的任务
  • SHUTDOWN:不会接收新任务并且会处理队列中的任务
  • STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断得看任务本身)
  • TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()
  • TERMINATED:terminated()执行完之后就会转变为TERMINATED

这五种状态并不能任意转换,只会有以下几种转换情况:

  • RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
  • (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调shutdownNow(),就会发生SHUTDOWN -> STOP
  • SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
  • STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
  • TIDYING -> TERMINATED:terminated()执行完后就会自动转换

线程池中的线程是如何关闭的

  • 我们一般会使用thread.start()方法来开启一个线程,那如何停掉一个线程呢?
  • Thread类提供了一个stop(),但是标记了@Deprecated,为什么不推荐用stop()方法来停掉线程呢?
  • 因为stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,但是调用的时候根本不知道线程刚刚在做什么,任务做到哪一步了,这是很危险的。
  • 这里强调一点,stop()会释放线程占用的synchronized锁(不会自动释放ReentrantLock锁,这也是不建议用stop()的一个因素)。
public class ThreadTest {
    
    
    static int count = 0;
    static final Object lock = new Object();
    static final ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    
//                synchronized (lock) {
    
    
                reentrantLock.lock();
                for (int i = 0; i < 100; i++) {
    
    
                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
//                }
                reentrantLock.unlock();
            }
        });

        thread.start();
        Thread.sleep(5*1000);
        thread.stop();
//
//        Thread.sleep(5*1000);

        reentrantLock.lock();
        System.out.println(count);
        reentrantLock.unlock();

//        synchronized (lock) {
    
    
//            System.out.println(count);
//        }
    }
}

所以,我们建议通过自定义一个变量,或者通过中断来停掉一个线程,比如:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (stop) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        stop = true;
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

不同点在于,当我们把stop设置为true时,线程自身可以控制到底要不要停止,何时停止,同样,我们可以调用thread的interrupt()来中断线程:

public class ThreadTest {
    
    

    static int count = 0;
    static boolean stop = false;

    public static void main(String[] args) throws InterruptedException {
    
    

        Thread thread = new Thread(new Runnable() {
    
    
            public void run() {
    
    

                for (int i = 0; i < 100; i++) {
    
    
                    if (Thread.currentThread().isInterrupted()) {
    
    
                        break;
                    }

                    count++;
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        break;
                    }
                }
            }
        });

        thread.start();
        Thread.sleep(5 * 1000);
        thread.interrupt();
        Thread.sleep(5 * 1000);
        System.out.println(count);
    }
}

不同的地方在于,线程sleep过程中如果被中断了会接收到异常。
其实线程池中就是通过interrupt()来停止线程的,比如shutdownNow()方法中会调用

void interruptIfStarted() {
    
    
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
    
    
        try {
    
    
            t.interrupt();
        } catch (SecurityException ignore) {
    
    
        }
    }
}

线程池为什么一定得是阻塞队列

  线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队列中有任务过来就会拿到任务从而去执行任务。
  通过这种方法能最终确保,线程池中能保留指定个数的核心线程数,关键代码为:

try {
    
    
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    
    
    timedOut = false;
}

  某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。

线程发生异常,会被移出线程池吗

答案是会的,那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程池?
在这里插入图片描述

  • 在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线程也就自然消亡了,
  • 但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定的核心线程数。

Tomcat是如何自定义线程池的

Tomcat中用的线程池为org.apache.tomcat.util.threads.ThreadPoolExecutor,注意类名和JUC下的一样,但是包名不一样。
Tomcat会创建这个线程池:

public void createExecutor() {
    
    
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

注入传入的队列为TaskQueue,它的入队逻辑为:

public boolean offer(Runnable o) {
    
    
    //we can't do any checks
    if (parent==null) {
    
    
        return super.offer(o);
    }

    //we are maxed out on threads, simply queue the object
    if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
    
    
        return super.offer(o);
    }

    //we have idle threads, just add it to the queue
    if (parent.getSubmittedCount()<=(parent.getPoolSize())) {
    
    
        return super.offer(o);
    }

    //if we have less threads than maximum force creation of a new thread
    if (parent.getPoolSize()<parent.getMaximumPoolSize()) {
    
    
        return false;
    }

    //if we reached here, we need to add it to the queue
    return super.offer(o);
}

特殊在:

  • 入队时,如果线程池的线程个数等于最大线程池数才入队
  • 入队时,如果线程池的线程个数小于最大线程池数,会返回false,表示入队失败

这样就控制了,Tomcat的这个线程池,在提交任务时:

  • 仍然会先判断线程个数是否小于核心线程数,如果小于则创建线程
  • 如果等于核心线程数,会入队,但是线程个数小于最大线程数会入队失败,从而会去创建线程

所以随着任务的提交,会优先创建线程,直到线程个数等于最大线程数才会入队。

当然其中有一个比较细的逻辑是:在提交任务时,如果正在处理的任务数小于线程池中的线程个数,那么也会直接入队,而不会去创建线程,也就是上面源码中getSubmittedCount的作用。

线程池的核心线程数、最大线程数该如何设置

线程池中有两个非常重要的参数:

  • corePoolSize:核心线程数,表示线程池中的常驻线程的个数
  • maximumPoolSize:最大线程数,表示线程池中能开辟的最大线程个数

那这两个参数该如何设置呢?

我们对线程池负责执行的任务分为三种情况:

  • CPU密集型任务,比如找出1-1000000中的素数
  • IO密集型任务,比如文件IO、网络IO
  • 混合型任务

  CPU密集型任务的特点时,线程在执行任务时会一直利用CPU,所以对于这种情况,就尽可能避免发生线程上下文切换。
  比如,现在我的电脑只有一个CPU,如果有两个线程在同时执行找素数的任务,那么这个CPU就需要额外的进行线程上下文切换,从而达到线程并行的效果,此时执行这两个任务的总时间为:
  任务执行时间2+线程上下文切换的时间
  而如果只有一个线程,这个线程来执行两个任务,那么时间为:
  任务执行时间
2
  所以对于CPU密集型任务,线程数最好就等于CPU核心数,可以通过以下API拿到你电脑的核心数:

Runtime.getRuntime().availableProcessors()

只不过,为了应对线程执行过程发生缺页中断或其他异常导致线程阻塞的请求,我们可以额外在多设置一个线程,这样当某个线程暂时不需要CPU时,可以有替补线程来继续利用CPU。

所以,对于CPU密集型任务,我们可以设置线程数为:CPU核心数+1

我们在来看IO型任务,线程在执行IO型任务时,可能大部分时间都阻塞在IO上,假如现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么很有可能这10个线程都阻塞在了IO上,这样这10个CPU就都没活干了,所以,对于IO型任务,我们通常会设置线程数为:2*CPU核心数

不过,就算是设置为了2*CPU核心数,也不一定是最佳的,比如,有10个CPU,线程数为20,那么也有可能这20个线程同时阻塞在了IO上,所以可以再增加线程,从而去压榨CPU的利用率。

通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,我们就可以设置更多的线程,但是,线程肯定不是越多越好,
我们可以通过以下这个公式来进行计算:
线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )

  • 线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在了IO
  • 线程运行总时间:指的是线程执行完某个任务的总时间

线程池中基本属性和方法源码解析

在线程池的源码中,会通过一个AtomicInteger类型的变量ctl,来表示线程池的状态和当前线程池中的工作线程数量。
一个Integer占4个字节,也就是32个bit,线程池有5个状态:

  • RUNNING:线程池正常运行中,可以正常的接受并处理任务
  • SHUTDOWN:线程池关闭了,不能接受新任务,但是线程池会把阻塞队列中的剩余任务执行完,剩余任务都处理完之后,会中断所有工作线程
  • STOP:线程池停止了,不能接受新任务,并且也不会处理阻塞队列中的任务,会中断所有工作线程
  • TIDYING:当前线程池中的工作线程都被停止后,就会进入TIDYING
  • TERMINATED:线程池处于TIDYING状态后,会执行terminated()方法,执行完后就会进入TERMINATED状态,在ThreadPoolExecutor中terminated()是一个空方法,可以自定义线程池重写这个方法

2个bit能表示4种状态,那5种状态就至少需要三个bit位,比如在线程池的源码中就是这么来表示的:

private static final int COUNT_BITS = Integer.SIZE - 3;

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;

Integer.SIZE为32,所以COUNT_BITS为29,最终各个状态对应的二级制为:

  • RUNNING:11100000 00000000 00000000 00000000
  • SHUTDOWN:00000000 00000000 00000000 00000000
  • STOP:00100000 00000000 00000000 00000000
  • TIDYING:01000000 00000000 00000000 00000000
  • TERMINATED:01100000 00000000 00000000 00000000

所以,只需要使用一个Integer数字的最高三个bit,就可以表示5种线程池的状态,而剩下的29个bit就可以用来表示工作线程数,比如,假如ctl为:11100000 00000000 00000000 00001010,就表示线程池的状态为RUNNING,线程池中目前在工作的线程有10个,这里说的“在工作”意思是线程活着,要么在执行任务,要么在阻塞等待任务。

同时,在线程池中也提供了一些方法用来获取线程池状态和工作线程数,比如:

// 29,二进制为00000000 00000000 00000000 00011101
private static final int COUNT_BITS = Integer.SIZE - 3;

// 00011111 11111111 11111111 11111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// ~CAPACITY为11100000 00000000 00000000 00000000
// &操作之后,得到就是c的高3位
private static int runStateOf(int c)     {
    
     
    return c & ~CAPACITY; 
}

// CAPACITY为00011111 11111111 11111111 11111111
// &操作之后,得到的就是c的低29位
private static int workerCountOf(int c)  {
    
     
    return c & CAPACITY; 
}

同时,还有一个方法:

private static int ctlOf(int rs, int wc) {
    
     
    return rs | wc; 
}

就是用来把运行状态和工作线程数量进行合并的一个方法,不过传入这个方法的两个int数字有限制,rs的低29位都得为0,wc的高3位都得为0,这样经过或运算之后,才能得到准确的ctl。

同时,还有一些相关的方法

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;

// c状态是否小于s状态,比如RUNNING小于SHUTDOWN
private static boolean runStateLessThan(int c, int s) {
    
    
    return c < s;
}

// c状态是否大于等于s状态,比如STOP大于SHUTDOWN
private static boolean runStateAtLeast(int c, int s) {
    
    
    return c >= s;
}

// c状态是不是RUNNING,只有RUNNING是小于SHUTDOWN的
private static boolean isRunning(int c) {
    
    
    return c < SHUTDOWN;
}

// 通过cas来增加工作线程数量,直接对ctl进行加1
// 这个方法没考虑是否超过最大工作线程数的(2的29次方)限制,源码中在调用该方法之前会进行判断的
private boolean compareAndIncrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect + 1);
}

// 通过cas来减少工作线程数量,直接对ctl进行减1
private boolean compareAndDecrementWorkerCount(int expect) {
    
    
    return ctl.compareAndSet(expect, expect - 1);
}

execute方法

当执行线程池的execute方法时:

public void execute(Runnable command) {
    
    
    
    if (command == null)
        throw new NullPointerException();
    
    // 获取ctl
    // ctl初始值是ctlOf(RUNNING, 0),表示线程池处于运行中,工作线程数为0
    int c = ctl.get();
    
    // 工作线程数小于corePoolSize,则添加工作线程,并把command作为该线程要执行的任务
    if (workerCountOf(c) < corePoolSize) {
    
    
        // true表示添加的是核心工作线程,具体一点就是,在addWorker内部会判断当前工作线程数是不是超过了corePoolSize
        // 如果超过了则会添加失败,addWorker返回false,表示不能直接开启新的线程来执行任务,而是应该先入队
        if (addWorker(command, true))
            return;
        
        // 如果添加核心工作线程失败,那就重新获取ctl,可能是线程池状态被其他线程修改了
        // 也可能是其他线程也在向线程池提交任务,导致核心工作线程已经超过了corePoolSize
        c = ctl.get();
    }
    
    // 线程池状态是否还是RUNNING,如果是就把任务添加到阻塞队列中
    if (isRunning(c) && workQueue.offer(command)) {
    
    
        
        // 在任务入队时,线程池的状态可能也会发生改变
        // 再次检查线程池的状态,如果线程池不是RUNNING了,那就不能再接受任务了,就得把任务从队列中移除,并进行拒绝策略
        
        // 如果线程池的状态没有发生改变,仍然是RUNNING,那就不需要把任务从队列中移除掉
        // 不过,为了确保刚刚入队的任务有线程会去处理它,需要判断一下工作线程数,如果为0,那就添加一个非核心的工作线程
        // 添加的这个线程没有自己的任务,目的就是从队列中获取任务来执行
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 如果线程池状态不是RUNNING,或者线程池状态是RUNNING但是队列满了,则去添加一个非核心工作线程
    // 实际上,addWorker中会判断线程池状态如果不是RUNNING,是不会添加工作线程的
    // false表示非核心工作线程,作用是,在addWorker内部会判断当前工作线程数已经超过了maximumPoolSize,如果超过了则会添加不成功,执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}

addWorker方法

addWorker方法是核心方法,是用来添加线程的,core参数表示添加的是核心线程还是非核心线程。
在看这个方法之前,我们不妨先自己来分析一下,什么是添加线程?
实际上就要开启一个线程,不管是核心线程还是非核心线程其实都只是一个普通的线程,而核心和非核心的区别在于:
如果是要添加核心工作线程,那么就得判断目前的工作线程数是否超过corePoolSize
如果没有超过,则直接开启新的工作线程执行任务
如果超过了,则不会开启新的工作线程,而是把任务进行入队
如果要添加的是非核心工作线程,那就要判断目前的工作线程数是否超过maximumPoolSize
如果没有超过,则直接开启新的工作线程执行任务
如果超过了,则拒绝执行任务
所以在addWorker方法中,首先就要判断工作线程有没有超过限制,如果没有超过限制再去开启一个线程。

并且在addWorker方法中,还得判断线程池的状态,如果线程池的状态不是RUNNING状态了,那就没必要要去添加线程了,当然有一种特例,就是线程池的状态是SHUTDOWN,但是队列中有任务,那此时还是需要添加添加一个线程的。

那这种特例是如何产生的呢?

我们前面提到的都是开启新的工作线程,那么工作线程怎么回收呢?不可能开启的工作线程一直活着,因为如果任务由多变少,那也就不需要过多的线程资源,所以线程池中会有机制对开启的工作线程进行回收,如何回收的,后文会提到,我们这里先分析,有没有可能线程池中所有的线程都被回收了,答案的是有的。

首先非核心工作线程被回收是可以理解的,那核心工作线程要不要回收掉呢?其实线程池存在的意义,就是提前生成好线程资源,需要线程的时候直接使用就可以,而不需要临时去开启线程,所以正常情况下,开启的核心工作线程是不用回收掉的,就算暂时没有任务要处理,也不用回收,就让核心工作线程在那等着就可以了。

但是!在线程池中有这么一个参数:allowCoreThreadTimeOut,表示是否允许核心工作线程超时,意思就是是否允许核心工作线程回收,默认这个参数为false,但是我们可以调用allowCoreThreadTimeOut(boolean value)来把这个参数改为true,只要改了,那么核心工作线程也就会被回收了,那这样线程池中的所有工作线程都可能被回收掉,那如果所有工作线程都被回收掉之后,阻塞队列中来了一个任务,这样就形成了特例情况。

猜你喜欢

转载自blog.csdn.net/beautybug1126/article/details/132072664