Java的线程

简介: 本篇文章介绍了1. Java的线程生命周期;2. Java的线程状态切换;3. Java线程API的使用。

介绍线程

线程是系统调度的最小单元,一个进程可以包含多个线程,线程是负责执行二进制指令的。

每个线程有自己的程序计数器、栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。

对于任何一个进程来讲,即便我们没有主动去创建线程,进程也是默认有一个主线程的。


守护线程(Daemon Thread)

有的时候应用中需要一个长期驻留的服务程序,但是不希望这个服务程序影响应用退出,那么我们就可以将这个服务程序设置为守护线程,如果 Java 虚拟机发现只有守护线程存在时,将结束进程。

在 Java 中将线程设置为守护线程,具体的实现代码如下所示:

public static void main(String[] args) {
    Thread daemonThread = new Thread();
    // 必须在线程启动之前设置
    daemonThread.setDaemon(true);
    daemonThread.start();
}

通用的线程生命周期

在操作系统层面,线程有生命周期。

对于有生命周期的事物,要学好它,只要能搞懂生命周期中各个节点的状态转换机制就可以了。

通用的线程生命周期基本上可以用下图这个 “五态模型” 来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。

1651248522968-14a3c935-b45e-4ab7-bb1c-f7de1e93bf1d.png

这“五态模型”的详细情况如下所示。


初始状态

初始状态,指的是线程已经被创建,但是还不允许被 CPU 调度。

初始状态属于编程语言特有的,这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。

在 Java 中,初始状态相当于是创建了 Thread 类的对象,但是还没有调用 Thread#start() 方法。


可运行状态

可运行状态,指的是线程可以被操作系统调度,但是线程还没有开始执行。

在可运行状态下,真正的操作系统线程已经被创建。多个线程处于可运行状态时,操作系统会根据调度算法选择一个线程运行。

在 Java 中,可运行状态相当于是调用了 Thread#start() 方法,但是线程还没有被分配 CPU 执行。


运行状态

当有空闲的 CPU 时,操作系统会将空闲的 CPU 分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就从可运行状态转换成了运行状态。

在 Java 中,运行状态相当于是调用了 Thread#start() 方法,并且线程被分配 CPU 执行。


休眠状态

如果运行状态的线程调用了一个阻塞的 API(例如以阻塞的方式读取文件)或者等待某个事件(例如条件变量),那么线程的状态就会从运行状态转换到休眠状态,同时释放 CPU 的使用权,休眠状态的线程永远没有机会获得 CPU 的使用权。

当等待的资源或条件满足后,线程就会从休眠状态转换到可运行状态,并等待 CPU 调度。


终止状态

线程执行完毕或者出现异常,线程就会进入终止状态,即线程的生命周期终止。


这五种状态在不同编程语言里会有简化合并。例如:

  • C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;
  • Java 程序设计语言把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 Java 虚拟机层面不关心这两个状态,因为 Java 虚拟机把线程调度交给操作系统处理了。

除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态(这个下面我们会详细讲解)。

Java 的线程生命周期

不同的程序设计语言对于操作系统线程进行了不同的封装,下面我们学习一下 Java 的线程生命周期。

Java 程序设计语言中,线程共有六种状态,分别是:

  1. NEW(初始状态)
  2. RUNNABLE(可运行 / 运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

NEW(初始状态)、TERMINATED(终止状态)和通用的线程生命周期中的语义相同。

在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即通用的线程生命周期中的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有机会获得 CPU 的使用权。

所以 Java 中的线程生命周期可以简化为下图:

1651248522988-a3b8cd59-986b-49ee-8743-c262c7a1c180.png


其中,可以将 BLOCKED、WAITING、TIMED_WAITING 理解为导致线程处于休眠状态的三种原因。

  • 那具体是哪些情形会导致线程从 RUNNABLE 状态转换到这三种状态呢?
  • 而这三种状态又是何时转换回 RUNNABLE 的呢?
  • 以及 NEW、TERMINATED 和 RUNNABLE 状态是如何转换的?

下面我们详细讲解。

Java 的线程状态切换

从 NEW 到 RUNNABLE 状态

刚创建 Thread 类的对象时,线程处于 NEW 状态。

NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。

从 NEW 状态转换到 RUNNABLE 状态只要调用线程对象的 start() 方法就可以了,具体的实现代码如下所示:

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("hello");
        }
    });
    thread.start();
}

从 RUNNABLE 到 TERMINATED 状态

线程执行完 Thrad#run() 方法后,会自动从 RUNNABLE 状态转换到 TERMINATED 状态。

如果执行 run() 方法的时候异常了抛出,也会导致线程终止,进入 TERMINATED 状态 。

1. RUNNABLE 与 BLOCKED 的状态转换

只有一种场景会触发 RUNNABLE 与 BLOCKED 的状态转换,就是线程等待 synchronized 的隐式锁。

  • 当使用 synchronized 申请加锁失败时,该线程的状态就会从 RUNNABLE 转换到 BLOCKED 状态。
  • 当等待的线程获得锁时,该线程的状态就会从 BLOCKED 状态转换到 RUNNABLE 状态。

如果你熟悉操作系统线程的生命周期的话,可能会有个疑问:线程调用阻塞式 API 时,是否会转换到 BLOCKED 状态呢?在操作系统层面,线程是会转换到休眠状态的,但是在 Java 虚拟机层面,Java 线程的状态不会发生变化,也就是说 Java 线程的状态会依然保持 RUNNABLE 状态。

Java 虚拟机层面并不关心操作系统调度相关的状态,因为在 Java 虚拟机看来,等待 CPU 的使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。

而我们说的 Java 线程在调用阻塞式 API 时,线程会阻塞,指的是操作系统线程的状态,并不是 Java 线程的状态。

2. RUNNABLE 与 WAITING 的状态转换

总体来说,有三种场景会触发 RUNNABLE 与 WAITING 的状态转换。


第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object#wait() 方法。

这里应该调用的是锁对象的 wait() 方法,具体的实现代码如下所示:

public void method() throws InterruptedException {
    synchronized (this) {
        this.wait();
    }
}
  • 当调用 wait() 方法时,调用方法的线程的状态从 RUNNABLE 状态转换到 WAITING 状态
  • 当调用 notify() 方法时,被唤醒的线程的状态从 WAITING 状态转换到 RUNNABLE 状态

第二种场景,调用无参数的 Thread#join() 方法。

join() 是一种线程同步方法,例如有一个线程对象 thread A:

  • 当调用 A.join() 方法时,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。
  • 当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。

Thread#join() 方法的实现基于 Object#wait()。


第三种场景,调用 LockSupport#park() 方法。

LockSupport 类,也许你有点陌生,其实 Java 并发包中锁的实现都用到了 LockSupport#park() / unpark()。

  • 当调用 LockSupport.park() 方法时,调用方法的线程的状态从 RUNNABLE 转换到 WAITING。
  • 当调用 LockSupport.unpark(Thread thread) 方法时,被唤醒的线程的状态从 WAITING 状态转换到 RUNNABLE 状态

总结来说:Object#wait() 和 LockSupport#park() 方法使线程的状态转换到 WAITING。

3. RUNNABLE 与 TIMED_WAITING 的状态转换

总体来说,有五种场景会触发 RUNNABLE 与 TIMED_WAITING 的状态转换:

  1. 获得 synchronized 隐式锁的线程,调用带超时参数的 Object#wait(long timeout) 方法;
  2. 调用带超时参数的 Thread#join(long millis) 方法;(底层调用 Object#wait(long timeout) )
  3. 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;
  4. 调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
  5. 调用带超时参数的 Thread.sleep(long millis) 方法;

这里你会发现:

  • TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。
  • 与 RUNNABLE 与 WAITING 的状态转换 相比,多了一个 Thread.sleep() 场景。

Java 线程 API 的使用

线程的创建

创建线程的几种方式:

  1. 继承 Thread 类,重写 run() 方法。
  2. 实现 Runnable 接口,实现其中的 run() 方法。将该实现类的对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象。
  3. 实现 Callable 接口,实现其中的 call() 方法。将该实现类的对象作为参数传递到 FutureTask 类的构造器中,创建FutureTask 类的对象。将 FutureTask 类的对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象。Callable 它解决了 Runnable 无法返回结果的困扰。

「实现 Runnable 接口」VS「继承 Thread 类」

  • 通过实现(implements)的方式没有类的单继承性的局限性
  • 实现的方式更适合处理多个线程有共享数据的情况

「实现 Callable 接口」VS「实现 Runnable 接口」

  • call() 可以有返回值
  • call() 可以抛出异常被外面的操作捕获,获取异常的信息
  • 「实现 Callable 接口」支持泛型

// 自定义线程对象
class MyThread extends Thread {
    public void run() {
        // 线程需要执行的代码
        ......
    }
}

// 创建线程对象
MyThread myThread = new MyThread();
// 实现Runnable接口
class Runner implements Runnable {
    @Override
    public void run() {
        // 线程需要执行的代码
        ......
    }
}

// 创建线程对象
Thread thread = new Thread(new Runner());
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyTask task = new MyTask();
    // FutureTask 用于接收运算结果
    FutureTask futureTask = new FutureTask<>(task);
    Thread thread = new Thread(futureTask);

    thread.start();
    // FutureTask 可用于线程间同步 (当前线程等待其他线程执行完成之后,当前线程才继续执行)
    // get() 返回值即为 FutureTask 构造器参数 Callable 实现类实现的 call() 的返回值
    System.out.println(futureTask.get());
}

public class MyTask implements Callable {
    @Override
    public String call() {
        // 若不需要返回值,可 return null;
        return "ok";
    }
}

线程的执行

创建好 Thread 类的对象后,通过调用 Thread#start() 方法创建线程执行任务。

线程执行要调用 start() 而不是直接调用 run(),直接调用 run() 方法只会在当前线程上同步执行 run() 方法的内容,而不会启动新线程。调用 start() 方法的作用:

  1. 启动一个新的线程
  2. 新的线程调用 run() 方法

线程的停止

有时候我们需要强制中断 run() 方法的执行,例如 run() 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop() 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的方式是调用 interrupt() 方法。Thread#interrupt() 配合合适的代码,即可优雅的实现线程的终止。

stop() 和 interrupt() 方法的区别。

  • stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了。
  • interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,线程也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

异常

当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他的线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。

上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他的线程调用了该线程的 interrupt() 方法。

当线程 A 处于 RUNNABLE 状态时:

  • 当线程 A 处于 RUNNABLE 状态,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他的线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;
  • 当线程 A 处于 RUNNABLE 状态,并且阻塞在 java.nio.channels.Selector 上时,如果其他的线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。


主动检测

还有一种是主动检测,如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他的线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。

参考资料

第17讲 | 一个线程两次调用start()方法会出现什么情况? (yuque.com)

09 | Java线程(上):Java线程的生命周期 (yuque.com)

06 | 线程池基础:如何用线程池设计出更“优美”的代码?-极客时间 (geekbang.org)

11丨线程:如何让复杂的项目并行执行? (yuque.com)

相关文章
|
8天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
17天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
23 9
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
34 1