Skip to content

Commit ad96be3

Browse files
author
JasonLou
committed
update
1 parent 16bd8f2 commit ad96be3

19 files changed

Lines changed: 1567 additions & 27 deletions

File tree

‎Java应届生面试突击/Java基础/05_Java基础专题.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ System.out.println((a == b));
3838
java 中 String 的+运算符编译后其实是转换成了这样的代码:
3939
3. String b = new StringBuilder().append("a").append(bb).toString();
4040
其中,StringBuilder 的 toString 方法,使用 new String(...);来构造一个 String 对象。
41+
4142
public String toString() { // StringBuilder 的 toString 方法
4243
// Create a copy, don't share the array
4344
return new String(value, 0, count);

‎Java应届生面试突击/多线程和并发/01_多线程和并发专题.md‎

Lines changed: 398 additions & 12 deletions
Large diffs are not rendered by default.

‎Java应届生面试突击/多线程和并发/02_线程池的问题.md‎

Lines changed: 299 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,305 @@
1010

1111

1212
### <a name="1">1. 什么是线程池?线程池的工作原理?</a>
13-
### <a name="2">2. 使用线程池的好处?</a>
14-
### <a name="3">3. 线程池的注意事项。</a>
15-
### <a name="4">4. 简单线程池的设计。</a>
13+
&ensp;&ensp;&ensp;&ensp;
14+
一个线程池管理了一组***工作线程***,同时它还包括了一个用于放置等待执行
15+
任务的任务队列(阻塞队列)。 <br>
16+
&ensp;&ensp;&ensp;&ensp;
17+
默认情况下,在创建了线程池后,线程池中的线程数为 ***0***。当***任务提交给
18+
线程池之后的处理策略如下***: <br>
19+
&ensp;&ensp;&ensp;&ensp;
20+
1. 如果此时线程池中的数量***小于 corePoolSize(核心池的大小)***,即
21+
使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务(***也
22+
就是每来一个任务,就要创建一个线程来执行任务***)。 <br>
23+
&ensp;&ensp;&ensp;&ensp;
24+
2. 如果此时线程池中的数量***大于等于 corePoolSize***,但是***缓冲队列
25+
workQueue 未满***,那么任务被放入缓冲队列,则该任务会等待空闲线程将其
26+
取出去执行。 <br>
27+
&ensp;&ensp;&ensp;&ensp;
28+
3. 如果此时线程池中的数量***大于等于 corePoolSize***, ***缓冲队列
29+
workQueue 满***,并且线程池中的数量***小于 maximumPoolSize(线程池
30+
最大线程数)***,建新的线程来处理被添加的任务。 <br>
31+
&ensp;&ensp;&ensp;&ensp;
32+
4. 如果此时线程池中的数量***大于等于 corePoolSize , 缓冲队列
33+
workQueue 满***,并且线程池中的数量***等于 maximumPoolSize***,那么通
34+
过 RejectedExecutionHandler 所指定的策略(***任务拒绝策略***)来处理此任务。
35+
也就是处理任务的优先级为: ***核 心 线 程 corePoolSize 、 任 务 队 列
36+
workQueue、最大线程 maximumPoolSize,如果三者都满了,使用
37+
handler 处理被拒绝的任务***。 <br>
38+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
48+
1.通过重复利用已创建的线程,***减少在创建和销毁线程上***所花的时间以及***系
49+
统资源的开销***。 <br>
50+
&ensp;&ensp;&ensp;&ensp;
51+
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就可以立即
52+
执行。 <br>
53+
&ensp;&ensp;&ensp;&ensp;
54+
3.提高线程的可管理性。使用线程池可以对线程进行统一的分配和监控。 <br>
55+
&ensp;&ensp;&ensp;&ensp;
56+
4.***如果不使用线程池***,有可能造成系统创建大量线程而***导致消耗完系统内
57+
存***。 <br>
58+
59+
60+
#### 对于原理,有几个接口和类值得我们关注:
61+
&ensp;&ensp;&ensp;&ensp;
62+
***Executor 接口*** <br>
63+
&ensp;&ensp;&ensp;&ensp;
64+
***Executors 类*** <br>
65+
&ensp;&ensp;&ensp;&ensp;
66+
***ExecutorService 接口*** <br>
67+
&ensp;&ensp;&ensp;&ensp;
68+
***AbstractExecutorService 抽象类*** <br>
69+
&ensp;&ensp;&ensp;&ensp;
70+
***ThreadPoolExecutor 类*** <br>
71+
72+
&ensp;&ensp;&ensp;&ensp;
73+
Executor 是一个顶层接口,在它里面只声明了一个方法 execute(Runnable),返回
74+
值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的; <br>
75+
&ensp;&ensp;&ensp;&ensp;
76+
然后 ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、
77+
invokeAll、invokeAny 以及 shutDown 等; <br>
78+
&ensp;&ensp;&ensp;&ensp;
79+
抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了
80+
ExecutorService 中声明的所有方法; <br>
81+
&ensp;&ensp;&ensp;&ensp;
82+
然后 ThreadPoolExecutor 继承了类 AbstractExecutorService。
83+
public static ScheduledExecutorService ***newScheduledThreadPool***(int
84+
corePoolSize) <br>
85+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
146+
***1. newSingleThreadExecutor*** <br>
147+
&ensp;&ensp;&ensp;&ensp;
148+
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串
149+
行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
150+
此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 <br>
151+
&ensp;&ensp;&ensp;&ensp;
152+
***2.newFixedThreadPool*** <br>
153+
&ensp;&ensp;&ensp;&ensp;
154+
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的
155+
最大大小。线程池的大小一旦达到最大值就会保持不变,***如果某个线程因为执行异常而结
156+
束,那么线程池会补充一个新线程***。 <br>
157+
&ensp;&ensp;&ensp;&ensp;
158+
***3. newCachedThreadPool*** <br>
159+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
177+
不过在 java doc 中,并***不提倡我们直接使用 ThreadPoolExecutor 创
178+
建线程池,而是使用 Executors 类中提供的几个静态方法来创建线程池***。 <br>
179+
180+
下面解释一下构造器中各个参数的含义: <br>
181+
&ensp;&ensp;&ensp;&ensp;
182+
***corePoolSize***:核心池的大小。默认情况下,在创建了线程池后,线程池中
183+
的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中
184+
的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中。 <br>
185+
&ensp;&ensp;&ensp;&ensp;
186+
***maximumPoolSize***:线程池最大线程数,它表示在线程池中最多能创建多
187+
少个线程。 <br>
188+
&ensp;&ensp;&ensp;&ensp;
189+
***keepAliveTime***:默认情况下,只有当线程池中的线程数大于 corePoolSize
190+
时, keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,
191+
即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到
192+
keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。 <br>
193+
&ensp;&ensp;&ensp;&ensp;
194+
***unit***:参数 keepAliveTime 的时间单位。
195+
&ensp;&ensp;&ensp;&ensp;
196+
***workQueue***:一个阻塞队列,任务缓存队列,即 workQueue,它用来存
197+
放等待执行的任务。 <br>
198+
&ensp;&ensp;&ensp;&ensp;
199+
workQueue 的类型为 BlockingQueue<Runnable>,通常可以取下面三种类型: <br>
200+
&ensp;&ensp;&ensp;&ensp;
201+
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小; <br>
202+
&ensp;&ensp;&ensp;&ensp;
203+
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列
204+
大小,则默认为 Integer.MAX_VALUE; <br>
205+
&ensp;&ensp;&ensp;&ensp;
206+
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接
207+
新建一个线程来执行新来的任务。在某次添加元素后必须等待其他线程取走后才能继续添
208+
加。 <br>
209+
&ensp;&ensp;&ensp;&ensp;
210+
ThreadFactory:线程工厂,主要用来创建线程。 <br>
211+
&ensp;&ensp;&ensp;&ensp;
212+
handler:表示当拒绝处理任务时的策略,有以下***四种取值***: <br>
213+
&ensp;&ensp;&ensp;&ensp;
214+
1.***AbortPolicy***:直接抛出异常(默认的) <br>
215+
&ensp;&ensp;&ensp;&ensp;
216+
2.***DiscardPolicy***:直接丢弃任务 <br>
217+
&ensp;&ensp;&ensp;&ensp;
218+
3.***DiscardOldestPolicy***:丢弃队列中最旧(队头)的任务,并执行当前
219+
任务 <br>
220+
&ensp;&ensp;&ensp;&ensp;
221+
4.***CallerRunsPolicy***:不用线程池中的线程执行,用调用者所在线程执
222+
行。 <br>
223+
&ensp;&ensp;&ensp;&ensp;
224+
在 ThreadPoolExecutor 类中有几个非常重要的方法: <br>
225+
&ensp;&ensp;&ensp;&ensp;
226+
***execute() <br>
227+
&ensp;&ensp;&ensp;&ensp;
228+
submit() <br>
229+
&ensp;&ensp;&ensp;&ensp;
230+
shutdown() <br>
231+
&ensp;&ensp;&ensp;&ensp;
232+
shutdownNow()*** <br>
233+
234+
235+
###### execute 和 submit 区别:
236+
&ensp;&ensp;&ensp;&ensp;
237+
submit 有返回值,execute 没有返回值。 所以说可以根据任务有无返回
238+
值选择对应的方法。 <br>
239+
&ensp;&ensp;&ensp;&ensp;
240+
*** submit 方便异常的处理。 如果任务可能会抛出异常,而且希望外面的调
241+
用者能够感知这些异常,那么就需要调用 submit 方法,通过捕获 Future.get
242+
抛出的异常。*** <br>
243+
244+
245+
###### shutdown()和 shutdownNow()的区别:
246+
&ensp;&ensp;&ensp;&ensp;
247+
shutdown()和 shutdownNow()是用来关闭线程池的。 <br>
248+
&ensp;&ensp;&ensp;&ensp;
249+
shutdown 方法:此方法执行后不得向线程池再提交任务,如果有空闲线
250+
程则销毁空闲线程,等待所有正在执行的任务及位于阻塞队列中的任务执行结
251+
束,然后销毁所有线程。 <br>
252+
&ensp;&ensp;&ensp;&ensp;
253+
shutdownNow 方法:此方法执行后不得向线程池再提交任务,如果有空
254+
闲线程则销毁空闲线程,取消所有位于阻塞队列中的任务,并将其放入
255+
List<Runnable>容器,作为返回值。取消正在执行的线程(实际上仅仅是设
256+
置正在执行线程的中断标志位,调用线程的 interrupt 方法来中断线程)。
257+
258+
#### 线程池的注意事项
259+
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 <br>
260+
&ensp;&ensp;&ensp;&ensp;
261+
(1)***线程池的大小***。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及
262+
应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与 CPU
263+
数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可
264+
采用自适应算法来动态调整线程池的大小,以提高 CPU 的有效利用率和系统的整体性能。 <br>
265+
&ensp;&ensp;&ensp;&ensp;
266+
(2)***并发错误***。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意***避
267+
免死锁***现象的发生。 <br>
268+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
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+
&ensp;&ensp;&ensp;&ensp;
294+
***如何合理的配置 java 线程池?如 CPU 密集型的任务,基本线程池
295+
应该配置多大?IO 密集型的任务,基本线程池应该配置多大?用有界
296+
队列好还是无界队列好?任务非常多的时候,使用什么阻塞队列能获取
297+
最好的吞吐量?*** <br>
298+
299+
##### 答:
300+
&ensp;&ensp;&ensp;&ensp;
301+
1)配置线程池时,CPU 密集型任务可以少配置线程数,大概和机器的 cpu 核数相
302+
当,可以使得每个线程都在执行任务。 <br>
303+
&ensp;&ensp;&ensp;&ensp;
304+
2)IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽
305+
可能多的线程,2*cpu 核数。 <br>
306+
&ensp;&ensp;&ensp;&ensp;
307+
3)有界队列和无界队列的配置需区分业务场景,一般情况下配置有界队列,在一些
308+
可能会有爆发性增长的情况下使用无界队列。 <br>
309+
&ensp;&ensp;&ensp;&ensp;
310+
4)任务非常多时,使用非阻塞队列使用 CAS 操作替代锁可以获得好的吞吐量。
311+
synchronousQueue 吞吐率最高。 <br>
16312

17313

18314
---

0 commit comments

Comments
 (0)