Java并发编程之线程池实践及实现原理

1,181 阅读13分钟

一、线程池介绍

随着计算机的高速发展,多核cpu已经成为主流。为了让计算机充分发挥性能,使用多线程编程已然成为了程序猿为提升服务性能的必备武器。而Java提供的线程池能够很好的帮助咱们管理线程以及很方便的并行执行任务。如何合理的使用线程池以及熟悉其运行原理是每个程序员的基本技能。本篇结合线程池源码,咱们一起深入了解她的设计思想。最后回归实践结合相应demo进一步加深印象。

线程池到底是个啥?

通俗点讲线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升你系统的性能。

池化资源思想,线程池(thread pool)就是一种基于池化思想管理线程的工具。可以借助这种思想来解决开发中遇到的问题,提高资源的重复利用率来提升服务性能。

本文描述的是JavaJ.U.C包下的ThreadPoolExecutor类。

使用线程池能带来哪些好处呢?

  • 降低资源消耗:通过重复利用已经创建的线程来降低频繁创建、销毁所造成的消耗。
  • 提高线程的可管理性:线程是稀缺资源,无限制的创建不仅会消耗大量的系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以很方便管理以及监控。
  • 提高相应速度:有任务就立即执行,不需要在等待创建线程来执行任务。

它的存在到底是为了解决啥问题?

线程池的存在主要就是为了解决资源管理问题。在并发环境中,系统在某一时刻不知道有多少任务需要执行,不知道需要多少资源投入。各种的不确定性会严重影响系统的稳定性。

二、线程池的核心设计与实现

ThreadPoolExecutor类是线程池中最核心的一个类,想要透彻了解Java的线程池,必须先了解这个类。下面我们来一起看下ThreadPoolExecutor类的具体实现源码。

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

public class ThreadPoolExecutor extends AbstractExecutorService {
	......
 	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,RejectedExecutionHandler handler);
        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,ThreadFactory threadFactory,
        RejectedExecutionHandler handler);
        ......
}

从上面的代码可以看出来,ThreadPoolExecutor继承了AbstractExecutorService类,并提供了四个构造方法,事实上前面三个构造方法都是通过调用第四个构造方法初始化工作的。

下面解释下构造方法中参数的含义:

  • corePoolSize:核心线程池大小。默认情况下线程池中并没有任何线程。只有当任务来了之后,才回去创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize时,就会把之后提交的任务缓存到队列当中去。
  • maximumPoolSize:线程池中最大线程数量,她表示在线程池中最多只能创建多少个线程,
  • keepAliveTime:没有任务的情况下,线程存活的最长时间。默认情况下,只有当线程池中的线程数量大于corePoolSize时,keepAliveTime才会起作用。
  • unit:keepAliveTime时间单位。如下:
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue:一个阻塞队列,用来缓存等待执行的任务。一般常用如下几个:
ArrayBlockingQueue; 
LinkedBlockingQueue;
SynchronousQueue;
  • threadFactory:线程工厂,主要用来创建线程。
  • handler:拒绝策略,有一下四种:
ThreadPoolExecutor.AbortPolicy;//丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy;//也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy;//丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy;//由调用线程处理该任务 

ok,咱们来一起看下ThreadPoolExecutor到底是怎么玩的,首先来看下ThreadPoolExecutorUML类图,了解下继承关系:

  1. Executors:最顶层接口,主要用于将任务提交和任务执行解耦,用户只需要提供Runnale对象,将任务的执行逻辑提交到Executors中,由Executors完成任务的调配和执行。
  2. ExecutorService:提供了生命周期的管理的方法,返回Future对象,以及可以跟踪一个或多个异步任务执行返回的Future的方法。增加管理线程池的方法,比如shutdown可平滑关闭ExecutorService
  3. AbstractExecutorService:抽象类,实现了ExecutorService接口的方法,将执行任务流程实现,下层只需要执行任务就行。
  4. ThreadPoolExecutor:核心实现类,主要用于管理线程、执行任务。

ThreadPoolExecutor既然是核心,当然要细说下ThreadPoolExecutor是如何运行的呢?如下图所示:

一个生产、消费者模型:

生产者用于将任务提交,根据相应的规则走不同的流程。消费者主要用于线程管理,分配线程,任务执行完成继续获取新的任务执行,当最终获取不到任务时,线程就会根据相应的规则被回收掉。

源码剖析

上面的UML图也已经可以看出ThreadPoolExecutor大概的结构关系,下面咱莪们进一步剖析深入。

ThreadPoolExecutor她的顶级父类是Executor接口,只包含了一个方法execute,这个方法主要就是执行提交的任务对象。

public interface Executor {
   execute(Runnable command);
}

Executor#execute的实现是在ThreadPoolExecutor类中:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
	......
}

上来就一脸懵逼看到个ctl变量,也不知道干啥用的,代码找到她的定义:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

这里看注释说明,她主要是对线程池的运行状态和线程池中有效线程数量控制的一个字段,包含如下两个信息:

  • workerCount:当前有效的线程数量。
  • runState:当前线程池的状态。

线程池运行的状态描述以及运行时状态转换流程:

RUNNING:  接受新任务并处理排队的任务。
SHUTDOWN: 不接受新的任务,但是可以继续处理已经排队的任务。
STOP: 不在接受新的任务,不处理已经阻塞的任务,和中断已经在进行中的任务。
TIDYING: 所有任务全部终止,有效线程数为0。运行terminated()钩子方法。
TERMINATED: terminated() 执行完成后进入该状态。

JDK中注释描述的生命周期状态转换:

* RUNNING -> SHUTDOWN
*    On invocation of shutdown(), perhaps implicitly in finalize()
* (RUNNING or SHUTDOWN) -> STOP
*    On invocation of shutdownNow()
* SHUTDOWN -> TIDYING
*    When both queue and pool are empty
* STOP -> TIDYING
*    When pool is empty
* TIDYING -> TERMINATED
*    When the terminated() hook method has completed

线程池生命周期流程图如下:

ctl是一个AtomicInteger,一共有32位,线程池的状态需要3位来表示,workerCount有29位,所以代码中规定线程池的有效线程数最多为(2^29)-1。

private static final int COUNT_BITS = Integer.SIZE - 3;//32-3=29 线程熟练占的位数
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;低29表示最大线程数

// 高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;

private static int runStateOf(int c)     { return c & ~CAPACITY; }//计算当前运行的状态
private static int workerCountOf(int c)  { return c & CAPACITY; }//计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; }//状态加线程数量生成ctl

再次回到ThreadPoolExecutor#execute的方法:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();//由她可以获取到当前有效线程数和线程池的状态
        //1. 获取当前正在运行的线程数是否小于核心线程数,是则创建一个新的线程去执行,否则将任务放到任务队列中去。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))//创建工作线程去执行任务
                return;
            c = ctl.get();
        }
        //2. 当前核心线程中线程全部都在运行,将线程放到任务队列中去。
        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);
        }
        //3. 插入队列不成功,并且当前线程数量小于最大线程池数量,创建新的线程执行任务,创建失败拒绝任务抛出异常。
        else if (!addWorker(command, false))
            reject(command);//任务拒绝抛出异常
    }

execute完成任务的调度,检查线程池运行的状态、运行线程数、运行策略。是直接申请线程执行或是将任务放入队列还是直接拒绝任务。

  1. 首先检查线程池是否是在RUNNING状态,如果不是直接拒绝。
  2. 如果workerCount < corePoolSize,有效线程数量小于核心线程数则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,有效线程数量大于等于核心线程数量,并且任务队列没有满,则将任务放到任务队列中去。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,有效线程数量大于等于核心线程数量、有效线程数小于最大线程数,并且任务队列已经满了,则直接启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,有效线程数大于等于最大线程数时,并且有效线程数任务队列已经满了,则根据拒绝策略来处理新提交的任务。

大概流程图如下:

接着execute方法继续看,在execute方法中的第一步即是判断当前核心线程数是否还有空闲线程,如果有则通过addWorker方法创建线程执行任务,以下是addWorker代码,以下代码略长,看起来比较吓人:

    private boolean addWorker(Runnable firstTask, boolean core) {
    	//1. 循环CAS操作,将线程池中的线程数+1;
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                //core等于true代表往核心线程池中增加线程;false:往最大线程池中增加。线程数量超标直接返回。
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //修改CAS ctl+1 成功退出,失败继续。
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                //如果线程池的状态发生改变回到retry到外层循环。
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

	//2. 新建线程并加入到线程池中。
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);//将线程封装成worker工作线程
            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()) // 线程处于获取状态
                            throw new IllegalThreadStateException();
                        //将新建的线程加入到线程池
                        workers.add(w);
                        int s = workers.size();//工作线程数量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;//新构建的工作线程加入成功
                    }
                } finally {
                    mainLock.unlock();
                }
                //线程添加成功,开启新创建的线程
                if (workerAdded) {
                    t.start();//这个start实际上执行的是worker中的run方法。
                    workerStarted = true;
                }
            }
        } finally {
        //如果没能成功创建工作线程
            if (! workerStarted)
                addWorkerFailed(w);//失败后需要将工作线程移除并将ctl恢复。
        }
        return workerStarted;
    }

工作线程被成功添加到工作线程集合后,则开始start执行,这里的start是Worker工作线程中的run方法。继承AQS,具有锁的功能,实现Runnable,具有线程的功能:

private final class Worker 
	extends AbstractQueuedSynchronizer 
	implements Runnable {

        //线程池中真正运行的线程。
        final Thread thread;
        //线程包装的任务。
        Runnable firstTask;
        //记录当前线程完成的任务数量。
        volatile long completedTasks;

        Worker(Runnable firstTask) {
            setState(-1); // 设置AQS同步状态位-1,禁止中断 直到调用runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);//通过线程工厂来创建一个线程
        }

        public void run() {
            runWorker(this);//运行工作线程
        }
}

可以看出,Worker类的run方法实际上调用的还是ThreadPoolExecutor的runworker方法。下面一起看一下ThreadPoolExecutor的runworker源代码:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                //如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    //开始执行任务前的hook
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                    	//执行任务
                        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 {
                    	//执行任务后的hook
                        afterExecute(task, thrown);
                    }
                } finally {
                    //执行完毕充值task,completedTasks++,解锁
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            //当获取不到任务时,主动回收当前线程
            processWorkerExit(w, completedAbruptly);
        }
    }

当前任务不为null或者从队列中获取的任务不为null时,worker线程就一直执行任务。当获取不到务时,循环结束,开始回收线程。runWorker方法中最重要的是getTask()方法,她不断的从阻塞队列中获取任务交给线程执行,来一起看看这个怎么玩的。

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

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

            //如果线程池状态为SHUTDOWN 并且队列为空或者状态处于STOP或者terminate时,有效线程池数-1,返回null回收线程。
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // 如果allowCoreThreadTimeOut为ture或者当前有效线程数大于核心线程数,则需要超时回收。
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
	    //如果有效线程数大于最大线程数或者为超时且不允许超时回收 并且有效线程数大于1或者队列为空则有效线程数-1,返回null 回收该线程。
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
            //如果允许空闲回收,则调用阻塞队列的poll,否则take,一直等到队列中有可取任务。
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                    //去到任务,返回结果任务
                if (r != null)
                    return r;
                //执行到这里说明在允许时间内没有获取到任务
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

allowCoreThreadTimeOut为false,线程即使空闲也不会被销毁;倘若为ture,在keepAliveTime内仍空闲则会被销毁。 如果线程允许空闲等待而不被销毁timed == false,workQueue.take任务。如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行; 如果线程不允许无休止空闲timed == true, workQueue.poll任务。如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;

到这已经撸完了具体的实现,看的懵逼一定要自己也结合代码看看,下面来搞一把看看具体怎玩的。

三、线程池的实践

前面我们讨论了关于线程池的实现原理,这一节我们来看一下它的具体使用:

public class ThreadPoolTest {

    @Test
    public void test()  {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 200,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1000));
        for (int i = 0; i < 20; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程池中线程数目:" + threadPoolExecutor.getPoolSize() + ",队列中等待执行的任务数目:" +
                            threadPoolExecutor.getQueue().size() + ",已执行玩别的任务数目:" + threadPoolExecutor.getCompletedTaskCount());
                }
            });
        }
	threadPoolExecutor.shutdown();
    }
}

一个很简单demo,很快就可以使用JDK提供的线程池来实现。当然Executors还提供了一些静态方法可以使用,如下:

Executors.newFixedThreadPool(2);//创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
Executors.newCachedThreadPool();//创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
Executors.newScheduledThreadPool(1);//创建一个定长线程池,支持定时及周期性任务执行。
Executors.newSingleThreadExecutor();//创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newWorkStealingPool();//使用所有available processors(Java虚拟机的处理器数量)作为目标并行度级别,创建工作窃取线程池。底层采用ForkJoinPool实现。

好了,看完了可以自己来实现一把试试了!!!

好了,看完了可以自己来实现一把试试了!!!

好了,看完了可以自己来实现一把试试了!!!

写在最后

JAVA本身提供的API已经可以让我们快速的进行基于线程池的多线程开发,但是我们必须要为我们写的代码负责,每一个参数的设置和策略的选择跟不同应用场景有绝对的关系。然而对于不同参数和不同策略的选择并不是一件容易的事情。实际应用中还是应该结合具体场景来使用,今天就到这了,希望这篇文章能给大家带来帮助。喜欢加关注,后续持续更新!!!