线程的核心原理

现代操作系统(如Windows、Linux、Solaris)提供了强大的线程管理能力,Java不需要再进行独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如Solaris操作系统)上,JVM甚至将每个Java线程一对一地对应到操作系统的本地线程,彻底将线程调度委托给操作系统。

1.线程的调度与时间片

由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。对于不同的操作系统、不同的CPU,线程的CPU时间片长度都不同。假定操作系统(比如Windows XP)线程的时间片长度为20毫秒,在一个2GHz的CPU上,一个时间片可以计算的次数是20亿/(1000/20)=4000万次,也就是说,一个时间片内的计算量是非常巨大的。目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。

  1. 分时调度模型:系统平均分配CPU的时间片,所有线程轮流占用CPU,即在时间片调度的分配上所有线程“人人平等”。图1-10所示是一个分时调度的简单例子,三个线程轮流得到CPU时间片,一个线程执行时,另外两个线程处于就绪状态。

    image-20220411231219434

  2. 抢占式调度模型:系统按照线程优先级分配CPU时间片。优先级高的线程优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。由

于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。

2. 线程的优先级

在Thread类中有一个实例属性和两个实例方法,专门用于进行线程优先级相关的操作。与线程优先级相关的成员属性为:image-20220412103843814

与Thread类线程优先级相关的实例方法为:

1
2
public final int getPriority();  // 获取线程优先级。
public final void setPriority(int priority);//设置线程优先级。

Thread实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。优先级最大值为10,最小值为1,Thread类中定义的三个优先级常量如下:

image-20220412103859878

Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会就越多,但也不是绝对的。

3. 线程的生命周期

Java中线程的生命周期分为6种状态。Thread类有一个实例属性和一个实例方法专门用于保存和获取线程的状态。其中,用于保存线程Thread实例状态的实例属性为:

image-20220412105656984

Thread类用于获取线程状态的实例方法为:

image-20220412105714696

Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态,具体如下:

image-20220412105733071

在Thread.State定义的6种状态中,有4种是比较常见的状态,它们是:NEW(新建)状态、RUNNABLE(可执行)状态、TERMINATED(终止)状态、TIMED_WAITING(限时等待)状态。

1. NEW状态

Java源码对NEW状态的说明是:创建成功但是没有调用start()方法启动的Thread线程实例都处于NEW状态。当然,并不是Thread线程实例的start()方法一经调用,其状态就从NEW状态到RUNNABLE状态,此时并不意味着线程立即获取CPU时间片并且立即执行,中间需要一系列操作系统的内部操作。

2. RUNNABLE状态

前面讲到,当调用了Thread实例的start()方法后,下一步如果线程获取CPU时间片开始执行,JVM将异步调用线程的run()方法执行其业务代码。那么在run()方法被异步调用之前,JVM做了哪些事情呢?

JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式委托操作系统的线程管理API完成的。当Java线程的Thread实例的start()方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状态。操作系统中线程的就绪状态是什么状态的呢?

JVM的线程状态与其幕后的操作系统线程状态之间的转换关系简化后如图所示

image-20220412105831150

一个操作系统线程如果处于就绪状态,就表示“万事俱备,只欠东风”,即该线程已经满足执行条件,但是还不能执行。处于就绪状态的线程需要等待系统的调度,一旦就绪状态被系统选中,获得CPU时间片,线程就开始占用CPU,开始执行线程的代码,这时线程的操作系统状态发生了改变,进入了运行状态。

在操作系统中,处于运行状态的线程在CPU时间片用完之后,又回到就绪状态,等待CPU的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间被系统反复地调度,这种情况会一直持续,直到线程的代码逻辑执行完成或者异常终止。这时线程的操作系统状态又发生了改变,进入线程的最后状态——TERMINATED状态。

就绪状态和运行状态都是操作系统中的线程状态。在Java语言中,并没有细分这两种状态,而是将这两种状态合并成同一种状态——RUNNABLE状态。因此,在Thread.State枚举类中,没有定义线程的就绪状态和运行状态,只是定义了RUNNABLE状态。这就是Java线程状态和操作系统中线程状态不同的地方。

总之,NEW状态的Thread实例调用了start()方法后,线程的状态将变成RUNNABLE状态。尽管如此,线程的run()方法不一定会马上被并发执行,需要在线程获取了CPU时间片之后才真正启动并发执行。

3. TERMINATED状态

处于RUNNABLE状态的线程在run()方法执行完成之后就变成终止状态TERMINATED了。当然,如果在run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止,线程也会变成TERMINATED状态。

4. TIMED_WAITING状态

线程处于一种特殊的等待状态,准确地说,线程处于限时等待状态。能让线程处于限时等待状态的操作大致有以下几种:

  1. Thread.sleep(int n):使得当前线程进入限时等待状态,等待时间为n毫秒。
  2. Object.wait():带时限的抢占对象的monitor锁。
  3. Thread.join():带时限的线程合并。
  4. LockSupport.parkNanos():让线程等待,时间以纳秒为单位。
  5. LockSupport.parkUntil():让线程等待,时间可以灵活设置。

4. interrupt

Java语言提供了stop()方法终止正在运行的线程,但是Java将Thread的stop()方法设置为过时,不建议大家使用。为什么呢?因为使用stop()方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于调用stop()方法来终止线程可能会产生不可预料的结果,因此不推荐调用stop()方法。

一个线程什么时候可以退出呢?当然只有线程自己才能知道。所以,这里介绍一下Thread的interrupt()方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态。

当我们调用线程的interrupt()方法时,它有两个作用:

  1. 如果此线程处于阻塞状态(如调用了Object.wait()方法),就会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。更确切地说,如果线程被Object.wait()、Thread.join()和Thread.sleep()三种方法之一阻塞,此时调用该线程的interrupt()方法,该线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早终结被阻塞状态。
  2. 如果此线程正处于运行之中,线程就不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。

如果线程的interrupt()方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。

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
package com.crazymakercircle.mutithread.basic.use;
import com.crazymakercircle.util.Print;
import org.junit.Test;
import static com.crazymakercircle.util.ThreadUtil.sleepMilliSeconds;
import static com.crazymakercircle.util.ThreadUtil.sleepSeconds;

/**
* Created by 尼恩@疯狂创客圈.
*/

public class InterruptDemo {

public static final int SLEEP_GAP = 5000;//睡眠时长
public static final int MAX_TURN = 50;//睡眠次数

static class SleepThread extends Thread {
static int threadSeqNumber = 1;

public SleepThread() {
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}

public void run() {
try {
Print.tco(getName() + " 进入睡眠.");
// 线程睡眠一会
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
Print.tco(getName() + " 发生被异常打断.");
return;
}
Print.tco(getName() + " 运行结束.");
}

}

public static void main(String args[]) throws InterruptedException {

Thread thread1 = new SleepThread();
thread1.start();
Thread thread2 = new SleepThread();
thread2.start();
sleepSeconds(2);//等待2秒
thread1.interrupt(); //打断线程1
sleepSeconds(5);//等待5秒
thread2.interrupt(); //打断线程2,此时线程2已经终止
sleepSeconds(1);//等待1秒
Print.tco("程序运行结束.");
}

//测试用例:获取异步调用的结果
//Thread.interrupt()方法并不像Thread.stop()方法那样中止一个正在运行的线程,其作用是设置线程的中断状态位(为true),至于线程是死亡、等待新的任务还是继续运行至下一步,就取决于这个程序本身。线程可以不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。总之,Thread.interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,线程是否停止执行,需要用户程序去监视线程的isInterrupted()状态,并进行相应的处理。
@Test
public void testInterrupted2() {
Thread thread = new Thread() {
public void run() {
Print.tco("线程启动了");
//一直循环
while (true) {
Print.tco(isInterrupted());
sleepMilliSeconds(SLEEP_GAP);

//如果调用 interrupt 为true,退出死循环
if (isInterrupted()) {
Print.tco("线程结束了");
return;
}
}
}
};
thread.start();
sleepSeconds(2);//等待2秒
thread.interrupt(); //打断线程1
sleepSeconds(2);//等待2秒
thread.interrupt();
}
}

5. Join

线程的合并是一个比较难以说清楚的概念,什么是线程的合并呢?举一个例子,假设有两个线程A和B。现在线程A在执行过程中对另一个线程B的执行有依赖,具体的依赖为:线程A需要将线程B的执行流程合并到自己的执行流程中(至少表面如此),这就是线程合并,被动方线程B可以叫作被合并线程。这个例子中的线程A合并线程B的伪代码大致为:

image-20220412141716434

1.线程的join操作的三个版本

join()方法是Thread类的一个实例方法,有三个重载版本:

image-20220412141734969

调用join()方法的要点:

  1. join()方法是实例方法,需要使用被合并线程的句柄(或者指针、变量)去调用,如threadb.join()。执行threadb.join()这行代码的当前线程为合并线程(甲方),进入TIMED_WAITING等待状态,让出CPU。
  2. 如果设置了被合并线程的执行时间millis(或者millis+nanos),并不能保证当前线程一定会在millis时间后变为RUNNABLE。
  3. 如果主动方合并线程在等待时被中断,就会抛出InterruptedException受检异常。

调用join()方法的语句可以理解为合并点,合并的本质是:线程A需要在合并点等待,一直等到线程B执行完成,或者等待超时。

为了方便表达,本书将依赖的线程A叫作甲方线程,被依赖的线程B叫作乙方线程。简单理解线程合并就是甲方线程调用乙方线程的join()方法,在执行流程上将乙方线程合并到甲方线程。甲方线程等待乙方线程执行完成后,甲方线程再继续执行,如图1-12所示。

如果乙方线程无限制长时间地执行,甲方线程可以进行限时等待:甲方线程等待乙方线程执行一定时间后,如果乙方还没有完成,甲方线程再继续执行。

调用join()方法的优势是比较简单,劣势是join()方法没有办法直接取得乙方线程的执行结果。

image-20220412144746644

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
package com.crazymakercircle.mutithread.basic.use;
import com.crazymakercircle.util.Print;
import static com.crazymakercircle.util.ThreadUtil.sleepSeconds;

/**
* Created by 尼恩@疯狂创客圈.
*/

public class JoinDemo {

public static final int SLEEP_GAP = 5000;//睡眠时长
public static final int MAX_TURN = 50;//睡眠次数

static class SleepThread extends Thread {
static int threadSeqNumber = 1;

public SleepThread() {
super("sleepThread-" + threadSeqNumber);
threadSeqNumber++;
}

public void run() {
try {
Print.tco(getName() + " 进入睡眠.");
// 线程睡眠一会
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
Print.tco(getName() + " 发生被异常打断.");
return;
}
Print.tco(getName() + " 运行结束.");
}

}

public static void main(String args[]) {
Thread thread1 = new SleepThread();
sleepSeconds(20);
Print.tco("启动 thread1.");
thread1.start();
try {
thread1.join();//合并线程1,不限时
} catch (InterruptedException e) {
e.printStackTrace();
}

sleepSeconds(20);
Print.tco("启动 thread2.");
//启动第二条线程,并且进行限时合并,等待时间为1秒
Thread thread2 = new SleepThread();
thread2.start();
try {
thread2.join(5000);//限时合并,限时1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
Print.tco("线程运行结束.");
}
}

2. join线程的WAITING状态

线程的WAITING(等待)状态表示线程在等待被唤醒。处于WAITING状态的线程不会被分配CPU时间片。执行以下两个操作,当前线程将处于WAITING状态:

  1. 执行没有时限(timeout)参数的thread.join()调用:在线程合并场景中,若线程A调用B.join()去合入B线程,则在B执行期间线程A处于WAITING状态,一直等线程B执行完成。
  2. 执行没有时限(timeout)参数的object.wait()调用:指一个拥有object对象锁的线程,进入相应的代码临界区后,调用相应的object的wait()方法去等待其“对象锁”(Object Monitor)上的信号,若“对象锁”上没有信号,则当前线程处于WAITING状态,如图1-13所示。

image-20220412145823830

通过Jstack指令DUMP出来上面JoinDemo演示实例执行过程中的线程信息,当main线程合入sleepThread-1线程的时候,sleepThread-1处于TIMED_WAITING状态(sleep操作导致),main线程也处于WAITING状态。Jstack指令的部分输出结果截取如下:

image-20220412145803334

3. join线程的TIMED_WAITING状态

线程的TIMED_WAITING状态表示在等待唤醒。处于TIMED_WAITING状态的线程不会被分配CPU时间片,它们要等待被唤醒,或者直到等待的时限到期。

在线程合入场景中,若线程A在调用B.join()操作时加入了时限参数,则在B执行期间线程A处于TIMED_WAITING状态。若B在等待时限内没有返回,则线程A结束等待TIMED_WAITING状态,恢复成RUNNABLE状态。

通过Jstack指令DUMP出来JoinDemo演示实例执行过程中的线程信息,在main线程合入sleepThread-2线程的时候,sleepThread-2处于TIMED_WAITING状态(sleep操作导致),main线程也处于TIMED_WAITING状态。Jstack指令的部分输出结果截取如下:

image-20220412145946599

5. 线程的yield操作

线程的yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出CPU的执行权限,使得CPU去执行其他的线程。处于让步状态的JVM层面的线程状态仍然是RUNNABLE状态,但是该线程所对应的操作系统层面的线程从状态上来说会从执行状态变成就绪状态。线程在yield时,线程放弃和重占CPU的时间是不确定的,可能是刚刚放弃CPU,马上又获得CPU执行权限,重新开始执行。

yield()方法是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是让线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,yield()方法只有一个版本:

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
package com.crazymakercircle.mutithread.basic.use;
import com.crazymakercircle.util.Print;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static com.crazymakercircle.util.ThreadUtil.sleepSeconds;

public class YieldDemo {
public static final int MAX_TURN = 100;//执行次数
public static AtomicInteger index = new AtomicInteger(0);//执行编号

// 记录线程的执行次数
private static Map<String, AtomicInteger> metric = new HashMap<>();
//输出线程的执行次数
private static void printMetric() {
Print.tco("metric = " + metric);
}

static class YieldThread extends Thread {
static int threadSeqNumber = 1;
public YieldThread() {
super("YieldThread-" + threadSeqNumber);
threadSeqNumber++;
metric.put(this.getName(), new AtomicInteger(0));
}

public void run() {
for (int i = 1; i < MAX_TURN && index.get() < MAX_TURN; i++) {
Print.tco("线程优先级:" + getPriority());
index.incrementAndGet();
metric.get(this.getName()).incrementAndGet();
if (i % 2 == 0) {
//让步:出让执行的权限
Thread.yield();
}
}
//输出线程的执行次数
printMetric();
Print.tco(getName() + " 运行结束.");
}
}

@Test
public void test1() {
Thread thread1 = new YieldThread();
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new YieldThread();
thread2.setPriority(Thread.MIN_PRIORITY);
Print.tco("启动线程.");
thread1.start();
thread2.start();
sleepSeconds(100);
}
}

在以上演示案例中,一共启动了两个让步演示线程,两个线程每执行两次操作就让出CPU。但是两个线程的优先级有区别,YieldThread-1的优先级为Thread.MAX_PRIORITY(值为10),YieldThread-2的优先级为Thread.MIN_PRIORITY(值为1),从输出的结果可以看出,优先级高的YieldThread-1执行的次数比优先级低的YieldThread-2执行的次数多很多。得到的结论是:线程调用yield之后,操作系统在重新进行线程调度时偏向于将执行机会让给优先级较高的线程。

总结起来,Thread.yeid()方法有以下特点:

  1. yield仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
  2. yield不能保证使得当前正在运行的线程迅速转换到就绪状态。
  3. 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。

6. 线程的daemon操作

Java中的线程分为两类:守护线程与用户线程。守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的GC(垃圾回收)线程,这些GC线程就是守护线程,提供幕后的垃圾回收服务。使用Jstack指令DUMP某个JVM进程时,截取到的GC线程如下:

image-20220412150253473

举一个比较通俗的例子,守护线程在JVM中相当于保姆的角色:只要JVM实例中尚存在任何一个用户线程没有结束,守护线程就能执行自己的工作;只有当最后一个用户线程结束,守护线程随着JVM一同结束工作。

1.守护线程的基本操作

在Thread类中,有一个实例属性和两个实例方法,专门用于进行守护线程相关的操作。

  1. 实例属性daemon:保存一条Thread线程实例的守护状态,默认为false,表示线程默认为用户线程。

image-20220412150331349

  1. 实例方法setDaemon(…):此方法将线程标记为守护线程或者用户线程。setDaemon(true)将线程设置为守护线程,setDaemon(false)将线程设置为用户线程。

    image-20220412150404472

  2. 实例方法isDaemon():获取线程的守护状态,用于判断该线程是不是守护线程。

image-20220412150415956

2.守护线程的基本操作演示实例

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
package com.crazymakercircle.mutithread.basic.use;
import com.crazymakercircle.util.Print;
import static com.crazymakercircle.util.ThreadUtil.getCurThread;
import static com.crazymakercircle.util.ThreadUtil.sleepMilliSeconds;

/**
* Created by 尼恩@疯狂创客圈.
*/
public class DaemonDemo {
public static final int SLEEP_GAP = 500; //每一轮的睡眠时长
public static final int MAX_TURN = 4; //用户线程执行轮次

//守护线程实现类
static class DaemonThread extends Thread {
public DaemonThread() {
super("daemonThread");
}
public void run() {
Print.synTco("--daemon线程开始.");
for (int i = 1; ; i++) {
Print.synTco("--轮次:" + i + "--守护状态为:" + isDaemon());
// 线程睡眠一会
sleepMilliSeconds(SLEEP_GAP);
}
}
}

public static void main(String args[]) throws InterruptedException {
Thread daemonThread = new DaemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
Thread userThread = new Thread(() ->
{
Print.synTco(">>用户线程开始.");
for (int i = 1; i <= MAX_TURN; i++) {
Print.synTco(">>轮次:" + i + " -守护状态为:" + getCurThread().isDaemon());
sleepMilliSeconds(SLEEP_GAP);
}
Print.synTco(">>用户线程结束.");
}, "userThread");
userThread.start();
// 主线程合入userThread,等待userThread执行完成
// userThread.join();
Print.synTco(" 守护状态为:" + getCurThread().isDaemon());
Print.synTco(" 运行结束.");
}
}

本例创建了两个线程:一个线程为守护线程,其名称为daemonThread,使用继承Thread的方式创建;另一个线程为用户线程,其名称为userThread,使用Lambda表达式新建了一个Runnable实例后,传入Thread构造器来创建。

在守护线程daemonThread线程的run方法中设置了一for死循环(没有条件判断表达式的循环),启动之后,理论上永远也不会停止。程序中使用setDaemon(true)语句将daemonThread线程设置成守护线程。

在用户线程userThread线程的匿名run()方法中设置了一个能循环4轮的for循环。每一轮循环,输出当前的轮次和当前线程的守护状态等信息。程序中,userThread线程的daemon属性还是默认的false值,因此该线程为用户线程。

从例子的输出结果来看,main线程也是一条用户线程。main线程在创建和启动了daemonThread和userThread后,就提前结束了。虽然main线程结束了,但是两条线程还在继续执行,其中就有一条是用户线程,所以进程还不能结束。当剩下的一条用户线程userThread的run()方法执行完成后,userThread线程执行结束。这时,所有的用户线程执行已经完成,JVM进程就随之退出了。在JVM退出时,守护线程daemonThread还远远没有结束,还在死循环的执行中。但是JVM不管这些,强行终止了所有守护线程的执行。

3.守护线程与用户线程的关系

从是否为守护线程的角度,对Java线程进行分类,分为用户线程和守护线程。守护线程和用户线程的本质区别是:二者与JVM虚拟机进程终止的方向不同。用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止,如图1-14所示。

image-20220412150736413

换个角度来理解,守护线程提供服务,是守护者,用户线程享受服务,是被守护者。只有用户线程全部终止了,相当于没有了被守护者,守护线程也就没有工作可做了,也就可以全部终止了。当然,用户线程全部终止,JVM进程也就没有继续的必要了。反过来说,只要有一个用户线程没有终止,JVM进程也不会退出。

但是在终止维度上,守护线程和JVM进程没有主动关系。也就是说,哪怕是守护线程全部被终止,JVM虚拟机也不一定终止。

4.守护线程的要点

使用守护线程时,有以下几点需要特别注意:

  1. 守护线程必须在启动前将其守护状态设置为true,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。具体来说,如果线程为守护线程,就必须在线程实例的start()方法调用之前调用线程实例的setDaemon(true),设置其daemon实例属性值为true。
  2. 守护线程存在被JVM强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄、数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
  3. 守护线程创建的线程也是守护线程。

在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。

5.守护线程创建的线程也是守护线程

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
package com.crazymakercircle.mutithread.basic.use;
import com.crazymakercircle.util.Print;
import static com.crazymakercircle.util.ThreadUtil.getCurThreadName;
import static com.crazymakercircle.util.ThreadUtil.sleepMilliSeconds;

/**
* Created by 尼恩@疯狂创客圈.
*/

public class DaemonDemo2 {
public static final int SLEEP_GAP = 500;
public static final int MAX_TURN = 5;
static class NormalThread extends Thread {
static int threadNo = 1;
public NormalThread() {
super("normalThread-" + threadNo);
threadNo++;
}
public void run() {
for (int i = 0; ; i++) {
sleepMilliSeconds(SLEEP_GAP);
Print.synTco(getName() + ", 守护状态为:" + isDaemon());
}
}
}

public static void main(String args[]) throws InterruptedException {
Thread daemonThread = new Thread(() ->
{
for (int i = 0; i < 5; i++) {
Thread normalThread = new NormalThread();
// normalThread.setDaemon(false);
normalThread.start();
}
}, "daemonThread");
daemonThread.setDaemon(true);
daemonThread.start();
//这里,一定不能让main线程结束,否则看不到结果
sleepMilliSeconds(SLEEP_GAP);

Print.synTco(getCurThreadName() + " 运行结束.");
}
}

本例中使用Lambda表达式方式创建一个daemonThread线程,这是一个守护线程。在这个守护线程的业务代码中又创建了5个普通线程。5个新线程启动前,虽然daemon状态没有被设置为true(预期是用户线程),但是从程序的结果中可以看出,实际上这5个线程的daemon状态为true,都是守护线程。

在本例中,main线程加上Thread.sleep(SLEEP_GAP)语句让main线程等待一段时间再结束。这是非常必要的,为什么呢?因为另外的6个线程都是守护线程。只有main线程是唯一的一个用户线程,为了看到守护线程所输出的部分结果,一定不能让main线程提前结束。

如果要将守护线程所创建的线程调整为用户线程,可以通过setDaemon(false)显式地将这些线程设置为用户线程。上面的代码可以进行如下调整,将创建的5个新线程调整为用户线程:

image-20220412150956890

调整之后,再一次运行以上示例,大家可以分析一下执行的结果。