Java的线程

news2025/1/18 8:49:52

介绍线程

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

每个线程有自己的程序计数器、栈(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()方法会出现什么情况?-极客时间 (geekbang.org)

09 | Java线程(上):Java线程的生命周期 (geekbang.org)

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

11 | 线程:如何让复杂的项目并行执行?-极客时间 (geekbang.org)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/523754.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux的基本权限

基本权限 1.权限的概念2.Linux上的用户分类2.1. 如何切换用户&#xff1f; 3.Linux的权限管理3.1. 文件访问者的分类&#xff08;人&#xff09;3.2.文件类型和访问权限&#xff08;文件属性&#xff09;3.2.1. 文件类型3.2.2. 基本权限 3.3. 文件权限值的表示方法3.3.1. 字符表…

Mac中idea快捷键(Keymap->macOS)

背景&#xff1a; mac&#xff1a;MacBook Pro&#xff08;13英寸&#xff0c;M2&#xff0c;2022年&#xff09; 系统版本&#xff1a;12.4 idea快捷键配置&#xff1a;本文快捷键设置基于macOS&#xff08;Keymap->macOS&#xff09; 一、常用快捷键 1.commandF 在当前…

java类和对象2

文章目录 一、Java实例变量和类变量二、Java实例方法和类方法三、Java方法重载四、Java this关键字五、Java包六、Java import语句七、Java访问权限八、Java基本类型的类封装九、Java jar文件总结 一、Java实例变量和类变量 实例变量和类变量的定义 在声明成员变量时&#xf…

微信云开发之云函数小例子

环境 微信开发者工具 Stable 1.06.2303220云开发控制台 v1.5.47 概述 云函数即在云端&#xff08;服务器端&#xff09;运行的函数&#xff0c;代码运行在云端Node.js中。我们可以使用云函数SDK中提供的数据库和存储API进行数据库和存储的操作。 云函数的独特优势在于与微信…

centos,搭建ftp服务;vsftpd

安装vsftpd 使用yum安装vsftpd&#xff1a; yum install -y vsftpd 配置vsftpd 编辑vsftpd配置文件&#xff1a; vim /etc/vsftpd/vsftpd.conf 以下是一些常见的配置项&#xff1a; anonymous_enableNO local_enableYES write_enableYES local_umask022 dirmessage_enableYE…

职场新人必备:10个写进备忘录的职场tips

作为职场新人&#xff0c;如何快速适应、获得成长和提升自己&#xff0c;是每个人都面临的挑战。在这个竞争激烈的职场中&#xff0c;拥有一些实用的职场tips&#xff0c;可以帮助你更好地应对各种挑战&#xff0c;获得成功。在本文中&#xff0c;我们将分享10个写进备忘录的职…

es8388 驱动详解

音频的基本框架 分为三个部分&#xff1a; 1&#xff09; 整体的绿框&#xff0c;为machine&#xff0c;例如你的PC等&#xff0c;负责图中 “线”的部分的关联&#xff0c;及黄色连接器的管理。也就是说不属于 paltform和codec管理的部分&#xff0c;都划归machine管理。 对应…

【Python socket】零基础也能轻松掌握的学习路线与参考资料

Python socket是Python程序语言中与网络编程密切相关的库之一&#xff0c;可以让程序员在网络世界中建立服务和客户端之间的连接。Python socket提供了一系列关键的概念和方法&#xff0c;让用户能够轻松地构建基于网络的程序&#xff0c;如基于TCP和UDP的应用程序。学习Python…

图(Graph)

Graph 图由顶点&#xff08;vertex /ˈvɜːrteks/&#xff09;和边&#xff08;edge /edʒ/&#xff09;组成的一种结构。 顶点的集合V&#xff0c;边的集合是E&#xff0c;所以图记为G &#xff08;V&#xff0c;E&#xff09; 总结: 顶点是一维数组,而边是二维数组; 假如顶点…

Pycharm无法添加Conda新建的虚拟环境

Pycharm无法添加Conda新建的虚拟环境&#xff0c;点击没反应&#xff0c;在idea.log文件中报错&#xff1a;CondaPythonLegacy - Can’t find python path to use, will use conda run instead 1.问题描述&#x1f50d; 在PyCharm中&#xff0c;依次单击File>Settings>P…

Netty核心技术一--Netty介绍和应用场景

1. 学习要求 要求已经掌握了 Java 编程&#xff0c; 主要技术构成&#xff1a; Java OOP 编程、Java 多线程编程、JavaIO 编程 、Java 网络编程、常用的Java 设计模式(比如观察者模式&#xff0c;命令模式&#xff0c;职责链模式 )、常用的数据结构(比如 链表) 2 Netty的介绍…

C++: 计时器类的设计和实现

文章目录 1. 目的2. 功能列表3. 基础功能&#xff1a;获取耗时4. API 设计: Timer 类5. 单元测试6. API 实现 1. 目的 使用 C Class 的形式&#xff0c;封装原本 C语言的获取时间的函数&#xff0c;提供更容易使用的计时器调用。 使用 C03&#xff0c;原因是和先前的线程安全队…

ATT-ACK靶场渗透(一)

开启内网环境 kali攻击机ip为 192.168.88.156 主机发现、端口扫描 进行主机发现 nmap -sS 192.168.88.156/24 -Pn 192.168.88.1和192.168.88.2可能为网关之类的&#xff0c;不管 weblogic漏洞利用 192.168.88.182开放了80端口和7001&#xff08;weblogic&#xff09;端口…

超越预期:ConvNeXt技术催生YOLOv5目标检测巨变 ,实现超准确率

目录 引言一、ConvNeXt的介绍1、目标检测的重要性2、YOLOv5的介绍3、ConvNeXt原理和特点4、ConvNeXt结构 二、相关研究综述1、目标检测的基础原理和流程2、YOLOv5的特点与局限性3、ConvNeXt技术在目标检测中的应用现状 三、ConvNeXt在YOLOv5中的应用与改进1、安装PyTorch和torc…

阿里Java工程规约(来源阿里)

一、应用分层 1.开放接口层&#xff1a; 可直接封装 Service 接口暴露成 RPC 接口&#xff1b;通过 Web 封装成 http 接口&#xff1b;网关控制层等。 2.终端显示层&#xff1a; 各个端的模板渲染并执行显示层。当前主要是 velocity 渲染&#xff0c;JS 渲染&#xff0c;JSP 渲…

[golang gin框架] 33.Gin 商城项目- 微信支付操作相关功能讲解

一.微信支付准备工作 准备工作 申请条件: 个体工商户 、企业、政府及事业单位 PC网站接入支付官网,其他(app,小程序,公众号等)可参考 PC网站接入支付 需要获取内容 appid&#xff1a;应用 APPID&#xff08;必须配置&#xff0c;开户邮件中可查看&#xff09; MCHID&#xff1a…

Java Socket和ServerSocket 使用

在Java中&#xff0c;Socket和ServerSocket是用于创建网络连接的重要类。Socket类用于创建客户端套接字&#xff0c;而ServerSocket类用于创建服务器套接字。在本文中&#xff0c;我们将讨论Socket和ServerSocket的作用、使用方法以及相关代码示例。 Socket的作用 Socket是Jav…

【连续介质力学】张量的并矢和性质1

张量的代数操作 并矢 Dyadic 两个向量的张量积是一个并矢&#xff0c;得到一个二阶张量 u ⃗ v ⃗ u ⃗ ⨂ v ⃗ A \vec u \vec v \vec u \bigotimes \vec v A u v u ⨂v A 其中&#xff0c; ⨂ \bigotimes ⨂是张量乘积&#xff0c;任意张量可以表示成并矢的线性组合 …

戴尔 Dell XPS 13 7390电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板戴尔 Dell XPS 13 7390 处理器Intel Core i7-10510u已驱动 内存 16 GB ( 酷兽 DDR4 3200MHz 8GB x 2 )已驱动 硬盘三星 SSD 860 EVO 250GB (250 …

【iOS】--KVC与KVO

键值编码&#xff08;KVC&#xff09;与键值监听&#xff08;KVO&#xff09; KVC&#xff08;Key Value Coding&#xff09;允许以字符串的形式间接操作对象的属性。 简单的KVC 最基本的KVC由NSKeyValueCoding协议提供支持&#xff0c;最基本的操作属性的两个方法如下 set…