forked from qinxuewu/docs
-
Notifications
You must be signed in to change notification settings - Fork 12
/
多线程和JVM知识总结.md
353 lines (276 loc) · 40.8 KB
/
多线程和JVM知识总结.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
## 多线程
#### 1.并行和并发有什么区别?
* 并行是指两个或者多个事件在同一时刻发生
* 并发是指两个或多个事件在同一时间间隔发生
* 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件
* 在一台处理器上“同时”处理多个任务是并行,在多台处理器上同时处理多个任务。如hadoop分布式集群
#### 2.ThreadLocal是什么?
* ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰
* `ThreadLocal`内部还有一个静态内部类ThreadLocalMap,该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。
* 每个`Thread`内部都有一个`ThreadLocal.ThreadLocalMap`类型的成员变量,该成员变量用来存储实际的`ThreadLocal`变量副本。
* `ThreadLocal`并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
#### 2.1 ThreadLocal为什么会内存泄漏?
* 每个Thread都有一个`ThreadLocal.ThreadLocalMap`的map,该map的key为`ThreadLocal`实例,它为一个弱引用,因为弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与C当前线程存在一个强引用关系,会导致value无法回收,如果这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏情况。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那就更坑了。
#### 3.Lock接口拥有synchronized所不具备的哪些特性?
* 与Synchronized不同,获取到锁的线程能够相应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
* 超时获取锁,以及可以尝试非阻塞地获取锁(调用后可立即返回锁是否获取成功)
* Lock是一个接口,它需要程序员自己定义了锁获取和释放的基本操作
#### 4.线程和进程的区别?
- 进程是资源分配最小单位,线程是程序执行的最小单位
- 线程是进程的一个执行单元。线程也被称为轻量级进程。
- 线程执行开销小,但是不利于资源的管理和保护。对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
- 进程执行开销大,但是能够很好的进行资源管理和保护。要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
#### 5.守护线程是什么?
- 守护线程,专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,那么jvm就会退出(即停止运行)——此时,连jvm都停止运行了,守护线程当然也就停止执行了。
#### 6.创建线程有哪几种方式?
- 继承Thread类和实现Runnable接口,以及线程池技术
- 通过Callable接口并实现call()方法,该call()方法将作为线程执行体,并且有返回值
#### 7.说一下 runnable 和 callable 有什么区别?
- 相同点:两者都是接口。都可以用来创建多线程。都需要调用Thread.start()启动线程
- 不同点:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果。
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
- Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞
#### 8.说一下 synchronized 底层实现原理?
- synchronized加在普通方法上,锁的当前对象的实例。也叫对象锁
- synchronized加在static方法上.锁的事当前类的class对象。也就是当前类的字节码文件对象,当类加载进内存,就会产生字节码文件对象
- 同步方法块,锁是括号里面的对象。同步代码块是使用monitorenter和monitorexit指令实现的.JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
- 同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现
- synchronized用的锁是存在Java对象头里的.如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头
- java对象头里的Mark-Word里默认存储对象的HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
#### 9.synchronized锁的升级与对比
- jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
- 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
#### 10。自旋锁
- 自旋锁:所谓自就是让线程执行一段无意义的循环,防止不会被立即挂起。看持有锁的线程是否会很快释放锁。优点是避免线程切换带来的开销,缺点是:占用了处理器的时间。在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
#### 11.适应自旋锁
- 适应自旋锁:所谓自适应就意味着自旋的次数不再是固定的。而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多。反之如果自旋很少能成功,那吗在以后获取这个锁时自选次数会减少或者直接忽略掉自旋操作。
#### 12.锁消除和锁粗化
- 锁消除:如果JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。比如StringBuffer的append()方法,Vector的add()方法
- 锁粗化:就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
#### 13.synchronized的锁升级流程之偏向锁
- 当一个线程访问同步代码块时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程进入和退出同步块时,不需要执行CAS操作来加锁和解锁,只需要简单的测试下对象头的Mark-Word是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已获取到锁
- 如果测试失败,则需要在测试下Mark-Word中偏向锁的表示是否设置为1(表示当前是偏向锁);如果没有设置,则使用CAS竞争;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 偏向锁使用了一种等到竞争出现才释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
- 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)
- 它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程处于不活动状态,则会将对象头设置成无所状态
- 如果线程仍然活着,拥有偏向锁的线程会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程
#### 14.synchronized的锁升级流程之轻量级锁
- 引入轻量级锁的目的是为了减轻重量级锁使用操作系统的互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁
- 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(官方称为Displaced Mark Word)
- 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
- 如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
- 轻量级解锁时,会使用原子的CAS操作将Displaced-Mark-Word替换回到对象头。如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁
- 轻量级锁的加锁和释放锁都是使用CAS操作来执行的
#### 14.synchronized的锁升级流程之重量级锁
- 重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
#### 15.偏向锁,轻量锁,重量级锁的优缺点对比
- 偏向锁: 加锁和解锁不需要额外的消耗,和执行非同步方法只存在纳秒级的差距。缺点是如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块
- 轻量级锁:竞争的线程不会阻塞,提高了线程的响应速度。缺点是如果始终得不到锁竞争的线程,使用自旋会消耗CPU。适用于追求响应时间,同步块执行速度非常快
- 重量级锁:线程竞争不使用自旋,不会消耗CPU。缺点是线程阻塞,响应时间慢。适用追求吞吐量,同步块执行时间过长。
#### 16.原子操作的实现原理
- 原子操作意为不可被中断的一个或一系列操作
- 处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
- 总线锁:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线锁输出此信号时,其它处理器就会被阻塞,该处理器将独占内存。
- 缓存锁:所谓 "缓存锁定“是指内存区域如果被锁定在缓存处理器缓存行中,并且Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内存的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会是缓存行无效。
- java如何实现原子操作: 使用循环CAS和锁的方式实现原子操作
#### 17.CAS实现原子操作的三大问题
- JVM中的CAS是利用处理的cmpxchg(汇编指令,比较并交换操作数)来实现的
- ABA问题(CAS操作时需要先检查值是否变化,但是一个值是A接着被改为B 后面又修改为A,CAS操作就会认为他们没有变化。ABA的解决思路是利用版本号)
- 循环时长开销大(自旋CAS长时间不成功会增大CPU的开销)
- 只能保证一个共享变量的原子操作。解决思路把多个变量合并为一个变量操作。JDK1.5开始提供了AtomicReference保证对象引用之间的原子性,就可以把多个变量放在同一个对象里进行CAS操作
#### 18.线程有哪些状态?
- 新建(NEW):新创建了一个线程对象。
- 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
- 运行(RUNNING):可运行状态(runnable)的线程获得了cpu时间片(timeslice) ,执行程序代码。
- 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu使用权,暂时停止运行。阻塞的情况分三种:等待阻塞(wait方法), 同步阻塞(线程在获取对象的同步锁时), 其他阻塞(Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时)
#### 19.sleep() 和 wait() 有什么区别?
- sleep()方法是Thread的静态方法,而wait是Object实例方法
- wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用
- wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
- sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
#### 20.notify()和 notifyAll()有什么区别?
- notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会;由JVM确定唤醒哪个线程,而且不是按优先级
- notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;
#### 21.线程的 run()和 start()有什么区别?
- run()是在主线程中执行方法,和调用普通方法一样;(按顺序执行,同步执行)
- start()方法:是创建了新的线程,在新的线程中执行;(异步执行)
- 启动一个线程,当然要调用strat()
#### 22.创建线程池有哪几种方式?
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小
- newCachedThreadPool:创建一个可缓存的线程池。此线程池不会对线程池大小做限制。线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
#### 22.线程池ThreadPoolExecutor的工作流程?
* 当提交一个新任务到线程池后,线程池首先会判断核心线程池(corePoolSize)里的线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务。
* 如果核心线程池corePoolSize的线程都被占用在执行任务,线程池判断工作队列是否已满,如果工作队列没有满:则将新提交的任务存储到工作队列中,
* 如果工作队列已满:判断线程池(maximumPoolSize)的线程是否处于工作状态,如果没有,则创建一个新的工作线程来执行任务。
* 如果线程池已满,则交给饱和策略处理这个任务
#### 23.线程池都有哪些状态?
- RUNNING(运行中):线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。线程池的初始化状态是RUNNING。
- SHUTDOWN(关掉):调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
- STOP(停止):调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- tidying:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
- terminated(终止):线程池彻底终止,就变成terminated状态。 线程池处在tidying状态时,执行完terminated()之后,就会由 tidying -> terminated。
#### 24.线程池中 submit()和 execute()方法有什么区别?
- 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中
#### 25.在java 程序中怎么保证多线程的运行安全?
- 线程安全在三个方面体现.如果可以保证以下三个方面,那马多线程的运行安全就得到了保证。一般是通过加锁的方式实现,分布式环境下。则要使用分布式锁
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序
#### 26.什么是死锁?如何避免死锁?
- 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行,就是死锁
- 按顺序加锁,每个获取锁的时候加上个时限,按线程间获取锁的关系检测线程间是否发生死锁,如果发生死锁就执行一定的策略,如终断线程或回滚操作等。
#### 27.说一下 atomic 的原理?
- 在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。
- Atomic包下的类是通过CAS操作来实现原子性的。jdk8直接使用了Unsafe的getAndAddInt方法
#### 28.说一下 volatile的实现原理?
- `volatile`修饰的变量可以禁止指令重排序和保证了内存可见性和单一操作的原子性,类似`i++`这样的复合操作的原子性保证不了
- 有`volatile`关键字修饰的共享变量进行写操作数,会多出一个`lock`前缀指令。`lock`前缀指令其实就相当于一个内存屏障。在多处理器下,会将当前处理器工作内存的数据回写到主内存中,并且这个回写操作会其它线程中缓存该内存地址的数据无效。相当于会在写操作后,发出一个信号给缓存了这个数的线程,告诉它们值更新了,需要从主内存中从新获取
- 在`JVM`底层`volatile`是采用“`内存屏障`”来实现
- `volatile`经常用于两个两个场景:状态标记两、单列模式中的`DCL`。
* 当第一个操作为普通变量的读/写时,如果第二个操作是`volatile`写,则编译器不能重排序这个两个操作。
* 当第一个操作是`volatile`读时,第二个操作不管是什么都不能重排序,这个规则确保volatile读之后的操作不会排序的它之前。
* 当一个操作是volatile写时,第二个操作时volatile读时,不能重排序
#### 29.volataile的内存语义及其实现?
- 如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
- 当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
- 当第一个操作volatile写,第二操作为volatile读时,不能重排序。
#### 29.说一下happens-before的理解?
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性
#### 30.说一下happens-before八种规则?
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
#### 31.什么是AQS?
- AQS抽象的队列式同步器。是实现JUC核心基础组件。
- AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
- AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
- 实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
- AQS提供两种同步状态的获取与释放:独占式(该方法对中断不敏感,获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除)和共享式(在同一时刻可以有多个线程获取同步状态)
- 总结:在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的对尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。
#### 32.ReentrantLock与synchronized的区别
- 两者实现方式不同一个基于JVM层面,一个基于JDK源码实现
- synchronized是隐式的获取锁和释放,ReentrantLock是显示的获取或释放锁,并且有锁超时,锁中断等功能
- ReentrantLock提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而synchronized则一旦进入锁请求要么成功要么阻塞,所以相比synchronized而言,ReentrantLock会不容易产生死锁些。
#### 33.如何确保N个线程可以访问N个资源同时又不导致死锁
- 使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出 现死锁了。
#### 34.yield()方法有什么用?
- Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行
#### 35.CyclicBarrier和CountDownLatch的区别?
- CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
- CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
- CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
#### 36.Semaphore有什么作用以及实现原理?
- Semaphore就是一个信号量,它的作用是限制某段代码块的并发数
- Semaphore有非公平和公平模式,默认是非公平的。当Semaphore设置为1时,可以排它锁使用,同一个时刻,只能限制一个线程访问。和CountDownLatch一样的,内部都有一个Sync内部类,基于AQS实现同步状态的释放和获取。
#### 37.说一说对ReentrantReadWriteLock的理解?
- ReentrantReadWriteLock内部维护了一对锁,读锁和写锁。支持重入和公平以及平非公平模式。读锁是共享式的,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
- 写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时(还未获到),读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
- 写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。
- 锁降级:遵循获取写锁,获取读锁在释放写锁的次序,写锁可以降级为读锁
- 锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新
#### 38 说一说Condition的理解?
* `Condition`是一个接口,与`Lock`配合可以实现的等待通知模式,类似`Object`的`wait`和`notify`。获取一个`Condition`对象需要调用Lock的`newCondition`方法或得`ConditionObject`,是AQS的一个内部类。Condition操作需要获取想关联的锁
* 一个线程获取锁后,通过调用Condition的`await()`方法,会释放锁,然后构造成节点并将节点从尾部加入等待队列,并进入等待状态。
* 当线程调用`signal()`方法后,程序首先检查当前线程是否获取了锁,然后通过`doSignal(Node first)`方法唤醒同步队列的等待时间最长的节点(首节点)。在唤醒节点之前,会将节点移动到同步队列中,被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。
#### 39.说一说你对exchanger的理解?
* Exchanger类允许在两个线程之间定义同步点。当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中
* Exchanger算法的核心是通过一个可交换数据的slot,以及一个可以带有数据item的参与者。
* 在Exchanger中,如果一个线程已经到达了exchanger节点时,如果它的伙伴节点在该线程到达之前已经调用了exchanger方法,则它会唤醒它的伙伴然后进行数据交换,得到各自数据返回。如果它的伙伴节点还没有到达交换点,则该线程将会被挂起,等待它的伙伴节点到达被唤醒,完成数据交换。如果当前线程被中断了则抛出异常,或者等待超时了,则抛出超时异常。
#### 说一说对ConcurrentHashMap的理解?
* 在多线程环境下,HashMap的put方法会引起死循环,是因为并发执行put方法会造成`Entry`链表形成环形数据结构,导致next指向一直不为空,就会产生死循换获取Entry。
* `ConcurrentHashMap`是一种线程安全的HashMap。相对于HashTable和Collections.synchronizedMap(),ConcurrentHashMap具有更好的性能和伸缩性。
* JDK1.8以前采用的是分段锁机制(`Segment`+`HashEntry`),将数据分成一段一段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其它段的数据能被其它线程访问。其中Segment在实现上继承了ReentrantLock,这样就自带了锁的功能
* `put实现`:当执行put方法插入数据时,根据key的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据
* `size实现`:因为ConcurrentHashMap是可以并发插入数据的,所以在准确计算元素时存在一定的难度,一般的思路是统计每个Segment对象中的元素个数,然后进行累加,但是这种方式计算出来的结果并不一样的准确的,因为在计算后面几个Segment的元素个数时,已经计算过的Segment同时可能有数据的插入或则删除。jdk1.8以前中是先采用不加锁的方式,连续计算元素的个数,最多计算3次,如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
* 1.8 中使用`CAS+synchronized+Node+红黑树`的实现方式。当链表长度达到8时,将链表转化为红黑树,当链表长度小于6时,将红黑树转化为链表。
* 详细分析:https://www.jianshu.com/p/e694f1e868ec
#### 说一下CoucurrentLinkedQueue的理解?
* `ConcurrentLinkedQueue`是一个基于链接节点的无边界的线程安全队列,它采用先进先出原则对元素进行排序,插入元素放入队列尾部,出队时从队列头部返回元素,利用CAS方式实现的
* `CoucurrentLinkedQueue`的结构由头节点和尾节点组成的,都是使用`volatile`修饰的。每个节点由节点元素`item`和指向下一个节点的`next`引用组成.
*入队:
## JVM
#### 说下一Java的内存模型
- Java内存模型是JVM的抽象模型,就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
- 目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
- Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存.不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
#### 说一下jvm 的主要组成部分?及其作用?
- JVM基本上由三部分组成:类加载器,执行引擎,运行时数据区
- 类加载器:在JVM启动时以及程序运行时将需要加载的class文件加载到JVM中
- 执行引擎:负责执行class文件中包含的字节码指令,相当于物理机器上的CPU
- 运行时数据区:将划分给Java程序的内存划分成几个区来模拟物理机器上的存储、记录和调度功能
#### 说一下 jvm 运行时数据区?
- 线程私有的:虚拟机栈,本地方法栈,程序计数器
- 线程共享的 方法区,堆
- 程序计数器可以看作是当前线程所执行的字节码行号指示器。通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要这个计数器来完成
- 虚拟机栈:每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程
- 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
- 方法区主要用于存放已经被虚拟机加载的类信息,如常量,静态变量,即时编译器编译后的代码等。和Java堆一样不需要连续的内存,并且可以动态扩展
- Java 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。堆内存也分为 新生代、老年代。
#### 说一下堆栈的区别?
- 栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
- 堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
#### 队列和栈是什么?有什么区别?
- 队列(Queue):是限定只能在表的一端进行插入和在另一端进行删除操作的线性表;
- 栈(Stack):是限定只能在表的一端进行插入和删除操作的线性表。
- 队列是先进先出,栈是先进后出
- 队列:基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,因为在遍历的过程中不影响数据结构,所以遍历速度要快;
- 栈:只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,而且在遍历数据的同时需要为数据开辟临时空间,保持数据在遍历前的一致性。
#### 什么是双亲委派模型?
- 如果一个类收到加载请求时,它不会先自己去尝试加载这个类,而是委派给父类加载器去加载,只有当父类加载器在自己的搜索范围找不到这个类时,才会委派给子类加载器去执行加载。
- 优点:加载的类是同一个,保证内库更安全,缺点效率低
#### 说一下类加载的执行过程?
- 类的加载过程分为加载,验证,准备,解析,初始化,使用,卸载七个阶段。其中主准备,解析和初始化统称为链接阶段。其中类加载工作由ClassLoader及其子类负责。
- 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定
- 类装载包括了加载,连接(验证、准备、解析(可选)),初始化
- 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中
- 验证是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误
- 主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值
- 解析是将常量池内的符号引用替换为直接引用的过程
- 初始化,这个阶段主要是对类变量初始化,是执行类构造器的过程。
- 使用阶段包括主动引用和被动引用
- 卸载,类所有的实例都已经被回收,加载该类的ClassLoader已经被回收,该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法,jvm就会在方法区垃圾回收的时候对类进行卸载
#### 说一下类加载器有哪些?
- 启动类加载器:负责加载JRE的核心类库,如jre目标下的rt.jar,charsets.jar等.
- 扩展类加载器:负责加载JRE扩展目录ext中JAR类包
- 系统类加载器:负责加载ClassPath路径下的类包
- 用户自定义加载器:负责加载用户自定义路径下的类包
#### 怎么判断对象是否可以被存活?
- java是使用根搜索算法判断对象是否存活的
- 通过一系列的名为“GC-roots"的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象的GC-roots没有任何引用链相连时,则证明此对象是不可用的
- 可以作为GC-Roots对象有虚拟机栈中的引用对象,方法区中的类静态属性引用对象,方法区中的常量引用的对象,本地方法中JNI(即一般说的native方法)的引用的对象。
#### java 中都有哪些引用类型?
- 四种:强引用,软引用,弱引用,虚引用
- 只要强引用还存在,垃圾回收期永远不会回收掉被引用的对象
- 软引用:用来描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出前,将会把这些对象列进回收范围之内并进行第二次回收,如果这此次回收还是没有足够的内存,才会抛出内存溢出
- 弱引用:用来描述非必须的对象,但是它的强度比软引用更弱一下,被弱引用关联的对象,只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,只会回收被弱引用关联的对象
- 虚引用:被称为幽灵引用或幻引用,是最弱的一种引用关系。为一个对象设置虚引用的目的就是在对象被回收时收到一个系统通知。
#### 说一下 jvm 有哪些垃圾回收算法?
- 标记-清除算法:算法分为标记和清除两个阶段。首先先标记所有要被回收的对象,标记完成后再统一清除被标记的对象。效率低,会产生大量不连续的内存碎片
- 复制算法:将可用内存按容量划分为大小相等的两块,每次只用其中的一块,当这一块内存用完,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
- 标记-整理算法:标记过程仍然与标记-清楚算法一样。但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
- 分代收集算法:根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用标记-清理或标记-整理算法来进行回收
#### 说一下 jvm 有哪些垃圾回收器?
- Serial收集器:一个单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作。在进行垃圾收集时必须暂停其它所有的工作线程,直接到结束。是虚拟机运行在Client模式下的默认新手代收集器,简单而高效
- ParNew收集器:Serial收集器的多线程版本,使用多条线程收集。是许多运行在Server模式下的虚拟机首选新生代收集器。且目前除了Serial收集器,只有它可以与CMS收集器配合工作
- Parallel Scavenge收集器:它是一款新生代收集器。使用复制算法收集,又是并行的多线程收集器
- Serial Old收集器:它是Serial收集器的老年代版本,是一个单线程收集器,使用标记-整理算法收集
- Parallel Old收集器:它是Parallel-Scavenge收集器的老年代版本,使用多线程和标记-整理算法。JDK1.6才开始提供。
- CMS收集器:是一种以获取最短回收停顿时间的为目标的收集器。基于标记-清楚算法实现。运作过程分为四个阶段。初始标记,并发标记,重新标记,并发清除。
- G1收集器:将整个Java堆分为多个大小相等的独立区域。虽然保留新生代和老年代,但它们不再是物理隔离,都是一部分不需要连续的集合。特点是并行与并发充分利用CPU缩短停顿时间。分代收集,空间整合不会产生内存空间碎片,可预测的停顿。有计划的避免回收整个Java堆。
#### 常用的 jvm 调优的参数都有哪些?
- -Xms20M:表示设置JVM启动内存的最小值为20M,必须以M为单位
- -Xmx20M:表示设置JVM启动内存的最大值为20M
- -verbose:gc:表示输出虚拟机中GC的详细情况
- -Xss128k:表示可以设置虚拟机栈的大小为128k
- -Xoss128k:表示设置本地方法栈的大小为128k
- -XX:PermSize=10M:表示JVM初始分配的永久代(方法区)的容量,必须以M为单位