|
10 | 10 |
|
11 | 11 |
|
12 | 12 | ### <a name="1">1. 什么是线程池?线程池的工作原理?</a> |
13 | | -### <a name="2">2. 使用线程池的好处?</a> |
14 | | -### <a name="3">3. 线程池的注意事项。</a> |
15 | | -### <a name="4">4. 简单线程池的设计。</a> |
| 13 | +     |
| 14 | + 一个线程池管理了一组***工作线程***,同时它还包括了一个用于放置等待执行 |
| 15 | +任务的任务队列(阻塞队列)。 <br> |
| 16 | +     |
| 17 | + 默认情况下,在创建了线程池后,线程池中的线程数为 ***0***。当***任务提交给 |
| 18 | +线程池之后的处理策略如下***: <br> |
| 19 | +     |
| 20 | + 1. 如果此时线程池中的数量***小于 corePoolSize(核心池的大小)***,即 |
| 21 | +使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(***也 |
| 22 | +就是每来一个任务,就要创建一个线程来执行任务***)。 <br> |
| 23 | +     |
| 24 | + 2. 如果此时线程池中的数量***大于等于 corePoolSize***,但是***缓冲队列 |
| 25 | +workQueue 未满***,那么任务被放入缓冲队列,则该任务会等待空闲线程将其 |
| 26 | +取出去执行。 <br> |
| 27 | +     |
| 28 | + 3. 如果此时线程池中的数量***大于等于 corePoolSize***, ***缓冲队列 |
| 29 | +workQueue 满***,并且线程池中的数量***小于 maximumPoolSize(线程池 |
| 30 | +最大线程数)***,建新的线程来处理被添加的任务。 <br> |
| 31 | +     |
| 32 | + 4. 如果此时线程池中的数量***大于等于 corePoolSize , 缓冲队列 |
| 33 | +workQueue 满***,并且线程池中的数量***等于 maximumPoolSize***,那么通 |
| 34 | +过 RejectedExecutionHandler 所指定的策略(***任务拒绝策略***)来处理此任务。 |
| 35 | +也就是处理任务的优先级为: ***核 心 线 程 corePoolSize 、 任 务 队 列 |
| 36 | +workQueue、最大线程 maximumPoolSize,如果三者都满了,使用 |
| 37 | +handler 处理被拒绝的任务***。 <br> |
| 38 | +     |
| 39 | + ***5. 特别注意,在 corePoolSize 和 maximumPoolSize 之间的线程 |
| 40 | +数会被自动释放。当线程池中线程数量大于 corePoolSize 时,如果某线程 |
| 41 | +空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不 |
| 42 | +大于 corePoolSize。这样,线程池可以动态的调整池中的线程数。*** <br> |
| 43 | +[02_1_1](/data/images/Java应届生面试突击/多线程和并发/02_1_1.png) <br> |
| 44 | + |
| 45 | + |
| 46 | +#### 使用线程池的好处: |
| 47 | +     |
| 48 | + 1.通过重复利用已创建的线程,***减少在创建和销毁线程上***所花的时间以及***系 |
| 49 | +统资源的开销***。 <br> |
| 50 | +     |
| 51 | + 2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即 |
| 52 | +执行。 <br> |
| 53 | +     |
| 54 | + 3.提高线程的可管理性。使用线程池可以对线程进行统一的分配和监控。 <br> |
| 55 | +     |
| 56 | + 4.***如果不使用线程池***,有可能造成系统创建大量线程而***导致消耗完系统内 |
| 57 | +存***。 <br> |
| 58 | + |
| 59 | + |
| 60 | +#### 对于原理,有几个接口和类值得我们关注: |
| 61 | +     |
| 62 | + ***Executor 接口*** <br> |
| 63 | +     |
| 64 | + ***Executors 类*** <br> |
| 65 | +     |
| 66 | + ***ExecutorService 接口*** <br> |
| 67 | +     |
| 68 | + ***AbstractExecutorService 抽象类*** <br> |
| 69 | +     |
| 70 | + ***ThreadPoolExecutor 类*** <br> |
| 71 | + |
| 72 | +     |
| 73 | + Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回 |
| 74 | +值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的; <br> |
| 75 | +     |
| 76 | + 然后 ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、 |
| 77 | +invokeAll、invokeAny 以及 shutDown 等; <br> |
| 78 | +     |
| 79 | + 抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了 |
| 80 | +ExecutorService 中声明的所有方法; <br> |
| 81 | +     |
| 82 | + 然后 ThreadPoolExecutor 继承了类 AbstractExecutorService。 |
| 83 | +public static ScheduledExecutorService ***newScheduledThreadPool***(int |
| 84 | +corePoolSize) <br> |
| 85 | +     |
| 86 | + 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代 Timer 类。 <br> |
| 87 | +[02_1_2](/data/images/Java应届生面试突击/多线程和并发/02_1_2.png) <br> |
| 88 | + |
| 89 | + |
| 90 | + |
| 91 | +##### Executor 接口 |
| 92 | +public interface Executor { |
| 93 | +void execute(Runnable command); |
| 94 | +} |
| 95 | +     |
| 96 | + Executor 接口只有一个方法 execute(),并且需要传入一个 Runnable |
| 97 | +类型的参数。那么它的作用自然是 具体的执行参数传入的任务。 <br> |
| 98 | + |
| 99 | +##### ExecutorService 接口 |
| 100 | +```java |
| 101 | +public interface ExecutorService extends Executor { |
| 102 | + void shutdown(); |
| 103 | + List<Runnable> shutdownNow(); |
| 104 | + boolean isShutdown(); |
| 105 | + <T> Future<T> submit(Callable<T> task); |
| 106 | + <T> Future<T> submit(Runnable task, T result); |
| 107 | + Future<?> submit(Runnable task); |
| 108 | + <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) |
| 109 | + throws InterruptedException; |
| 110 | + ...... |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | + |
| 115 | +##### Executors 类: |
| 116 | + |
| 117 | +它主要用来创建线程池。 |
| 118 | + |
| 119 | +Executors.newSingleThreadExecutor(); //创建容量为 1 的缓冲池 |
| 120 | +Executors.newFixedThreadPool(int n); //创建固定容量大小的缓冲池 |
| 121 | +Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池最大容量为Integer.MAX_VALUE(无界线程池) |
| 122 | + |
| 123 | +下面是这三个静态方法的具体实现: |
| 124 | +```java |
| 125 | +public static ExecutorService newSingleThreadExecutor() { |
| 126 | + return new FinalizableDelegatedExecutorService |
| 127 | + (new ThreadPoolExecutor(1, 1, |
| 128 | + 0L, TimeUnit.MILLISECONDS, |
| 129 | + new LinkedBlockingQueue<Runnable>())); |
| 130 | +} |
| 131 | + |
| 132 | +public static ExecutorService newFixedThreadPool(int nThreads) { |
| 133 | + return new ThreadPoolExecutor(nThreads, nThreads, |
| 134 | + 0L, TimeUnit.MILLISECONDS, |
| 135 | + new LinkedBlockingQueue<Runnable>()); |
| 136 | +} |
| 137 | + |
| 138 | +public static ExecutorService newCachedThreadPool() { |
| 139 | + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, |
| 140 | + 60L, TimeUnit.SECONDS, |
| 141 | + new SynchronousQueue<Runnable>()); |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +     |
| 146 | + ***1. newSingleThreadExecutor*** <br> |
| 147 | +     |
| 148 | + 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串 |
| 149 | +行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。 |
| 150 | +此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 <br> |
| 151 | +     |
| 152 | + ***2.newFixedThreadPool*** <br> |
| 153 | +     |
| 154 | + 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的 |
| 155 | +最大大小。线程池的大小一旦达到最大值就会保持不变,***如果某个线程因为执行异常而结 |
| 156 | +束,那么线程池会补充一个新线程***。 <br> |
| 157 | +     |
| 158 | + ***3. newCachedThreadPool*** <br> |
| 159 | +     |
| 160 | + 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就 |
| 161 | +会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添 |
| 162 | +加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系 |
| 163 | +统(或者说 JVM)能够创建的最大线程大小。 |
| 164 | + |
| 165 | +##### ThreadPoolExecutor 类 |
| 166 | +[02_1_3](/data/images/Java应届生面试突击/多线程和并发/02_1_3.png) <br> |
| 167 | + |
| 168 | + |
| 169 | +在 ThreadPoolExecutor 类中提供了四个构造方法: <br> |
| 170 | +```java |
| 171 | +public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit, |
| 172 | + BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler); |
| 173 | + |
| 174 | +``` |
| 175 | + |
| 176 | +     |
| 177 | + 不过在 java doc 中,并***不提倡我们直接使用 ThreadPoolExecutor 创 |
| 178 | +建线程池,而是使用 Executors 类中提供的几个静态方法来创建线程池***。 <br> |
| 179 | + |
| 180 | +下面解释一下构造器中各个参数的含义: <br> |
| 181 | +     |
| 182 | + ***corePoolSize***:核心池的大小。默认情况下,在创建了线程池后,线程池中 |
| 183 | +的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中 |
| 184 | +的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。 <br> |
| 185 | +     |
| 186 | + ***maximumPoolSize***:线程池最大线程数,它表示在线程池中最多能创建多 |
| 187 | +少个线程。 <br> |
| 188 | +     |
| 189 | + ***keepAliveTime***:默认情况下,只有当线程池中的线程数大于 corePoolSize |
| 190 | +时, keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize, |
| 191 | +即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 |
| 192 | +keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。 <br> |
| 193 | +     |
| 194 | + ***unit***:参数 keepAliveTime 的时间单位。 |
| 195 | +     |
| 196 | + ***workQueue***:一个阻塞队列,任务缓存队列,即 workQueue,它用来存 |
| 197 | +放等待执行的任务。 <br> |
| 198 | +     |
| 199 | + workQueue 的类型为 BlockingQueue<Runnable>,通常可以取下面三种类型: <br> |
| 200 | +     |
| 201 | + 1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小; <br> |
| 202 | +     |
| 203 | + 2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列 |
| 204 | +大小,则默认为 Integer.MAX_VALUE; <br> |
| 205 | +     |
| 206 | + 3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接 |
| 207 | +新建一个线程来执行新来的任务。在某次添加元素后必须等待其他线程取走后才能继续添 |
| 208 | +加。 <br> |
| 209 | +     |
| 210 | + ThreadFactory:线程工厂,主要用来创建线程。 <br> |
| 211 | +     |
| 212 | + handler:表示当拒绝处理任务时的策略,有以下***四种取值***: <br> |
| 213 | +     |
| 214 | + 1.***AbortPolicy***:直接抛出异常(默认的) <br> |
| 215 | +     |
| 216 | + 2.***DiscardPolicy***:直接丢弃任务 <br> |
| 217 | +     |
| 218 | + 3.***DiscardOldestPolicy***:丢弃队列中最旧(队头)的任务,并执行当前 |
| 219 | +任务 <br> |
| 220 | +     |
| 221 | + 4.***CallerRunsPolicy***:不用线程池中的线程执行,用调用者所在线程执 |
| 222 | +行。 <br> |
| 223 | +     |
| 224 | + 在 ThreadPoolExecutor 类中有几个非常重要的方法: <br> |
| 225 | +     |
| 226 | + ***execute() <br> |
| 227 | +     |
| 228 | + submit() <br> |
| 229 | +     |
| 230 | + shutdown() <br> |
| 231 | +     |
| 232 | + shutdownNow()*** <br> |
| 233 | + |
| 234 | + |
| 235 | +###### execute 和 submit 区别: |
| 236 | +     |
| 237 | + submit 有返回值,execute 没有返回值。 所以说可以根据任务有无返回 |
| 238 | +值选择对应的方法。 <br> |
| 239 | +     |
| 240 | + *** submit 方便异常的处理。 如果任务可能会抛出异常,而且希望外面的调 |
| 241 | +用者能够感知这些异常,那么就需要调用 submit 方法,通过捕获 Future.get |
| 242 | +抛出的异常。*** <br> |
| 243 | + |
| 244 | + |
| 245 | +###### shutdown()和 shutdownNow()的区别: |
| 246 | +     |
| 247 | + shutdown()和 shutdownNow()是用来关闭线程池的。 <br> |
| 248 | +     |
| 249 | + shutdown 方法:此方法执行后不得向线程池再提交任务,如果有空闲线 |
| 250 | +程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结 |
| 251 | +束,然后销毁所有线程。 <br> |
| 252 | +     |
| 253 | + shutdownNow 方法:此方法执行后不得向线程池再提交任务,如果有空 |
| 254 | +闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入 |
| 255 | +List<Runnable>容器,作为返回值。取消正在执行的线程(实际上仅仅是设 |
| 256 | +置正在执行线程的中断标志位,调用线程的 interrupt 方法来中断线程)。 |
| 257 | + |
| 258 | +#### 线程池的注意事项 |
| 259 | +虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 <br> |
| 260 | +     |
| 261 | + (1)***线程池的大小***。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及 |
| 262 | +应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与 CPU |
| 263 | +数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可 |
| 264 | +采用自适应算法来动态调整线程池的大小,以提高 CPU 的有效利用率和系统的整体性能。 <br> |
| 265 | +     |
| 266 | + (2)***并发错误***。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意***避 |
| 267 | +免死锁***现象的发生。 <br> |
| 268 | +     |
| 269 | + (3)***线程泄漏***。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中 |
| 270 | +就会发生线程泄漏现象。 <br> |
| 271 | + |
| 272 | +#### 简单线程池的设计 |
| 273 | +一个典型的线程池,应该包括如下几个部分: <br> |
| 274 | +1、线程池管理器(ThreadPool),用于启动、停用,管理线程池 <br> |
| 275 | +2、工作线程(WorkThread),线程池中的线程 <br> |
| 276 | +3、***请求接口***(WorkRequest),***创建请求对象***,以供工作线程调度任务的执行 <br> |
| 277 | +4、***请求队列***(RequestQueue),用于存放和提取请求 <br> |
| 278 | +5、***结果队列***(ResultQueue),用于存储请求执行后返回的结果 <br> |
| 279 | + |
| 280 | +     |
| 281 | + 线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue) |
| 282 | +添加请求,***这些请求事先需要实现请求接口***,即传递工作函数、参数、结果处理函数、以 |
| 283 | +及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请 |
| 284 | +求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池 |
| 285 | +管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调 |
| 286 | +用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结 |
| 287 | +果队列,工作线程通过轮询 requestQueue 获得任务,主线程通过查看结果队列,获得执 |
| 288 | +行结果。因此,***对这个队列的设计,要实现线程同步***,以及一定阻塞和超时机制的设计, |
| 289 | +以防止因为不断轮询而导致的过多 cpu 开销。 <br> |
| 290 | +[02_1_4](/data/images/Java应届生面试突击/多线程和并发/02_1_4.png) <br> |
| 291 | + |
| 292 | +##### 问: |
| 293 | +     |
| 294 | + ***如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池 |
| 295 | +应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界 |
| 296 | +队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取 |
| 297 | +最好的吞吐量?*** <br> |
| 298 | + |
| 299 | +##### 答: |
| 300 | +     |
| 301 | + 1)配置线程池时,CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相 |
| 302 | +当,可以使得每个线程都在执行任务。 <br> |
| 303 | +     |
| 304 | + 2)IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽 |
| 305 | +可能多的线程,2*cpu 核数。 <br> |
| 306 | +     |
| 307 | + 3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些 |
| 308 | +可能会有爆发性增长的情况下使用无界队列。 <br> |
| 309 | +     |
| 310 | + 4)任务非常多时,使用非阻塞队列使用 CAS 操作替代锁可以获得好的吞吐量。 |
| 311 | +synchronousQueue 吞吐率最高。 <br> |
16 | 312 |
|
17 | 313 |
|
18 | 314 | --- |
|
0 commit comments