Java 并发 - 线程基础
# Java 并发 - 线程基础
- 线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?
- 通常线程有哪几种使用方式?
- 线程的中断方式有哪些?
- 线程的互斥同步方式有哪些? 如何比较和选择?
- 线程之间有哪些协作方式?
- 线程间有哪些通信方式?
# 1. 进程与线程基本概念
# 1.1 进程与线程产生背景
最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。这样效率非常低下,在很多时候,计算机都处在等待状态。
批处理操作系统
后来有了批处理操作系统,把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高。
进程的提出
人们对于计算机的性能要求越来越高,现有的批处理操作系统并不能满足人们的需求,而批处理操作系统的瓶颈在于内存中只存在一个程序,那么内存中能不能存在多个程序呢?这是人们亟待解决的问题。
于是,科学家们提出了进程的概念。
进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。
程序:用某种编程语言(java、python等)编写,能够完成一定任务或者功能的代码集合,是指令和数据的有序集合,是一段静态代码。
此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行。
使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同一时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源。
对操作系统的要求进一步提高
虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
比如杀毒软件在检测用户电脑时,如果在某一项检测中卡住了,那么后面的检测项也会受到影响。或者说当你使用杀毒软件中的扫描病毒功能时,在扫描病毒结束之前,无法使用杀毒软件中清理垃圾的功能,这显然无法满足人们的要求。
线程的提出
那么能不能让这些子任务同时执行呢?于是人们又提出了线程的概念,让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。
使用线程之后,事情就变得简单多了。当用户使用扫描病毒功能时,就让扫描病毒这个线程去执行。同时,如果用户又使用清理垃圾功能,那么可以先暂停扫描病毒线程,先响应用户的清理垃圾的操作,让清理垃圾这个线程去执行。响应完后再切换回来,接着执行扫描病毒线程。
注意:操作系统是如何分配时间片给每一个线程的,涉及到线程的调度策略,有兴趣的同学可以看一下《操作系统》,本文不做深入详解。
总之,进程和线程的提出极大的提高了操作系统的性能。进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
多进程的方式也可以实现并发,为什么我们要使用多线程?
多进程方式确实可以实现并发,但使用多线程,有以下几个好处:
- 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
- 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
进程和线程的区别
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
# 1.2 上下文切换
上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器是cpu内部的少量的速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。
程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体实现依赖于特定的系统。
举例说明 线程A - B
1.先挂起线程A,将其在cpu中的状态保存在内存中。
2.在内存中检索下一个线程B的上下文并将其在 CPU 的寄存器中恢复,执行B线程。
3.当B执行完,根据程序计数器中指向的位置恢复线程A。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。如何减少系统中上下文切换次数,是提升多线程性能的一个重点课题。
# 2. 线程状态转换(生命周期)
在 Java 领域,实现并发程序的主要手段就是多线程。线程是操作系统里的一个概念,虽然各种不同的开发语言如 Java、C# 等都对其进行了封装,但是万变不离操作系统。Java 语言里的线程本质上就是操作系统的线程,它们是一一对应的。
虽然不同的开发语言对于操作系统线程进行了不同的封装,但是对于线程的生命周期这部分,基本上是雷同的。所以,我们可以先来了解一下通用的线程生命周期模型,这部分内容也适用于很多其他编程语言;然后再详细有针对性地学习一下 Java 中线程的生命周期。
# 2.1 通用线程状态转换
通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。
- 初始状态(ready):指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
- 可运行状态(ready):指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
- 运行状态(running):当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
- 休眠状态(waitting):运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
- 终止状态(terminated):线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。
除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态(这个下面我们会详细讲解)。
# 2.2 Java 中线程状态转换
Java 中线程有 6 个状态:
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
2
3
4
5
6
7
8
9
线程状态转换图如下:
这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。
# 2.2.1 NEW
处于 NEW 状态的线程尚未启动,这里的尚未启动指的是还没有调用 Thread 的 start() 方法。
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}
2
3
4
从上面可以看出,只是创建了线程而并没有调用 start() 方法,此时线程处于 NEW 状态。
关于 start() 的两个引申问题
- 反复调用同一个线程的 start() 方法是否可行?
- 假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start() 方法是否可行?
要分析这两个问题,我们先来看看 start() 的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
我们可以看到,在start()内部,这里有一个threadStatus 的变量。如果它不等于0,调用start()是会直接抛出异常的。
当调用 thread.start() 方法,线程处于 RUNNABLE 状态且 threadStatus 肯定不等于 0,同理线程执行完毕(此时处于TERMINATED状态),threadStatus 肯定也不等于 0,所以都不能再次调用线程的 start() 方法,都会抛出 IllegalThreadStateException 异常。
# 2.2.2 RUNNABLE
表示当前线程正在运行中。处于RUNNABLE状态的线程在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。
Java中线程的RUNNABLE状态
看了操作系统线程的几个状态之后我们来看看Thread源码里对RUNNABLE状态的定义:
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
2
3
4
5
6
Java 线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 ready 和 running 两个状态的。
# 2.2.3 BLOCKED
阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。
我们用 BLOCKED 状态举个生活中的例子:
假如今天你下班后准备去食堂吃饭。你来到食堂仅有的一个窗口,发现前面已经有个人在窗口前了,此时你必须得等前面的人从窗口离开才行。 假设你是线程t2,你前面的那个人是线程t1。此时t1占有了锁(食堂唯一的窗口),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。
# 2.2.4 WAITING
无限期等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒。
调用如下3个方法会使线程进入等待状态:
- Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
- Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
- LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
我们延续上面的例子继续解释一下 WAITING 状态:
你等了好几分钟现在终于轮到你了,突然你们有一个“不懂事”的经理突然来了。他把你拉到一旁叫你待会儿再吃饭,说他下午要去作报告,赶紧来找你了解一下项目的情况。你心里虽然有一万个不愿意但是你还是从食堂窗口走开了。
此时,假设你还是线程 t2,你的经理是线程t1。虽然你此时都占有锁(窗口)了,“不速之客”来了你还是得释放掉锁。此时你 t2 的状态就是 WAITING 。然后经理 t1 获得锁,进入 RUNNABLE 状态。
要是经理 t1 不主动唤醒你 t2(notify、notifyAll..),可以说你 t2 只能一直等待了。
# 2.2.5 TIMED_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
- Thread.sleep(long millis):使当前线程睡眠指定时间;
- Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
- Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
- LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
- LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
我们继续延续上面的例子来解释一下TIMED_WAITING状态:
到了第二天中午,又到了饭点,你还是到了窗口前。突然间想起你的同事叫你等他一起,他说让你等他十分钟他改个 bug。你就离开了窗口。很快十分钟过去了,你见他还没来,你想都等了这么久了还不来,那还是先去吃饭好了。
这时你还是线程 t1,你改 bug 的同事是线程 t2。t2 让 t1 等待了指定时间,此时 t1 等待期间就属于TIMED_WATING 状态。t1 等待 10 分钟后,就自动唤醒,拥有了去争夺锁的资格。
# 2.2.6 TERMINATED
终止状态。此时线程已执行完毕。
# 2.3 BLOCKED 与 RUNNABLE 状态的转换
我们在上面说到:处于 BLOCKED 状态的线程是因为在等待锁的释放。假如这里有两个线程 a 和 b,a 线程提前获得了锁并且暂未释放锁,此时 b 就处于 BLOCKED 状态。我们先来看一个例子:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} 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
大家可能会觉得线程 a 会先调用同步方法,同步方法内又调用了 Thread.sleep() 方法,必然会输出TIMED_WAITING,而线程 b 因为等待线程 a 释放锁所以必然会输出 BLOCKED。
其实不然,有两点需要值得大家注意,一是在测试方法 blockedTest() 内还有一个 main 线程,二是启动线程后执行 run 方法还是需要消耗一定时间的。
测试方法的 main 线程只保证了 a,b 两个线程调用 start() 方法(转化为 RUNNABLE 状态),如果 CPU 执行效率高一点,还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。
当然,如果 CPU 执行效率低一点,其中某个线程也是可能打印出 BLOCKED 状态的(此时两个线程已经开始争夺锁了)。
要是想要打印出 BLOCKED 状态我该怎么处理呢?BLOCKED 状态的产生需要两个线程争夺锁才行。那我们处理下测试方法里的 main 线程就可以了,让它在线程 a 和 b start() 进入 RUNNABLE 状态
这里需要注意的是 main 线程休息的时间,要保证在线程争夺锁的时间内,不要等到前一个线程锁都释放了你再去争夺锁,此时还是得不到 BLOCKED 状态的。修改上面的代码:
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
b.start();
for(int i = 0; i < 15; i++) {
System.out.println(a.getName() + i + ":" + a.getState()); // 输出?
System.out.println(b.getName() + i + ":" + b.getState()); // 输出?
Thread.sleep(300L);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在这个例子中两个线程的状态转换如下
- a 的状态转换过程:RUNNABLE(
a.start()
) -> TIMED_WATING(Thread.sleep()
)-> RUNABLE(sleep()
时间到)-> BLOCKED(未抢到锁) -> TERMINATED - b的状态转换过程:RUNNABLE(
b.start()
) -> BLOCKED(未抢到锁) -> TIMED_WATING(Thread.sleep()
)-> RUNABLE(sleep()时间到)-> TERMINATED
斜体表示可能出现的状态, 大家可以在自己的电脑上多试几次看看输出。同样,这里的输出也可能有多钟结果。
# 2.4 WAITING 与 RUNNABLE 状态的转换
根据转换图我们知道有 3 个方法可以使线程从 RUNNABLE 状态转为 WAITING 状态。
Object.wait()
调用 wait() 方法前线程必须持有对象的锁。
线程调用 wait() 方法时,会释放当前的锁,直到有其他线程调用 notify()/notifyAll() 方法唤醒等待锁的线程。
需要注意的是,其他线程调用 notify() 方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用 wait() 方法的线程。
同样,调用 notifyAll() 方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
调用join()方法,会一直等待这个线程执行完毕(转换为 TERMINATED 状态)。
我们再把上面的例子线程启动那里改变一下:
public void blockedTest() { ······ a.start(); a.join(); b.start(); System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED System.out.println(b.getName() + ":" + b.getState()); }
1
2
3
4
5
6
7
8要是没有调用 join方法,main 线程不管a线程是否执行完毕都会继续往下走。
a 线程启动之后马上调用了 join 方法,这里 main 线程就会等到 a 线程执行完毕,所以这里 a 线程打印的状态固定是 TERMINATED。
至于b线程的状态,有可能打印 RUNNABLE(尚未进入同步方法),也有可能打印 TIMED_WAITING(进入了同步方法)。
**LockSupport.park() **
调用 LockSupport.park() 方法。其中的 LockSupport 对象,也许你有点陌生,其实 Java 并发包中的锁,都是基于它实现的。调用 LockSupport.park() 方法,当前线程会阻塞,线程的状态会从 RUNNABLE 转换到 WAITING。调用 LockSupport.unpark(Thread thread) 可唤醒目标线程,目标线程的状态又会从 WAITING 状态转换到 RUNNABLE。
# 2.5 TIMED_WAITING 与 RUNNABLE 状态的转换
TIMED_WAITING 与 WAITING 状态类似,只是 TIMED_WAITING 状态等待的时间是指定的。
Thread.sleep(long)
使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入 RUNNABLE 状态。
Object.wait(long)
wait(long) 方法使线程进入 TIMED_WAITING 状态。这里的 wait(long) 方法与无参方法 wait() 相同的地方是,都可以通过其他线程调用notify()或notifyAll()方法来唤醒。
不同的地方是,有参方法 wait(long) 就算其他线程不来唤醒它,经过指定时间long之后它会自动唤醒,拥有去争夺锁的资格。
Thread.join(long)
join(long) 使当前线程执行指定时间,并且使线程进入TIMED_WAITING 状态。
我们再来改一改刚才的示例:
public void blockedTest() { ······ a.start(); a.join(1000L); b.start(); System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING System.out.println(b.getName() + ":" + b.getState()); }
1
2
3
4
5
6
7
8这里调用 a.join(1000L),因为是指定了具体 a 线程执行的时间的,并且执行时间是小于 a 线程 sleep 的时间,所以 a 线程状态输出 TIMED_WAITING。b 线程状态仍然不固定(RUNNABLE 或 BLOCKED)。
调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
调用带超时参数的 **LockSupport.parkUntil(long deadline) **方法。
# 2.6 从 NEW 到 RUNNABLE 状态
Java 刚创建出来的 Thread 对象就是 NEW 状态,如下:
// 实现Runnable接口
class Runner implements Runnable {
@Override
public void run() {
// 线程需要执行的代码
......
}
}
// 创建线程对象
Thread thread = new Thread(new Runner());
2
3
4
5
6
7
8
9
10
NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。从 NEW 状态转换到 RUNNABLE 状态很简单,只要调用线程对象的 start() 方法就可以了,示例代码如下:
MyThread myThread = new MyThread();
// 从NEW状态转换到RUNNABLE状态
myThread.start();
2
3
# 2.7 从 RUNNABLE 到 TERMINATED 状态
线程执行完 run() 方法后,会自动转换到 TERMINATED 状态,当然如果执行 run() 方法的时候异常抛出,也会导致线程终止。
# 3. 线程的使用方式
有三种线程使用方式:
- 继承 Thread 类,并重写 run() 方法
- 实现 Runnable 接口的 run() 方法
- 实现 Callable 接口的 call() 方法
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
# 3.1 继承 Thread
首先我们来看看怎么用Thread
和Runnable
来写一个Java多线程程序。首先是继承Thread
类:
public class Demo {
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
需要注意,在调用 myThread.start() 后,线程才算启动。
我们在程序里面调用了 start() 方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用 run() 方法。
注意不可多次调用 start() 方法。在第一次调用 start() 方法后,再次调用 start() 方法会抛出IllegalThreadStateException 异常。
# 3.2 实现 Runnable 接口
接着我们来看一下Runnable
接口(JDK 1.8 +):
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
2
3
4
可以看到Runnable
是一个函数式接口,这意味着我们可以使用 Java 8的函数式编程 来简化代码。
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
// Java 8 函数式编程,可以省略 MyThread 类
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
}).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 3.3 Thread 类
# 3.3.1 Thread 构造函数
Thread
类是一个Runnable
接口的实现类,我们来看看Thread
类的源码。
查看Thread
类的构造方法,发现其实是简单调用一个私有的init
方法来实现初始化。init
的方法签名:
// Thread类源码
// 片段1 - init方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
// 片段2 - 构造函数调用init方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
// 片段3 - 使用在init方法里初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 片段4 - 两个对用于支持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们挨个来解释一下init
方法的这些参数:
g:线程组,指定这个线程是在哪个线程组下;
target:指定要执行的任务;
name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见片段2;
acc:见片段3,用于初始化私有变量
inheritedAccessControlContext
。这个变量有点神奇。它是一个私有变量,但是在
Thread
类里只有init
方法对它进行初始化,在exit
方法把它设为null
。其它没有任何地方使用它。一般我们是不会使用它的,那什么时候会使用到这个变量呢?可以参考这个stackoverflow的问题:Restrict permissions to threads which execute third party software (opens new window);inheritThreadLocals:可继承的
ThreadLocal
,见片段4,Thread
类里面有两个私有属性来支持ThreadLocal
,我们会在后面的章节介绍ThreadLocal
的概念。
实际情况下,我们大多是直接调用下面两个构造方法:
Thread(Runnable target)
Thread(Runnable target, String name)
2
# 3.3.2 Thread 类常用函数
这里介绍一下Thread类的几个常用的方法:
currentThread():静态方法,返回对当前正在执行的线程对象的引用;
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield 在英语里有放弃的意思,同样,这里的 yield() 指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了 yield() 方法,程序在调度的时候,也还有可能继续运行这个线程的;
sleep():静态方法,休眠当前正在执行的线程,单位为毫秒。sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } }
1
2
3
4
5
6
7join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;
setDaemon() : 守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。设置线程为守护线程。
public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.setDaemon(true); }
1
2
3
4
# 3.3.3 Thread 类与 Runnable 接口的比较
实现一个自定义的线程类,可以有继承Thread
类或者实现Runnable
接口这两种方式,它们之间有什么优劣呢?
- 由于 Java 是单继承,多实现的特性,所以 Runnable 接口使用起来比 Thread 类更加方便
- Runnable 接口更加符合面向对象编程风格,将线程单独进行对象的封装
- Runnable 接口降低了线程对象和线程任务的耦合性
- 如果使用线程时不需要使用 Thread 类的方法,那么使用 Runnable 明显更加轻量
所以,我们通常优先使用“实现Runnable
接口”这种方式来自定义线程类。
# 3.4 Callable、Future 与 FutureTask
通常来说,我们使用 Thread 类和 Runnable 接口来创建一个新线程,但它们有个弊端,就是 run() 方法没有返回值。而有时候我们希望开启一个新线程去执行任务,当任务执行完成后有一个返回值。
JDK 提供了 Callable 和 Future 接口解决这个问题,这也就是所谓的”异步“模型。
# 3.4.1 Callable 接口
Callable 和 Runnable 接口类似,都是只有一个抽象方法的函数式接口。只不过 Callable 提供的 call() 方法有返回值,并且支持泛型。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
2
3
4
Callable
一般是配合线程池工具ExecutorService
来使用的。比如 ExecutorService可以使用
submit方法来让一个
Callable接口执行。它会返回一个
Future,后续的程序可以通过这个
Future的
get`方法得到结果。
这里可以看一个简单的使用 demo:
// 自定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调用get方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get方法。
System.out.println(result.get());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出结果:
2
# 3.4.2 Future 接口
Future 接口只有几个简单的方法:
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
2
3
4
5
6
7
8
cancel
方法指试图取消一个线程的执行。注意是试图取消,并不一定能取消成功。因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean
类型的返回值是“是否取消成功”的意思。参数paramBoolean
表示是否采用中断的方式取消线程执行。
所以有时候,为了让任务有能够取消的功能,就使用Callable
来代替Runnable
。如果为了可取消性而使用 Future
但又不提供可用的结果,则可以声明 Future<?>
形式类型、并返回 null
作为底层任务的结果。
# 3.4.3 FutureTask 类
上面介绍了Future
接口。这个接口有一个实现类叫FutureTask
。FutureTask
是实现的RunnableFuture
接口的,而RunnableFuture
接口同时继承了Runnable
接口和Future
接口:
public class FutureTask<V> implements RunnableFuture<V> {
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
2
3
4
5
6
7
8
9
10
11
那FutureTask
类有什么用?为什么要有一个FutureTask
类?前面说到了Future
只是一个接口,而它里面的cancel
,get
,isDone
等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask
类来供我们使用。使用示例如下:
// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用上与第一个Demo有一点小的区别。首先,调用submit
方法是没有返回值的。这里实际上是调用的submit(Runnable task)
方法,而上面的 Demo,调用的是submit(Callable<T> task)
方法。然后,这里是使用FutureTask
直接取get
取值,而上面的 Demo 是通过submit
方法返回的Future
去取值。
在很多高并发的环境下,有可能 Callable 和 FutureTask 会创建多次。FutureTask 能够在高并发环境下确保任务只执行一次。
# 3.4.4 FutureTask 的几种状态
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
state 表示任务的运行状态,初始状态为 NEW。运行状态只会在 set、setException、cancel 方法中终止。COMPLETING、INTERRUPTING 是任务完成后的瞬时状态。
# 4. 线程中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
# 4.1 InterruptedException
有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的姿势其实是调用 interrupt() 方法。
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输出结果:
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.example.demo.DemoController$MyThread.run(DemoController.java:73)
2
3
# 4.2 interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
public class InterruptExample {
private static class MyThread2 extends Thread {
@Override
public void run() {
while (!interrupted()) {
// ..
}
System.out.println("Thread end");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread2 = new MyThread2();
thread2.start();
thread2.interrupt();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出结果:
Thread end
stop() 和 interrupt() 方法的主要区别是什么呢?
stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了。
而 interrupt() 方法就温柔多了,interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。
思考问题:下面代码的本意是当前线程被中断之后,退出while(true),这段代码是否正确呢?
Thread th = Thread.currentThread();
while(true) {
if (th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
可能出现无限循环,线程在 sleep 期间被打断了,抛出一个 InterruptedException 异常,try catch 捕捉此异常,应该重置一下中断标示,因为抛出异常后,中断标示会自动清除掉。正确写法:
Thread th = Thread.currentThread();
while(true) {
if(th.isInterrupted()) {
break;
}
// 省略业务代码无数
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 4.3 Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow();
System.out.println("Main run");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
输出结果:
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.example.demo.DemoApplication.lambda$main$0(DemoApplication.java:19)
2
3
4
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
Future<?> future = executorService.submit(() -> {
// ..
});
future.cancel(true);
2
3
4
# 5. 线程互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
# 5.1 synchronized
同步一个代码块
public void func() { synchronized (this) { // ... } }
1
2
3
4
5它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
public class SynchronizedExample { public void func1() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } } public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e1.func1()); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16输出结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func1()); executorService.execute(() -> e2.func1()); }
1
2
3
4
5
6
70 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
1同步一个方法
public synchronized void func () { // ... }
1
2
3它和同步代码块一样,作用于同一个对象。
同步一个类
public void func() { synchronized (SynchronizedExample.class) { // ... } }
1
2
3
4
5作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public class SynchronizedExample { public void func2() { synchronized (SynchronizedExample.class) { for (int i = 0; i < 10; i++) { System.out.print(i + " "); } } } } public static void main(String[] args) { SynchronizedExample e1 = new SynchronizedExample(); SynchronizedExample e2 = new SynchronizedExample(); ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> e1.func2()); executorService.execute(() -> e2.func2()); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16输出结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1同步一个静态方法
public synchronized static void fun() { // ... }
1
2
3作用于整个类。
# 5.2 ReentrantLock
ReentrantLock 是 java.util.concurrent (J.U.C) 包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
输出结果:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
# 5.3 两者比较
锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。
# 使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
# 6. 线程间通信
# 6.1 锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。一个锁同一时间只能被一个线程持有。
线程之间有一个同步的概念,可以解释为:线程同步是线程之间按照一定的顺序执行。为了达到线程同步,我们可以使用锁来实现它。前面已经介绍了 Java 通过 synchronized 关键字和 ReentrantLock 可重入锁实现线程之间的同步。
# 6.2 等待/通知机制
上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。而等待/通知机制是另一种方式。
Java多线程的等待/通知机制是基于Object
类的wait()
方法和notify()
, notifyAll()
方法来实现的。
notify() 方法会随机叫醒一个正在等待的线程,而 notifyAll() 会叫醒所有正在等待的线程。
一个锁同一时刻只能被一个线程持有。而假如线程 A 现在持有了一个锁lock
并开始执行,它可以使用lock.wait()
让自己进入等待状态。这个时候,lock
这个锁是被释放了的。
这时,线程 B 获得了lock
这个锁并开始执行,它可以在某一时刻,使用lock.notify()
,通知之前持有lock
锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。
需要注意的是,这个时候线程 B 并没有释放锁
lock
,除非线程 B 这个时候使用lock.wait()
释放锁,或者线程 B 执行结束自行释放锁,线程A才能得到lock
锁。
使用代码实现如下:
public class WaitAndNotify {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
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
输出如下:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4
2
3
4
5
6
7
8
9
10
在这个 Demo 里,线程 A 和线程 B 首先打印出自己需要的东西,然后使用notify()
方法叫醒另一个正在等待的线程,然后自己使用wait()
方法陷入等待并释放lock
锁。
需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。
# 6.3 信号量
JDK提供了一个类似于“信号量”功能的类Semaphore
。但本文不是要介绍这个类,而是介绍一种基于volatile
关键字的自己实现的信号量通信。
后面会有专门的章节介绍volatile
关键字,这里只是做一个简单的介绍。
volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。
比如我现在有一个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?
代码:
public class Signal {
private static volatile int signal = 0;
static class ThreadA implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 0) {
System.out.println("threadA: " + signal);
signal++;
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 1) {
System.out.println("threadB: " + signal);
signal = signal + 1;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
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
输出如下:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4
2
3
4
5
可以看到,使用了一个volatile
变量signal
来实现了“信号量”的模型。这里需要注意的是,volatile
变量需要进行原子操作。
需要注意的是,signal++
并不是一个原子操作,所以我们在实际开发中,会根据需要使用synchronized
给它“上锁”,或者是使用AtomicInteger
等原子类。并且上面的程序也并不是线程安全的,因为执行while
语句后,可能当前线程就暂停等待时间片了,等线程醒来,可能 signal 已经大于等于5了。
这种实现方式并不一定高效,本例只是演示信号量
信号量的应用场景:
假如在一个停车场中,车位是我们的公共资源,线程就如同车辆,而看门的管理员就是起的“信号量”的作用。
因为在这种场景下,多个线程(超过2个)需要相互合作,我们用简单的“锁”和“等待通知机制”就不那么方便了。这个时候就可以用到信号量。其实JDK中提供的很多多线程通信工具类都是基于信号量模型的。
# 6.4 管道
管道是基于“管道流”的通信方式。JDK提供了PipedWriter
、 PipedReader
、 PipedOutputStream
、 PipedInputStream
。其中,前面两个是基于字符的,后面两个是基于字节流的。
这里的示例代码使用的是基于字符的:
public class Pipe {
static class ReaderThread implements Runnable {
private PipedReader reader;
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
System.out.println("this is reader");
int receive = 0;
try {
while ((receive = reader.read()) != -1) {
System.out.print((char)receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable {
private PipedWriter writer;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("this is writer");
int receive = 0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader); // 这里注意一定要连接,才能通信
new Thread(new ReaderThread(reader)).start();
Thread.sleep(1000);
new Thread(new WriterThread(writer)).start();
}
}
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
// 输出:
this is reader
this is writer
test
2
3
4
我们通过线程的构造函数,传入了PipedWrite
和PipedReader
对象。可以简单分析一下这个示例代码的执行流程:
- 线程ReaderThread开始执行,
- 线程ReaderThread使用管道reader.read()进入”阻塞“,
- 线程WriterThread开始执行,
- 线程WriterThread用writer.write("test")往管道写入字符串,
- 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
- 线程ReaderThread接受到管道输出的字符串并打印,
- 线程ReaderThread执行完毕。
管道通信的应用场景:
这个很好理解。使用管道多半与 I/O 流相关。当我们一个线程需要先另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。
# 6.5 其他通信相关
# 6.5.1 join()
join() 方法是 Thread 类的一个实例方法。它的作用是让当前线程陷入“等待”状态,等 join 的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到 join 方法了。
示例代码:
public class Join {
static class ThreadA implements Runnable {
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println("我是子线程,我睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意 join() 方法有两个重载方法,一个是 join(long), 一个是 join(long, int)。
通过源码你会发现,join() 方法及其重载方法底层都是利用了 wait(long) 这个方法。
对于 join(long, int),通过查看源码(JDK 1.8)发现,底层并没有精确到纳秒,而是对第二个参数做了简单的判断和处理。
# 6.5.2 sleep()
sleep 方法是 Thread 类的一个静态方法。它的作用是让当前线程睡眠一段时间。它有这样两个方法:
- Thread.sleep(long)
- Thread.sleep(long, int)
同样,查看源码(JDK 1.8)发现,第二个方法貌似只对第二个参数做了简单的处理,没有精确到纳秒。实际上还是调用的第一个方法。
这里需要强调一下:**sleep 方法是不会释放当前的锁的,而 wait 方法会。**这也是最常见的一个多线程面试题。
它们还有这些区别:
- wait 可以指定时间,也可以不指定;而 sleep 必须指定时间。
- wait 释放 cpu 资源,同时释放锁;sleep 释放 cpu 资源,但是不释放锁,所以易死锁。
- wait 必须放在同步块或同步方法中,而 sleep 可以在任意位置。
# 6.5.3 ThreadLocal 类
ThreadLocal 是一个本地线程副本变量工具类。内部是一个弱引用的 Map 来维护。这里不详细介绍它的原理,而是只是介绍它的使用,以后有独立章节来介绍 ThreadLocal 类的原理。
关于 ThreadLocal 类的原理,可以先看看这几篇文章:https://yasinshaw.com/articles?keyword=threadlocal
有些朋友称 ThreadLocal 为线程本地变量或线程本地存储。严格来说,ThreadLocal 类并不属于多线程间的通信,而是让每个线程有自己”独立“的变量,线程之间互不影响。它为每个线程都创建一个副本,每个线程可以访问自己内部的副本变量。
ThreadLocal 类最常用的就是 set 方法和 get 方法。示例代码:
public class ThreadLocalDemo {
static class ThreadA implements Runnable {
private ThreadLocal<String> threadLocal;
public ThreadA(ThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadA输出:" + threadLocal.get());
}
static class ThreadB implements Runnable {
private ThreadLocal<String> threadLocal;
public ThreadB(ThreadLocal<String> threadLocal) {
this.threadLocal = threadLocal;
}
@Override
public void run() {
threadLocal.set("B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ThreadB输出:" + threadLocal.get());
}
}
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(new ThreadA(threadLocal)).start();
new Thread(new ThreadB(threadLocal)).start();
}
}
}
// 输出:
ThreadA输出:A
ThreadB输出:B
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
可以看到,虽然两个线程使用的同一个 ThreadLocal 实例(通过构造方法传入),但是它们各自可以存取自己当前线程的一个值。
那 ThreadLocal 有什么作用呢?如果只是单纯的想要线程隔离,在每个线程中声明一个私有变量就好了呀,为什么要使用 ThreadLocal?
如果开发者希望将类的某个静态变量(user ID 或者 transaction ID)与线程状态关联,则可以考虑使用ThreadLocal。
最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。数据库连接和 Session 管理涉及多个复杂对象的初始化和关闭。如果在每个线程中声明一些私有变量来进行操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接。
# 6.5.4 InheritableThreadLocal 类
InheritableThreadLocal类与ThreadLocal类稍有不同,Inheritable是继承的意思。它不仅仅是当前线程可以存取副本值,而且它的子线程也可以存取这个副本值。
# 7. 线程组和线程优先级
# 7.1 线程组
Java 中用 ThreadGroup 来表示线程组,我们可以使用线程组对线程进行批量控制。
ThreadGroup 和 Thread 的关系就如同他们的字面意思一样简单粗暴,每个 Thread 必然存在于一个 ThreadGroup中,Thread 不能独立于 ThreadGroup 存在。执行 main() 方法线程的名字是 main,如果在 new Thread 时没有显式指定,那么默认将父线程(当前执行 new Thread 的线程)线程组设置为自己的线程组。示例:
public class Demo {
public static void main(String[] args) {
Thread testThread = new Thread(() -> {
System.out.println("testThread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:" +
Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
输出结果:
执行main所在线程的线程组名字: main
执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0
2
3
4
ThreadGroup 管理着它下面的 Thread,ThreadGroup 是一个标准的向下引用的树状结构,这样设计的原因是防止"上级"线程被"下级"线程引用而无法有效地被 GC 回收。
# 7.2 线程优先级
Java 中线程优先级可以指定,范围是 1~10。但是并不是所有的操作系统都支持 10 级优先级的划分(比如有些操作系统只支持 3 级划分:低,中,高),Java 只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java 默认的线程优先级为 5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。
通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread
类的setPriority()
实例方法来设定线程的优先级。
public class Demo {
public static void main(String[] args) {
Thread a = new Thread();
System.out.println("我是默认线程优先级:"+a.getPriority());
Thread b = new Thread();
b.setPriority(10);
System.out.println("我是设置过的线程优先级:"+b.getPriority());
}
}
2
3
4
5
6
7
8
9
输出结果:
我是默认线程优先级:5
我是设置过的线程优先级:10
2
既然有 1-10 的级别来设定了线程的优先级,那么是不是可以在业务实现的时候,采用这种方法来指定一些线程执行的先后顺序?对于这个问题,答案是:不行~
Java 中的优先级来说不是特别的可靠,Java 程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的。
我们通过代码来验证一下:
public class Demo {
public static class T1 extends Thread {
@Override
public void run() {
super.run();
System.out.println(String.format("当前执行的线程是:%s,优先级:%d",
Thread.currentThread().getName(),
Thread.currentThread().getPriority()));
}
}
public static void main(String[] args) {
IntStream.range(1, 10).forEach(i -> {
Thread thread = new Thread(new T1());
thread.setPriority(i);
thread.start();
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
某次输出:
当前执行的线程是:Thread-17,优先级:9
当前执行的线程是:Thread-1,优先级:1
当前执行的线程是:Thread-13,优先级:7
当前执行的线程是:Thread-11,优先级:6
当前执行的线程是:Thread-15,优先级:8
当前执行的线程是:Thread-7,优先级:4
当前执行的线程是:Thread-9,优先级:5
当前执行的线程是:Thread-3,优先级:2
当前执行的线程是:Thread-5,优先级:3
2
3
4
5
6
7
8
9
Java 提供一个线程调度器来监视和控制处于 RUNNABLE 状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个 Java 程序都有一个默认的主线程,就是通过 JVM 启动的第一个线程 main 线程。
还有一种线程称为守护线程(Daemon),守护线程默认的优先级比较低。
如果某线程是守护线程,那如果所有的非守护线程都结束了,这个守护线程也会自动结束。
应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
一个线程默认是非守护线程,可以通过 Thread 类的 setDaemon(boolean on) 来设置。
在之前,我们有谈到一个线程必然存在于一个线程组中,那么当线程和线程组的优先级不一致的时候将会怎样呢?我们用下面的案例来验证一下:
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("t1");
threadGroup.setMaxPriority(6);
Thread thread = new Thread(threadGroup,"thread");
thread.setPriority(9);
System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());
System.out.println("我是线程的优先级"+thread.getPriority());
}
2
3
4
5
6
7
8
输出:
我是线程组的优先级6 我是线程的优先级6
所以,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
# 7.3 线程组常用方法
获取当前的线程组名字:
Thread.currentThread().getThreadGroup().getName()
复制线程组:
// 获取当前的线程组
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
// 复制一个线程组到一个线程数组(获取Thread信息)
Thread[] threads = new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
2
3
4
5
线程组统一异常处理:
package com.func.axc.threadgroup;
public class ThreadGroupDemo {
public static void main(String[] args) {
ThreadGroup threadGroup1 = new ThreadGroup("group1") {
// 继承ThreadGroup并重新定义以下方法
// 在线程成员抛出unchecked exception
// 会执行此方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + ": " + e.getMessage());
}
};
// 这个线程是threadGroup1的一员
Thread thread1 = new Thread(threadGroup1, new Runnable() {
public void run() {
// 抛出unchecked异常
throw new RuntimeException("测试异常");
}
});
thread1.start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 7.4 线程组的数据结构
线程组还可以包含其他的线程组,不仅仅是线程。首先看看 ThreadGroup
源码中的成员变量
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
private final ThreadGroup parent; // 父亲ThreadGroup
String name; // ThreadGroupr 的名称
int maxPriority; // 线程最大优先级
boolean destroyed; // 是否被销毁
boolean daemon; // 是否守护线程
boolean vmAllowSuspension; // 是否可以中断
int nUnstartedThreads = 0; // 还未启动的线程
int nthreads; // ThreadGroup中线程数目
Thread threads[]; // ThreadGroup中的线程
int ngroups; // 线程组数目
ThreadGroup groups[]; // 线程组数组
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后看看构造函数:
// 私有构造函数
private ThreadGroup() {
this.name = "system";
this.maxPriority = Thread.MAX_PRIORITY;
this.parent = null;
}
// 默认是以当前ThreadGroup传入作为parent ThreadGroup,新线程组的父线程组是目前正在运行线程的线程组。
public ThreadGroup(String name) {
this(Thread.currentThread().getThreadGroup(), name);
}
// 构造函数
public ThreadGroup(ThreadGroup parent, String name) {
this(checkParentAccess(parent), parent, name);
}
// 私有构造函数,主要的构造函数
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
this.name = name;
this.maxPriority = parent.maxPriority;
this.daemon = parent.daemon;
this.vmAllowSuspension = parent.vmAllowSuspension;
this.parent = parent;
parent.add(this);
}
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
第三个构造函数里调用了checkParentAccess
方法,这里看看这个方法的源码:
// 检查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
parent.checkAccess();
return null;
}
// 判断当前运行的线程是否具有修改线程组的权限
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这里涉及到
SecurityManager
这个类,它是 Java 的安全管理器,它允许应用程序在执行一个可能不安全或敏感的操作前确定该操作是什么,以及是否是在允许执行该操作的安全上下文中执行它。应用程序可以允许或不允许该操作。比如引入了第三方类库,但是并不能保证它的安全性。
其实 Thread 类也有一个 checkAccess() 方法,不过是用来当前运行的线程是否有权限修改被调用的这个线程实例。(Determines if the currently running thread has permission to modify this thread.)
总结来说,线程组是一个树状的结构,每个线程组下面可以有多个线程或者线程组。线程组可以起到统一控制线程的优先级和检查线程的权限的作用。
# 8. 参考
- http://concurrent.redspider.group/article/01/1.html
- https://pdai.tech/md/java/thread/java-thread-x-thread-basic.html
- https://blog.csdn.net/Evankaka/article/details/51627380