本文部分摘自《Java 并发编程的艺术》
线程简介
1. 什么是线程?
现代操作系统在运行一个程序时,会为其创建一个进程,一个进程里可以创建多个线程。现代操作系统调度的最小单元是线程,也叫轻量级进程。这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能访问共享的内存变量。处理器在这些线程上高速切换,让使用者觉得这些线程在同时执行
2. 为什么使用多线程?
使用多线程的原因主要有以下几点:
-
更多的处理器核心
通过使用多线程技术,将计算逻辑分配到多个处理器核心上,可以显著减少程序的处理时间
-
更快的响应时间
有时我们会编写一些较为复杂的代码(主要指业务逻辑),可以使用多线程技术,将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列)。这样做的好处是响应用户请求的线程能够尽可能快地处理完成,缩短了响应时间
-
更好的编程模型
Java 已经为多线程编程提供了一套良好的编程模型,开发人员只需根据问题需要建立合适的模型即可
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程分配到若干时间片,当线程的时间片用完了发生线程调度,并等待下次分配。线程分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或少分配一些处理器资源的线程属性
在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范围从 1 ~ 10,在线程构建时可以通过 setPriority(int) 方法来修改优先级,默认优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。不过,在不同的 JVM 以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略线程优先级的设定
public class Priority {
private static volatile boolean notStart = true;
private static volatile boolean notEnd = true;
public static void main(String[] args) throws Exception {
List<Job> jobs = new ArrayList<Job>();
for (int i = 0; i < 10; i++) {
int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
Job job = new Job(priority);
jobs.add(job);
Thread thread = new Thread(job, "Thread:" + i);
thread.setPriority(priority);
thread.start();
}
notStart = false;
TimeUnit.SECONDS.sleep(10);
notEnd = false;
for (Job job : jobs) {
System.out.println("Job Priority : " + job.priority + ", Count : " + job.jobCount);
}
}
static class Job implements Runnable {
private int priority;
private long jobCount;
public Job(int priority) {
this.priority = priority;
}
@Override
public void run() {
while (notStart) {
Thread.yield();
}
while (notEnd) {
Thread.yield();
jobCount++;
}
}
}
}
运行该示例,在笔者机器上对应的输出如下
笔者使用的环境为:Win10 + JDK11,从输出可以看到线程优先级起作用了
线程的状态
Java 线程在运行的生命周期中可能处于下表所示的六种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但还没调用 start() 方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在自身的生命周期中,并不是固定地处于某一状态,而是随着代码的执行在不同的状态之间进行切换
Daemon 线程
Daemon 线程是一种支持型线程,主要被用作程序中后台调度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在 Daemon 线程的时候,Java 虚拟机将退出。可以调用 Thread.setDaemon(true) 将线程设置为 Daemon 线程
使用 Daemon 线程需要注意两点:
- Daemon 属性需要在启动线程之前设置,不能在启动线程之后设置
- 在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行或关闭清理资源的逻辑。因为在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行
启动和终止线程
1. 构造线程
在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需提供线程需的属性,如线程所属的线程组、是否是 Daemon 线程等信息
2. 启动线程
线程对象在初始化完成之后,调用 start() 方法即可启动线程
3. 理解中断
中断可以理解为线程的一个标识位属性,标识一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程可以通过调用该线程的 interrupt() 方法对其进行中断操作
线程通过检查自身是否被中断进行响应,线程通过 isInterrupted() 来进行判断是否被中断,也可以调用静态方法 Tread.interrupted() 对当前线程的中断标识位进行复位。如果线程已经处于终结状态,即时线程被中断过,在调用该对象的 isInterrupted() 时依旧会返回 false
许多声明抛出 InterruptedException 的方法在抛出异常之前,Java 虚拟机会先将该线程的中断标识位清除,然后抛出 InterruptedException,此时调用 isInterrupted() 方法将会返回 false
在下面的例子中,首先创建两个线程 SleepThread 和 BusyThread,前者不停地睡眠,后者一直运行,分别对两个线程分别进行中断操作,观察中断标识位
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
// sleepThread 不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread 不停的运行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠 5 秒,让 sleepThread 和 busyThread 充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止 sleepThread 和 busyThreaad 立刻退出
SleepUtils.second(2);
}
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils.second(10);
}
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}
输出如下
从结果可以看出,抛出 InterruptedException 的线程 SleepThread,其中断标识位被清除了,而一直忙碌运行的线程 BusyThread 的中断标识位没有被清除
4. 安全地终止线程
前面提到的中断操作是一种简便的线程间交互方式,适合用来取消或停止任务。除了中断以外,还可以利用一个 boolean 变量来控制是否需要停止任务并终止线程
下面的示例中,创建了一个线程 CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作
public class Shutdown {
public static void main(String[] args) throws InterruptedException {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠一秒,main 线程对 CountThread 进行中断,使 CountThread 能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠一秒,main 线程对 Runner two 进行中断,使 CountThread 能够感知 on 为 false 而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
main 线程通过中断操作和 cancel() 方法均可使 CountThread 得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,更加安全和优雅