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

大纲内容

概念

生命周期
核心参数
源码分析
  • execute(Runable command)
  • addWorker(Runable firstTask,boolean core)
    runWorker(this)
    getTask()
    processWorkerExit(Work w,boolean co m)
问题1: 如何合理配置线程池参数?
问题2:当线程池中的线程执行异常了代码,线程池如何处理?
问题3:线程池中的线程何时被回收?
问题4:线程池中有哪几把锁?

在这里插入图片描述

详细文案可以看下链接, 关注一波, 后期分享经典面试题
并发(五):细讲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 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© < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning© && 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();
}
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值