第一章 线程的基础概念
JUC是 java.util.concurrent
的简写,在并发编程中使用的工具类。在 JDK 官方手册中可以看到 JUC 相关的 jar 包有三个。
# 一、基础概念
# 1.1 进程与线程A
什么是进程?
进程是指运行中的程序。 比如我们使用钉钉,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。
什么线程?
线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。
举个栗子:房子与人
比如现在有一个100平的房子,这个方式可以看做是一个进程
房子里有人,人就可以看做成一个线程。
人在房子中做一个事情,比如吃饭,学习,睡觉。这个就好像线程在执行某个功能的代码。
所谓进程就是线程的容器,需要线程利用进程中的一些资源,处理一个代码、指令。最终实现进程锁预期的结果。
进程和线程的区别:
- 根本不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位。
- 资源方面:同一个进程下的线程共享进程中的一些资源。线程同时拥有自身的独立存储空间。进程之间的资源通常是独立的。
- 数量不同:进程一般指的就是一个进程。而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程。
- 开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通讯,相当方面。
- ………………
# 1.2 多线程
什么是多线程?
多线程是指:单个进程中同时运行多个线程。
多线程的不低是为了提高CPU的利用率。
可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。
这样可以大幅度的提升程序的效率,提高用户的体验。
比如Tomcat可以做并行处理,提升处理的效率,而不是一个一个排队。
不如要处理一个网络等待的操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率是可以得到大幅度提升的。
多线程的局限
- 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
- 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
- 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
# 1.3 串行、并行、并发
什么是串行:串行就是一个一个排队,第一个做完,第二个才能上。
什么是并行:并行就是同时处理。(一起上!!!)
什么是并发:这里的并发并不是三高中的高并发问题,这里是多线程中的并发概念(CPU调度线程的概念)。CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。
并行囊括并发。
并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。
单核CPU无法实现并行效果,单核CPU是并发。
# 1.4 同步异步、阻塞非阻塞
同步
与异步
:执行某个功能后,被调用者是否会主动反馈信息
阻塞
和非阻塞
:执行某个功能后,调用者是否需要一直等待结果的反馈。
两个概念看似相似,但是侧重点是完全不一样的。
同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。
同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。
异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。
异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。
异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。
# 二、线程的创建
线程的创建分为三种方式:
# 2.1 继承Thread类 重写run方法
启动线程是调用
start
方法,这样会创建一个新的线程
,并执行线程的任务。如果直接调用
run
方法,这样会让当前线程
执行run方法中的业务逻辑。
public class MiTest {
public static void main(String[] args) {
MyJob t1 = new MyJob();
t1.start();
for (int i = 0; i < 100; i++) {
System.out.println("main:" + i);
}
}
}
class MyJob extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("MyJob:" + i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.2 实现Runnable接口 重写run方法
Runnable
是一个表示任务的接口,你可以将实现了 Runnable
接口的对象(任务)交给一个线程去执行。当线程启动时,它会调用 Runnable
对象的 run()
方法,从而执行任务的具体逻辑。 (这样写可以解耦合,目的是为了让线程和线程要干的活分开)
public class MiTest {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main:" + i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("MyRunnable:" + i);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 常用的方式(匿名内部类 or lambda)
- 匿名内部类方式:
// 使用匿名内部类创建一个Runnable对象,然后将其传递给Thread的构造函数 Thread t1 = new Thread(new Runnable() { @Override public void run() { // 这里覆盖Runnable接口的run方法 for (int i = 0; i < 1000; i++) { System.out.println("匿名内部类:" + i); } } });
1
2
3
4
5
6
7
8
9
10 - lambda方式:
// 使用Lambda表达式创建一个Runnable对象,然后将其传递给Thread的构造函数 Thread t2 = new Thread(() -> { // 使用Lambda表达式简化Runnable接口的实现 for (int i = 0; i < 100; i++) { System.out.println("lambda:" + i); } });
1
2
3
4
5
6
7
# Thread 与 Runnable 的关系
new Thread(new Runnable)
的部分源码:
// new Thread(new Runnable) 的方法如下:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
// 省略 ...
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
2
3
4
5
6
7
8
9
10
11
12
可以看到传入的 Runnable 被存在 Thread 的 target 变量里,然后调用 run()
方法时,其实就是调用 Runnable 的 run()
方法。
- 建议
Runnable
实现多线程,这样可以让 Thread 线程类和 Runnable 任务类分开,实现组合调用 - 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
# 2.3 实现Callable 重写call方法,配合FutureTask
- Callable一般用于有返回结果的非阻塞的执行方法,同步非阻塞。
FutureTask
本质上是一个任务包装器,它可以将一个实现了Callable
接口的对象(任务)包装成一个实现了Runnable
接口的对象。这样,我们就可以将这个任务(FutureTask
)交给一个线程去执行。- 另外,
FutureTask
还实现了Future
接口,这意味着我们可以使用FutureTask
对象来获取任务的执行结果,以及查询任务的执行状态(如任务是否已完成、是否已取消等)。
public class MiTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1. 创建MyCallable
MyCallable myCallable = new MyCallable();
//2. 创建FutureTask,传入Callable
FutureTask futureTask = new FutureTask(myCallable);
//3. 创建Thread线程
Thread t1 = new Thread(futureTask);
//4. 启动线程
t1.start();
//5. 做一些操作
//6. 要结果,如果任务尚未完成,此方法会阻塞等待
Object count = futureTask.get();
System.out.println("总和为:" + count);
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 100; i++) {
count += i;
}
return count;
}
}
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
# FutureTask与Callable的关系
FutureTask
是java.util.concurrent
包中的一个类,它实现了RunnableFuture
接口,这个接口又扩展了Runnable
和Future<V>
接口。这允许FutureTask
既可以作为Runnable
被线程执行,又可以作为Future
返回计算结果。
原理简述
FutureTask
作为Runnable
的实现:FutureTask
实现了Runnable
接口,使其可以被Thread
类作为任务执行。当Thread
类的实例启动时(即调用start()
方法),它实际上是在新的线程中执行FutureTask
的run
方法。允许使用
Callable
:FutureTask
构造函数接收一个Callable<V>
实例。与Runnable
不同,Callable
可以有返回值,并且可以抛出异常。FutureTask
在其run
方法中执行Callable
任务,并保存计算结果或异常,使得这些结果或异常可以在未来的某个时刻通过get
方法被检索。阻塞与返回:调用
get()
方法会阻塞调用线程直到任务执行完成,即计算结果变得可用或任务被取消。这是通过内部维护一个状态来实现的,状态反映了任务的执行情况(例如,是否完成或取消)。如果任务已完成,get()
方法立即返回结果;如果任务还未完成,get()
方法将阻塞直到任务完成。
// 注意下面很多关键代码都省略了,只保留了关键执行逻辑
public class FutureTask<V> implements RunnableFuture<V> {
// FutureTask构造函数,需要一个Callable对象
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable; // 保存Callable任务
this.state = NEW; // 初始化任务状态为NEW
}
// 执行核心run方法
public void run() {
// 判断任务状态等逻辑,略
try {
V result = callable.call(); // 尝试执行Callable任务
set(result); // 保存正常结果
} catch (Exception e) {
setException(e); // 保存异常结果
}
}
// 保存正常结果
protected void set(V v) {
// 尝试将任务状态从NEW更新为COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v; // 保存任务执行的结果
// 使用UNSAFE.putOrderedInt确保outcome的写入对其他线程可见,并将状态更新为NORMAL
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 更新任务状态为NORMAL,表示任务正常完成
finishCompletion(); // 完成后续的清理和通知等待的线程
}
}
// 阻塞获取结果
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING) // 如果任务还未完成
s = awaitDone(false, 0L); // 阻塞等待任务完成
return report(s); // 返回任务结果
}
// 等待任务完成
private int awaitDone(boolean timed, long nanos) throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
// 处理中断逻辑
}
int s = state;
if (s > COMPLETING) {
// 如果任务已完成
return s;
} else if (s == COMPLETING) {
// 如果任务即将完成,让出CPU时间片,让其他线程工作
Thread.yield();
} else if (q == null) {
// 初始化等待节点
q = new WaitNode();
} else if (!queued) {
// 将当前线程加入等待队列
} else if (timed) {
// 如果设置了超时,处理超时逻辑
} else {
// 阻塞当前线程,等待唤醒
LockSupport.park(this);
}
}
}
// 处理并返回最终结果
private V report(int s) throws ExecutionException {
Object x = outcome; // 从outcome字段获取结果或异常
if (s == NORMAL) // 如果任务状态为NORMAL,表示正常完成
return (V)x;
if (s >= CANCELLED) // 如果任务状态表示被取消
throw new CancellationException();
throw new ExecutionException((Throwable)x); // 如果是其他状态,抛出执行异常
}
}
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
# 2.4 基于线程池构建线程
追其底层,其实只有一种,实现Runnble
# 2.5 几种创建方式的区别
- 继承Thread类:这种方式的缺点是由于Java的单继承性,如果已经继承了Thread类,那么就不能再继承其他类。在多线程共享对象时,需要使用static修饰的成员变量.
- 实现Runnable接口:这种方式可以避免由于Java的单继承性而带来的限制,并且它更好地支持多个线程共享一个实例。实现Runnable接口的类只需要将实例传递给Thread类的构造方法,多个线程就可以共享同一个target实例的资源,这样就无需使用static成员来实现共享。
- 实现Callable接口:从JDK1.5开始,Java提供了一种新的创建线程的方式,那就是实现Callable接口,这种方式的好处在于线程有返回值,并且能抛出异常。然而,创建这样的线程稍微麻烦一些,需要通过FutureTask包装器,它是一个包装器,只是用来将Callable转换成Future和Runnable,它同时实现了两个接口。
无论你选择何种方式创建线程,只要你有多个线程共享数据,就需要处理以下问题。
- 数据一致性问题:如果多个线程同时读写这个静态变量,那么可能会发生数据不一致的问题。比如,一个线程在读取变量的值的同时,另一个线程修改了这个值,那么前一个线程读取的就是一个过时的值。
- 线程同步问题:由于所有线程共享同一个静态变量,我们通常需要使用同步机制(如synchronized关键字)来保证操作的原子性,以避免数据错误。
# 三、线程的使用
# 3.1 线程的状态
网上对线程状态的描述很多,有5种,6种,7种,都可以接受
5中状态一般是针对传统的线程状态来说(操作系统层面)
Java中给线程准备的6种状态
可以在在Thread类中查看枚举State
就能看到以下六中状态:
NEW
:Thread对象被创建出来了,但是还没有执行start方法。RUNNABLE
:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)BLOCKED
、WAITING
、TIME_WAITING
:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程BLOCKED
:synchronized没有拿到同步锁,被阻塞的情况WAITING
:调用wait方法就会处于WAITING状态,需要被手动唤醒TIME_WAITING
:调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒TERMINATED
:run方法执行完毕,线程生命周期到头了
在Java代码中验证一下效果
NEW:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 这里不执行任何操作
});
// 打印出NEW,因为线程t1已被创建但尚未启动
System.out.println(t1.getState());
}
2
3
4
5
6
7
RUNNABLE:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
// 无限循环使线程保持运行状态
}
});
t1.start(); // 启动线程
Thread.sleep(500); // 确保线程已启动
// 打印出RUNNABLE,因为线程t1正在运行
System.out.println(t1.getState());
}
2
3
4
5
6
7
8
9
10
11
BLOCKED:
// 如果`t1`线程和主线程操作的是不同的`obj`实例,那么它们就不会相互阻塞。
// 这是因为每个对象实例都有自己独立的锁,不同实例之间的锁互不影响。
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
// t1线程尝试进入同步块,但由于main线程持有锁,t1线程被阻塞
}
});
synchronized (obj) {
t1.start(); // 启动线程t1
Thread.sleep(500); // 确保t1有足够的时间尝试进入同步块
// 打印出BLOCKED,因为t1线程试图获取已被main线程持有的锁
System.out.println(t1.getState());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
WAITING:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
try {
obj.wait(); // t1线程在obj对象上等待,进入WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start(); // 启动线程
Thread.sleep(500); // 确保线程t1有足够的时间进入wait状态
// 打印出WAITING,因为t1线程正在obj上等待
System.out.println(t1.getState());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000); // t1线程睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start(); // 启动线程
Thread.sleep(500); // 主线程等待500毫秒,这时t1线程还在sleep
// 打印出TIMED_WAITING,因为t1线程正在执行带有时间限制的等待
System.out.println(t1.getState());
}
2
3
4
5
6
7
8
9
10
11
12
13
TERMINATED:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(500); // t1线程睡眠500毫秒然后结束
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start(); // 启动线程
Thread.sleep(1000); // 主线程等待1秒,确保t1线程已经执行完毕
// 打印出TERMINATED,因为t1线程已经运行完毕
System.out.println(t1.getState());
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.2 线程的常用方法
# 3.2.1 获取当前线程
Thread的静态方法获取当前线程对象
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 获取当前线程的方法
Thread main = Thread.currentThread();
System.out.println(main);
// 输出的字符串格式是"Thread[线程名称,线程优先级,线程组名称]";
// 例如:"Thread[main,5,main]"
}
2
3
4
5
6
7
8
# 3.2.2 线程的名字
在构建Thread对象完毕后,一定要设置一个有意义的名称,方面后期排查错误
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式一
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()); // 获取当前线程名称
});
t1.setName("模块-功能-刷题"); // 在线程启动之前设置线程名称
t1.start();
// 方式二
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()); // 获取当前线程名称
},"模块-功能-点赞"); // 在创建线程时直接通过构造函数设置线程名称
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.2.3 线程的优先级
Java中线程优先级是用来指示线程相对于其他线程执行的优先顺序。优先级由1到10的整数表示,Thread
类中定义了三个静态变量:MIN_PRIORITY
(1),NORM_PRIORITY
(5),和MAX_PRIORITY
(10)。
- 设置优先级:使用
setPriority(int priority)
方法。默认情况下,线程被赋予NORM_PRIORITY
(5)。 - 有效范围:优先级值必须在
MIN_PRIORITY
到MAX_PRIORITY
之间,否则会抛出IllegalArgumentException
。
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t1:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("t2:" + i);
}
});
t1.setPriority(1); // 设置线程优先级
t2.setPriority(10); // 设置线程优先级
t2.start();
t1.start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 3.2.4 线程的让步
线程让步是指当前正在执行的线程主动放弃其当前的CPU执行权,但不会放弃锁资源。在Java中,这可以通过Thread.yield()
静态方法实现。调用yield()
方法后,当前线程从运行状态
转变为就绪状态
,重新等待CPU调度,给其他同优先级的线程运行的机会。
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
if(i == 50){
Thread.yield(); // 提示线程调度器让出当前线程对 CPU 的使用
}
System.out.println("t1:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100; i++) {
System.out.println("t2:" + i);
}
});
t2.start();
t1.start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.2.5 线程的休眠
Thread的静态方法,让当前线程暂停执行,进入等待状态,直到休眠时间结束或被中断。
sleep有两个方法重载:
static void sleep(long millis)
:使当前线程休眠指定的毫秒数。static void sleep(long millis, int nanos)
:使当前线程休眠指定的毫秒数加上指定的纳秒数。- 中断:如果线程在
sleep
期间被中断,它会提前结束休眠,抛出InterruptedException
,表示线程的休眠被打断。
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(1000); // 线程休眠
System.out.println(System.currentTimeMillis());
}
2
3
4
5
# 3.2.6 线程的强占
Thread的非静态方法join方法,需要在某一个线程下去调用这个方法。
Thread
类的join
方法是一个让当前线程等待另一个线程完成的方法。当你在一个线程中调用另一个线程的join
方法时,当前线程会暂停执行,直到被join
的线程执行完毕或者达到了指定的等待时间。这是一个非常有用的方法,用来协调不同线程之间的工作流程。下面是对这个概念的简化解释:
使用join()
方法
- 场景: 当你在主线程中调用
t1.join()
, - 效果: 主线程会停下来等待
t1
线程完成所有的工作。只有当t1
线程的任务全部完成后,主线程才会继续执行。
使用join(long millis)
方法
- 场景: 当你在主线程中调用
t1.join(2000)
, - 效果: 主线程会等待最多2000毫秒(也就是2秒)让
t1
线程运行。如果这2秒内t1
线程完成了任务,主线程会立即恢复执行。如果t1
线程在2秒内没有完成,那么主线程会在等待了2秒后继续执行,不管t1
线程是否已经执行完毕。
public static void main(String[] args) throws InterruptedException {
// 创建一个新的线程t1
Thread t1 = new Thread(() -> {
// t1线程执行的任务:打印10次消息,每次打印后暂停1秒
for (int i = 0; i < 10; i++) {
System.out.println("t1:" + i);
try {
Thread.sleep(1000); // 让t1线程暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start(); // 启动t1线程
// 主线程自身也执行类似的任务:打印10次消息,每次打印后暂停1秒
for (int i = 0; i < 10; i++) {
System.out.println("main:" + i);
try {
Thread.sleep(1000); // 主线程暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 1){
try {
// 主线程会等待直到2秒后继续执行
t1.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
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
# 3.2.7 守护线程
非守护线程(Non-Daemon Thread):只要有任何一个非守护线程还在运行,JVM就不会退出。JVM会等待所有的非守护线程都执行完成后才会停止。主线程和你通过
new Thread()
创建的线程默认都是非守护线程。守护线程(Daemon Thread):守护线程主要是为其他线程(比如用户线程)服务的;当所有的非守护线程都结束时,JVM不会等待守护线程完成,而是直接退出。这意味着,如果JVM中只剩下守护线程运行,JVM就会退出。守护线程通常用于执行一些后台任务,比如垃圾回收线程。
当你在
main
方法(或任何非守护线程中)创建一个新线程时,这个新线程默认是非守护线程。使用
setDaemon(true)
方法可以将线程设置为守护线程。这个方法必须在线程启动之前调用。注意:一旦线程启动,你不能修改它的守护状态。
public static void main(String[] args) throws InterruptedException {
// 创建一个新的线程t1
Thread t1 = new Thread(() -> {
// t1线程执行的任务:循环打印消息,并在每次打印后暂停1秒
for (int i = 0; i < 10; i++) {
System.out.println("t1:" + i);
try {
Thread.sleep(1000); // 让t1线程暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 将t1线程设置为守护线程。这一行必须在t1线程启动之前执行。
// 守护线程的特点是:它不会阻止JVM的退出。当所有的非守护线程都结束时,
// JVM会自动退出,不会等待守护线程完成。
t1.setDaemon(true);
// 启动t1线程
t1.start();
// 程序运行很快就结束了
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2.8 线程的等待和唤醒
当我们在多线程编程中使用对象锁来协调线程间的操作时,wait
和notify
/notifyAll
方法是控制线程等待和唤醒的重要机制。
public class MiTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程t1
Thread t1 = new Thread(() -> {
sync(); // 调用同步方法
},"t1");
// 创建线程t2
Thread t2 = new Thread(() -> {
sync(); // 调用同步方法
},"t2");
t1.start(); // 启动线程t1
t2.start(); // 启动线程t2
// 主线程等待12秒,确保t1和t2进入wait状态
Thread.sleep(12000);
// 在MiTest类的锁上进行同步
synchronized (MiTest.class) {
// 唤醒所有等待MiTest类锁的线程
MiTest.class.notifyAll();
}
}
// 定义一个同步方法,这个方法在MiTest类的锁上同步
public static synchronized void sync() {
try {
for (int i = 0; i < 10; i++) {
if(i == 5) {
// 当i等于5时,当前线程在MiTest类的锁上等待
// 这会释放锁并使当前线程进入等待状态,直到其他线程调用notify()或notifyAll()
MiTest.class.wait();
}
Thread.sleep(1000); // 每次循环延时1秒
// 打印当前线程的名字
System.out.println(Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
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
- 使用
wait
方法让线程等待:- 当一个线程执行到一个同步(
synchronized
)块或方法中的wait()
调用时,它会停止执行后面的代码并释放
它持有的对象锁。 - 这意味着该线程进入了对象的“等待池”,在那里它会等待直到其他线程调用相同对象的
notify()
或notifyAll()
方法。
- 当一个线程执行到一个同步(
- 使用
notify
或notifyAll
唤醒等待的线程:notify()
方法会从等待池中随机选择一个线程,使其准备重新获取对象锁。一旦当前线程释放锁,被唤醒的线程会尝试获得锁并继续执行。notifyAll()
方法会唤醒等待池中的所有线程,这些线程随后将进入锁池,并在锁可用时尝试获取锁。
wait
和notify
/notifyAll
的使用条件:- 这些方法必须在同步(
synchronized
)上下文中调用,因为它们涉及到对象锁和类锁的操作。只有在持有对象锁或类锁的线程内部,才能调用该对象的wait
、notify
或notifyAll
方法。 - 当线程在类锁上等待时(如通过
MiTest.class.wait()
),只要任何线程获取到这个类的类锁,就可以唤醒在此类锁上等待的线程,而这与类的实例无关。 - 当线程在对象锁上等待时,只有获取到那个特定对象锁的线程才能唤醒在该对象上等待的线程。
- 如果在非同步上下文中调用这些方法,会抛出
IllegalMonitorStateException
异常。
- 这些方法必须在同步(
- 释放锁与重新竞争锁:
- 调用
wait()
的线程会释放锁,进入等待状态。当通过notify
或notifyAll
被唤醒时,线程不会立即执行,而是要重新竞争对象锁。只有当它再次获取到锁之后,才能继续执行。
- 调用
# 3.3 线程的结束方式
线程结束方式很多,最常用就是让线程的run方法结束,无论是return结束,还是抛出异常结束,都可以。
# 3.3.1 stop方法(不用)
强制让线程结束,无论你在干嘛,不推荐使用当然当然方式,但是,他确实可以把线程干掉
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
t1.stop();
System.out.println(t1.getState());
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.3.2 使用共享变量(很少会用)
这种方式用的也不多,有的线程可能会通过死循环来保证一直运行。
咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(flag){
// 处理任务
}
System.out.println("任务结束");
});
t1.start();
Thread.sleep(500);
flag = false;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3.3.3 interrupt方式
Java提供了一种中断机制来请求停止线程。每个线程都有一个内部的中断状态标志,可以通过调用线程的interrupt()
方法来设置这个标志。当线程中的代码检查到中断请求时,可以适当地响应中断,从而安全地停止线程。
Thread.currentThread().isInterrupted()
:这个非静态方法用于检查当前线程是否被中断(中断状态标志是否为true
),调用后不会改变中断状态标志。主要用于“查看”而不改变中断状态。Thread.interrupted()
:这个静态方法也用于检查当前线程是否被中断,但与isInterrupted()
不同的是,它会清除中断状态标志,即如果此前中断状态标志为true
,调用后会变为false
。这个方法适用于需要“重置”中断状态的场景。
通过打断等待(WAITING)或限时等待(TIMED_WAITING)状态的线程,然后通过捕获和处理异常来安全地停止线程,是Java多线程编程中一种常用且推荐的线程停止方式。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
// 获取任务
// 拿到任务,执行任务
// 没有任务了,让线程休眠
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("基于打断形式结束当前线程");
return;
}
}
});
t1.start();
Thread.sleep(500);
t1.interrupt(); // 调用 interrupt() 方法来通知
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 中断信号:调用线程的
interrupt()
方法会向该线程发出一个中断请求,这实质上是将线程的中断状态标志设置为true
。 - 阻塞方法和中断:如果线程在执行某些阻塞操作(如
Thread.sleep()
,Object.wait()
,Thread.join()
等)时被中断,这些方法如果检测
到中断状态标志为true
,就会抛出一个InterruptedException
异常,并且在抛出异常前,JVM会自动清除中断状态,即将中断状态标志重新设置为false
。 - 异常处理和程序控制权:捕获到
InterruptedException
后,程序控制权回到了程序员手中。此时程序员可以选择如何响应中断:- 可以选择忽略这个中断(这通常不是一个好的做法)。
- 可以做一些清理工作后结束线程或者方法的执行。
- 可以决定重新设置中断状态(通过
Thread.currentThread().interrupt()
),以便在后续的执行中继续响应中断。
- 程序继续执行:抛出
InterruptedException
本身并不会自动结束线程或者方法的执行。如果不显式处理这个异常来结束线程,线程会继续执行那个异常处理块之后的代码。所以,中断机制提供了一种协作式的线程停止策略,它需要线程的实现者明确地检查和响应中断请求。
为什么阻塞状态被打断会清除中断状态标志?
目的是为了避免重新进入阻塞状态,如果中断状态没有被自动清除,那么任何后续的阻塞操作(如再次调用sleep
)都会立即因为已设置的中断状态而抛出InterruptedException
,这并不是被中断线程所希望的行为。
# 3.4 wait和sleep的区别?
所属类
- sleep():属于
Thread
类的静态方法。 - wait():属于
Object
类的方法。
锁的处理
- sleep():当线程执行
sleep()
方法时,不会释放持有的任何锁资源。即使在sleep()
期间,线程仍然保持对锁的占有。 - wait():当线程执行
wait()
方法时,会释放掉它持有的对该对象的锁,允许其他线程进入同步代码块或方法。一旦notify()
或notifyAll()
被调用,线程可以再次竞争重新获取锁。
唤醒条件
- sleep():线程在指定的时间过后会自动唤醒并继续执行。
- wait():需要通过
notify()
或notifyAll()
方法被显式唤醒。如果没有收到通知,线程会一直在等待状态中。
使用场合
- sleep():可以在任何地方调用,不依赖对象锁。通常用于暂时挂起执行的线程,不涉及锁的操作或对象的等待/通知机制。
- wait():必须在同步块或同步方法中调用(即在持有对象锁的上下文中)。用于线程间的协作和通信,如等待某个条件满足时再继续执行。
对象监视器(ObjectMonitor)
- wait():执行
wait()
会将线程放入对象的等待集合(WaitSet)中。这是基于对象监视器机制的一部分,确保了只有在持有相应对象锁的情况下,才能调用wait()
,以避免并发访问冲突。
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于scholar (opens new window) (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。