JavaEE初阶第四期:解锁多线程,从 “单车道” 到 “高速公路” 的编程升级(二)

专栏:JavaEE初阶起飞计划

个人主页:手握风云

目录

一、Thread类及常用方法

2.1. Thread的常见构造方法

2.2. Thread的常见属性

2.3. 启动一个线程

2.4. 中断一个线程

2.5. 等待一个线程

2.6. 休眠当前线程


一、Thread类及常用方法

2.1. Thread的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象并命名
Thread(Runnable target, String name)使用Runnable对象创建线程对象并命名

        name参数用来给线程取名字。名字叫啥,不影响线程的执行,不同的名字更利于调试。

public class Demo1 {public static void main(String[] args) {// 创建线程对象Thread t1 = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello t1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t1.start();// 使用Runnable对象创建线程Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello t2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t2.start();// 创建线程对象并使用name参数命名Thread t3 = new Thread("这是我的t3线程") {@Overridepublic void run() {while (true) {System.out.println("hello t3");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t3.start();// 使用Runnable对象创建线程并使用name参数命名Thread t4 = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello t4");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}, "这是我的t4线程");t4.start();}
}

        如上图所示,我们会发现这里没有main主线程。这是因为start执行完毕后,main方法就执行结束了,对应的主线程也结束了,随之自动销毁。

        对于ThreadGroup线程组,把若干个线程放到同一个组里面,这样的话就可以给每个组里的线程设置相同的属性。但这个并不常用,更多的是用到线程池。

2.2. Thread的常见属性

属性获取方法
IDgetID()
名称getName()
状态getState()
优先级getPriority()
是否为后台进程isDaemon()
是否存活isAlive()
是否被中断isInterrupted

        ID是线程的唯一标识,不同的线程不会重复。获取名称常用于调试和日志记录,帮助开发者了解当前正在执行的线程名称。线程状态可以分为NEW(新建)、RUNNABLE(就绪/运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)和TERMINATED(消亡)。线程的优先级是一个整数,表示线程在运行时的重要程度。Java中线程的优先级范围从1到10,其中1是最低优先级,10是最高优先级。

        isDaemon()方法用于判断线程是否为后台线程。后台进程,当线程没运行完,进程可以就结束,无论有多少个后台进程,都无法阻止进程结束。对应的还有前台进程,当线程没运行完,进程就不会结束,当有多个线程时,就得所有线程结束才能结束进程。main线程和自己创建的线程都是前台进程,剩下的都是后台进程。如果一个线程做得任务很重要,这个任务必须要做完,应该设置为前台线程;相反如果任务无关紧要,就可以设置为后台进程。isAlive()方法用于判断线程是否在运行。当一个线程启动后,它会一直运行,直到run()方法执行完毕或者线程被中断。isInterrupted()方法用于判断线程是否被中断。当一个线程被中断时,它的中断状态会被设置为true,可以通过isInterrupted()方法来检查这个状态。

public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"This is mine");t.start();System.out.println(t.getId());System.out.println(t.getName());System.out.println(t.getState());System.out.println(t.getPriority());System.out.println(t.isDaemon());System.out.println(t.isAlive());System.out.println(t.isInterrupted());}
}

2.3. 启动一个线程

        调用start()方法会真正调用系统中的API,线程跑起来之后就会自动执行到run()。调用start的方法本身就很快,一旦执行start,代码就会自动往下执行,不会产生任何的阻塞等待。

public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();System.out.println("hello main");}
}

        执行结果如上图所示,大部分情况下都是hello main在前,也可能会有例外。这是因为在start之后,main线程和t线程两个执行流,是一个并发执行关系。操作系统对于线程的调度是随机的,如果执行完start恰好被调度出CPU,此时CPU下次执行main还是t就不确定了。

        一个线程对象只能被启动一次。线程执行了start之后,就是就绪状态或者阻塞状态,对于这两种状态,不能再启动了。

public class Demo2 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("hello thread");});t.start();System.out.println("hello main");t.start();}
}

        总结:使用Interrupt方法时,t线程没有使用sleep等阻塞操作,t的isInterrupted()方法返回true,通过循环条件结束t线程;t线程使用了sleep等阻塞操作,t的isInterrupted()方法也会返回true,但sleep如果被提前唤醒,抛出InterruptedException异常。

2.4. 中断一个线程

        这里不要跟操作系统里的中断搞混,线程的中断准确来说应该叫“打断”或者“终止”。正常情况下,一个线程需要把入口方法执行完,才能够使线程结束。但有时候,我们希望线程能够提前终止,尤其是在线程休眠的时候。这时就需要通过打断线程的操作,也需要线程本身代码做出配合。

  • 通过变量
public class Demo3 {public static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (flag) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("hello main");Thread.sleep(5000);flag = false;System.out.println("让t线程终止");}
}

  • 通过内置的标志位isInterrupted()

        Thread对象中,包含了一个布尔变量,如果为false,说明没有人去尝试终止这个线程;如果为true,说明有人尝试终止。

public class Demo4 {public static void main(String[] args) {Thread t = new Thread(() -> {while (!t.isInterrupted()) {}});}
}

        此时这个代码是有错的,原因如下图所示:变量t未初始化,因为此处针对lambda表达式的定义是在new Thread()之前。

        我们可以使用Thread类内部的静态方法currentThread(),用于获取对当前正在执行的线程对象的引用。

public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");}});t.start();// 主线程休眠4秒Thread.sleep(4000);// 中断线程t// 把标志位false改为truet.interrupt();}
}

        这里可能打印的结果有点多,我们也可以让t线程休眠1秒。

public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();// 主线程休眠4秒Thread.sleep(5000);// 中断线程t// 把标志位false改为truet.interrupt();}
}

        我们发现当线程抛出异常时,但线程并没有终止结束。线程里面有个奇怪的设定:如果线程t正在休眠,此时在main中调用interrupt()方法,就能把sleep提前唤醒。InterruptedException支持sleep提前唤醒,通过一场区分sleep是睡足了还是提前醒了。sleep提前唤醒,触发异常之后,然后sleep就会把isInterrupted标志位给重置为false。

        之所以会有这样奇怪的设定,是为了给程序员留下更多的操作空间。提前唤醒,可能还存在一些“还未完成的工作”,让程序员自行决定线程t是继续执行、立即结束还是稍等一会结束。上面的代码相当于完全忽视了终止请求。

public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {// 当线程t没有被中断时,打印"hello thread"while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {// 打印异常调用栈//// e.printStackTrace();// 触发异常就会结束循环,终止线程break;}}});t.start();// 主线程休眠4秒Thread.sleep(5000);// 中断线程t// 把标志位false改为truet.interrupt();}
}

        上述几种方式本质上都是线程t自己决定自己是否要终止,相当于main只是给t提供了一个“建议”而不是强制结束。

2.5. 等待一个线程

        有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱。线程等待,约定了两个线程结束的先后顺序。

        哪个线程中调用的join,该线程就是等待的一方;join前面的引用,该线程就是被等的一方。

public class Demo5 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t.start();// 主线程中,可以对线程t进行等待System.out.println("主线程等待之前");// 由于主线程也可能触发阻塞,也可能抛出InterruptedException异常t.join();System.out.println("主线程等待之后");}
}

        join这个等待是个死等,只要被等的线程没有结束,join都会始终阻塞。如果上面是个死循环,那么主线程就会永远等待t线程。

        上面是t线程等待主线程,也可以让主线程等待t线程。

public class Demo6 {public static void main(String[] args) throws InterruptedException {// 获取主线程Thread mainThread = Thread.currentThread();Thread t = new Thread(() -> {try {System.out.println("t线程等待之前");mainThread.join();System.out.println("t线程等待之后");} catch (InterruptedException e) {e.printStackTrace();}});t.start();// 主线程等待t线程for (int i = 0; i < 10; i++) {System.out.println("hello main");Thread.sleep(1000);}}
}

        当然也可以让两个线程同时等待对方,但是这样写代码是意义的,会造成两个线程都无法结束,都无法完成对方的等待操作。

        虽然join会触发阻塞,但也不一定会触发,比如在主线程等待t线程之前,t线程已经结束了,此时join就不会阻塞。join默认会是死等,但死等这种情况不太好,如果程序出现了意外,永远等不到结果。所以join方法还有其它的重载版本,可以指定一个最大等待时间(超时时间)。

方法说明
join()等待线程结束
join(long millis)等待线程结束,最多等millis毫秒
join(long millis,int nanos)更高精度
public class Demo7 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t.start();System.out.println("等待之前");// 等3秒之后就不等了t.join(3000);System.out.println("等待之后");}
}

2.6. 休眠当前线程

        Thread.sleep本质就是让线程的状态变成“阻塞”状态,此线程就不参与CPU调度了。休眠时间到了,线程的状态恢复成就绪状态(不是立即执行),才能参与CPU调度。

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

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

相关文章

elk+filebeat收集springboot项目日志

目录 步骤 1: 安装和配置Elasticsearch 步骤 2: 安装和配置Logstash&#xff08;可选&#xff09; 步骤 3: 安装和配置Filebeat 步骤 4: 安装和配置Kibana 要使用ELK&#xff08;Elasticsearch, Logstash, Kibana&#xff09;堆栈和Filebeat来收集Spring Boot项目的日志&am…

基于Python实现(控制台)UDP传输协议的可靠文件传输工具

LFTP Design 简介 LFTP是一个采用python3实现的基于UDP传输协议的可靠文件传输工具 特点 基于UDP 采用python3编程语言&#xff0c;socket的类型均为socket(AF_INET,SOCK_DGRAM)实现 实现100%可靠性传输 使用SR&#xff08;选择重传&#xff09;协议保证所有报文都正确接收…

【Go-7】面向对象编程

7. 面向对象编程 面向对象编程&#xff08;Object-Oriented Programming&#xff0c;简称OOP&#xff09;是一种编程范式&#xff0c;通过将数据和行为封装在对象中&#xff0c;以提高代码的可重用性、可维护性和扩展性。虽然Go语言不像传统的OOP语言&#xff08;如Java、C&am…

PHP语法基础篇(六):数组

PHP 中的数组实际上是一个有序映射。映射是一种把 values 关联到 keys 的类型。此类型针对多种不同用途进行了优化&#xff1b;它可以被视为数组、列表&#xff08;向量&#xff09;、哈希表&#xff08;映射的实现&#xff09;、字典、集合、堆栈、队列等等。本篇文章将记录数…

GitHub Actions 的深度解析与概念介绍

GitHub Actions 核心定义 Git Actions 是 GitHub 原生提供的 自动化工作流引擎&#xff0c;允许开发者在代码仓库中直接创建、测试、部署代码。其本质是通过事件驱动&#xff08;Event-Driven&#xff09;的自动化管道&#xff0c;将软件开发中的重复任务抽象为可编排的流程。…

TestCafe 全解析:免费开源的 E2E 测试解决方案实战指南

在2025年的数字化浪潮中&#xff0c;Web应用的稳定性和用户体验成为企业竞争的关键&#xff0c;而端到端&#xff08;E2E&#xff09;测试则是确保质量的“守护者”&#xff01;想象一下&#xff0c;您的电商平台因表单错误导致用户流失&#xff0c;或者支付流程因浏览器兼容性…

[CVPR 2025] 高效无监督Prompt与偏好对齐驱动的半监督医学分割

CVPR 2025 | 优化SAM&#xff1a;高效无监督Prompt与偏好对齐驱动的半监督医学分割 论文信息 标题&#xff1a;Enhancing SAM with Efficient Prompting and Preference Optimization for Semi-supervised Medical Image Segmentation作者&#xff1a;Aishik Konwer, Zhijian…

【C++】责任链模式

目录 一、模式核心概念与结构二、C++ 实现示例:员工请假审批系统三、责任链模式的关键特性四、应用场景五、责任链模式与其他设计模式的关系六、C++ 标准库中的责任链模式应用七、优缺点分析八、实战案例:Web 请求过滤器链九、实现注意事项如果这篇文章对你有所帮助,渴望获得…

dp进阶,树形背包(dfs+01)

顾名思义&#xff0c;就是在对树进行搜索的时候&#xff0c;由于限制了子节点选根节点必选和节点数限制&#xff0c;所以需要额外利用背包来维护最大值 假设根节点就是0&#xff0c;我们很容易 发现&#xff0c;这就是一个正常的树求和&#xff0c;但是限制了节点数量&#xf…

微信小程序安卓手机输入框文字飘出输入框

最近在开发微信小程序遇到一个问题&#xff0c;安卓手机输入框文字飘出输入框&#xff0c;但是ios系统的手机则正常。 使用情景&#xff1a;做了一个弹窗&#xff0c;弹窗内是表单&#xff0c;需要填写一些信息&#xff0c;但是在填写信息时光标不显示&#xff0c;输入的内容飘…

3 大语言模型预训练数据-3.2 数据处理-3.2.2 冗余去除——3.后缀数组(Suffix Array)在大模型数据去重中的原理与实战

后缀数组&#xff08;Suffix Array&#xff09;在大模型数据去重中的原理与实战 一、后缀数组的核心原理与数据结构二、后缀数组去重的核心流程1. **文档预处理与合并**2. **构建后缀数组**3. **计算最长公共前缀&#xff08;LCP&#xff09;数组**4. **基于LCP检测重复文档** …

数据库外连接详解:方式、差异与关键注意事项

&#x1f504; 数据库外连接详解&#xff1a;方式、差异与关键注意事项 外连接用于保留至少一个表的全部行&#xff0c;即使另一表无匹配记录。以下是三种外连接方式的深度解析&#xff1a; &#x1f50d; 一、外连接的三种类型 1. 左外连接 (LEFT OUTER JOIN) 作用&#xf…

vscode把less文件生成css文件配置,设置生成自定义文件名称和路径

1.下载less插件 在插件市场搜索 less 2.设置生成配置 3.修改out属性 "less.compile": {"compress": false, // 是否删除多余空白字符 一行显示[压缩]"sourceMap": false, // 是否创建文件目录树&#xff0c;true的话会自动生成一个 .css.map …

探索相机成像的奥秘 - 齐次坐标、径向失真和图像传感器倾斜

引言 大家好&#xff01;今天我们将一起探索相机成像背后的一些关键技术概念&#xff1a;齐次坐标、径向失真和图像传感器倾斜。这些概念对于理解相机如何捕捉和处理图像至关重要。我们将通过简单易懂的语言和严谨的公式来详细解释这些概念。 齐次坐标&#xff08;Homogeneou…

校企协同育人,智慧养老实训基地助力人才就业无忧

随着我国人口老龄化程度不断加深&#xff0c;智慧养老产业蓬勃发展&#xff0c;对专业人才的需求日益迫切。校企协同打造智慧养老实训基地&#xff0c;成为解决人才供需矛盾、提升人才培养质量的重要途径。通过科学的建设方案&#xff0c;智慧养老实训基地能够为学生提供实践平…

从需求到落地:一个AI训练平台的售前全流程复盘

目录 一、项目背景:客户要建自己的AI训练平台 二、需求梳理三板斧:并发量、存储带宽、模型种类 1. 并发训练量 2. 存储带宽需求 3. 模型类型与参数规模 三、解决方案设计:GPU选型 + 高速网络 + 存储架构 ✅ GPU服务器选型 ✅ 网络与通信架构 ✅ 存储与数据缓存 四…

织梦DedeCMS转WordPress

最近&#xff0c;有个用户找模板兔迁移网站&#xff0c;源站用的dede&#xff0c;需要转成wp&#xff0c;文章数量大概7000-8000篇&#xff0c;其中有个需求是保证旧文章的链接有效&#xff0c;在wp上的新文章与旧文章的链接类型不一样&#xff0c;所以这涉及到伪静态来处理跳转…

installGo.sh

#!/bin/bash # 检查是否以root用户运行 if [ "$(id -u)" -ne 0 ]; then echo "请使用root权限运行此脚本" exit 1 fi # 检查是否安装了必要的工具 for cmd in curl wget tar; do if ! command -v $cmd &> /dev/null; then echo…

【技术难题】el-table的全局数据排序实现示例,不受分页影响,以及异步请求带来的页面渲染问题

参考链接:https://blog.csdn.net/qq_35770559/article/details/131183121 问题代码 编辑页面detail.vue <el-form title="列表信息" name="detail"><el-form><el-form-item><el-buttontype="cyan"icon="el-icon-p…

非功能测试

非功能测试范畴&#xff1a;界面测试&#xff0c;易用性测试&#xff0c;兼容性测试&#xff0c;文档测试&#xff0c;安装/卸载测试等等 界面测试 1.窗体界面测试 1.窗体定义&#xff1a;指整个软件窗口&#xff0c;也可称为窗口&#xff0c;是界面测试的基本单位 2.控件分…