字节跳动秋招面经:后端开发工程师,已拿意向书
![894a3587da797eb4786f6094b3f083d9.png](https://img-blog.csdnimg.cn/img_convert/894a3587da797eb4786f6094b3f083d9.png)
01 什么是线程池
线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源
02 使用线程池的好处
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
- 对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现
03 线程池的主要组件
![730df69da9aad4793c8fb9755a2292dd.png](https://img-blog.csdnimg.cn/img_convert/730df69da9aad4793c8fb9755a2292dd.png)
一个线程池包括以下四个基本组成部分:
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务
- 工作线程(WorkThread):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制
04 ThreadPoolExecutor类
讲到线程池,要重点介绍java.uitl.concurrent.ThreadPoolExecutor类,ThreadPoolExecutor线程池中最核心的一个类,ThreadPoolExecutor在JDK中线程池常用类UML类关系图如下:
![2eee27c219d14e5ea564d48d351714c6.png](https://img-blog.csdnimg.cn/img_convert/2eee27c219d14e5ea564d48d351714c6.png)
我们可以通过ThreadPoolExecutor来创建一个线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
4.1 创建一个线程池需要输入几个参数
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
- maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
- RejectedExecutionHandler(拒绝策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n AbortPolicy:直接抛出异常。
- keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
4.2 向线程池提交任务
我们可以通过execute()或submit()两个方法向线程池提交任务,不过它们有所不同
- execute()方法没有返回值,所以无法判断任务知否被线程池执行成功
threadsPool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub }});
- submit()方法返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值
try { Object s = future.get(); } catch (InterruptedException e) { // 处理中断异常 } catch (ExecutionException e) { // 处理无法执行任务异常 } finally { // 关闭线程池 executor.shutdown();}
4.3 线程池的关闭
我们可以通过shutdown()或shutdownNow()方法来关闭线程池,不过它们也有所不同
- shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
4.4 ThreadPoolExecutor执行的策略
/** * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {@code RejectedExecutionHandler}. * * @param command the task to execute * @throws RejectedExecutionException at discretion of * {@code RejectedExecutionHandler}, if the task * cannot be accepted for execution * @throws NullPointerException if {@code command} is null */ 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. * 如果当前的线程数小于核心线程池的大小,根据现有的线程作为第一个Worker运行的线程, * 新建一个Worker,addWorker自动的检查当前线程池的状态和Worker的数量, * 防止线程池在不能添加线程的状态下添加线程 * * 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. * 如果线程入队成功,然后还是要进行double-check的,因为线程池在入队之后状态是可能会发生变化的 * * 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. * * 如果task不能入队(队列满了),这时候尝试增加一个新线程,如果增加失败那么当前的线程池状态变化了或者线程池已经满了 * 然后拒绝task */ int c = ctl.get(); //当前的Worker的数量小于核心线程池大小时,新建一个Worker。 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到BlockingQueue。 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command))//recheck防止线程池状态的突变,如果突变,那么将reject线程,防止workQueue中增加新线程 reject(command); else if (workerCountOf(recheck) == 0)//上下两个操作都有addWorker的操作,但是如果在workQueue.offer的时候Worker变为0, //那么将没有Worker执行新的task,所以增加一个Worker. addWorker(null, false); } //如果workQueue满了,那么这时候可能还没到线程池的maxnum,所以尝试增加一个Worker else if (!addWorker(command, false)) reject(command);//如果Worker数量到达上限,那么就拒绝此线程 }
![98fafa8d456b7844b35a82a2c8503d2f.png](https://img-blog.csdnimg.cn/img_convert/98fafa8d456b7844b35a82a2c8503d2f.png)
- 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
- 线程数量达到了corePools,则将任务移入队列等待
- 队列已满,新建线程(非核心线程)执行任务
- 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler)抛出异常
新建线程 -> 达到核心数 -> 加入队列 -> 新建线程(非核心) -> 达到最大数 -> 触发拒绝策略
4.5 四种拒绝策略
(1)AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); }
(2)DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
(3)DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } }
(4)CallerRunPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } }
05 Java通过Executors提供四种线程池
(1) CachedThreadPool():可缓存线程池
- 线程数无限制
- 有空闲线程则复用空闲线程,若无空闲线程则新建线程 一定程序减少频繁创建/销毁线程,减少系统开销
(2)FixedThreadPool():定长线程池
- 可控制线程最大并发数(同时执行的线程数)
- 超出的线程会在队列中等待
(3)ScheduledThreadPool():
- 定时线程池。
- 支持定时及周期性任务执行。
(4)SingleThreadExecutor():单线程化的线程池
- 有且仅有一个工作线程执行任务
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
5.1 newCachedThreadPool
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
public class ThreadPoolExecutorTest1 {public static void main(String[] args) {ExecutorService cachedThreadPool = Executors.newCachedThreadPool();for (int i = 0; i < 1000; i++) {final int index = i;try {Thread.sleep(index * 1000);} catch (Exception e) {e.printStackTrace();}cachedThreadPool.execute(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName()+":"+index);}});}}}
![7a03220adb154412c1df4f3d94d43f37.png](https://img-blog.csdnimg.cn/img_convert/7a03220adb154412c1df4f3d94d43f37.png)
5.2 newFixedThreadPool
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,指定线程池中的线程数量和最大线程数量一样,也就线程数量固定不变
示例代码如下
public class ThreadPoolExecutorTest {public static void main(String[] args) {ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);// 每隔两秒打印3个数for (int i = 0; i < 10; i++) {final int index = i;fixedThreadPool.execute(new Runnable() {public void run() {try {System.out.println(Thread.currentThread().getName()+":"+index);//三个线程并发Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});}}}
![f4f03ba2e1f8e131007d1bff93ba93b9.png](https://img-blog.csdnimg.cn/img_convert/f4f03ba2e1f8e131007d1bff93ba93b9.png)
5.3 newscheduledThreadPool
newscheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下.表示延迟1秒后每3秒执行一次
public class ThreadPoolExecutorTest3 {public static void main(String[] args) {ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);scheduledThreadPool.scheduleAtFixedRate(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + ": delay 1 seconds, and excute every 3 seconds");}}, 1, 3, TimeUnit.SECONDS);// 表示延迟1秒后每3秒执行一次}}
![a1270440486f01ed8b39d3133b75999b.png](https://img-blog.csdnimg.cn/img_convert/a1270440486f01ed8b39d3133b75999b.png)
5.4 newSingleThreadExecutor
newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
public class ThreadPoolExecutorTest4 {public static void main(String[] args) {ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {final int index = i;singleThreadExecutor.execute(new Runnable() {public void run() {try {System.out.println(Thread.currentThread().getName() + ":" + index);Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});}}}
结果依次输出,相当于顺序执行各个任务。使用JDK自带的监控工具来监控我们创建的线程数量,运行一个不终止的线程,创建指定量的线程,来观察
![5f07c87bb83eebcd01396d36b2c86b77.png](https://img-blog.csdnimg.cn/img_convert/5f07c87bb83eebcd01396d36b2c86b77.png)
06 线程池参数设置
参数的设置跟系统的负载有直接的关系,下面为系统负载的相关参数:
- tasks,每秒需要处理的的任务数(针对系统需求)
- threadtasks,每个线程每钞可处理任务数(针对线程本身)
- responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒。
6.1 corePoolSize
系统每秒有tasks个任务需要处理理,则每个线程每钞可处理threadtasks个任务。,则需要的线程数为:tasks/threadtasks,即tasks/threadtasks个线程数。
假设系统每秒任务数为100 ~ 1000,每个线程每钞可处理10个任务,则需要100 / 10至1000 / 10,即10 ~ 100个线程。那么corePoolSize应该设置为大于10,具体数字最好根据8020原则,因为系统每秒任务数为100 ~ 1000,即80%情况下系统每秒任务数小于1000 * 20% = 200,则corePoolSize可设置为200 / 10 = 20。
6.2 queueCapacity
任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为 所有核心线程每秒处理任务数 * 每个任务响应时间 = 每秒任务总响应时间 ,即(corePoolSize*threadtasks)responsetime: (2010)*2=400,即队列长度可设置为400
6.3 maxPoolSize
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(tasks - queueCapacity)/ threadtasks 即(1000-400)/10,即60个线程,可将maxPoolSize设置为60。
队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡
6.4 keepAliveTime
当负载降低时,可减少线程数量,当线程的空闲时间超过keepAliveTime,会自动释放线程资源。默认情况下线程池停止多余的线程并最少会保持corePoolSize个线程
6.5 allowCoreThreadTimeout
默认情况下核心线程不会退出,可通过将该参数设置为true,让核心线程也退出。
07 线程池的五种状态
![c02a72ef46aaa5e882f252b67cc74796.png](https://img-blog.csdnimg.cn/img_convert/c02a72ef46aaa5e882f252b67cc74796.png)
- 线程池的初始化状态是RUNNING,能够接收新任务,以及对已添加的任务进行处理。
- 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
- 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
- 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。 线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
08 关闭线程池
线程池提供两种关闭线程池方法:shutDown()和shutdownNow()
8.1 shutDown()
当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
8.2 shutdownNow()
根据JDK文档描述,大致意思是:执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
09 读者福利
读到这里的朋友还可以免费领取一份我自己的秋招复习资料(Java岗),下面给大家展示一下整理的部分秋招复习资料,有需要的小伙伴可以转发+私信关键词【资料】免费领取哦~
MySQL20个必备面试题(含解析)+21条MySQL性能调优实践
![b9c85e6fb5169ddfb1a6f15350ec803a.png](https://img-blog.csdnimg.cn/img_convert/b9c85e6fb5169ddfb1a6f15350ec803a.png)
面试专题(含详细解答)+Java学习书籍资料
![cb17ceedeaa4e06fb6cfe904e1b05347.png](https://img-blog.csdnimg.cn/img_convert/cb17ceedeaa4e06fb6cfe904e1b05347.png)
我的Redis学习笔记+学习思维脑图(docker,jvm,java,mybatis,redis,mysql,spring,分布式,微服务,算法,设计模式)
![5df0cda8efccc93dcff328799b82c323.png](https://img-blog.csdnimg.cn/img_convert/5df0cda8efccc93dcff328799b82c323.png)