@@ -20,15 +20,15 @@ head:
20
20
21
21
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
22
22
23
- 如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
23
+ 如下图所示,在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(` .exe ` 文件的运行)。
24
24
25
25
![ 进程示例图片-Windows] ( https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png )
26
26
27
27
### 何为线程?
28
28
29
29
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的** 堆** 和** 方法区** 资源,但每个线程有自己的** 程序计数器** 、** 虚拟机栈** 和** 本地方法栈** ,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
30
30
31
- Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
31
+ Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。
32
32
33
33
``` java
34
34
public class MultiThread {
@@ -59,13 +59,13 @@ public class MultiThread {
59
59
60
60
## 请简要描述线程与进程的关系,区别及优缺点?
61
61
62
- ** 从 JVM 角度说进程和线程之间的关系**
62
+ 从 JVM 角度说进程和线程之间的关系。
63
63
64
64
### 图解进程和线程的关系
65
65
66
66
下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。
67
67
68
- ![ ] ( ../jvm/pictures/java内存区域/Java运行时数据区域JDK1 .8.png)
68
+ ![ Java 运行时数据区域(JDK1.8 之后) ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/jvm/java-runtime-data-areas-jdk1 .8.png)
69
69
70
70
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的** 堆** 和** 方法区 (JDK1.8 之后的元空间)** 资源,但是每个线程有自己的** 程序计数器** 、** 虚拟机栈** 和 ** 本地方法栈** 。
71
71
@@ -97,11 +97,18 @@ public class MultiThread {
97
97
98
98
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
99
99
100
- ## 说说并发与并行的区别?
100
+ ## 并发与并行的区别
101
101
102
102
- ** 并发** :两个及两个以上的作业在同一 ** 时间段** 内执行。
103
103
- ** 并行** :两个及两个以上的作业在同一 ** 时刻** 执行。
104
104
105
+ 最关键的点是:是否是 ** 同时** 执行。
106
+
107
+ ## 同步和异步的区别
108
+
109
+ - ** 同步** : 发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
110
+ - ** 异步** :调用在发出之后,不用等待返回结果,该调用直接返回。
111
+
105
112
## 为什么要使用多线程呢?
106
113
107
114
先从总体上来说:
@@ -120,33 +127,42 @@ public class MultiThread {
120
127
121
128
## 说说线程的生命周期和状态?
122
129
123
- Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
130
+ Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
124
131
125
- ![ Java 线程的状态 ] ( https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png )
132
+ - NEW: 初始状态,线程被创建出来但没有被调用 ` start() ` 。
133
+ - RUNNABLE: 运行状态,线程被调用了 ` start() ` 等待运行的状态。
134
+ - BLOCKED :阻塞状态,需要等待锁释放。
135
+ - WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
136
+ - TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
137
+ - TERMINATED:终止状态,表示该线程已经运行完毕。
126
138
127
- 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源 [ issue # 736 ] ( https://github.com/Snailclimb/JavaGuide/issues/736 ) ):
139
+ 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
128
140
129
- ![ ] ( ./images/interview-questions/java-life-cycle.png )
141
+ Java 线程状态变迁图(图源: [ 挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误 ] ( https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w ) ):
130
142
131
- > 订正(来自 [ issue736 ] ( https://github. com/Snailclimb/JavaGuide/issues/736 ) ):原图中 wait 到 runnable 状态的转换中, ` join ` 实际上是 ` Thread ` 类的方法,但这里写成了 ` Object ` 。
143
+ ![ Java 线程状态变迁图 ] ( https://guide-blog-images.oss-cn-shenzhen.aliyuncs. com/github/javaguide/java/concurrent/640.png )
132
144
133
145
由上图可以看出:线程创建之后它将处于 ** NEW(新建)** 状态,调用 ` start() ` 方法后开始运行,线程这时候处于 ** READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 ** RUNNING(运行)** 状态。
134
146
135
147
> 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态(图源:[ HowToDoInJava] ( https://howtodoinJava.com/ " HowToDoInJava ") :[ Java Thread Life Cycle and Thread States] ( https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/ " Java Thread Life Cycle and Thread States ") ),所以 Java 系统一般将这两个状态统称为 ** RUNNABLE(运行中)** 状态 。
136
148
>
137
- > ** 为什么 JVM 没有区分这两种状态呢?** (摘自:[ java线程运行怎么有第六种状态 ? - Dawell的回答 ] ( https://www.zhihu.com/question/56494969/answer/154053599 ) ) 现在的< b >时分</ b > (time-sharing)< b > 多任务</ b > (multi-task)操作系统架构通常都是用所谓的“< b > 时间分片</ b > (time quantum or time slice)”方式进行< b >抢占式</ b > (preemptive)轮转调度(round-robin式 )。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
149
+ > ** 为什么 JVM 没有区分这两种状态呢?** (摘自:[ Java 线程运行怎么有第六种状态 ? - Dawell 的回答 ] ( https://www.zhihu.com/question/56494969/answer/154053599 ) ) 现在的时分 (time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式 (preemptive)轮转调度(round-robin 式 )。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
138
150
139
151
![ RUNNABLE-VS-RUNNING] ( https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png )
140
152
141
- 当线程执行 ` wait() ` 方法之后,线程进入 ** WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 ** TIMED_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 ` sleep(long millis) ` 方法或 ` wait(long millis) ` 方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 ** BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() ` 方法之后将会进入到 ** TERMINATED(终止)** 状态。
153
+ - 当线程执行 ` wait() ` 方法之后,线程进入 ** WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
154
+ - ** TIMED_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 ` sleep(long millis) ` 方法或 ` wait(long millis) ` 方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。
155
+ - 当线程进入 ` synchronized ` 方法/块或者调用 ` wait ` 后(被 ` notify ` )重新进入 ` synchronized ` 方法/块,但是锁被其它线程占有,这个时候线程就会进入 ** BLOCKED(阻塞)** 状态。
156
+ - 线程在执行完了 ` run() ` 方法之后将会进入到 ** TERMINATED(终止)** 状态。
142
157
143
- 相关阅读:[ 挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误 ] ( https://mp.weixin.qq.com/s/UOrXql_LhOD8dhTq_EPI0w ) 。
158
+ 相关阅读:[ 线程的几种状态你真的了解么? ] ( https://mp.weixin.qq.com/s/R5MrTsWvk9McFSQ7bS0W2w ) 。
144
159
145
160
## 什么是上下文切换?
146
161
147
162
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
163
+
148
164
- 主动让出 CPU,比如调用了 ` sleep() ` , ` wait() ` 等。
149
- - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死 。
165
+ - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死 。
150
166
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
151
167
- 被终止或结束运行
152
168
@@ -235,12 +251,12 @@ Thread[线程 2,5,main]waiting get resource1
235
251
236
252
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
237
253
238
- > ** 安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列。
254
+ > ** 安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3.....Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 ` <P1、P2、P3.....Pn> ` 序列为安全序列。
239
255
240
256
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
241
257
242
258
``` java
243
- new Thread (() - > {
259
+ new Thread (() - > {
244
260
synchronized (resource1) {
245
261
System . out. println(Thread . currentThread() + " get resource1" );
246
262
try {
@@ -256,7 +272,7 @@ Thread[线程 2,5,main]waiting get resource1
256
272
}, " 线程 2" ). start();
257
273
```
258
274
259
- Output
275
+ 输出:
260
276
261
277
```
262
278
Thread[线程 1,5,main]get resource1
@@ -273,17 +289,29 @@ Process finished with exit code 0
273
289
274
290
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
275
291
276
- ## 说说 sleep() 方法和 wait() 方法区别和共同点?
292
+ ## sleep() 方法和 wait() 方法对比
293
+
294
+ ** 共同点** :两者都可以暂停线程的执行。
295
+
296
+ ** 区别** :
297
+
298
+ - ** ` sleep() ` 方法没有释放锁,而 ` wait() ` 方法释放了锁** 。
299
+ - ` wait() ` 通常被用于线程间交互/通信,` sleep() ` 通常被用于暂停执行。
300
+ - ` wait() ` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 ` notify() ` 或者 ` notifyAll() ` 方法。` sleep() ` 方法执行完成后,线程会自动苏醒,或者也可以使用 ` wait(long timeout) ` 超时后线程会自动苏醒。
301
+ - ` sleep() ` 是 ` Thread ` 类的静态本地方法,` wait() ` 则是 ` Object ` 类的本地方法。为什么这样设计呢?
302
+
303
+ ## 为什么 wait() 方法不定义在 Thread 中?
304
+
305
+ ` wait() ` 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(` Object ` )都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(` Object ` )而非当前的线程(` Thread ` )。
306
+
307
+ 类似的问题:** 为什么 ` sleep() ` 方法定义在 ` Thread ` 中?**
277
308
278
- - 两者最主要的区别在于:** ` sleep() ` 方法没有释放锁,而 ` wait() ` 方法释放了锁** 。
279
- - 两者都可以暂停线程的执行。
280
- - ` wait() ` 通常被用于线程间交互/通信,` sleep() ` 通常被用于暂停执行。
281
- - ` wait() ` 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 ` notify() ` 或者 ` notifyAll() ` 方法。` sleep() ` 方法执行完成后,线程会自动苏醒。或者可以使用 ` wait(long timeout) ` 超时后线程会自动苏醒。
309
+ 因为 ` sleep() ` 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
282
310
283
- ## 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法 ?
311
+ ## 可以直接调用 Thread 类的 run 方法吗 ?
284
312
285
313
这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
286
314
287
- new 一个 Thread,线程进入了新建状态。调用 ` start() ` 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 ` start() ` 会执行线程的相应准备工作,然后自动执行 ` run() ` 方法的内容,这是真正的多线程工作。 但是,直接执行 ` run() ` 方法,会把 ` run() ` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
315
+ new 一个 ` Thread ` ,线程进入了新建状态。调用 ` start() ` 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 ` start() ` 会执行线程的相应准备工作,然后自动执行 ` run() ` 方法的内容,这是真正的多线程工作。 但是,直接执行 ` run() ` 方法,会把 ` run() ` 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
288
316
289
317
** 总结: 调用 ` start() ` 方法方可启动线程并使线程进入就绪状态,直接执行 ` run() ` 方法的话不会以多线程的方式执行。**
0 commit comments