并发(五):细讲JAVA线程池

134 阅读12分钟

大纲内容\

  • 概念
  • 生命周期
  • 核心参数
  • 源码分析
    • execute(Runable command)
    • addWorker(Runable firstTask,boolean core)
    • runWorker(this)
    • getTask()
    • processWorkerExit(Work w,boolean co m)


问题1: 如何合理配置线程池参数?问题2:当线程池中的线程执行异常了代码,线程池如何处理?问题3:线程池中的线程何时被回收?问题4:线程池中有哪几把锁?

image.png 详细内容可以看下链接, 关注一波, 后期分享经典面试题

并发(五):细讲JAVA线程池

并发(五):细讲JAVA线程池

并发(五):细讲JAVA线程池

并发(五):细讲JAVA线程池


概念
本模块的中心不是单线程,单线程就是:一个线程,对应执行的任务,线程只有一个,可是处理的任务可以有很多,只有累死的一头牛(线程),没有耕坏的地(任务)。单线程的生命周期:创建-执行-销毁,最影响性能的是:创建和销毁,而线程池可以很好的帮我们管控线程,监控线程,循环利用线程。
线程池的好处:避免频繁的创建和销毁线程,省去这一部分的消耗时间,

注意:避免频繁的创建和销毁

  • 降低资源消耗,可以重复利用已创建的线程,降低线程创建和销毁的开销。
  • 提高响应速度,当任务到达时,若线程池中存在可用线程,则可以直接使用,避免了创建的消耗。
  • 提高线程的可管理性,使用线程池可以统一分配,调优和监控,runWork()方法中有beforeExecute()方法,可以对代码进行一些监控。\


缺点:如何合理的配置线程池参数,可能就是最大的缺点。\


生命周期

线程池中存在很多都是IF判断逻辑,都是判断线程池的状态,常见的状态如下

  • running:能接受新提交的任务,并且也能处理阻塞队列中的任务,线程池的初始状态。
  • shutdown:停止状态,不再接受新提交的任务,但可以处理阻塞队列中的剩余任务。
  • stop:不再接受新提交的任务,同时也不处理阻塞队列中的剩余任务。
  • tidying:所有的线程任务都终止,并且工作线程数为0。
  • terminated:在terminated()方法执行完,线程池就进入这个状态,标识线程池终结了,关闭了。

\

可以通过调用线程池的shutdown()和shutdownNow()方法来关闭线程池,原理是便利整个线程池中的工作线程,然后for循环逐步调用interrupt()方法来中断线程。

shutdown():把线程池状态设置为shutdown状态,不再接受新的任务,只处理剩下的任务,类似于肯德基外卖,超过10点之后就不支持下单了,但是可以处理再9:55下单之后的外卖单。

shutdownNow():把线程池状态设置为stop状态,不再接受新的任务,也不处理剩下的任务,类似于女朋友生气,家里的饭也不吃了,下单的外卖在路上也不吃了。

\

图片

**
**核心参数

下面两张图配合着看。\

图片简述:新任务来了,先交给核心线程处理,当核心线程处理不过来,来了超量任务1,先放到任务队列中,然后核心线程处理完后,依次去拉取任务处理,如果超量任务2来了同时任务队列满了,则创建一些普通线程取处理,如果超量任务3来了,只能采取拒绝策略。图片公司有正式员工和兼职员工,正常任务都是正式员工来处理的,当任务增加时,正式员工忙不过来,公司可以先把任务存储到仓库中,等正式员工闲下来了,领导调度正式员工来处理,当仓库满了,任务还在巨增,则需要招聘兼职员工来处理,如果兼职员工也干不完,则只能采取拒绝任务了。当兼职员工干完了,就要辞退了。
正式员工:核心线程数。
兼职员工:普通线程数。领导:线程池中的getTask()方法。
任务:runable对象。
拒绝策略:对应线程池中的四种拒绝策略。仓库:对应线程池中的队列。辞退兼职员工:过了存活的的生命周期。
\

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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
  • corePoolSize:核心线程数量,当提交一个任务到线程池中,线程池会创建一个线程来处理该任务,默认情况下,核心线程被创建后,会一直存在。如果调用了prestartAllCoreThread()方法,线程池会提前创建好所有的核心线程数。

\

  • maximumPoolSize:最大线程数量,线程池中能容量的总线程数,当达到该值时,后续的新任务都会被淘汰策略给抛弃掉。如果使用了无界阻塞队列,则使用该参数的意义不大。\

为什么意义不大呢?线程池中先创建核心线程数去处理任务,当核心线程都在处理任务时,新来的任务都会放在阻塞队列中,若阻塞队列的长度固定,一旦塞满,才会去创建普通线程来处理,此时无界阻塞队列的长度是Integer.MAX长度。几个亿的任务在队列中,系统早就OOM了。

  • keepAliveTime:线程存活的时间,默认是非核心线程数的存活时间,如果超过了此时间,则回收过期的非核心线程,如果将allowCoreThreadTimeOut设置为true,则核心线程也存在存活的时间。

\

  • millseconds:线程存活的时间单位,时/分/秒。

\

  • ThreadFactory:用来创建线程的工厂,可以给线程定义名称。

\

  • runableTaskQueue:线程池用到的阻塞队列,队列都具备先进先出的特点
    • ArrayBlockingQueue:是一个基于数组的数据结构的有界队列,尽量指定队列大小,防止队列中任务过多,造成OOM。
    • LinkedBlockingQueue:是一个基于链表的有界队列,长度规定Integer.MAX长度,已经很长了,固还是要指定队列大小,防止队列中的任务过多,造成OOM。
    • synchrnousQueue:一个不存储元素的阻塞队列,每次put操作必须要等另外一个线程take操作,否则插入会一直阻塞,即put和take操作要同时一起使用。
    • priorityBlockingQueue:一个具有优先级的无限阻塞队列,可以打破先进先出的规则,使用compareTo()方法来指定元素排序。
    • DelayQueue:一个可以实现延迟获取的无界阻塞队列,有点类似延时队列,在队列时,可以指定多久在队列中拿一次任务。
    • LinkedTransferQueue:一个由链表组成的无界阻塞队列,暂时未在任何对方见过,多了transfer()和tryTransfer()方法。
    • LinkedBlockingDeque:一个由链表组成的双向阻塞队列,队列头和队列尾都能添加和移除元素。

\

  • rejectHanlder:队列中任务满时,同时达到最大线程数量,新来的任务需要采取拒绝策略,线程池具备四种拒绝策略    
    • AbortPolicy:直接抛出RejectExecutionException异常,这是默认的拒绝策略,
    • discardPolicy:直接丢弃新来的任务,不抛异常。
    • discardOldestPocily:直接丢弃队列中的第一个任务,让新来的任务插队到第一个,
    • CallerRunsPolicy:使用主线程执行当前新来的任务。


****源码分析任务A先执行,首先会执行execute(Runable command) ,主要目的如下\

图片

 线程池中的线程执行任务由两种情况1:在execute中传入一个任务,会让一个线程去执行,具体方法是由addWorker封装任务。2:当第一步骤中的任务被核心线程处理完后,会尝试去队列中调用getTask()方法来执行队列中的任务。
execute(Runable command)

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);
}


线程池提交任务execute()和submit(),其实submit还是在方法内部调用execute()方法,只不过把结果封装到了future类型的对象中。\

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功,但可以看到抛出的异常
submit()方法用于提交有返回值的任务,线程池会把结果封装进Future对象中,异常信息也可以封装在里面,并且通过get()方法来获取返回值,get()方法会一直阻塞当前线程直到任务完成。


假设线程A,调用了addWorker()方法,fitstTask是传进来的任务,core为核心线程标识,将firstTask封装进Worker()类中,此处使用可重入锁ReentrantLock的目的是为了向集合Set中新增数据,同时设置线程值中的最大线程数,然后释放锁,当集合中的线程添加成功时,执行worker中的Thread的start(),开启线程后会调用runWork()方法。addWorker(Runnable firstTask,boolean core)

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {


//..

      //两个for循环目的是判断线程池的状态,同时通过cas算法将线程数量+1,然后跳出循环


}

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

****addWorker的目的是为了校验线程池的状态,如果校验通过,尝试封装任务进Worker类中,线程池是由HashSet所维护的。


**Worker()
**

实现了Runnable接口,同时继承了AQS框架,固Worker类也是一把锁

核心参数


/** Thread this worker is running in.  Null if factory fails. */


final Thread thread;





/** Initial task to run.  Possibly null. */
Runnable firstTask;

 

Worker(Runnable firstTask) {

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


}




public void run() {
    runWorker(this);
}

Worker主要是维护线程的中断状态,每次new一个Worker()对象时,线程都是通过线程工厂所创建的,可以理解为一个Worker类就对应一个线程。当firstTask为空时,线程会尝试去阻塞队列中拉取任务执行,通过getTask()方法去拉取。

\


runWorker(Worker worker)

如果任务为空,则当前线程尝试去阻塞队列中拉取任务执行,如果任务不为空,则优先处理当前线程手头的任务,不用去拉取阻塞队列中的任务。
最终执行run()方法,while循环中:
第一次:当任务不为空,则先执行任务。第二次:当任务执行完了,getTask()会尝试去阻塞队列中拉取任务,如果阻塞任务中没有任务,poll()方法会阻塞当前线程。

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 {
                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);
    }
}

\

getTask()

for循环调用:

1:获取线程池中的工作线程数,获取线程池的状态。\

2:判断线程池的状态是否合法,是否>=SHUTDOWN状态,阻塞队列是否为空,如果为true,则直接返回null,如果为false,则执行3.

3:从阻塞队列中获取任务,若是poll(),如果任务不为空,则直接返回该任务,如果任务为空,则阻塞线程,等待获取任务。

\

**processWorkerExit(Worker w,boolean completedAbruptly)
**

目的:尝试在线程池中删除线程,通过remove()方法去删除对应的线程,如果runWoker()中某个线程执行任务抛出了异常,要把对应的线程给remove掉,重新生成一个新的线程addWorker(null,false)。

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);
    }
}

问题1: 如何合理配置线程池参数?

先区分任务的性质:CPU密集型任务,IO密集型任务,混合型任务。

再区分任务的优先级:高,中,低。

再区分任务的执行时间:长,中,短。

任务是否存在第三方框架依赖:Redis依赖,Es依赖,数据库依赖。

如果是处理CPU密集型任务,应该把配置尽量配置小一点,这种一般会少IO交互,偏于计算能力比如设置n+1个线程,比CPU多一个线程是为了CPU处于空闲状态,多一个线程可以充分利用cpu的空闲时间,也是为了防止任务暂停带来的影响。

如果是处理IO密集型任务,这种一般是多I/O交互,依靠第三方框架,可以设置2n个线程。\

n是cpu个数,通过调用runtime.getRuntime()。availableProcess()方法来获取。这里设置的线程数都是核心线程数。

问题2:当线程池中的线程执行异常了代码,线程池如何处理? 若使用的是execute(),则线程直接抛出异常若使用的submit(),使用了setException(),把异常信息给封装了,调用get()并catch可以捕获到异常。遇见异常的线程都会在HashSet中给remove掉,同时生成一个新的线程,线程之间出现异常互不影响。
问题3:线程池中的线程何时被回收? 其实核心线程和非核心线程只是一种概念,执行execute()先创建的肯定是核心线程,当线程在getTask()时,队列中若不存在任务,会调用processWorkerExit()方法来删除集合中的当前Worker类。非核心线程回收:当超过了存活时间,一直都未获取到最新的任务,则会回收掉该线程,

假设核心线程数为2,最大线程数为3,普通线程存活时间为30s,链表阻塞队列长度为2。若每个任务执行1s,假设一开始就有五个任务,每个线程都持有任务,同时队列也塞满了,每隔三秒分发一个任务,先创建的是核心线程,当线程都处理完任务同时也处理了队列中的任务,3个空闲线程在接受每个三秒分发的任务时,是轮训的方式去处理任务,比如A处理完一次,下一个三秒就让B处理,下一次就让C给处理,是因为维护了一个条件等待队列,这样就导致普通线程永远都不会被回收。

问题4:线程池中有哪几把锁? 第一把锁ReentrantLock,主要是addWorker()方法中,锁线程池中最大的线程数和HashSet的修改,可能这里有人会问,HashSet是线程不安全的,为用锁ReentrantLock来保证线程安全,为什么不用JUC下的线程安全的Set呢?用ReentrantLock主要是为了避免中断风暴,即避免正在中断的线程又进行中断,如shutdown()方法中的interruptIdleWorkers()方法,如果不用ReentrantLock锁,无法保证串行执行,假设有五个线程都来并行调用interruptIdleWorkers()方法,每个线程都要对HashSet执行一次中断,带来的重复中断操作。第二把锁是Worker类,主要是继承了AQS,不允许重入,同时重写了lock和tryLock()方法。lock在runWorker()方法中用到,tryLock()在interruptIdleWorkers()中用到。

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(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();
    }
}