深入浅出 JAVA 之线程 - Thread 详解
文章目录
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
系统环境:
- JDK 版本: OpenJDK 11
一、线程模型
https://www.sohu.com/a/451667558_100181197
在计算机科学中,线程模型通常被分为用户线程模型、内核线程模型和混合型线程模型三种。具体如下:
1.1 用户线程模型 (User-Level Thread Model)
用户线程模型 (User-level Thread Model,简称 ULT) 是在应用程序层面上实现的线程模型,这种模型的被称为 "用户级线程",因为线程的创建、调度、管理、同步等全部在用户空间中完成,不需要涉及操作系统内核的支持。
用户级线程通常由应用程序或者运行库来实现,其对内核空间的访问仅限于少量的系统调用。用户级线程的优点在于能够更快速地创建和销毁线程,线程的上下文切换也更加高效。此外,用户级线程还支持轻量级的协程(Coroutine)切换,可以更加高效地实现线程间的通信和同步。
然而,用户级线程的缺点在于其对系统资源的使用受到限制。由于内核无法感知和管理用户级线程的状态,因此无法进行优化和资源调度。例如,用户级线程无法使用操作系统提供的多线程调度算法、基于时间片的调度方式、以及与 I/O 设备相关的异步调度等。这些限制会导致用户级线程的性能不如内核级线程。此外,当一个用户级线程被阻塞时,其它用户级线程也会被挂起,这可能会导致整个应用程序的性能下降。
1.2 内核线程模型 (Kernel-Level Thread Model)
内核线程模型 (Kernel-level Thread Model,简称 KLT) 是在操作系统层面上实现的线程模型,这种模型的线程被称为 "内核级线程",因为线程的创建、调度、管理、同步等都是由操作系统内核来完成的。
内核级线程的优点在于其使用了操作系统提供的完整的多线程支持,因此能够实现更加复杂和全面的线程管理和调度,包括基于时间片的调度、基于优先级的调度、I/O 设备的异步调度等。此外,内核级线程还可以利用多处理器系统提供的硬件并行性来实现更加高效的线程执行。
然而,内核级线程的缺点在于其创建和销毁线程的开销较大,线程的上下文切换也相对比较耗时。此外,内核级线程的同步和通信机制需要频繁访问内核空间,导致性能下降。另外,内核级线程对内存和其他系统资源的需求较大,因此在高并发和大规模的应用程序中,可能会导致系统资源的耗尽和竞争。
总之,内核级线程模型相对于用户级线程模型,具有更完备的线程调度、管理和同步机制,适用于需要高度并发和复杂线程交互的场景;而用户级线程模型则更适用于轻量级线程调度和协程切换的场景。
1.3 混合型线程模型 (Hybrid Thread Model)
混合型线程模型 (Hybrid Thread Model) 是一种结合了内核级线程模型和用户级线程模型的线程模型,利用了两种模型的优点来提高线程的执行效率和性能。
混合型线程模型中,每个用户级线程都会关联一个内核级线程,当用户级线程阻塞时,内核级线程会被调度以避免阻塞。同时,内核级线程的数量可以根据当前系统的负载和资源情况进行动态调整,以保证线程的执行效率和负载均衡。
混合型线程模型的优点在于可以充分利用系统资源,提高线程的并发能力和执行效率,同时还可以保证线程的可靠性和稳定性。此外,混合型线程模型还可以提供更好的线程调度和同步机制,以及更灵活的线程管理策略,适用于不同大小和类型的应用程序。
然而,混合型线程模型的实现较为复杂,需要充分考虑内核级线程和用户级线程之间的同步和通信机制,以及线程的动态调度和资源管理等方面的问题。同时,混合型线程模型可能会导致一些竞争和错乱问题,需要特别注意线程之间的同步和数据共享问题。
二、Java 线程 Thread 概述
2.1 Java 线程 Thread 简介
Java 线程是 Java 中的一种基本机制,使得我们可以让程序并行地执行多个任务。Java 线程是由操作系统内核级线程映射而来的,可以通过 Thread 类来创建和管理。在 Java 程序中,主线程是默认存在的,程序会在主线程中执行 main() 方法,而我们可以通过创建新的线程来完成其他任务,实现多线程并发执行。
Java 线程的使用可以在多种场景中发挥作用,例如多人在线游戏、并发编程、服务器端程序等。在 Java 中,我们可以使用 Thread 类或者实现 Runnable 接口来创建线程,并可以通过 synchronized 关键字来实现线程同步,保证多个线程之间的数据共享和协作正确性。
2.2 Java 线程 Thread 特点
- 线程是轻量级的执行单元,创建和销毁线程的开销比进程小;
- 线程是共享内存的,线程之间共享内存数据,可以方便地进行数据交换和通信;
- 线程是抢占式的,多个线程之间会竞争 CPU 时间片,线程执行顺序不确定;
- 线程是可伸缩的,可以根据需要动态创建和销毁线程,适应不同的程序负载和资源需求;
- 线程是异步的,不同的线程可以并行地执行不同的任务,提高程序的并发效率和响应速度;
四、线程 Thread 状态
Java Thread 状态指的是线程在其生命周期中可能处于的不同状态,在 Java 中的 Thread 类中定义了一些枚举来表达线程不同的状态。常用的线程状态有以下几种:
- NEW (新建状态): 表示线程已经被创建,还未调用 start() 方法,所以线程尚未开始执行。
- RUNNABLE (运行状态): 表示线程正在运行中,处于可执行状态。在这个状态下线程正在调用 CPU 执行任务,或者正在等待 CPU 调度。
- BLOCKED (阻塞状态): 表示线程正在等待获取一个内部的对象锁,或者正在等待 I/O 操作的完成。在这个状态下,线程被阻塞无法继续执行。
- WAITING (等待状态): 表示线程正在等待另一个线程执行特定的操作。在这个状态下,线程会一直等待某个特定的条件被满足,直到其他线程发出通知或者中断当前线程。
- TIMED_WAITING (有时限等待状态): 类似于 WAITING 状态,这个状态下的线程会等待特定的条件被满足,但是等待时间有限制。在这个状态下,线程会在指定的时间段内等待条件被满足,如果超时,则自动返回 RUNNABLE 状态。
- TERMINATED (终止状态): 表示线程已经执行完毕,线程的生命周期已经结束,线程对象将被垃圾回收。
了解线程的不同状态对于多线程编程非常重要,我们可以通过查询线程状态来调试和优化线程代码,尤其是在调试阻塞或死锁问题的时候。
五、线程 Thread 常用方法
静态方法
- sleep(long millis): 使当前线程在接下来的时间(毫秒)内进行休眠。
- sleep(long millis, int nanos): 使当前线程在接下来的时间(毫秒/纳秒)内进行休眠。
- interrupted(): 检查当前线程是否中断,并将中断状态清除。
- activeCount(): 获取当前活动线程的数量。
- enumerate(Thread tarray[]): 获取系统中的所有线程,并将它们存储在 tarray 数组中。
- dumpStack(): 将当前线程的堆栈跟踪打印到标准错误流。
- checkAccess(): 检查当前线程是否有权限修改该线程。
- getAllStackTraces(): 获取当前正在运行的所有线程及其堆栈信息的。
- setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh): 设置应用程序的默认未捕获异常处理器。
- getDefaultUncaughtExceptionHandler(): 获取应用程序的默认未捕获异常处理器。
实例方法
- start(): 使当前线程启动。
- run(): 运行当前线程中的逻辑。
- interrupt(): 使当前线程中断。
- isInterrupted:() 检查当前线程的中断状态,并将中断状态清除。
- stop(): 强制当前线程停止运行 (该方法有安全隐患已经被废弃)。
- suspend(): 挂起当前线程 (该方法会导致线程死锁或死循环等问题,所以已经被废弃)。
- resume(): 恢复挂起的线程 (该方法会导致线程死锁或死循环等问题,所以已经被废弃)。
- setPriority(int newPriority): 设置线程优先级。
- getPriority(): 获取线程优先级。
- setName(String name): 设置线程名称。
- getName(): 获取线程名称。
- getThreadGroup(): 获取当前线程所属的线程组。
- setDaemon(boolean on): 将线程设置为守护线程或者普通线程。
- isDaemon(): 断线程是否为守护线程。
- setContextClassLoader(ClassLoader cl): 设置线程的上下文类加载器。
- getContextClassLoader(): 获取线程的上下文类加载器。
- getId(): 获取线程ID。
- getState(): 获取线程状态。
- setUncaughtExceptionHandler(UncaughtExceptionHandler eh): 设置指定线程的未捕获异常处理器。
- getUncaughtExceptionHandler(): 获取指定线程的未捕获异常处理器。
- join(): 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,直至线程执行结束使之前正在执行的线程恢复。
- join(long millis): 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,在指定时间(毫秒)后使之前正在执行的线程恢复。
- join(long millis, int nanos): 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,在指定时间(毫秒/纳秒)后使之前正在执行的线程恢复。
Native 方法
- currentThread(): 获取当前正在执行的线程。
- yield(): 使当前线程让出 CPU 时间片,让其它线程先执行。
- holdsLock(Object obj): 判断当前线程是否持有指定对象的锁。
- countStackFrames(): 计算此线程中堆栈帧的数量 (该方法执行时会使线程挂起,所以即被废弃)。
- isAlive(): 判断当前线程是否存活。
六、线程 Thread 常用方法示例
6.1 示例-调用 start 方法启动线程
start()
方法用于启动线程,下面是一个创建线程并启动的简单示例:
1public class CreateThreadDemo {
2
3 public static void main(String[] args) {
4 // 创建线程
5 Thread thread = new Thread(new Runnable() {
6 @Override
7 public void run() {
8 System.out.println(Thread.currentThread().getName() + "--线程启动");
9 }
10 });
11 // 设置线程名称
12 thread.setName("测试线程");
13 // 启动线程
14 thread.start();
15 }
16
17}
6.2 示例-调用 interrupt 方法终止线程
isInterrupted()
方法用于检测当前线程对象是否被中断。下面是一个使用 isInterrupted()
方法的示例:
1public class InterruptThreadDemo {
2
3 public static void main(String[] args) throws InterruptedException {
4 // 创建线程
5 Thread myThread = new Thread(new Runnable() {
6 @Override
7 public void run() {
8 // 循环执行任务,每次执行前判断当前线程是否被中断
9 while (!Thread.currentThread().isInterrupted()) {
10 try {
11 System.out.println("线程正在运行...");
12 // 线程睡眠 1 秒钟
13 TimeUnit.SECONDS.sleep(1);
14 } catch (InterruptedException e) {
15 // 如果当前线程调用了 wait 或 sleep 处于堵塞状态,那么线程调用 interrupt 方法后就会抛出 InterruptedException 异常
16 // 所以这里需要捕获 InterruptedException 异常,捕获后需要调用 Thread.currentThread().interrupt() 方法中断线程
17 Thread.currentThread().interrupt();
18 System.out.println("线程被中断");
19 }
20 }
21 System.out.println("线程结束");
22 }
23 });
24 // 启动线程
25 myThread.start();
26 // 等待 5 秒后调用 interrupt 方法 MyThread 线程中断
27 TimeUnit.SECONDS.sleep(5);
28 myThread.interrupt();
29 }
30
31}
在上面的示例中,创建一个子线程 myThread,子线程中会使用 while 循环来执行线程的任务,同时检测线程对象是否被中断。如果线程被中断,则跳出循环。在跳出循环后,我们可以执行线程的清理操作。
然后在主线程中启动子线程,并且调用 TimeUnit.SECONDS.sleep(5)
方法来让主线程等待 5 秒钟。最后我们调用 thread.interrupt()
方法来中断子线程。
在中断子线程后,子线程中的 while 判断里面条件 isInterrupted()
方法会返回 true
,从而跳出 while 循环。
6.3 示例-调用配置未捕获异常处理器
setUncaughtExceptionHandler()
方法用于设置当前线程未捕获异常的处理器。下面是一个使用 setUncaughtExceptionHandler()
方法的示例:
1public class ExampleUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
2
3 public void uncaughtException(Thread thread, Throwable exception) {
4 // 处理未捕获的异常
5 System.out.println("异常捕获器-捕获异常: " + exception.getMessage());
6 }
7
8}
1public class UncaughtExceptionHandlerDemo {
2
3 public static void main(String[] args) {
4 // 创建线程,并抛出运行时异常
5 Thread myThread = new Thread(new Runnable() {
6 @Override
7 public void run() {
8 // 当前线程中抛出一个未捕获的异常
9 throw new RuntimeException("Uncaught Exception");
10 }
11 });
12 // 设置未捕获异常的处理器
13 myThread.setUncaughtExceptionHandler(new ExampleUncaughtExceptionHandler());
14 // 启动线程
15 myThread.start();
16 }
17
18}
在上面的示例中,先定义了一个 ExampleUncaughtExceptionHandler 类,实现了 Thread.UncaughtExceptionHandler 接口,并重写了 uncaughtException
方法,用于处理未捕获的异常。
然后在 main 方法中,创建了一个新线程 Thread 对象,并定义对象在 run 方法中抛出一个运行时异常,并且设置未捕获异常的处理器为我们自定义异常处理器 ExampleUncaughtExceptionHandler。
当我们启动线程后,执行 run 方法时,示例代码中将抛出未捕获的异常时,这时候就会执行异常处理器 ExampleUncaughtExceptionHandler 类中的 uncaughtException
方法,处理线程中抛出的异常。
6.4 示例-使用 join 切换线程
join()
方法用于暂时当前正在执行的线程,使执行 join()
方法的线程先行执行。下面是一个使用 join()
方法的示例:
1public class ThreadJoinDemo {
2
3 public static void main(String[] args) throws InterruptedException {
4 //循环 5 次
5 for (int i = 0; i < 5; i++) {
6 // 创建线程
7 Thread myThread = new Thread(new Runnable() {
8 @Override
9 public void run() {
10 System.out.println("子线程执行完毕");
11 }
12 });
13
14 // 启动子线程
15 myThread.start();
16 // 调用join()方法
17 myThread.join();
18 System.out.println("主线程执行完毕");
19 System.out.println("-----------------------------");
20 }
21 }
22
23}
在上面的示例中,有俩个线程,分别是 "主线程" 和 "子线程",当子线程调用 start()
方法后,子线程可能第一时间内并不会先执行,而本示例中的目的就是先让子线程先执行,主线程后执行。
所以,在本示例代码中,主线程中会执行五次循环,每次循环中会创建一个子线程,子线程会输出 "子线程执行完毕" 内容,当启动子线程后主线程调用 myThread.join()
方法,确保子线程先行执行,子线程执行完后再执行 "主线程执行完毕"。
七、线程 Thread 源码 - 属性
1/**
2 * 线程名称
3 */
4private volatile String name;
5
6/**
7 * 线程优先级 (范围是1~10,其中1最低优先级,10是最高优先级)。
8 */
9private int priority;
10
11/**
12 * 线程是否是守护线程
13 *
14 * 注: 守护线程是一种特殊的线程,当所有非守护线程结束后,JVM 就会自动退出,而不管守护线程是否已经执行完毕。
15 * 该变量默认为 false,即默认线程为普通线程,需要显式设置为 true 才能将线程设置为守护线程。
16 */
17private boolean daemon = false;
18
19/**
20 * 该线程要执行的任务
21 */
22private Runnable target;
23
24/**
25 * 线程归属组
26 */
27private ThreadGroup group;
28
29/**
30 * 此线程的上下文 ClassLoader
31 */
32private ClassLoader contextClassLoader;
33
34/**
35 * 创建线程时的堆栈大小 (即给新线程分配的内存大小)
36 */
37private final long stackSize;
38
39/**
40 * 线程ID
41 */
42private final long tid;
43
44/**
45 * 分配给线程的唯一序号,JVM 会通过该序号对进程进行调度和管理。
46 */
47private static long threadSeqNumber;
48
49/**
50 * 线程状态
51 */
52private volatile int threadStatus;
53
54/**
55 * 阻塞线程的对象
56 *
57 * 当一个线程被 park 阻塞时,JVM 会将 parkBlocker 设置为阻塞线程的对象。
58 * 在 unpark 对线程进行唤醒时,JVM 会将 parkBlocker 清空。
59 *
60 * 该变量是 volatile 类型的,因为它需要保证阻塞线程和唤醒线程之间的可见性,
61 * 即在多线程操作时可以及时地感知到 parkBlocker 的变化。
62 */
63volatile Object parkBlocker;
64
65/**
66 * 中断线程时所阻塞的对象
67 *
68 * 当一个线程被中断时,如果该线程正在等待某个资源或者处于阻塞状态,那么 JVM 会将 blocker 设置为相应的资源或阻塞对象,
69 * 从而使得该线程在中断处理程序中能够快速地被唤醒。
70 *
71 * 这个 blocker 变量是 volatile 类型的,是为了保证在多线程操作时的可见性。
72 */
73private volatile Interruptible blocker;
74
75/**
76 * 锁住 blocker 和 parkBlocker 两个变量的锁对象
77 */
78private final Object blockerLock = new Object();
79
80/**
81 * 线程最小优先级。
82 */
83public static final int MIN_PRIORITY = 1;
84
85/**
86 * 线程默认优先级
87 */
88public static final int NORM_PRIORITY = 5;
89
90/**
91 * 线程最大优先级
92 */
93public static final int MAX_PRIORITY = 10;
94
95/**
96 * 存储线程的局部变量
97 *
98 * ThreadLocal 是一种线程局部变量,它提供了一种在多线程环境下保持变量的副本独立性的方法。每个线程都有
99 * 一个 ThreadLocalMap 对象,用于存储该线程的所有 ThreadLocal 变量和对应的值,每个 ThreadLocal 变量
100 * 都是线程私有的,即每个线程都有自己的一份副本,不会被其他线程所访问或修改。
101 *
102 * 使用 ThreadLocal 变量时,可以通过 get() 和 set() 方法来获取或设置当前线程的变量值。
103 * ThreadLocal 通常被用于实现线程安全的单例模式和避免线程之间的变量共享等场景。
104 */
105ThreadLocal.ThreadLocalMap threadLocals = null;
106
107/*
108 * 存储可以被子线程继承的线程局部变量。
109 *
110 * 它是 ThreadLocal 的一个改进版本,能够保证线程的子孙线程都可以访问到线程局部变量的值,而不仅仅是当前线程。
111 * 原理与 ThreadLocal 相同,每个线程都有自己的一份副本。
112 *
113 * 在使用 InheritableThreadLocal 时,只需将变量声明为 static final 类型即可,即可被所有子线程继承,并且
114 * 可以在子线程中修改它的值,对父线程不会产生影响。
115 *
116 * 子线程创建时,会自动将父线程中 InheritableThreadLocal 的值复制过来。通过继承,InheritableThreadLocal 可
117 * 以在一定程度上降低并发编程难度,并且使代码更加简洁。
118 */
119ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
八、线程 Thread 源码 - 关键方法
8.1 启动线程方法 start()
该方法主要是启动线程。虽然说该方法是启动线程,但是在该方法内部并没有真正启动线程的逻辑,而真正的逻辑是放在了 Native 类型的 start0() 方法中,调用该方法可以使操作系统底层创建一个新的线程,并将 Java 对象和操作系统中的线程关联起来,从而启动该线程。
需要注意的是 start() 方法只能被调用一次。如果多次调用该方法,则会抛出 IllegalThreadStateException 异常。
1/**
2 * 启动线程
3 *
4 * @throws IllegalThreadStateException 如果线程已经启动则抛出非法线程状态异常
5 */
6public synchronized void start() {
7 // 如果线程状态不为 0,则说明线程已经启动过,则抛出非法线程状态异常
8 if (threadStatus != 0) {
9 throw new IllegalThreadStateException();
10 }
11 // 通知组该线程即将启动,以便可以将其添加到组的线程列表中,并且可以减少组的未启动计数。
12 group.add(this);
13 // 设置启动状态
14 boolean started = false;
15 try {
16 // 启动线程,该方法是启动线程的核心方法
17 start0();
18 // 如果它成功启动线程则将 started 标志设置为 true
19 started = true;
20 } finally {
21 try {
22 // 如果 started=false,则表明线程启动失败
23 // 则将调用线程组的 threadStartFailed() 方法来处理这个失败的线程
24 if (!started) {
25 group.threadStartFailed(this);
26 }
27 } catch (Throwable ignore) {
28 }
29 }
30}
31
32/**
33 * 启动线程的关核心方法
34 *
35 * 该方法是启动线程的核心方法,是一个 Native 方法,即底层实现是由操作系统提供的,
36 * Java 虚拟机对该方法进行了封装提供给 Java 开发者使用。该方法执行时,会在底层操作
37 * 系统中创建一个新的线程,并将 Java 对象和操作系统中的线程关联起来,从而启动该线程。
38 */
39private native void start0();
注意: 调用 start() 方法后,其实并不会立即启动线程,它只是通知 JVM 要启动一个新的线程,并让 JVM 在合适的时间启动线程。
8.2 线程运行方法 run()
Java 中 Thread 类中的 public void run() 方法是线程类的重要方法,它用于定义线程的执行逻辑。当一个线程被创建并启动后,JVM 会调用该线程对象的 run() 方法,开始执行线程的具体操作。
在 run() 方法中,我们可以编写线程的逻辑代码,实现我们需要在单独线程中执行的任务。可以将方法中的代码视为独立的线程代码块,可以包括各种语句、循环、分支、方法调用等。当该方法执行完成后,线程就会自动退出。
由于 Thread 类实现了 Runnable 接口,因此它的 run() 方法也是实现了 Runnable 接口中的 run() 方法。Java 中可以通过覆盖 Thread 类的 run() 方法或者实现 Runnable 接口的 run() 方法来创建线程。在 run() 方法中,我们可以编写线程的核心逻辑,然后通过创建线程对象并调用 start() 方法来启动线程。
1/**
2 * 运行线程中的业务逻辑
3 */
4public void run() {
5 if (target != null) {
6 target.run();
7 }
8}
8.3 线程插队方法 join()
join()
1/**
2 * 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,直至线程执行结束使之前正在执行的线程恢复。
3 *
4 * @throws InterruptedException 如果当前线程被中断,则会抛出 InterruptedException 异常,并清除当前线程的中断状态。
5 */
6public final void join() throws InterruptedException {
7 join(0);
8}
join(long millis)
1/*
2 * 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,在指定时间(毫秒)后使之前正在执行的线程恢复。
3 *
4 * @param millis 休眠时间(毫秒)
5 * @throws IllegalArgumentException 如果传入的毫秒数是负数,会抛出 IllegalArgumentException 异常。
6 * @throws InterruptedException 如果当前线程被中断,则会抛出 InterruptedException 异常,并清除当前线程的中断状态。
7 */
8public final synchronized void join(long millis) throws InterruptedException {
9 long base = System.currentTimeMillis();
10 long now = 0;
11
12 if (millis < 0) {
13 throw new IllegalArgumentException("timeout value is negative");
14 }
15
16 if (millis == 0) {
17 while (isAlive()) {
18 wait(0);
19 }
20 } else {
21 while (isAlive()) {
22 long delay = millis - now;
23 if (delay <= 0) {
24 break;
25 }
26 wait(delay);
27 now = System.currentTimeMillis() - base;
28 }
29 }
30}
join(long millis, int nanos)
1/**
2 * 使正在执行的线程等待,使调用当前 join() 方法线程先行执行,在指定时间(毫秒/纳秒)后使之前正在执行的线程恢复。
3 *
4 * @throws InterruptedException
5 * @param millis 休眠时间(毫秒)
6 * @param nanos 休眠时间(纳秒)
7 * @throws IllegalArgumentException 如果传入的毫秒数是负数,会抛出 IllegalArgumentException 异常。
8 * @throws InterruptedException 如果当前线程被中断,则会抛出 InterruptedException 异常,并清除当前线程的中断状态。
9 */
10public final synchronized void join(long millis, int nanos) throws InterruptedException {
11
12 if (millis < 0) {
13 throw new IllegalArgumentException("timeout value is negative");
14 }
15
16 if (nanos < 0 || nanos > 999999) {
17 throw new IllegalArgumentException(
18 "nanosecond timeout value out of range");
19 }
20
21 if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
22 millis++;
23 }
24
25 join(millis);
26}
8.4 线程中断方法 interrupt()
该方法用于中断线程。调用该方法会使线程抛出一个 InterruptedException 异常,如果线程目前没有处于阻塞状态,那么线程的中断状态会被设置为 true,可以通过 isInterrupted() 方法检查线程的中断状态。如果线程目前处于阻塞状态,那么线程会被唤醒并抛出一个 InterruptedException 异常。
具体来说,该方法有以下几个作用:
- ① 中断线程阻塞:如果线程正处于阻塞状态(如调用了sleep()、wait()、join()等方法),则可以通过interrupt()方法中断线程的阻塞状态。
- ② 中断线程运行:如果线程没有处于阻塞状态,调用interrupt()方法后,线程的中断状态会被设置为true。在线程的代码中可以定期检查中断状态,从而安全地终止线程运行。
- ③ 检查并清除中断状态:在线程的代码中可以使用isInterrupted()方法检查线程的中断状态,并根据需要采取相应的行动。注意执行该方法并不会清除线程的中断状态,只有调用了interrupted()方法才会清除中断状态。
需要注意的是,对于某些阻塞方法 (如 Object 的 wait(),Thread 的 join() 等),在方法内部可能会清除线程的中断状态,因此在使用这些方法时需要时刻注意线程的中断状态。
1/**
2 * 使当前线程中断
3 */
4public void interrupt() {
5 // 判断当前线程是否等于当前线程对象
6 if (this != Thread.currentThread()) {
7 // 检查当前线程是否有权限修改这个线程对象
8 checkAccess();
9
10 // 使用 synchronized 锁保证只有一个线程执行下面操作
11 synchronized (blockerLock) {
12 // 获取当前线程正在阻塞的对象(如果有的话),将其保存在局部变量 b 中
13 Interruptible b = blocker;
14 // 判断当前线程是否正在进行 I/O 操作阻塞,如果是则调用 interrupt0() 将当前线程的中断状态设置为 true,
15 // 并将中断信息发送给正在阻塞的对象。
16 if (b != null) {
17 interrupt0();
18 b.interrupt(this);
19 return;
20 }
21 }
22 }
23
24 // 将当前线程的中断状态设置为 true,表示当前线程已经被中断。
25 interrupt0();
26}
8.5 线程休眠方法 sleep(long millis)
sleep(long millis)
1/**
2 * 使当前线程在接下来的时间(毫秒)内进行休眠。
3 * (注: 休眠的精度和准确性受系统定时器和调度器的影响,并且在休眠期间线程不会释放锁)
4 *
5 * @param millis 休眠时间(毫秒)
6 * @throws IllegalArgumentException 如果传入的毫秒数是负数,会抛出 IllegalArgumentException 异常。
7 * @throws InterruptedException 如果当前线程被中断,则会抛出 InterruptedException 异常,并清除当前线程的中断状态。
8 */
9public static native void sleep(long millis) throws InterruptedException;
sleep(long millis, int nanos)
1/**
2 * 使当前线程在接下来的时间(毫秒/纳秒)内进行休眠。
3 * (注: 休眠的精度和准确性受系统定时器和调度器的影响,并且在休眠期间线程不会释放锁)
4 *
5 * @param millis 休眠时间(毫秒)
6 * @param nanos 休眠时间(纳秒)
7 * @throws IllegalArgumentException 如果传入的毫秒数是负数,会抛出 IllegalArgumentException 异常。
8 * @throws InterruptedException 如果当前线程被中断,则会抛出 InterruptedException 异常,并清除当前线程的中断状态。
9 */
10public static void sleep(long millis, int nanos) throws InterruptedException {
11 if (millis < 0) {
12 throw new IllegalArgumentException("timeout value is negative");
13 }
14
15 if (nanos < 0 || nanos > 999999) {
16 throw new IllegalArgumentException(
17 "nanosecond timeout value out of range");
18 }
19
20 if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
21 millis++;
22 }
23
24 sleep(millis);
25}
8.6 判断线程是否中断方法 interrupted()
该方法会返回当前线程的中断状态,如果当前线程被中断过,则返回 true,否则返回 false。同时,该方法会将中断状态清除,即将中断状态重置为 false。
另外,调用该方法会清除线程的中断状态,因此如果需要保留线程的中断状态,应该使用isInterrupted方法。例如,可以在线程的run方法中使用while(!Thread.interrupted()){...}的形式,实现在不清除中断状态的情况下检查线程是否被中断。
1/**
2 * 检查当前线程是否中断,并将中断状态清除。
3 *
4 * @return 如果当前线程已被中断则返回 true,否则返回 false。
5 */
6public static boolean interrupted() {
7 return currentThread().isInterrupted(true);
8}
8.7 检查线程是否中断方法 isInterrupted()
检查线程的是否中断,且不清除中断状态。
调用该方法会返回线程的中断状态,如果线程被中断过,则返回 true,否则返回 false。该方法不会清除线程的中断状态,即不会将中断状态重置为 false。 通常情况下,isInterrupted() 方法用于在线程执行过程中检查线程的中断状态,以便线程能够适时地响应中断请求。 在线程的run()方法中通常会使用while循环来不断地检查线程的中断状态。
需要注意的是 isInterrupted() 方法不会清除线程的中断状态,即中断状态会一直保持,直到使用 Thread.interrupted() 方法将中断状态清除为止。因此,在检查线程的中断状态时,应该注意不要清除中断状态,以免影响其他线程对该线程的中断状态检查
1/**
2 * 检查线程的是否中断,且不清除中断状态。
3 *
4 * @return 如果当前线程已被中断则返回 true,否则返回 false。
5 */
6public boolean isInterrupted() {
7 return isInterrupted(false);
8}
8.8 获取活动线程数量方法 activeCount()
该方法用于获取当前线程的数量。可以对活动线程数量进行估算,包括守护线程和非守护线程。其中活动线程指的是尚未终止的线程,当一个线程启动时,它会自动添加到活动线程的列表中,当它终止时,它会从列表中移除。因此,使用该方法可以得到当前活动线程的数量,这对于进行线程管理和调试非常有用。
1/**
2 * 获取当前活动线程的数量。
3 *
4 * @return 获取当前活动线程的数量。
5 */
6public static int activeCount() {
7 return currentThread().getThreadGroup().activeCount();
8}
注意: activeCount() 方法仅仅是一个估计值,它并不保证在调用过程中线程状态不会发生变化。
8.9 获取系统中所有线程方法 enumerate(Thread tarray[])
该方法用于获取系统中的所有线程,并将它们存储在 tarray 数组中,然后返回数组中的线程数量。当调用该方法时 JVM 会暂停所有线程的执行,以便获取所有线程的信息,因此该方法对于系统性能有一定的影响。
该方法通常用于进行线程管理和调试。例如,我们可以使用该方法获取当前所有运行中的线程,并输出线程的状态以调试和定位问题。
1/**
2 * 获取系统中的所有线程,并将它们存储在 tarray 数组中,然后返回数组中的线程数量。
3 *
4 * @param tarray 用于存储线程的数组
5 * @return 储在 tarray 数组中的线程数
6 */
7public static int enumerate(Thread tarray[]) {
8 return currentThread().getThreadGroup().enumerate(tarray);
9}
注意: enumerate() 方法只能获取到当前在系统中运行的线程,如果有线程在调用该方法之后启动或终止,那么这些线程将无法被获取到。
九、线程 Thread 中的一些疑问
9.1 线程和进程的关系?
进程是系统内存分配的最小单位,拥有独立的内存空间,并且进程切换时资源开销比较大。线程是系统调度的最小单位,需要共享进程的内存空间,并且线程切换时资源开销比较小。
进程与线程都可以并发执行,并且一个应用至少存在一个进程,一个进程中至少包含一个线程,线程依赖进程,所以线程不能独立于进程而存在;
9.2 线程之间是如何通信的?
如果是同一个进程中的线程进行通信,则可以基于共享内存进行通信,通信过程中需要考虑并发问题,什么时候堵塞线程,什么时候唤醒线程,比如在 Java 中可以使用 wait()
方法堵塞线程,可以使用 notify()
方法唤醒线程。
如果不是同一个进程中的线程进行通信,则可以基于网络进行通信,通信过程中同样也需要考虑并发问题,这时可以使用锁来保证通信流程;
9.3 在 Java 中都有哪种创建线程方式?
在 JAVA 中常用创建线程的方式有三种:
- ① 继承 Thread 类,重写
run()
方法; - ② 实现 Runnable 接口,重写
run()
方法,并将实现接口的类传入 Thread 对象,以此方式来创建一个新的线程; - ③ 实现 Callable 接口,重写
call()
方法,并将实现接口的类传入 FutureTask 对象,再将 FutureTask 对象传入 Thread 对象,以此方式来创建一个新的线程;
9.4 如何终止线程?或者说终止线程的方法有哪些?
可以使用线程类 Thread 的 stop()
方法强行终止线程,不过该方法就像强行给电脑断电一样,可能会造成不可预料的后果,如导致数据不一致,所以不推荐使用。
而是推荐使用 Thread 对象中的 interrupt()
方法,结合 interrupted()
或者 isInterrupted()
方法,实现终止线程。
不过使用时需要注意,如果当前线程处于等待或者睡眠状态时,使用
interrupt()
方法会抛出 InterruptedException 异常,所以需要结合 try-catch 处理 InterruptedException 异常,来实现线程正常终止。
--- END ---
!版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。