【Java】Java线程池学习

Java线程池学习

线程是操作系统的调度和分配的基本单位,所以想要优化高并发系统的工程师少不了需要跟线程打交道,故此,学习线程的工作原理对于Java工程师是必不可少的进阶之路。

在学习线程之前,需要先想清楚自己学习线程应该到哪个程度才算满意?我打算通过几个问题来学习和校验自己的线程学习程度。

  1. 线程的生命周期?
  2. 如何复用线程?
  3. Java线程池的核心属性有哪些?其主要作用有哪些?
  4. 线程池新建线程的逻辑(有界队列、无界队列)?
  5. 线程池的拒绝策略?
  6. 如何监控线程池?
  7. 线程池状态的设计(相关位运算学习)?

线程的生命周期

在操作系统层面,线程包含五个状态,分别是:新建、就绪、运行、阻塞、结束。
而Java语言定义了另外五种线程状态:新建、运行、无限期等待、限期等待、阻塞、结束。其中Java的运行状态包括了操作系统层面的就绪与运行状态,Java的无限期等待、限期等待、阻塞为操作系统层面的阻塞状态。

新建

通过new关键字新建一个线程时,该线程就是新建状态,此时仅由JVM分配内存和初始化其成员对象,当新建线程调用start()方法,则线程进入就绪状态。

就绪

处于就绪状态的线程JVM会为其创建方法调用栈和程序计数器,处于就绪状态的线程并没有被cpu调用执行,而是处于等待cpu调度的状态。线程何时转为运行状态,取决于操作系统。

处于就绪状态的线程不能在调用start()方法,否则会报IllegaIThreadStateExccption异常。

线程进入就绪状态有这些情况:

  1. 新建线程调用start()方法
  2. 解除阻塞状态的线程
  3. 运行状态的线程调用yield()方法
Java线程调度

线程调度指系统为线程分配处理器使用权的过程,主要调度分为两种方式:协同式线程调度和抢占式线程调度,Java使用的线程调度方式是抢占式调度,但不确保某些极早的虚拟机使用协同式线程调度。

协同式线程调度

协同式调度的多线程系统中,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。

  • 好处是实现简单,而且由于线程要把自己的事情做完后才会进行线程切换,切换操作对线程自己是可知的,所以不存在线程同步问题。
  • 缺点是执行时间不可控,如果线程编写有问题,一直不通知系统进行线程切换,那么程序就会一直阻塞着。
抢占式线程调度

抢占式调度的多线程系统中,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(仅可通过yield()释放cpu资源,但是获得cpu资源不可控)。

虽然Java提供了设置优先级的方法setPriority(int newPriority),但是Java线程优先级并不靠谱,原因是Java的线程是通过映射到系统的原生线程上实现的,所以线程调度最终还是取决于操作系统,然而不同系统提供的优先级数却不定相同,例如Solaris中有2的32次方种优先级,而Windows只有7种,这就意味着Windows中优先级不能完全对应Java优先级;而且有些系统会自行改变优先级,这些情况都告诉我们不能太相信设置优先级带来的效果。

运行

就绪状态的线程获得cpu调度就会进入运行状态,此时开始执行run()方法的线程执行体,如果计算机只有一个cpu,则任何时刻只有一个线程处于运行状态,如果是对处理器的计算机,就可以同时有多个线程并行执行,当线程数大于机器处理器数时,则会存在多个线程在一个cpu上切换的现象,这也是不建议线程池中的核心线程数设置超过处理器数两倍的原因。

阻塞

运行状态的线程如果发生以下情况,会转为阻塞状态:

  1. 调用sleep()主动放弃占有的cpu资源,直到休眠时间结束由系统唤醒线程进入就绪状态
  2. 调用了阻塞式IO操作,在返回方法之前,该线程被阻塞,直到IO操作结束,线程进入就绪状态
  3. 试图获得同步监视器,但是该同步监视器被其他线程所拥有,知道成功获得同步监视器进入就绪状态
  4. 等待某个通知,直到获得其他线程发出的通知进入就绪状态
  5. 调用suspend()挂起,直到调用resdme()恢复,该方法容易造成死锁,已被废弃

结束

运行线程遇到如下情况会转为结束状态:

  1. run()call()方法运行结束,线程正常结束
  2. 线程抛出一个未捕获异常或error
  3. 调用stop()主动结束线程,该方法容易造成死锁,已被废弃。

已经结束的线程不能在通过start()方法试图重新唤起,这样会报否则会报IllegaIThreadStateExccption异常。

如何复用线程?

要学习复用线程池,就应该从源码入手。

首先,我们从执行任务代码处开始看起

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        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);
    }

很明显看出,线程池执行任务有四种情况:

  1. 如果正在运行的线程少于corePoolSize线程,使用传入的runnable作为其第一个任务来启动一个新线程;
  2. 当线程数等于核心线程数且任务队列不满时,任务入队列,复用线程去任务队列中拿任务执行;
  3. 当任务队列已满时且线程数小于最大线程数,创建新线程;当任务无法入队且线程创建失败时,拒绝服务。

具体线程怎么新增线程的,再看addwork()代码

private boolean addWorker(Runnable firstTask, boolean core) {
        // 方便学习,此处省略大量条件判断代码
        
        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);
                        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;
    }

跳过前面一堆条件判断代码后,可以很清楚的看到线程池是通过Worker类封装了线程的实例过程,当线程创建成功后,将线程放进Workers队列中,接着启动线程;如果创建失败则Workers队列移除该线程。

看到这里大概知道了线程的启动过程,但是线程的是如何创建的?如何将用户任务作为第一个任务?这些问题还没有得到很好的解答,带着这些问题可以继续看Woker类

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

可以看到Woker类的构造函数,发现线程是通过ThreadFactory创建,并把firstTask传给newThread.

直到现在代码也看了上百行,虽然知道了线程池创建线程的原理了,但是线程池的线程是如何复用的仍然半知半解,线程在完成第一个任务后为什么不会销毁,而是会去任务队列中找下一个任务的呢?答案就在眼前,我们再接着往下看

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

Woker类的run方法,它调用了runWorker()方法,继续刨根问底

/**
     * Main worker run loop.  Repeatedly gets tasks from queue and
     * executes them, while coping with a number of issues:
     *
     * 1. We may start out with an initial task, in which case we
     * don't need to get the first one. Otherwise, as long as pool is
     * running, we get tasks from getTask. If it returns null then the
     * worker exits due to changed pool state or configuration
     * parameters.  Other exits result from exception throws in
     * external code, in which case completedAbruptly holds, which
     * usually leads processWorkerExit to replace this thread.
     *
     * 2. Before running any task, the lock is acquired to prevent
     * other pool interrupts while the task is executing, and then we
     * ensure that unless pool is stopping, this thread does not have
     * its interrupt set.
     *
     * 3. Each task run is preceded by a call to beforeExecute, which
     * might throw an exception, in which case we cause thread to die
     * (breaking loop with completedAbruptly true) without processing
     * the task.
     *
     * 4. Assuming beforeExecute completes normally, we run the task,
     * gathering any of its thrown exceptions to send to afterExecute.
     * We separately handle RuntimeException, Error (both of which the
     * specs guarantee that we trap) and arbitrary Throwables.
     * Because we cannot rethrow Throwables within Runnable.run, we
     * wrap them within Errors on the way out (to the thread's
     * UncaughtExceptionHandler).  Any thrown exception also
     * conservatively causes thread to die.
     *
     * 5. After task.run completes, we call afterExecute, which may
     * also throw an exception, which will also cause thread to
     * die. According to JLS Sec 14.20, this exception is the one that
     * will be in effect even if task.run throws.
     *
     * The net effect of the exception mechanics is that afterExecute
     * and the thread's UncaughtExceptionHandler have as accurate
     * information as we can provide about any problems encountered by
     * user code.
     *
     * @param w the worker
     */
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 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
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    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 {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

为了更好理解,我把原码注释也放进来,再用我那不堪入目的英语水平简单翻译一下:

线程不断循环从任务队列中拿任务并执行,同时还解决一些问题:

  1. 如果线程已有第一个任务,就不需要从pool中拿,如果线程试图拿任务但是返回null,则修改线程池状态或配置参数然后退出,否则其他退出情况是外部代码异常引起的。
  2. 如果线程池正在运行任务,将获取锁以防止任务执行期间线程池被中断,并且除非线程池正在停止,否则线程不能设置其中断。
  3. 每个任务运行之前都会调用beforeExecute,如果引起异常,为了防止破坏循环,我们将销毁该线程而不是继续处理任务。
  4. 假设beforeExecute正常完成,线程将执行任务,并收集执行任务期间引发的任何异常以发送给afterExecute。而且我们分别处理RuntimeExceptionError(只要规格符合捕捉)和任意Throwables。因为线程不能在Runnable.run中重新抛出Throwables,所以将异常包装在线程的UncaughtExceptionHandler的Errors中。任何抛出的异常也会导致线程死亡。
  5. task.run完成后,线程会调用afterExecute,这可能还会引发异常,也会导致线程死亡。

可以看出,线程池为了保证自身的循环复用线程机制,宁愿霸道的结束在执行过程中出现异常的线程也不愿意回收它们,以引发不可知的错误导致循环破坏。

再上面看原码
while (task != null || (task = getTask()) != null)

可以看出,线程在不断循环从任务队列中getTask()拿任务执行

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.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

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

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

看完这段代码,终于知道了拿任务也不是那么简单的

  1. 如果线程数超过了最大线程数,只会返回null
  2. 线程等待超时也拿不到任务
    拿不到任务的后果就是被清理了,很现实啊
    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);
        } 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);
        }
    }

看到这里,我们已经清楚了线程池的线程复用原理了,在回顾一下:
首先,线程池启动,用户调用线程池执行task任务,此时会出现3种情况:

  1. 线程数小于核心线程数
  2. 线程数等于核心线程数且任务成功入队列
  3. 任务入队失败,尝试创建新线程

线程创建出来后,就会不断循环从任务队列中拿任务,当任务队列为空时,核心线程也会根据keepAliveTime处于活跃状态,继续不断循环拿任务,而超过核心线程数的线程在等待完第一个keepAliveTime后死亡。

Java线程池的核心属性有哪些?其主要作用有哪些?

线程池的属性有七个,分别是核心线程数corePoolSize,最大线程数maximumPoolSize,存活时间keepAliveTime,存活时间单位unit,任务队列workQueue,线程工厂threadFactory,拒绝策略handler

下面是ThreadPoolExecutor类的构造函数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. corePoolSize-核心线程数:线程池空闲时间仍可保留的线程数
  2. maximumPoolSize-最大线程数:线程池最大线程数
  3. keepAliveTime-存活时间:超过核心线程数的线程在空闲超过存活时间就会死亡
  4. unit-时间单位:keepAliveTime的时间单位
  5. workQueue-任务队列:当线程池没有空闲线程时,任务进入任务队列等待空闲线程获取执行
  6. threadFactory-线程创建工厂:线程创建工程,建议自定义该属性,方便给线程定义名称打印日志
  7. handler-拒绝策略:当线程池没有空闲线程且任务进入任务队列失败时,线程池拒绝执行任务的操作

线程池新建线程的逻辑(有界队列、无界队列)

  • 有界队列:当有新的任务需要执行,如果线程池线程数小于核心线程数,则创建新的线程执行任务;如果线程数等于核心线程数,则任务进入任务队列等待空闲线程执行;如果任务队列已满,则判断线程数小于最大线程数,创建新的线程执行任务,如果线程数等于最大线程数,则执行拒绝策略。
  • 无界队列:LinkedBlockQuery。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在如对失败的情况。当有任务需要执行,线程池的线程数小于核心线程数,则新建线程执行任务;当线程数等于核心线程数,则不增加线程,后续执行任务进入任务队列中等待,若任务创建和处理的速度差异很大,无界队列会保持快速增长,知道资源耗尽为止。

线程池的拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务,并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.CallerRunsPolicy:该任务被线程池拒绝,由调用 execute方法的线程执行该任务(通常为主线程);如果执行程序已关闭,则会丢弃该任务。
  3. ThreadPoolExecutor.DiscardOldestPolicy: 抛弃队列最前面的任务,然后重新尝试执行任务。
  4. ThreadPoolExecutor.DiscardPolicy:丢弃任务,不过也不抛出异常。

如何监控线程池

ThreadPoolExecutor提供的API方法如下:
1.getPoolSize():初始线程数
2. getCorePoolSize():核心线程数
3. getActiveCount():正在执行的任务数量
4. getCompletedTaskCount():已完成任务数量
5. getTaskCount():任务总数
6. getQueue().size()队列里缓存的任务数量
7. getLargestPoolSize():池中存在的最大线程数
8. getMaximumPoolSize():最大允许的线程数
9. getKeepAliveTime(TimeUnit.MILLISECONDS):线程空闲时间
10. isShutdown():线程池是否关闭
11. isTerminated():线程池是否终止

同时,ThreadPoolExecutor类预留给开发者进行扩展的方法:
12. shutdown():线程池延迟关闭时(等待线程池里的任务都执行完毕)
13. shutdownNow():线程池立即关闭
14. beforeExecute(Thread t, Runnable r):任务执行之前执行该方法
15. afterExecute(Runnable r, Throwable t):任务执行之后执行该方法

线程池状态的设计(相关位运算学习)

线程池状态设计涉及到了二进制的位运算,Doug Lea大佬巧妙的将线程的状态与线程数用一个int数解决,不得不让人钦佩一番。

关于线程池的位运算源码先贴上来,再来分析:

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

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

    // Packing and unpacking ctl
    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; }

通过源码,不难发现,线程池使用一个 int 类型来存储 线程池中当前的线程数量 与 状态。
其设计思想是:用 int 的高3位用来表示线程池的状态,int 的低29位用来表示当前线程池中的线程数量。
COUNT_BITS : 这个参数就是用来表示用于表示线程个数所占用的位数,这里等于 29。
CAPACITY : 线程池中线程的总个数,有了位运算的基本知识后,这个不难理解,00011111 11111111 11111111 11111111
为了方便查看,我将线程池五种状态的二进制作成下面的表格

statusBinary
RUNNING1110 0000 0000 0000 0000 0000 0000 0000
SHUTDOWN0000 0000 0000 0000 0000 0000 0000 0000
STOP0010 0000 0000 0000 0000 0000 0000 0000
TIDYING0100 0000 0000 0000 0000 0000 0000 0000
TERMINATED0110 0000 0000 0000 0000 0000 0000 0000

上面状态通过向左移动29位,可以保证这些状态的低29位都为0。
线程池使用变量 AtomicInteger ctl 来表示状态 与线程数量,其实现方法为:
private static int ctlOf(int rs, int wc) { return rs | wc; }

就是将 状态 与 线程数 做 | 运算,其实好理解。

上面的状态,低 29位都是 000,而用于表示线程数量的int ,高3位都为0,而 利用 a | 0 = a 的特性实现位的相互不影响。

那如何从一个 ctl 变量中快速得到当前的状态呢?

那如何从 ctl 中提取出状态呢?
private static int runStateOf(int c) { return c & ~CAPACITY; }
其实就是要提前 int 的高3 位置,通常的套路是使用 & 运算,参与与运算的另外一个操作数,高3为都是111,低29位置都是0,这样就能提前3位的 111 ,而不受低29位的1的影响,因为低29位都是0。故使用了 ~ CAPACITY,其中 CAPACITY 的值为:00011111 11111111 11111111 11111111,取反为 11100000 00000000 00000000 00000000

同样,如果要从 ctl 中 提取线程数量,则使用如下表达式。
private static int workerCountOf(int c) { return c & CAPACITY; }

好啦,以上就是我阅读线程池源码时的一些学习心得体会,阅读源码越深,对于技术的敬畏就更多,学习永无止步。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值