再聊线程池原理

    面对资源限制,为了更好的掌握充分利用有限的资源,所以有很多的池化技术,比如对象池,线程池,连接池等等,主要是用来更好的利用和控制池子里的资源,不至于线程过多导致系统内存,cpu,网络资源不足,过少又浪费cpu性能。
     之前的一篇 了解线程池中,主要是了解了线程池的使用和运行流程,此篇再次回顾线程池的工作原理及源码解析。

1、使用示例

首先来看一个平时经常用的线程池Demo:
class ThreadExecuteRunTask implements Runnable {

    @Override
    public void run() {
        try {
            sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) throws Exception {
         ExecutorService threadPool = new ThreadPoolExecutor(5, 10, 20, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(1000));
        ThreadExecuteRunTask task = new ThreadExecuteRunTask();
        threadPool.execute(task); //思考:只是传入了参数2和任务实现类task,并没有创建一个新的线程;为什么??

}

问题思考:我们只是传入了task任务类,并没有创建新的线程,然后我们的任务就被执行了,这其中的过程是怎么样的呢?

对于我们平时正常使用线程池的流程如下:
//第一步:创建线程,指定线程任务;
Thread thread = new Thread(new ThreadExecuteRunTask());
//第二步:启动线程
thread.start();

但是使用了线程池,就不必这样做了。

创建一个线程就不必了,线程的创建和销毁管理,任务的运行统统交给了线程池,通过线程池里的线程直接调用了task里的run方法,从而控制了线程的数量,也让线程的创建和任务的执行解耦。

2、线程池的简单实现

下面写了一段伪代码来代表线程池的实现原理,大致类似如下过程:
//实际类似:
        1、传入参数2,线程池就创建了2个常驻核心线程work1和work2;
    corePoolSize = 2;
    ExecutorService messageHandlerPool = new ThreadPoolExecutor(2, 5, 20, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>(1000));
    //预先创建2个worker线程
    Worker1
    run(){
        task.run(); //方法的调用,并不是创建新的线程来执行
    }
    worker1.start()

    Worker2
    run(){
        task.run();
    }
    worker12.start()

    2、提交任务后,通过创建的线程worder调用其run方法执行了,并没有创建新的线程,从而控制了线程的数量;
    threadPool.execute(new ThreadExecuteRunTask());

这里实现常驻2个线程,运行无数的任务

这里通过线程池里的常驻线程调用了task里的任务run()方法执行完毕,所以这里也没有创建新的线程,达到了控制线程数量的目的。示意图如下:
思考:那当任务量比较多的时候怎么办呢?
    当所有的核心worker线程都在努力工作的时候,没有空闲,之后的任务就需要被延迟执行,这时候加上了阻塞队列ArrayBolockingQueue来暂时缓存这些任务,达到了一个流控的作用;如果队列里缓存也达到最大,则再创建不大于MaxSize的线程帮助工作,下面是一个简单模拟了线程池工作流程的实现:
public class ThredPoolSample {

    static ArrayBlockingQueue abq = new ArrayBlockingQueue(10);
    public void init() {
        new Thread(() -> {
            while (true) {                        //创建一个线程,永远去等待执行任务;
                try {
                    Task task = (Task) abq.take(); //阻塞获取任务
                    task.run();                    //核心:调用任务的run方法而非创建线程;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static boolean sendData() {
        try {
            abq.put(new Task());                  //阻塞发送数据
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }


    public static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("do task");
        }
    }
}

    public static void main(String[] args) {
        sendData();                  //向队列里发送数据;
    }
}

好了,简单了解完大致的流程后,我们来分析一下JDK中的线程池是如何实现的。

3、线程池的实现原理

线程池的执行流程之前有分析过,这里不再赘述了,贴张图吧。

4、源码分析

下面来看一下线程池源码的具体实现:

4.1、execute()

从提交任务入口excute执行方法开始:
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);
}

ctl解析:

  • 高三位:代表当前线程池中线程的状态,用于控制线程的执行,比如通过FutureTask都是通过这个状态来获取线程的值;
  • 低29位:表示线程的数量;
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
@Native public static final int SIZE = 32;

//利用ctl的高三位表示线程的运行状态,低29位表示线程的数量;
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; //所有的任务已经结束,即将调用线程池terminal方法
private static final int TERMINATED =  3 << COUNT_BITS; //方法执行完毕

4.2、addWork()

addWork主要功能是 创建 添加一个线程任务到线程池, 其实就做两件事情
  1. 创建一个线程并添加线程池中,可以发现线程是放在HashSet集合中的;
  2. 添加线程数,通过csa机制去管理记录线程数及其状态;
/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
//Work的存储,存储在一个HashSet中:
private final HashSet<Worker> workers = new HashSet<Worker>();

private boolean addWorker(Runnable firstTask, boolean core) {
    retry: //goto 语句,两个自旋,一个breaak只能跳出一层循环,所以使用goto语句;
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //工作一:判断是否可以添加线程,
        //线程池处于SHUTDOWN状态
        //第一个判断:如果还要添加新任务,拒绝;
        //第二个判断:线程池处于SHUTDOWN状态,不允许接受新任务,但是会继续执行队列里的任务,如果队列里任务不为空,允许创建新线程;
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) { //工作二:自旋 - 创建线程
            int wc = workerCountOf(c);                         //得到work工作线程的数量
            if (wc >= CAPACITY ||                              //工作线程大于默认的线程数量
                wc >= (core ? corePoolSize : maximumPoolSize)) //工作线程大于核心的线程数量;返回false,不允许添加线程
                return false;
            if (compareAndIncrementWorkerCount(c)) //工作三:通过cas来增加线程的数量
                break retry;
            c = ctl.get();  // Re-read ctl         //再次获取ctl的值;
            if (runStateOf(c) != rs)               //如果不想等,说明线程的状态发生了变化                     
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
        //真正的构建线程
    boolean workerStarted = false; //是否启动线程标识
    boolean workerAdded = false;   //是否成功添加线程标识
    Worker w = null;
    try {
        w = new Worker(firstTask);  //创建一个线程
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();        //获取重入锁,避免线程安全问题
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w); //将心创建的线程添加到set集合中
                    int s = workers.size();
                    if (s > largestPoolSize) //判断是否超过最大的线程数,是线程池总共创建的线程数量;
                        largestPoolSize = s; //更新出现过的最大线程数量
                    workerAdded = true;      //线程创建成功
                }
            } finally {
                mainLock.unlock(); //解锁
            }
            if (workerAdded) {
                t.start();                  //在线程池中启动运行线程
                workerStarted = true;       //线程启动成功
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

4.3、任务Work的类结构

作用:Worker就是线程中常驻的工作线程,用于循环执行传入的任务;
思考:线程池的runWorker为啥不支持锁的重入而需要独占,为啥要重新实现AQS?
  • 因为线程在执行任务的时候,不允许重入。
    • 第一:获取锁lock,表示正在执行任务,任务只能被一个线程执行,不能重复执行;
    • 第二:线程shutdown关闭时候,通过 lock()去判断当前的线程池是否空闲,因为不能终止正在运行的线程否则将出现数据一致性问题;
  • 线程互斥锁的状态,state:
    • -1: 初始化状态
    • 0:   释放锁
    • 1:获得锁

//Work的实现:
private final class Worker
    extends AbstractQueuedSynchronizer //思考:为什么要继承AQS,?? 避免了重入锁,实现同步阻塞
    implements Runnable
{

/** Thread this worker is running in.  Null if factory fails. */
final Thread thread;          //真正执行任务的线程
/** Initial task to run.  Possibly null. */
Runnable firstTask;           //要执行的任务
/** Per-thread task counter */
volatile long completedTasks; //完成任务数,用于线程池的统计

/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker//初始化锁:-1 
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);  //第一步:创建新线程,并且把当前的this-Worker的实现当为参数传进去;
}

/** Delegates main run loop to outer runWorker  */
public void run() {
    runWorker(this); //第二步:执行run方法,Worker本身实现了Runnable接口;
}


protected boolean isHeldExclusively() {
    return getState() != 0;
}
   //重写AQS的获取锁的逻辑,不支持重入锁 ,获取锁 1 
protected boolean tryAcquire(int unused) {
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}
    //重写AQS的释放锁逻辑,没有重入锁  释放锁:0
protected boolean tryRelease(int unused) {
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
}

线程的创建与执行:

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

/** Delegates main run loop to outer runWorker  */
public void run() {
    runWorker(this);
}

4.4、runWorker()

主要作用,不断的去执行我们的任务,执行runWorker()三部曲:
  • 前置:beforeExecute();
  • 执行:task.run(); //这里很重要,实现了线程的复用,直接调用的任务里的run()方法;
  • 后置:afterExecute();
runWork具体执行步骤:
  • 重新写了常驻线程的run方法,真正的去执行一个任务;
  • 如果task不为空,则调用task.run()方法执行task;
  • 如果task为空,通过getTask()循环去取任务,并赋值给task。若取到的Runnable不为空,则执行该任务;
  • 执行完毕后,通过while循环继续getTask()取任务;
  • 如果getTask()取到的任务依然是空,那么整个runWorker()方法执行完毕;
思考: 线程池如何实现线程的复用?
  1. 当我们运行一个Worker的时候,也就是线程池里的线程,它启动 run方法的时候,会调用 runWorker(),之后会调用 getTask()在阻塞队列里 poll任务,获取任务后并不是启动一个线程来执行,而是通过调用任务里面的 run方法来执行任务,所以可以达到重用的目的;
  2. 具体流程及步骤: Worker->run()->runWorker()->while(true)-getTask()->task.run()
  3. run()方法只是去运行task的run方法,并没有创建新的start()线程,实现了线程的复用;
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {//这里的task  == 开篇提到的 task.run()
        //核心:在这个while循环里实现了线程的复用;如果task为空,则调用getTask到队列里去取任务;
        while (task != null || (task = getTask()) != null) {
            w.lock(); //这里不仅仅是为了阻塞,另一个重要的原因是为了不能终止我正在运行的任务,拒绝暴力停止;
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            //如果线程的状态处于stop,则需要保证不接受新的任务,不执行队列里的任务,中断正在执行的任务;
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                    //务执行之前;这里默认是没有实现的,我们可以重写做一些事情,比如统计监控的计时操作;
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    //开始执行任务中的run方法,这里很重要,我们为啥传入一个任务无需启动创建一个线程?
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    //务执行结束;这里默认也没有实现,打印出统计信息
                    afterExecute(task, thrown);
                }
            } finally {
                //置空任务(下次循环开始时,task依然为空,通过getTask来取)+ 记录worker完成的数量  + 解锁
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        //线程的回收:当线程在超时时间内没有获取到任务,或者满足了回收的条件,就将空闲的worker移除Hashset集合;
        processWorkerExit(w, completedAbruptly);
    }
}

线程的回收

getTask方法返回null时,在runWorker方法中会跳出while循环,然后会执行processWorkerExit方法,销毁工作线程。 这里也是注意到了线程池的严谨之处,销毁线程的时候都是互斥锁进行判断,不可终止一个正在运行的线程,这也是线程池自己实现AQS的原因。
private void processWorkerExit(Worker w, boolean completedAbruptly) {
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();       //上锁
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w); //删除一个任务,当run方法执行完毕后,线程由JVM自动回收
    } finally {
        mainLock.unlock(); //解锁
    }

    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

4.5、getTask()

getTask() 主要做两件事:
  1. 获取任务:队列里循环获取下一个任务,这里也是实现线程复用的关键;
  2. 回收非核心线程:当线程在等待的时间内没有获取到任务,回收设置了允许回收的空闲线程;
思考:线程的回收条件是什么? remove( runWorker())
  • 条件一:从源码我们可以看到只要线程在设置的 keepalive超时时间内,通过 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)没有获取到任务就会被回收掉;
  • 条件二:非核心线程自己运行结束,由JVM自动回收;
线程池容量设置
 ThreadPoolExecutor提供了动态调整线程池容量大小的方法:
  • setCorePoolSize()  : 设置核心线程池的大小
  • setMaximumPoolSize() : 设置线程池最大能创建线程数目的大小;
  • allowCoreThreadTimeOut(true); 表示核心线程数也可以被回收;
源码分析:
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //如果线程池的状态为SHUTDOWN,队列为空的情况,线程run执行完了怎么办?只能销毁,被JVM回收了
        //情况一:线程的状态为SHUTDOWN,拒绝接受新任务 + 执行完队列里的任务;
        //情况二:线程的状态为SHUTDOWNNOW,立即中断正在执行的线程,队列里的任务不会执行;
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null; //返回null,则当前的线程会退出,跳出runWorker的循环
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        //检查核心线程是否配置了允许超时的时间,进行超时控制;
        //情况一:allowCoreThreadTimeOut:核心线程不允许超时,但是可以设置;
        //情况二:corePoolSize:大于核心线程数量的线程,需要进行超时控制;
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                //配置了超时时间keepAliveTime 或者当前的线程数大于核心线程数,通过poll来进行超时控制,超时还没有数据,返回null
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : 
                //没有配置超时时间 或者 当前的线程数小于核心线程数,通过take()阻塞获取,直到有数据;
                workQueue.take();        
            if (r != null)
                return r; //如果拿到的任务不为空,则返回给线程处理
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

4.6、关闭线程池

思考: 线程池是如何保证不能中断一个正在执行的线程的?
方案:通过中断和锁机制来保护线程的执行完整的流程:
  • 方式一:线程的状态为SHUTDOWN,拒绝接受新任务 + 执行完队列里的任务;
  • 方式二:线程的状态为SHUTDOWNNOW,立即中断正在执行的线程,类似于命令:kill -9;队列里的任务不会执行;
推荐方式一优雅的实现关闭线程:shutdown(): 
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}


private void interruptIdleWorkers() {
    interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
           //这里停止线程的时候,会去尝试获取锁,但是我们在执行任务的时候已经获得了锁,并且该锁是非重入的,所以这里中断不了一个正在执行的线程
            if (!t.isInterrupted() && w.tryLock()) { 
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

4.7、线程的监控

通过runWorker()方法的分析我们知道,befoerExecutor和afterExecutor都是抽象的模版方法,所以我们可以自己去实现,做一些想做的事情。
/**
* 任务执行之前,记录任务开始时间
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
    startTimes.put(String.valueOf(r.hashCode()), new Date());
}

/**
* 任务执行之后,计算任务结束时间
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
    Date startDate = startTimes.remove(String.valueOf(r.hashCode())); //todo 获取的同时需要删除,否则容易引起内存的泄漏;
    Date finishDate = new Date();
    long diff = finishDate.getTime() - startDate.getTime();
    // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
    // 已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、
    // 最大允许的线程数、线程空闲时间、线程池是否关闭、线程池是否终止
    LOGGER.info("{}-pool-monitor: " +
                    "Duration: {} ms, PoolSize: {}, CorePoolSize: {}, Active: {}, " +
                    "Completed: {}, Task: {}, Queue: {}, LargestPoolSize: {}, " +
                    "MaximumPoolSize: {},  KeepAliveTime: {}, isShutdown: {}, isTerminated: {}",
            this.poolName,
            diff, this.getPoolSize(), this.getCorePoolSize(), this.getActiveCount(),
            this.getCompletedTaskCount(), this.getTaskCount(), this.getQueue().size(), this.getLargestPoolSize(),
            this.getMaximumPoolSize(), this.getKeepAliveTime(TimeUnit.MILLISECONDS), this.isShutdown(), this.isTerminated());
}

至此,我们已经分析完了一个工作线程从创建到销毁的全流程了。

5、小结

线程池的核心流程涉及的方法:
  • 1、executor:          //执行流程
  • 2、addWorker() :   //创建一个线程加入到线程池;
  • 3、runWorker():     //执行一个线程任务;
    • 3.1、beforeExecutor()
    • 3.2、task.run()     //执行run方法
    • 3.3、afterExecutor()
  • 4、getTask()    //获取任务
  • 5、shutdown() //线程关闭
OK----君子死知己,提剑出燕京。
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值