Skip to content

Commit 7133d65

Browse files
authored
Update 多线程面试题.md
1 parent fc50c9a commit 7133d65

File tree

1 file changed

+161
-5
lines changed

1 file changed

+161
-5
lines changed

docs/多线程面试题.md

Lines changed: 161 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
## 线程和进程的区别?
44

55
* 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。
6-
* 进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
7-
* 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。
6+
* 进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
7+
* 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。
88

99
## 创建线程有哪几种方式,如何实现?
1010

@@ -100,8 +100,8 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行
100100
* 结束(Terminated):已终止线程的状态,线程已经结束执行
101101

102102
## 线程的各种状态的切换(重要)
103-
<div align="center"> <img src="https://github.com/lvminghui/Java-Notes/blob/master/docs/imgs/线程状态切换.png"/> </div><br>
104103

104+
<div align="center"> <img src="https://github.com/lvminghui/Java-Notes/blob/master/docs/imgs/线程状态切换.png"/> </div><br>
105105

106106
1. 得到一个线程类,new出一个实例线程就进入new状态(新建状态)。
107107
2. 调用start方法就进入Runnable(可运行状态)
@@ -112,6 +112,10 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行
112112
7. 处于Running状态的线程如果调用了wait就会进入等待池,在等待池的线程如果等待时间到或者调用notify方法就会进入锁池。
113113
8. 处于Running状态的线程方法执行完毕或者异常退出就会进入死亡状态。
114114

115+
## 有哪几种实现生产者消费者模式的方法?
116+
117+
118+
115119
## 什么是上下文切换?
116120

117121
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式(程序计数器)。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
@@ -120,10 +124,10 @@ start()方法来启动一个线程,这时无需等待run方法体代码执行
120124

121125
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
122126

123-
## 创建线程池有哪几种方式
127+
## 创建线程池ThreadPoolExecutor有哪几种方式
124128

125129
①. newFixedThreadPool(int nThreads)
126-
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程
130+
创建一个固定线程数量的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,新的任务会暂存在任务队列中,待有线程空闲时便处理任务
127131

128132
②. newCachedThreadPool()
129133
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
@@ -142,11 +146,34 @@ STOP :不再接受新提交的任务,也不处理存量任务
142146
TIDYING :所有的任务都已终止
143147
TERMINATED : 结束方法terminated()执行完后进入该状态
144148

149+
## 线程池核心参数⭐
150+
151+
* **corePoolSize**:线程池里的线程数量,核心线程池大小
152+
153+
* **maxPoolSize**:线程池里的最大线程数量
154+
155+
* **workQueue**: 任务队列,用于存放提交但是尚未被执行的任务。
156+
157+
* keepAliveTime:当线程池中的线程数量大于 corePoolSize 时,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁; 参数的时间单位为 unit。
158+
* threadFactory:线程工厂,用于创建线程,一般可以用默认的
159+
* handler:拒绝策略
160+
145161
## 执行execute()方法和submit()方法的区别是什么呢?
146162

147163
1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
148164
2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future``get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
149165

166+
## 使用线程池比手动创建线程好在哪里?
167+
168+
169+
170+
## 线程池常用的阻塞队列?
171+
172+
* ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
173+
* LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。
174+
* PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。
175+
* DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
176+
150177
## 在 java 程序中怎么保证多线程的运行安全?
151178

152179
线程安全在三个方面体现:
@@ -158,3 +185,132 @@ TERMINATED : 结束方法terminated()执行完后进入该状态
158185
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
159186

160187
synchronized锁的不是代码是对象。
188+
189+
## 为什么要使用线程池⭐
190+
191+
1. 减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
192+
2. 可以根据系统的承受能力,调整线程池中工作线程的数目,放置因为消耗过多的内存,而把服务器累趴下
193+
194+
## 线程池满了,往线程池里提交任务会发生什么样的情况,具体分几种情况
195+
196+
- 如果你使用的**LinkedBlockingQueue(阻塞队列)**,也就是**无界队列**的话,没关系,**继续添加任务**到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个**无穷大的队列,可以无限存放任务**;如果你使用的是**有界队列**比方说**ArrayBlockingQueue**的话,任务首先会被添加到ArrayBlockingQueue中,**ArrayBlockingQueue满了**,则会使用**拒绝策略RejectedExecutionHandler**处理满了的任务,默认是**AbortPolicy**
197+
- 线程池的饱和策略:当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了**4种策略**
198+
199+
## 向线程池提交一个线程的原理/步骤⭐
200+
201+
也叫做ThreadPoolexecutor工作流程
202+
203+
* 先判断核心线程池是否已满
204+
* 如果没有满就创建线程
205+
* 如果满了就判断等待队列是否已满
206+
* 如果没满就加入等待队列
207+
* 如果满了就判断最大线程池是否已满
208+
* 没有满就提交给线程池
209+
* 满了就执行拒绝策略
210+
211+
## 如何向线程池提交任务,提交任务有几种方式有什么区别
212+
213+
**有2种**:分别为Runnable和Callable。
214+
215+
分别使用execute 方法和 submit 方法
216+
217+
### 线程池的拒绝策略⭐
218+
219+
1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
220+
2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
221+
3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
222+
4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
223+
224+
### 线程池的线程数量怎么确定
225+
226+
1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。
227+
2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。
228+
3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
229+
230+
## volatile⭐
231+
232+
volatile在多处理器开发中保证了共享变量的“ 可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
233+
234+
## Java 中是如何实现线程同步的?
235+
236+
1.同步方法 synchronized 关键字修饰的方法(悲观锁)
237+
238+
2.使用特殊域变量(volatile)实现线程同步(保持可见性,多线程更新某一个值时,比如说线程安全单例双检查锁)
239+
240+
3.ThreadLocal(每个线程获取的都是该变量的副本)
241+
242+
4.使用重入锁实现线程同步(相对 synchronized 锁粒度更细了,效率高)
243+
244+
​ 一个java.util.concurrent 包来支持同步。
245+
246+
​ ReentrantLock 类是可重入、互斥、实现了 Lock 接口的锁
247+
248+
​ ReentrantLock() : 创建一个 ReentrantLock 实例
249+
250+
​ lock() : 获得锁
251+
252+
​ unlock() : 释放锁
253+
254+
5.java.util.concurrent.atomic 包 (乐观锁)
255+
256+
​ 方便程序员在多线程环境下,无锁的进行原子操作
257+
258+
259+
260+
261+
262+
### Atomic类的CAS操作⭐
263+
264+
​ CAS,Compare and Swap即比较并交换。java.util.concurrent 包借助 CAS 实现了区别于 synchronized 同步锁的一种乐观锁。乐观锁就是每次去取数据的时候都乐观的认为数据不会被修改,所以不会上锁,但是在更新的时候会判断一下在此期间数据有没有更新。CAS 有3个操作数:内存值 V,旧的预期值 A,要修改的新值 B
265+
266+
267+
268+
## i++是线程安全的吗?
269+
270+
分2种情况
271+
272+
1. 局部变量肯定是线程安全的(原因:方法内局部变量是线程私有的)
273+
2. 成员变量多个线程共享时,就不是线程安全的(原因:成员变量是线程共享的,因为 i++ 是三步操作。
274+
275+
## Synchronized与Lock的区别⭐
276+
277+
* synchronized是java内置关键字在jvm层面,Lock是个java类。
278+
* synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。
279+
* synchronized会自动释放锁,Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
280+
* ReentrantLock更加灵活,提供了超时获取锁,可中断锁,在获取不到锁的情况会自己结束,而synchronized不可以
281+
* synchronized的锁不可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
282+
283+
## 乐观锁和悲观锁的区别?
284+
285+
#### 悲观锁
286+
287+
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现
288+
289+
#### 乐观锁
290+
291+
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。**乐观锁适用于多读的应用类型,这样可以提高吞吐量**
292+
293+
## 如何实现一个乐观锁?
294+
295+
在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加一。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
296+
297+
## ReentrantLock如何实现公平和非公平锁
298+
299+
公平锁需要系统维护一个有序队列,获取锁时会判断阻塞队列里是否有线程再等待,若有获取锁就会失败,并且会加入阻塞队列。
300+
301+
非公平锁获取锁时不会判断阻塞队列是否有线程再等待,所以对于已经在等待的线程来说是不公平的,但如果是因为其它原因没有竞争到锁,它也会加入阻塞队列。
302+
303+
进入阻塞队列的线程,竞争锁时都是公平的,因为队列为先进先出(FIFO)。
304+
305+
306+
307+
308+
309+
310+
311+
**TODO:**
312+
313+
多线程的线程池是如何工作的?底层代码和原理?
314+
315+
Thredlocal 实现原理?
316+

0 commit comments

Comments
 (0)