目录

Java 线程

DakingTech 原创不易,转载请注明出处

进程与线程

进程是程序的运行实例。运行一个 Java 程序的实质是启动一个 Java 虚拟机进程。

进程是操作系统进行资源(如内存空间和文件句柄)分配和调度的基本单位。线程是CPU分配的基本单位。

线程是进程的一个执行路径,一个进程中至少有一个线程,可以包含多个线程。

同一个进程中的所有线程共享该进程中的资源,如内存空间、文件句柄等。

Thread API

Thread 属性

名称 类型 用途 读写 注意事项
编号(ID) long 用于标识不同的线程 只读 不同线程拥有不同的ID。
这种编号的唯一性只在JVM的一次运行有效。
名称(Name) String 面向人,用来区分不同的线程。
默认值与线程编号有关,格式为Thread-编号
读写 可将不同线程的名称设置为相同值。
设置线程的名称有助于代码调试和问题定位。
类别(Daemon) boolean 值为true表示守护线程。 读写 该属性必须在线程启动之前设置。
优先级(Priority) int 给线程调度器的提示。 读写 不恰当的设置会导致严重的问题,如线程饥饿

Thread 方法

方法 用途 注意事项
void start() 启动相应线程 一个 Thread 实例的 start 方法只能被调用一次
void run() 用于实现线程的任务处理逻辑 该方法是由 JVM 调用
void join() 等待相应线程执行结束 若线程A调用线程B的 join 方法,那么线程A会暂停,直到线程B运行结束
static void yield() 使线程主动放弃其对处理器的占用 此方法是不可靠的,当前线程可能仍继续执行
static void sleep(long millis) 使线程休眠指定的时间 不会放弃持有的 monitor 锁
static Thread currentThread() 当前代码的执行线程
void setUncaughtException-
Handler(UncaughtExceptionHandler eh)
设置未捕获异常处理器
void interrupt() 中断线程 设置线程的中断标志为 true
static boolean interrupted() 检测当前线程是否被中断 此方法会清除线程的中断状态
boolean isInterrupted() 检测线程是否被中断 此方法不会清除线程的中断状态

Java 线程的创建

在 Java 中,创建线程的方法只有一种:创建 Thread 对象

因为线程启动之后运行的是Thread#run()中的逻辑,其run方法的源码如下,所以在实际开发中,常通过两种方式来创建线程:

  1. 继承 Thread 类并重写其run方法。
  2. 实现 Runnable 接口,并将其实现类对象传递给 Thread 构造函数。
1
2
3
4
5
6
7
8
9
private Runnable target;

@Override
public void run() {
    // target 为创建 Thread 时传入的 Runnable 对象
    if (target != null) {
        target.run();
    }
}

推荐使用实现 Runnable 接口的方式来创建线程,其优势如下:

  • Java 语言只能单继承,通过实现接口的方式,可以让实现类去继承除 Thread 之外的其他类。
  • 线程控制逻辑在 Thread 类中,业务运行逻辑在 Runnable 实现类中。
  • 实现 Runnable 接口的对象,可以被多个线程共享并执行。
  • 实现 Runnable 接口的对象可以交给 Thread 或类似执行器的 Java 并发对象来执行。

Java 线程的启动与运行

调用Thread#start()来启动相应的线程,即请求 JVM 运行相应的线程,而线程具体何时能够运行是由线程调度器决定。

运行一个线程实际上就是让 JVM 执行该线程的Thread#run(),从而使对应的任务处理逻辑代码得以执行,即 Thread 子类的run()被执行或Runnable#run()被执行。

同一个 Thread 是只允许调用一次start()的,之后再调用会抛出异常,这是因为线程状态已不是Thread.State.NEW

1
2
3
4
5
public synchronized void start() {
    // threadStatus 不等于 State.NEW 时
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
}

线程的生命周期状态

JDK 5起,线程状态被明确定义在java.lang.Thread.State这个公共枚举类中。

1
2
3
4
5
6
7
8
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW:表示线程已创建但未启动。一个线程只可能有一次处于此状态。

RUNNABLE:该状态是一个复合状态,包含 READY 和 RUNNING 两个子状态。

  • READY:线程可以被线程调度器进行调度。
  • RUNNING:线程正在被处理器执行。

BLOCKED:阻塞状态。

  • 线程发起一个阻塞式IO操作或者申请一个由其他线程持有的独占资源时,它会处于该状态。
  • 当阻塞式IO操作完成或者获得独占资源后,线程又转换为 RUNNABLE 状态。

WAITING:表示线程正在等待其他线程执行特定的操作。

TIMED_WAITING:该状态和 WAITING 类似,差别在于处于此状态的线程是带有时间限制地等待其他线程执行特定操作。当其他线程没有在指定等待时间内执行特定操作,该线程的状态自动转换为 RUNNABLE。

TERMINATED:表示线程已经执行结束。其Thread#run()正常结束或由于抛出异常而提前终止。

一个线程在整个生命周期中,只可能有一次处于 NEW 状态和 TERMINATED 状态。

/images/2021-03-12-17-51-50.png

Java 线程的类别

Java线程分为守护线程(Daemon Thread)和用户线程(User Thread,又叫非守护线程)。

这两者的区别在于:守护线程不影响Java程序的结束。

可以使用Thread#isDaemon()检查线程是否为守护线程。

可以使用Thread#setDaemon()将某个线程确立为守护线程。注意,此方法必须要在Thread#start()之前调用。

守护线程通常用于执行一些重要性不是很高的任务,如监视其他线程的运行情况。

Java 线程的中断

Java 线程中断机制是Java 线程之间协作的一套协议框架。

  • 中断可看作由一个线程发送给另外一个线程的一种指示,该指示用于表示发起线程希望目标线程停止其正在执行的操作。
  • 中断仅仅代表发起线程的一个诉求,而该诉求能否被满足则取决于目标线程自身。

Java 平台会为每个线程维护一个被称为中断标记的布尔型状态变量,用于表示相应线程是否接收到中断,而该标记为 true表示收到中断。

目标线程可以通过boolean isInterrupted()来获取中断标记值。

1
2
3
4
5
6
7
public boolean isInterrupted() {
    // ClearInterrupted 传入 false
    return isInterrupted(false);
}
// 最终通过本地方法来检测线程是否被中断。
// ClearInterrupted 为是否要清除中断标志
private native boolean isInterrupted(boolean ClearInterrupted);

可以通过static boolean interrupted()获取当前线程的中断标记值。注意,此方法会清除当前线程的中断标记,即将其中断标记设为 false。

1
2
3
4
5
public static boolean interrupted() {
    // 通过 currentThread() 获取当前线程
    // ClearInterrupted 传入 true
    return currentThread().isInterrupted(true);
}

调用一个线程的void interrupt()就是中断该线程,即将该线程的中断标记设为 true。

1
2
// interrupt() 最终是通过此本地方法来中断线程
private native void interrupt0();

目标线程检查中断标记后所执行的操作被称为目标线程对中断的响应,简称中断响应

中断响应一般有:

  • 无影响。目标线程不对中断进行响应。
  • 取消任务的运行。目标线程取消这一刻所执行的任务,但会继续处理接下来的任务。
  • 目标线程停止。目标线程终止执行。

Java 阻塞方法与中断响应

像申请 synchronized 内置锁这种不能响应中断的阻塞被称为重量级阻塞,对应的线程状态是 BLOCKED。

能够响应中断的阻塞称为轻量级阻塞,对应的线程状态是 WAITING 或 TIMED_WAITING。

Java 标准库中有许多轻量级阻塞的方法,它们对中断的响应方式都是抛出 InterruptedException,比如:

  • Object#wait()/wait(long)
  • Thread#sleep(long)
  • Thread#join()/join(long)
  • Lock#lockInterruptibly()
  • CountDownLatch#await()
  • CyclicBarrier#await()
  • BlockingQueue#take()/put(E)

这些能够响应中断的阻塞方法通常是在执行阻塞操作前判断中断标记,若为 true 则抛出 InterruptedException。而且依照惯例,凡是抛出 InterruptedException 的方法,通常会在其抛出该异常之前将当前线程的中断标记重置为 false

1
2
3
4
5
6
7
8
9
// Lock#lockInterruptibly() 的内部实现
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    // 获取并重置中断标记
    if (Thread.interrupted())
        // 抛出 InterruptedException 来响应中断
        throw new InterruptedException();
    ...
}

如果目标线程因为执行Lock#lockInterruptibly()等能够响应中断的轻量级阻塞方法而暂停时(线程状态是 WAITING 或 TIMED_WAITING),发起线程给该目标线程发送中断后,Java 虚拟机会将此目标线程唤醒以使其有机会响应中断,即抛出 InterruptedException。可见,给目标线程发送中断还能够产生唤醒目标线程的效果。

Java 线程的停止

Java 标准库并没有提供可以直接停止线程的 API,Thread#stop()/destroy()等都是已废弃的方法。这是因为如果强制停止线程,则线程中所使用的资源(文件描述符、网络连接等)不能正常关闭。

合理的线程停止是让线程自己运行完毕,即Thread#run()执行完毕。

主动停止一个线程的实现思路:为线程自定义一个线程停止标记(布尔型,一般用 volatile 修饰),当线程检测到该标机值为 true 时则设法让其run方法执行完毕。

光使用专门的线程停止标记仍然不够,还需要借助线程中断机制。如果线程停止标记为 true 时,目标线程可能因为执行了一些阻塞方法而暂停,此时线程停止标记根本不会对目标线程产生任何影响。此时需要给目标线程发送中断以将其唤醒,使之得以判断线程停止标记,然后设法让自己执行完毕。

参考资料

Java多线程编程实战指南(核心篇)- 第1章 走近Java世界中的线程

Java多线程编程实战指南(核心篇)- 5.6 对不起,打扰一下:线程中断机制

Java多线程编程实战指南(核心篇)- 5.7 线程停止:看似简单,实则不然

Java并发编程之美 - 第1章 并发编程线程基础

Java 并发实现原理:JDK 源码剖析 - 第 1 章 多线程基础