【多线程】线程的状态

news2024/11/19 16:21:33

1. 等待一个线程 join

有一天张三与小美在家里吃饭,张三早早的把饭吃完了,对小美说,等你把饭吃完了,我就去洗碗了!

此时张三就要等待小美吃完饭才能去洗碗,就好比线程 A 要等待线程 B 执行完,线程 A 才能接着往后干活!

等待线程,就是控制两个线程的结束顺序,使用 join 方法。

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        System.out.println("小美吃饭中!");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("小美吃完饭了!");
    });
    t.start();
    System.out.println("张三吃完饭了!");
    t.join(); // main 线程等待 t 线程执行完毕
    System.out.println("张三去洗碗了!");

这里当 t.start() 之后,t 和 main 线程就开始并发执行了,当 main 线程执行到 t.join() 表示 main 线程得等 t 线程执行完毕后,才能接着往后执行代码!只要 t 线程没有执行完毕,main 线程就会发生阻塞,一直阻塞到 t 线程执行完毕,也就是执行完对应的 run 方法!

像上述代码是可以等到 t 线程执行完毕了,如果 t 线程执行的任务里面出现了死循环怎么办呢?此时 main 线程是不是也得无休止的等待了?确实是这样的,但是 join 给我们提供了一个带参数的方法,等指定的时间,到点就不等了!

比如小美吃饭的速度特别慢,张三说,我就等你 5 秒钟,5 秒之后你还没吃完,我就自己洗碗了去了!

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        System.out.println("小美吃饭中!");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("小美吃完饭了!");
    });
    t.start();
    System.out.println("张三吃完饭了!");
    t.join(5000); // 只等待5秒
    System.out.println("张三去洗碗了!");
}

此时就能发现,小美没有吃完饭,但是张三还是去洗碗了,为什么呢?

因为 main 线程只等了 t 线程 5秒,但是 t 线程执行的 run 方法,可不止 5秒,里面还 sleep 了10秒钟,相当于小美还需要 10秒 才能吃完饭呢!

由此可见,这次张三肯定不会等小美吃完饭之后才去洗碗,就如同上面的打印结果一般。

此时还有一种情况,会不会小美比张三先吃完饭呢?

有可能!比如 main 线程执行 t.join() 的时候,但是 t 线程的 run 方法已经执行完了,此时 join 就不会阻塞了,就会立即返回!

同时 join 还提供了两个参数的版本:public void join(long millis, int nanos) 这个可以更高精度的等待,这里就不多说了。


2. 休眠当前线程

这个方法在前面也使用过,这里就来更细入的介绍一下。

让线程休眠,本质上就是让这个线程不参与调度了(不去 CPU 上执行了)。

通过调用 Thread 类中的 sleep 静态方法,传入指定时间,就能令线程休眠了。

注意:哪个线程里调用 Thread.sleep(1000),就让哪个线程休眠 1000 毫秒!

回顾一下上面讲过的知识,如果 main 线程中调用 t.join(),表示 main 线程要等待 t 线程结束,并且 main 线程进入阻塞状态。

而现在所讲的 sleep 方法,本质上是让线程休眠,也是进入了阻塞状态!

如何阻塞?这里需要了解两个玩意,就绪队列,阻塞队列。

当线程 A 调用 sleep() 方法,就会进入一个阻塞队列,当 sleep 结束,就会进入到就绪队列中。

阻塞队列里的线程,也就是暂时不参与 CPU 的调度了,就绪队列中的线程,随时可以被 CPU 调度!

所以一旦线程进入阻塞状态,对应的 PCB 就进入阻塞队列了, 因此暂时不能被 CPU 调度了,那如果线程阻塞结束,对应 PCB 进入到就绪队列,就一定会立即被 CPU 调度到吗?不一定!

这里一定要明确,CPU 是随机调度的!

这里来看一段代码:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        while (true) {
            System.out.println("hello");
        }
    });
    t.start();
    Thread.sleep(5000);
    System.out.println("main 继续执行");
}

这里 t 线程死循环打印 "hello",而 main 线程中休眠了 5000ms,那么一定刚好休眠 5000ms 后就打印 "main 线程继续执行" 吗?

不一定!此时虽然是 sleep(5000),但实际上考虑到调度的开销,对应线程是无法在唤醒之后就立即执行的,所以实际上的时间大概率要大于 5000ms。


3. 线程的状态

3.1 认识线程的六种状态

线程的状态是一个枚举类型 Thread.State,一个线程从创建到销毁都有着不同的状态。

这里就看下这个 State 枚举类型里面包含的所有状态:

public static void main(String[] args) throws InterruptedException {
    for (Thread.State s : Thread.State.values()) {
        System.out.println(s);
    }
}

NEW 状态:创建了 Thread 对象,但是还没有调用 start 方法(内核中还没创建 PCB)

TERMINATED:表示内核中的 PCB 已经执行完毕了(run执行完了),但是 Thread 对象还在

RUNNABLE:就绪状态,细分 1) 等待被 CPU 调度2) 正在 CPU 上执行,但在 Java 中,没有 运行时状态,都是 RUNNABLE(就绪) 状态

WAITING:调用 wait 或 join 时进入的状态,后面介绍

TIMED_WAITING:调用 sleep 进入的状态

BLOCKED:等待锁时产生的状态

最后这三个状态都是阻塞状态,都表示线程的 PCB 在阻塞队列中,只是为啥阻塞的原因不同而已,这些在后续内容中会详细展开介绍。

3.2 线程的状态转换

从这一节开始就提到过,一个线程从创建到销毁都有着不同的状态,下面就用一张简图和多段代码来理解线程工作中不同状态的转换。

下面就来看几段代码,通过代码来观察下线程的状态:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 10_0000; i++) {

        }
    });
    System.out.println("start 之前: " + t.getState());
    t.start();
    System.out.println("线程执行中的状态 : " + t.getState());
    Thread.sleep(10);
    System.out.println("线程结束后的状态 : " + t.getState());
}

当线程进入 TERMINATED 状态后,对应的 PCB 也就销毁了,也就无法再重新启动线程了,否则会抛异常,但是线程对应的对象 t 并没有被释放,我们仍然可以调用这个对象的一些方法属性,但是无法通过多线程来做一些事情了!

通过代码查看线程调用 sleep 进入 TIMED_WAITING 状态

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    for (int i = 0; i < 1000; i++) {
        System.out.println("t 的状态: " + t.getState());
    }
}

这里发现大量打印 TIMED_WAITING 状态是因为线程执行的 sleep(1) 休眠时间太长了(站在 CPU 的视角),CPU 1 毫秒可以做很多事,对于执行线程中的 for 循环来说,只会转瞬即逝,所以该线程大部分时间都在休眠,真正在 CPU 上执行的时间特别少,所以也就出现 TIMED_WAITING 状态比 RUNNABLE 多特别多!

目前只演示这四个状态,剩下的 WAITING,BLOCKED 等后续学习到了,会进行讲解。


4. 多线程的优势

前面讲了那么多多线程的理论知识,还很少写过代码,那么现在我们就来通过一小段代码来一下多线程的优势在哪。

● 现有两个变量 a,b 用两个循环对这两个变量分别自增 100 亿次:

单线程实现:

public static void main(String[] args) {
    long a = 0;
    long b = 0;
    long begin = System.currentTimeMillis(); //开始时间
    for (long i = 0; i < 100_00000000L; i++) {
        a++;
    }
    for (long i = 0; i < 100_00000000L; i++) {
        b++;
    }
    long end = System.currentTimeMillis(); //结束时间
    System.out.println(end - begin + " ms");
}
// 打印结果:5097 ms

多线程实现:

public static void main(String[] args) throws InterruptedException {
    long begin = System.currentTimeMillis(); //开始时间
    Thread t1 = new Thread(() -> {
        int a = 0;
        for (long i = 0; i < 100_00000000L; i++) {
            a++;
        }
    });
    Thread t2 = new Thread(() -> {
        int b = 0;
        for (long i = 0; i < 100_00000000L; i++) {
            b++;
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis(); //结束时间
    System.out.println(end - begin + " ms");
}
// 打印结果:2817 ms

上述两种实现方式,在不同配置的电脑上,执行的时间都不一样,但多线程肯定是比单线程要快的,但这里有个疑问,为啥多线程消耗的时间不是单线程消耗时间的一半呢?

按道理来说,如果单线程跑两个循环需要 5000ms,那用两个线程一个线程跑一个循环,不应该是 2500ms 吗?

实际上,t1 和 t2 在执行过程中,会经历很多次调度,有时候是并发执行的(两个线程在一个核心上快速切换),有时候是并行执行的(两个线程在不同的核心上执行),至于什么时候是并发,什么时候是并行?这些都是不确定的,取决于系统的配置,也取决于当前程序运行的环境,如果同一时刻你电脑上跑的程序很多,此时并行的概率就更小了。另外线程的调度也是有开销了,所以上述多线程版本执行时间肯定不会比单线程版本缩短 50%。

通过上述案例,不难发现在这种 CPU 密集型的任务中,多线程有着非常大的作用,可以充分利用 CPU 多核资源,从而加快程序的执行效率。

多线程一定能提高效率吗?

其实也不一定,如果电脑配置特别特别低,CPU 只有双核,其实也提高不了多少。

如果当前电脑跑了特别多程序,CPU 核心都满载了,这个时候启动更多的线程也没啥用。

学习到现在,看似多线程很美好,但殊不知有一个很大的危险正在向我们靠近,这也是多线程编程中让程序猿苦恼的事情,同时也是面试官经常问的问题 —— 线程安全!


下期预告:【多线程】线程安全问题

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

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

相关文章

Ansys Lumerical | 光子集成电路之PN 耗尽型移相器仿真工作流

01 说明 本文旨在介绍Ansys Lumerical针对有源光子集成电路中PN耗尽型移相器的仿真分析方法。通过FDE和CHARGE求解器模拟并计算移相器的性能指标&#xff08;如电容、有效折射率扰动和损耗等&#xff09;&#xff0c;并创建用于INTERCONNECT的紧凑模型&#xff0c;然后将其表征…

vue_03

文章目录 导航菜单功能的实现在Admin.vue中添加下列代码布局选择点击跳转事件 vuex的安装及配置安装配置新建store和index.js在index.js下写如下代码在main.js中引入store 封装菜单请求工具类新建menus.js编写menus.js文件 解决F5刷新数据丢失问题 导航菜单功能的实现 在Admin…

多项开发任务,如何做好任务分配和管理?

1、确定任务清单 任务精细化分解 需要将任务进行精细化分解&#xff0c;每个子任务时间最好不超过一周&#xff0c;明确子任务的目标、时间点和交付物。 多项开发任务&#xff0c;如何做好任务分配和管理&#xff1f; 2、优先级排序 需要将精细化好的任务&#xff0c;进行优先级…

TFTP+Filezilla文件双向传输(2)-ubuntu(VMware)-win10(host)

TFTP&#xff08;Trivial File Transfer Protocol,简单文件传输协议&#xff09;是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议&#xff0c;提供不复杂、开销不大的文件传输服务。端口号为69。 ftpd-hpa是tftp服务器 tftp-hpa是tftp客服端 inetd的全称…

设计尝试用Microsoft Visual Studio 在网络上架构一个电影院网络订票系统

设计尝试用Microsoft Visual Studio 在网络上架构一个电影院网络订票系统的设计&#xff0c;以使每一位客户不用出门就能看到最新的电影信息和预定电影票。本文从理论和实践两个角度出发&#xff0c;对一个具有数据挖掘功能电影院网络订票系统的设计进行设计与实现分析。随着电…

windows下运行dpdk下的helloworld

打开“本地安全策略”管理单元&#xff0c;在搜索框输入secpol。 打开本地策略->用户权限分配->锁定内存页->添加用户或组->高级->立即查找 输入电脑用户名&#xff0c;选择并添加。点击确定后&#xff0c;重启电脑。 安装内核驱动&#xff0c;下载地址https:…

srm 采购管理系统是如何赋能企业降本的?

近年来&#xff0c;随着全球经济的不断发展&#xff0c;企业的竞争也日趋激烈&#xff0c;企业为适应市场需求&#xff0c;加强产品创新和技术升级&#xff0c;加大产品研发和生产投入&#xff0c;然而在新冠肺炎疫情的影响下&#xff0c;许多企业经营出现困境。在这种情况下&a…

基于时间戳和序列号的mac地址生成策略

基于时间戳 基于时间戳生成MAC地址的具体操作步骤如下&#xff1a; 获取设备的出厂日期和时间。一般情况下&#xff0c;这个信息可以在设备的测试报告或者出厂记录中找到。 将设备的出厂日期和时间转换成时间戳格式。时间戳指的是一个从1970年1月1日00:00:00 UTC开始的秒数。…

自动化数据驱动?最全接口自动化测试yaml数据驱动实战,看这一篇就够了

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 我们在做自动化测…

ROS学习第十九节——TF静态坐标变换

1.坐标msg消息 订阅发布模型中数据载体 msg 是一个重要实现&#xff0c;首先需要了解一下&#xff0c;在坐标转换实现中常用的 msg:geometry_msgs/TransformStamped和geometry_msgs/PointStamped 前者用于传输坐标系相关位置信息&#xff0c;后者用于传输某个坐标系内坐标点的…

js面试题

在全局作用域下声明了一个变量 arr, 它的初始值是一个空数组 第二段代码&#xff0c;循环计数器变量i的初始值为0&#xff0c;循环条件是i的值小于2&#xff0c; 也就是说当i的值为0或者1时&#xff0c; 循环条件才能成立 才能够进入到循环体 当i的值为2时循环条件不成立&…

ConcurrentLinkedQueue

唯一一个使用cas实现的线程安全并发效率高的集合。 一、为什么叫松散队列&#xff1f; 链表是松散的&#xff0c;链表节点并不都是有效的&#xff0c;允许存在无效节点valnull&#xff0c;但是只有最后一个节点才能nextnull 为什么线程安全需要把链表做成松散的。就是因为入队…

蓝精灵协会 (The Smurfs‘ Society) 宣布与著名艺术家展开一系列的合作,打造传奇 PFP 系列

4 月 18 日&#xff0c;The Smurfs Society 将推出第一个由 Smurfs 品牌支持的官方 PFP 系列。该系列建立在链上游戏的基础之上&#xff0c;该游戏聚集了超过 85,000 名玩家&#xff0c;并在设计、创意和与著名艺术家的合作方面设立了新标准。 而最近&#xff0c;蓝精灵官方&am…

2023-04-21 学习记录--C/C++-实现升序降序(选择法)

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、选择法 —— 升序 ⭐️ &#xff08;一&#xff09;、思路 从左到右&#xff0c;除最后一个数 依次作为 “当前数” 与 其右…

STM32F4_模数转换器(ADC)详解

目录 1. ADC是什么 2. ADC主要特性 3. ADC框图 3.1 ADC开关控制 3.2 ADC时钟 3.3 通道选择 3.4 单次转换模式和连续转换模式 3.5 时序图 3.6 模拟看门狗 4 温度传感器 5. ADC中断 6. ADC初始化结构体 6.1 ADC相关实验配置 7. 相关寄存器 7.1 ADC控制寄存器&…

Deep Neural Network for YouTube Recommendation论文精读

这篇论文 Deep Neural Networks for YouTube Recommendations 是google的YouTube团队在推荐系统上DNN方面的尝试&#xff0c;发表在16年9月的RecSys会议。本文所介绍的YouTube的推荐系统主要包括Deep Candidate Generation model和Deep Ranking model两个部分&#xff1a;Deep …

AWS EC2使用过程总结

步骤1&#xff1a;开通AWS账号 需要一个邮箱、一个信用卡账号&#xff1b;有第一年的免费试用&#xff0c;EC2每个月免费试用750小时&#xff1b;注册完成后&#xff0c;得到实例管理平台&#xff1a; 步骤2&#xff1a;开通EC2实例 步骤3&#xff1a;开通网关和安全组&…

【Python小技巧】一步到位升级到pandas 2.0.0正式版

文章目录 前言一、Pandas是什么&#xff1f;二、Pandas 2.0.0的升级特性三、升级安装Pandas 2.0.0正式版总结 前言 工欲善其事必先利其器&#xff0c;大数据、AI时代&#xff0c;目前Python 最新版本是3.11 &#xff0c;而Pandas也刚刚完成大升级&#xff0c;进入 2.0.0时代。…

ChatGPT 中的人类反馈强化学习 (RLHF) 实战

目录 1 前言2 人类反馈强化学习 (RLHF)2.1 奖励模型 (RM)2.2 近端策略优化算法 (PPO) 3 总结4 参考 团队博客: CSDN AI小组 相关阅读 ChatGPT 简介大语言模型浅探一关于 ChatGPT 必看的 10 篇论文从 ELMo 到 ChatGPT&#xff1a;历数 NLP 近 5 年必看大模型 1 前言 在当今数字…

整型、浮点型与字符串相互转换(C/C++)

文章目录 1. 整型、浮点型 -> 字符串2. 字符串 -> 整型3.字符串 -> 浮点型 1. 整型、浮点型 -> 字符串 A. 函数原型&#xff1a;   B. 分析  通常整型转换一般都是准确的&#xff0c;但是浮点型进行转换的时候因为精度问题有效数字位可能会出现一些偏差.   C. …