Java多线程:线程安全

在这里插入图片描述

  • 👑专栏内容:Java
  • ⛪个人主页:子夜的星的主页
  • 💕座右铭:前路未远,步履不停

目录

  • 一、线程状态
    • 1、`New`(初始状态)
    • 2、`Terminated`(终止状态)
    • 3、`Runnable`(可运行状态)
    • 4、休眠状态
  • 二、线程安全
    • 1、线程不安全的原因
    • 2、Demo问题解决
    • 3、`synchronized`关键字
      • 3.1 互斥特性
      • 3.2 刷新内存
      • 3.3 可重入
      • 3.4 使用示例
    • 4、`volatile`关键字
      • 4.1 内存可见性问题
      • 4.2 `volatile`用法
  • 三、`wait` 和 `notify`
    • 1、`wait`方法
    • 2、`notify`方法
    • 3、 `wait` 和 `sleep` 的对比


一、线程状态

在Java中,线程的状态可以分为以下几种主要状态:New(新建状态)、Runnable(可运行状态)、Blocked(阻塞状态)、Waiting(等待状态)、Timed Waiting(定时等待状态)、Terminated(终止状态)。
在这里插入图片描述

1、New(初始状态)

New(新建状态):线程处于新建状态,已经创建了线程对象但尚未调用其start()方法。在这个状态下,线程对象已经被创建,但尚未分配系统资源。

    public static void main(String[] args) {Thread t = new Thread(()->{while (true){}});System.out.println(t.getState());t.start();}

在这里插入图片描述

2、Terminated(终止状态)

Terminated(终止状态):线程进入终止状态表示它已经执行完成或因异常而终止。一旦线程的run()方法完成,它就会进入终止状态。一旦线程终止,它将不再处于任何其他状态。

    public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.getState());t.start();t.join();System.out.println(t.getState());}

在这里插入图片描述

3、Runnable(可运行状态)

Runnable(就绪状态):线程在这个状态下已经被启动,可以运行。它可能正在执行,也可能处于等待CPU资源的状态,或者是在等待某个特定的条件(如等待I/O操作完成)。就绪状态,可以理解为两种情况:1.线程正在 CPU 上运行。 2. 线程在这里排队,随时都能去 cpu 上执行。

    public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(true){}});System.out.println(t.getState());t.start();Thread.sleep(1000);System.out.println(t.getState());}

在这里插入图片描述

4、休眠状态

Timed_Waiting(定时等待状态):线程进入定时等待状态是因为调用了具有超时参数的等待方法,如sleep()join()。它会在指定的时间间隔内等待,或者直到被唤醒或中断。

Blocked(阻塞状态):线程进入阻塞状态通常是因为它在等待某个条件满足而无法继续执行,例如等待某个锁。一旦条件满足,线程将进入Runnable状态。

Waiting(等待状态):线程进入等待状态是因为调用了wait()方法,或者类似的等待方法,它会一直等待直到被其他线程唤醒或中断。

总结:Blocked是因为锁产生了阻塞,Waiting是因为wait()方法产生的阻塞,Timed_Waiting是因为sleep()join()产生的阻塞。

    public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(t.getState());t.start();Thread.sleep(1000);System.out.println(t.getState());}

在这里插入图片描述

在这里插入图片描述

二、线程安全

在开始线程安全之前,先通过一个小Demo来感受一下线程安全。

class Counter{public int count = 0;public void increase(){count++;}
}
//线程安全演示
public class Demo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for(int i = 0;i<5000;i++){counter.increase();}});Thread t2 = new Thread(()->{for(int i = 0;i<5000;i++){counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}

这个例子中,两个线程针对同一个变量,进行循环自增,各自自增5000次。按照正常情况,预计结果应该是10000。

在这里插入图片描述
但是,实际结果却是7794,并且同样的代码每次运行结果居然还不一样!
正常来看,我们的代码肯定没有问题,但是还是出现了这个bug,这个 bug 其实就是线程安全问题。

1、线程不安全的原因

线程不安全的根本原因是:多个线程之间的调度是“随机的”,操作系统使用的是“抢占式”执行的策略来调度线程

根据这个根本原因,可以衍生出一些其他原因:

  1. 多个线程同时修改同一变量。 需要注意的是,一个线程修改一个变量、多个线程读取一个变量或多个线程修改多个不相关的变量通常是安全的。
  2. 进行的修改不是原子的。“原子” 通常指的是一个操作是不可分割的、不可中断的单位。
  3. 内存可见性引起的线程安全问题。
  4. 指令的重排序引起的线程安全问题。

2、Demo问题解决

知道了线程不安全的原因后,再看一下上面的Demo,分析为什么会出现线程不安全的问题。
上面的线程不安全问题的bug,主要的原因就是count++ 这个代码出现了问题。我们拆解一下count++ 这个操作。
这个count++ 操作其实本质上是三个步骤:

  1. 把内存中的数据加载到CPU的寄存器中(load)
  2. 把寄存器中的数据进行+1(add)
  3. 把寄存器中的数据写回到内存中(save)

如果上述的操作,出现在单线程中其实是不会出现任何问题的,但是出现在多线程中就会出现问题。因为是两个线程并发执行,线程的调度是随机的,抢占式的执行。

这个Demo中,除了根本原因外,还出现了上面问题中的两个问题,“多个线程同时修改同一变量” 、“进行的修改不是原子的。”
多个线程修改同一个变量这个是我们的需求,所以我们没办法去改变他,所以,我们只能去解决“进行的修改不是原子的”这个问题。

如何修改为原子操作?加锁!所谓加锁,就是把把一组操作,打包成为一个原子的操作。Java 中引入了一个synchronized关键字进行加锁。这个关键字在后面详细解释,先使用这个给count++操作进行加锁。

class Counter{public int count = 0;synchronized public void increase(){count++;}
}

使用synchronized 给方法加锁,进入方法就自动加锁(lock),出了方法就自动解锁(unlock)。
当这个这个方法加锁后,这个方法就变成如下的样子:
在这里插入图片描述
当 t1 加锁后,t2 也尝试加锁,t2 就会阻塞等待。等待到 t1 释放锁后才能加锁成功。直到 t1 完成了 count++,t2 才能真正进行 count++。把穿插执行变成了串行执行。

3、synchronized关键字

3.1 互斥特性

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。

  • 进入synchronized 修饰的代码块,相当于加锁
  • 退出synchronized 修饰的代码块,相当于解锁
    在这里插入图片描述

可以粗略理解为,每个对象在内存中存储的时候,都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”)。

  • 如果当前是 “无人” 状态,那么就可以使用,使用时需要设为 “有人” 状态。
  • 如果当前是 “有人” 状态,那么其他人无法使用,只能排队。

3.2 刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性。

3.3 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
什么是 “把自己锁死” ?一个线程没有释放锁, 然后又尝试再次加锁。

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

在这里插入图片描述

按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待。直到第一次的锁被释放,才能获取到第二个锁。但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作。这时候就会出现死锁,这种锁也叫不可重入锁。Java 中的 synchronized 是 可重入锁,因此没有上面的问题。

3.4 使用示例

下面有一些使用示例,但是大家只需要牢记一条规则:当两个线程针对同一个对象进行加锁的时候,就会出现竞争/锁冲突。一个线程先拿到锁,另外一个线程就会进行阻塞等待,知道第一个线程释放了锁之后,第二个线程才能获取到锁继续往下执行。针对那个对象加锁的并不重要,重要的是两个线程是不是针对同一个对象进行加锁。

修饰方法: 这种方式是修饰整个方法,即使方法中没有同步代码块,也会锁定这个方法,这种方式适用于整个方法需要同步的情况。

public synchronized void method() {// 同步代码块
}

修饰代码块: 这种方式是将同步代码块包在synchronized括号内,只有在执行到synchronized代码块时才会锁定,这种方式适用于只需要同步执行部分代码的情况。

public void method() {synchronized (this) {// 同步代码块}
}

修饰静态方法: 和修饰方法类似,这种方式是锁定整个静态方法,适用于整个静态方法需要同步的情况。

public synchronized static void method() {// 同步代码块
}

修饰类: 这种方式是锁定整个类,即使不同实例中的线程也会被锁定,适用于整个类需要同步的情况。

public void method() {synchronized (ClassName.class) {// 同步代码块}
}

4、volatile关键字

4.1 内存可见性问题

import java.util.Scanner;//内存可见性
public class Demo {private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (isQuit == 0){;}System.out.println("t1 执行结束");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.print("请输入isQuit的值:");isQuit = scanner.nextInt();});t1.start();t2.start();}
}

这段代码的理想状态: t1始终在进行while循环,t2则是要让用户通过控制台输入一个整数,作为isQuit的值。当用户输入的仍然是0的时候,t1线程继续执行。如果用户输入的非0,t1线程就应该循环结束。

但是,实际上是当输入非 0 值的时候,已经输入 isQuit 的值的时候,t1 线程还在继续执行,不符合实际的预期。
在这里插入图片描述
导致这个的原因是因为程序在编译运行的时候,Java 编译器和 jvm 可能会对代码进行一些优化。当你的代码实际执行的时候,编译器 jvm 就可能把你的代码给改了,在保持原有逻辑不变的情况下,提高代码的效率。

编译器优化本质上是靠代码智能的对你的代码进行分析判断,这个过程中大部分是 ok 的,能保证代码逻辑不变,但是如果遇见多线程了,此时优化就有可能出现差错。使程序原有的逻辑发生改变。
在这里插入图片描述
编译器/jvm 发现,在这个逻辑中,代码要反复快速的读取同一个内存的值,并且这个值每次读取的还是一样。此时,编译器做出了一个大胆的决定,直接把 load 操作给优化掉了,只是第一次执行 load 后续不再执行 load 操作,直接拿寄存器中的数据进行比较了。但是,编译器没有想到,程序员在另外一个线程中修改了 isQuit 的值,因此就出现了误判。

4.2 volatile用法

volatile 本质上是保证变量的内存可见性(禁止该变量的读操作被优化到读寄存器中),不是原子性。

代码在写入 volatile 修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

为了解决上面的问题,我们只需要用volatile 来修饰这个变量后,编译器就会明白,这个变量是易变的,编译器会禁止上述优化。

public class Demo {private volatile static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (isQuit == 0){;}System.out.println("t1 执行结束");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.print("请输入isQuit的值:");isQuit = scanner.nextInt();});t1.start();t2.start();}
}

三、waitnotify

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,waitnotify就是解决这个问题的。

1、wait方法

wait 需要做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁

wait 要释放当前的锁,那前提就是他必须要上锁。所以,wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常。
在这里插入图片描述
在这里插入图片描述

public class Demo4 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object){object.wait();}System.out.println("wait结束");}
}

在这里插入图片描述
这里的 wait 会阻塞到其他线程 notify 为止。其中最典型的一个场景就是,能够有效的避免线程饿死。

2、notify方法

notify 方法是唤醒等待的线程。

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
package thread;public class Demo {private static Object locker = new Object();public static void main(String[] args) {Thread t1 = new Thread(()->{while (true){synchronized (locker){System.out.println("t1 wait开始");try {locker.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 wait结束");}}});t1.start();Thread t2 = new Thread(()->{while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (locker){System.out.println("t2 notify 开始");locker.notify();System.out.println("t2 notify 结束");}}});t2.start();}
}

注意事项:

  1. 要想让notify能够顺利唤醒wait,就需要确保wait和notify都是使用同一个对象调用的。
  2. waitnotify都需要放到synchronized之内,虽然notify不涉及解锁操作,但是Java也强制要求notify 放到synchronized
  3. 如果notify的时候,另外一个线程没有处于wait状态,此时的notify相当于空打一炮,没有任何副作用。

3、 waitsleep 的对比

其实理论上 waitsleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。

硬说区别的话,就是如下:

  • wait 需要搭配 synchronized 使用.sleep不需要
  • waitObject 的方法,sleepThread 的静态方法

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

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

相关文章

C++类型转化cast from pointer to smaller type ‘int‘ loses information

代码如下 #include <iostream>int main() {int a 10;std::cout << (int)&a << std::endl;return 0; }编译 这段代码是要将地址转化成整数类型&#xff0c;但是在编译时编译器告诉我们这是错的&#xff0c;因为在C中&#xff0c;将指针转换为int类型的…

Spring基础 - Spring核心之面向切面编程(AOP)

Spring基础 - Spring核心之面向切面编程(AOP) 引入 Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦&#xff0c;这个就叫面向切面编程 - Aspect Oriented Programming (AOP)那么Spring框架又是如何实现AOP的呢&#xff1f; 这就引入代理技术&#xff0c;分静…

Sqlite3安装步骤

1、Sqlite3以下载文件&#xff0c;配置环境变量的方式进行安装。 2、下方链接为官方的下载地址。 sqlite下载地址 2.1、需要两个下载文件&#xff0c;解压后将他们放在一起&#xff0c;假设解压后的路径为E:\sqlite。 sqlite-dll-win-x64-3450100.zip sqlite-tools-win-x6…

C++自定义函数详解

个人主页&#xff1a;PingdiGuo_guo 收录专栏&#xff1a;C干货专栏 铁汁们新年好呀&#xff0c;今天我们来了解自定义函数。 文章目录 1.数学中的函数 2.什么是自定义函数 3.自定义函数如何使用&#xff1f; 4.值传递和引用传递&#xff08;形参和实参区分&#xff09; …

OLED调试简介

文章目录 一、介绍调试方法介绍OLED简介硬件电路OLED驱动函数 二、操作连接线路使用驱动函数显示内容 OLED.c的内容 一、介绍 调试方法介绍 OLED简介 硬件电路 OLED驱动函数 二、操作 连接线路 因为这两个引脚不做配置是浮空状态&#xff0c;在这里直接用电源给OLED供电 使…

嵌入式学习之Linux入门篇笔记——10,Linux连接档概念

配套视频学习链接&#xff1a;http://【【北京迅为】嵌入式学习之Linux入门篇】 https://www.bilibili.com/video/BV1M7411m7wT/?p4&share_sourcecopy_web&vd_sourcea0ef2c4953d33a9260910aaea45eaec8 目录 1.Linux 下的连接档种类 2.什么是 inode&#xff1f; 3.什…

Node.js JSON Schema Ajv依赖库逐步介绍验证类型和中文错误提示

在构建应用程序时&#xff0c;数据的有效性是至关重要的。为了确保传入的数据符合预期的格式和规范&#xff0c;我们可以使用 Ajv&#xff08;Another JSON Schema Validator&#xff09;进行验证。在这篇博文中&#xff0c;我们将从头开始学习 Ajv&#xff0c;逐步介绍验证类型…

Linux探秘之旅:透彻理解路径、命令与系统概念

目录 如何远程连接 远程登录简明指南 linux区别 1.严格区分大小写 2.linux的命令返回结果判断 3.如何查看网络信息 4.关于后缀名&#xff08;Linux不关心文件后缀&#xff09; 4.1 需要记忆的后缀 5.echo命令 6.linux一切皆文件 6.1比如磁盘的文件 6.2可执行文件 …

在面试中如何回复擅长vue还是react

当面试官问及这个问题的时候&#xff0c;我们需要思考面试官是否是在乎你是掌握vue还是react吗&#xff1f;&#xff1f;&#xff1f; 在大前端的一个环境下&#xff0c;当前又有AI人工智能的加持辅助&#xff0c;我们是不是要去思考企业在进行前端岗位人员需求的时候&#xf…

【原创】Qt库open62541 MinGW编译

一、前言 为了统一公司的驱动层开发&#xff0c;准备采用OpcUA的方式转发底层数据&#xff0c;而服务器有Windows Server&#xff0c;也有CentOS&#xff0c;因此想用Qt开发一个基于MinGW的OpcUA Server&#xff0c;这样就能跨平台部署。这里记录一下&#xff0c;希望对你也有用…

Android Studio无法安装Git问题解决(折中方案)

安装配置好studio&#xff0c;往往会使用git克隆github上面的项目&#xff0c;但是却发现git无法正确安装&#xff0c;本文将介绍如何解决git无法安装这一问题。 对于git安装&#xff0c;实际比较复杂&#xff0c;可以参考这一篇博客。 Git 详细安装教程&#xff08;详解 Gi…

Java多线程:`Thread`类

&#x1f451;专栏内容&#xff1a;Java⛪个人主页&#xff1a;子夜的星的主页&#x1f495;座右铭&#xff1a;前路未远&#xff0c;步履不停 目录 一、Thread的常见构造方法二、Thread 的常见属性三、Thread的常用方法1、start方法2、中断一个线程Ⅰ、通过共享标记Ⅱ、调用in…

Git版本与分支

目录 一、Git 二、配置SSH 1.什么是SSH Key 2.配置SSH Key 三、分支 1.为什么要使用分支 2.四个环境及特点 3.实践操作 1.创建分支 2.查看分支 3.切换分支 4.合并分支 5.删除分支 6.重命名分支 7.推送远程分支 8.拉取远程分支 9.克隆指定分支 四、版本 1.什…

[BUUTF]-PWN:wdb2018_guess解析

查看保护 查看ida 这道题并不复杂&#xff0c;只是要注意一点细节 完整exp&#xff1a; from pwn import* from LibcSearcher import* pprocess(./guess) premote(node5.buuoj.cn,28068) puts_got0x602020payloadba*0x128p64(puts_got) p.sendlineafter(bPlease type your gu…

国产三维剖面仪—MPAS-100相控参量阵浅地层剖面仪

最近声学所东海站邹博士发来了他们最新的浅地层剖面仪—MPAS-100相控参量阵浅地层剖面仪的资料&#xff0c;市场型号GeoInsight&#xff0c;委托Ocean Physics Technology公司销售&#xff0c;地大李师兄的公司负责技术支持。 MPAS-100相控参量阵浅地层剖面仪就是俗称的三维浅…

【玩转408数据结构】线性表——定义和基本操作

考点剖析 线性表是算法题命题的重点&#xff0c;该类题目实现相对容易且代码量不高&#xff0c;但需要最优的性能&#xff08;也就是其时间复杂度以及空间复杂度最优&#xff09;&#xff0c;这样才可以获得满分。所以在考研复习中&#xff0c;我们需要掌握线性表的基本操作&am…

Red Hat安装Red Hat OpenShift Local

文章目录 环境安装需求硬件操作系统软件包 安装 使用Red Hat OpenShift Local预设置设置Red Hat OpenShift Local启动实例访问OpenShift集群访问OpenShift web console使用OpenShift CLI访问OpenShift集群访问内部 OpenShift registry 使用odo部署示例应用安装odo 停止实例删除…

Qt QML学习(一):Qt Quick 与 QML 简介

参考引用 QML和Qt Quick快速入门全面认识 Qt Widgets、QML、Qt Quick 1. Qt Widgets、QML、Qt Quick 区别 1.1 QML 和 Qt Quick 是什么关系&#xff1f; 1.1.1 从概念上区分 QML 是一种用户界面规范和标记语言&#xff0c;它允许开发人员创建高性能、流畅的动画和具有视觉吸引…

LiteFlow规则引擎框架

LiteFlow规则引擎框架 Hi&#xff0c;我是阿昌&#xff0c;今天介绍一个规则引擎框架&#xff0c;LiteFlow&#xff1b; 一、前言 那首先得知道什么是规则引擎&#xff1f;规则引擎是 一种用于自动化处理业务规则的软件组件。 在软件行业中&#xff0c;规则引擎通常用于解决…

【Java IO】同步异步和阻塞非阻塞真正的区别!!!

先上结论&#xff1a; 同步异步和阻塞非阻塞真正的区别&#xff01;&#xff01;&#xff01; 假设某个进程正在运行下面这段代码&#xff1a; ...... operatorA......; read(); operatorB......; operatorC......;当进程执行完operatorA后开始进行read系统调用&#xff0c;…