【JavaEE】【多线程】Thread类讲解

目录

  • Thread构造方法
  • Thread 的常见属性
  • 创建一个线程
  • 获取当前线程引用
  • 终止一个线程
    • 使用标志位
    • 使用自带的标志位
  • 等待一个线程
  • 线程休眠
  • 线程状态
  • 线程安全
    • 线程不安全原因总结
    • 解决由先前线程不安全问题例子

Thread构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名(当前线程名)
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组

Thread 的常见属性

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

解释:

  • ID 是线程的唯一标识,不同线程不会重复,但是这里的id是Java给的id,不是前面PCB中说的id。
  • 名称在各种调试工具用到,前面构造方法给的名称就是这个。
  • 状态表示线程当前所处的一个情况。
  • 优先级高的线程理论上来说更容易被调度到,但是这个是系统微观程度上的,很难感知到。
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程(前台线程)结束后,才会结束运行,而后台线程不影响Java进程的结束,可以在start()调用前使用setDaemon(true)来设置线程为后台线程。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了

创建一个线程

在前一篇文章中就介绍了相关操作,在这简单提一下一定要使用线程变量名.start();创建一个新线程,start()方法是Java提供的API来调用系统中创建线程的方法。而run()方法是这个线程要干的事情,在线程创建好之后自动就会调用。
每个线程对象只能start一次

获取当前线程引用

方法说明
public static Thread currentThread();返回当前线程对象的引用

是静态方法直接使用Thread.currentThread();就可以获取到当前的线程引用。

终止一个线程

在Java中终止一个线程的思路就是让线程中的run()方法尽快结束。

使用标志位

由于线程迟迟不结束大多是因为里面有循环语句,我们就可以使用一个成员变量来控制循环的结束。
不能使用局部变量定义在main方法内,因为虽然lambda表达式可以捕获上层变量,但是这个变量不可以进行修改。

public class Demo {private static boolean isQuit = false;public static void main(String[] args) {Thread thread = new Thread(() ->{while(isQuit) {//具体操作  }});thread.start();isQuit = true;}
}

使用自带的标志位

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位,不建议使用,静态方法为所有线程共用的
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

Java中自带了标志位来标志是否结束循环。先使用Thread.currentThread()获取到当前线程,在.isInterrupted()获取标志位。然后再主进程中调用interrupte()方法来将标志位值修改为true。

public class Demo {public static void main(String[] args) {Thread thread = new Thread(() ->{while (!Thread.currentThread().isInterrupted()) {//操作}});thread.start();thread.interrupt();}
}

但是如果在线程中有捕获InterruptedException异常的语句,那么会在调用interrupte()同时捕获到该异常,并且消除标志位。
此时我们就可以在catch语句中自己选择是将线程结束还是进行其它操作。

public class Demo {public static void main(String[] args) {Thread thread = new Thread(() ->{while (!Thread.currentThread().isInterrupted()) {try {Thread.sleep(1000);} catch (InterruptedException e) {//1.不操作继续执行线程e.printStackTrace();//2.结束线程break;//3.进行其它操作}}});thread.start();thread.interrupt();}
}

等待一个线程

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)等待线程结束,最多等 millis 毫秒,但可以更高精度

在主线程中调用线程对象.join();就是等待线程对象执行完再执行主线程。
调用细节:

  • 调用线程对象.join();就会让该线程执行完才继续执行外面的线程,如果线程对象对应的线程一直不结束那么外面的线程就会一直等(死等)
  • 调用线程对象.join(long millis);就会在该线程执行millis毫秒后执行外面的线程。
  • 如果遇到调用join前线程已经结束,外面的线程不会陷入等待。

如下代码执行结果就是先打印5个thread线程,最后在打印main线程:

public class Demo6 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for(int i = 0; i < 5; i++) {System.out.println("thread线程");}});       thread。start();thread.join();System.out.println("main线程");}
}

线程休眠

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

在系统让线程休眠sleep中的参数毫秒后,线程会被唤醒从阻塞状态变成就绪状态,但不会马上执行,涉及到调度开销。所以实际使用的时间是大于sleep中的参数的。
并且在Windows和Linux系统上达到毫秒级误差。

线程状态

在操作系统里面进程和线程最重要的状态就是:就绪状态和阻塞状态。
在Java中又给线程又给线程赋予了一些其他状态。
线程的状态是一个枚举类型 Thread.State。

状态说明
newThread对象已经创建,但是start方法没有调用
terminatedThread对象还在,但是内核中线程已将结束了
Runnable就绪状态,线程已经在CPU上执行或者在CPU上等待执行
timed_waiting由于sleep这种固定时间产生的阻塞
waiting由于wait这种不固定时间产生的阻塞
blocked由于锁竞争产生的阻塞

线程安全

线程安全的简单说法就是符不符合预期:如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

例如以下代码:
我们的预期结果是10000,但是其实每次的结果都是不一样的,这种就是线程不安全。

public class Demo {private static int ret;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {ret++;}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {ret++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(ret);;}
}

就以上诉代码例子来讲解出现线程不安全的原因。

在CPU上实现自增操作主要有三步:

  1. 将数据给到CPU的寄存器中;
  2. 数据在寄存器中加1;
  3. 将数据返回到内存中。

就以一个thread1和一个thread2来说,每个线程都进行这三步操作,但是线程在CPU上又是随机调用的,这就相当于有六个位置随机坐,相当于排列组合的A66,当数据作为不同线程的开始值进入寄存器时就相当于两次自增只执行了一次。

但是线程调用就更加复杂了,线程数量不一样,顺序不一样,这就相当于有无数种可能了,所以结果是不可控的,就导致了线程不安全的情况。

线程不安全原因总结

在介绍线程不安全原因之前先介绍一个概念:原子性。

原子性:简单来讲就是执行一段代码连续执行完不被其他线程干扰。举个例子:

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

原因总结:

  • 操作系统调度线程是随机的(抢占式执行);
  • 多个线程对同一个变量进行修改;
  • 修改操作不是原子性的;
  • 内存可见性问题;
  • 指令重排序问题。

解决由先前线程不安全问题例子

要解决就要从原因入手:

  • 操作系统随机调度是操作系统带来的解决不了;
  • 多个线程对一个变量修改,有些可以规避,但有些根据需求无法规避。
  • 将操作改为原子性,可以通过synchronized关键字 加锁操作来实现。

语法:

synchronized(变量){
//修改操作
}

()括号内的变量不重要,作用是区分加锁对象是否一样,如果对同一个对象加锁,那么两个操作就会产生“blocked”锁竞争阻塞问题,后一个线程就会等到前一个线程解锁再执行。
进入左大括号 ‘{’ 就是加锁,出了右大括号 ‘}’ 就是解锁。

对上诉代码进行如下修改,就会出现预期结果10000:

public class Demo7 {private static int ret;public static void main(String[] args) throws InterruptedException {Object block = new Object();Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (block){ret++;}}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (block){ret++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(ret);;}
}

synchronized还可以修饰方法(静态方法也行)。

  • synchronized修饰实例方法:
class Counter{public int ret;public void increase1() {synchronized (this) {ret++;}}//简化版本synchronized public void increase2() {ret++;}
}
  • synchronized修饰静态方法:相当于修饰这个类
class Counter{private static int ret2;public static void increase3() {synchronized (Counter.class) {ret2++;}}//简化版本synchronized public static void increase4() {ret2++;}
}

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

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

相关文章

初始Redis

Mysql最大的问题在于,访问速度比较慢 而Redis是内存中存储数据的中间件,可以作为数据库使用,比较快,和Mysql相比,存储空间有限 Redis是在分布式系统中,才能发挥威力的,在单机程序,直接通过变量存储数据的方式,是比使用redis更优的选择 那么要求更大更快,就可以把redis和mysq…

修改银河麒麟操作系统V10(SP1)网卡名称为ethx

修改银河麒麟桌面操作系统V10&#xff08;SP1&#xff09;网卡名称为ethx 步骤一&#xff1a;查看当前网卡信息步骤二&#xff1a;修改GRUB配置文件步骤三&#xff1a;更新GRUB配置步骤四&#xff1a;编辑网络接口文件步骤五&#xff1a;重启机器 &#x1f496;The Begin&#…

【Kubernetes】常见面试题汇总(五十五)

目录 121. POD 创建失败&#xff1f; 122. POD 的 ready 状态未进入&#xff1f; 特别说明&#xff1a; 题目 1-68 属于【Kubernetes】的常规概念题&#xff0c;即 “ 汇总&#xff08;一&#xff09;~&#xff08;二十二&#xff09;” 。 题目 69-113 属于【Kube…

数据结构-排序1

1.排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序…

【项目安全设计】软件系统安全设计规范和标准(doc原件)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 资料获取&#xff1a;私信或者进主页。…

『网络游戏』窗口基类【06】

创建脚本&#xff1a;WindowRoot.cs 编写脚本&#xff1a; 修改脚本&#xff1a;LoginWnd.cs 修改脚本&#xff1a;LoadingWnd.cs 修改脚本&#xff1a;ResSvc.cs 修改脚本&#xff1a;LoginSys.cs 运行项目 - 功能不变 本章结束

【数据管理】DAMA-元数据专题

导读&#xff1a;元数据是关于数据的组织、数据域及其关系的信息&#xff0c;是描述数据的数据。在数据治理中&#xff0c;元数据扮演着至关重要的角色&#xff0c;是数据治理的基础和支撑。以下是对数据治理中元数据专题方案的详细介绍&#xff1a; 目录 一、元数据的重要性 …

Qt教程(001):Qt概述与安装

文章目录 一、Qt概述1.1 什么是Qt1.2 Qt优点1.3 Qt发展史1.4 支持的平台1.5 成功案例1.6 下载安装1.7 QtCreator介绍 一、Qt概述 1.1 什么是Qt Qt是一个跨平台的C图形用户界面应用程序框架。它为应用程序开发者提供建立艺术级图形界面所需的所有功能。它是完全面向对象的&…

如何高效使用Prompt与AI大模型对话

一、如何与人工智能对话 在人工智能的世界里&#xff0c;提示词&#xff08;Prompt&#xff09;就像是一把钥匙&#xff0c;能够解锁AI智能助手的潜力&#xff0c;帮助你更高效地获取信息、解决问题。但如何正确使用这把钥匙&#xff0c;却是一门艺术。本文将带你了解提示词的…

如何通过视觉分析检测车辆逆行行为

随着交通网络的快速扩展和车辆数量的持续增加&#xff0c;城市交通管理面临着前所未有的挑战。交通事故的多发原因之一是车辆逆行&#xff0c;这种行为不仅严重威胁其他车辆和行人的安全&#xff0c;也加重了交通拥堵问题。因此&#xff0c;如何有效监控并预防车辆逆行成为城市…

【Verilog学习日常】—牛客网刷题—Verilog进阶挑战—VL45

异步FIFO 描述 请根据题目中给出的双口RAM代码和接口描述&#xff0c;实现异步FIFO&#xff0c;要求FIFO位宽和深度参数化可配置。 电路的接口如下图所示。 双口RAM端口说明&#xff1a; 端口名 I/O 描述 wclk input 写数据时钟 wenc input 写使能 waddr input 写…

用 LoRA 微调 Stable Diffusion:拆开炼丹炉,动手实现你的第一次 AI 绘画

总得拆开炼丹炉看看是什么样的。这篇文章将带你从代码层面一步步实现 AI 文本生成图像&#xff08;Text-to-Image&#xff09;中的 LoRA 微调过程&#xff0c;你将&#xff1a; 了解 Trigger Words&#xff08;触发词&#xff09;到底是什么&#xff0c;以及它们如何影响生成结…

计组与体系软题1-数据表示与校验码

一、数的编码方式 题1-0的表示 题2-补码的补码原码 1. 这道题涉及到数的编码范围和进制转换2. 题3-采用补码的目的 二、编码范围 题1-补码的表示范围(-2^(n-1)~2 ^(n-1)-1) n是字长/位数&#xff0c;2^7128&#xff0c;范围为-128~127题2-原码范围&#xff08;-2^&#xff0…

LORD-GX5-45 ROS安装

1、驱动安装 https://github.com/LORD-MicroStrain/MSCL 上述下载 x64:C&#xff0c;在下载完的deb文件下执行 sudo dpkg -i <PACKAGE_NAME>.deb #install MSCL sudo apt install -f #install dependencies2、源码安装 #新建工作空间 mkdir -p ~…

【C++】认识匿名对象

文章目录 目录 文章目录前言一、对匿名对象的解读二、匿名对象的对象类型三、匿名对象的使用总结 前言 在C中&#xff0c;匿名对象是指在没有呗命名的情况下创建的临时对象。它们通常在单个语句中执行一系列操作或调用某个函数&#xff0c;并且不需要将结果存放进变量中。 匿名…

【STM32单片机_(HAL库)】4-2-1【定时器TIM】定时器输出PWM实现呼吸灯实验

1.硬件 STM32单片机最小系统LED灯模块 2.软件 pwm驱动文件添加定时器HAL驱动层文件添加GPIO常用函数定时器输出PWM配置步骤main.c程序 #include "sys.h" #include "delay.h" #include "led.h" #include "pwm.h"int main(void) {HA…

音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现

一、SCRIPTDATAVALUE类型 从《音视频入门基础&#xff1a;FLV专题&#xff08;9&#xff09;——Script Tag简介》中可以知道&#xff0c;根据《video_file_format_spec_v10_1.pdf》第80到81页&#xff0c;SCRIPTDATAVALUE类型由一个8位&#xff08;1字节&#xff09;的Type和…

动态代理有用吗?一文了解靠谱的动态代理有哪些标准

在当今互联网时代中&#xff0c;从网络安全、隐私保护、市场调研和互联网营销到软件测试、缓存管理和数据库连接&#xff0c;用户为了更好地完成此类工作&#xff0c;往往会使用动态代理&#xff0c;那么进一步了解动态代理、明确动态代理的使用场景和选择标准则是十分有必要的…

OJ在线评测系统 后端微服务架构 注册中心 Nacos入门到启动

注册中心 服务架构中的注册中心是一个关键组件&#xff0c;用于管理和协助微服务之间的通信。注册中心的主要职责是服务的注册和发现&#xff0c;确保各个微服务能够相互找到并进行调用。 主要功能&#xff1a; 服务注册&#xff1a;微服务在启动时&#xff0c;将自身信息&am…

vite学习教程01、vite构建vue2

文章目录 前言一、vite初始化项目二、修改配置文件2.1、修改main.js文件2.2、修改App.vue文件2.3、修改helloworld.vue2.4、修改vite.conf.js2.5、修改vue版本--修改package.json文件 三、安装vue2和vite插件四、启动服务资料获取 前言 博主介绍&#xff1a;✌目前全网粉丝3W&…