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://www.mzph.cn/news/676470.shtml

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

相关文章

以管理员权限删除某文件夹

到开始菜单中找到—命令提示符—右击以管理员运行 使用&#xff1a;del /f /s /q “文件夹位置” 例&#xff1a;del /f /s /q "C:\Program Files (x86)\my_code\.git"

Flink从入门到实践(二):Flink DataStream API

文章目录 系列文章索引三、DataStream API1、官网2、获取执行环境&#xff08;Environment&#xff09;3、数据接入&#xff08;Source&#xff09;&#xff08;1&#xff09;总览&#xff08;2&#xff09;代码实例&#xff08;1.18版本已过时的&#xff09;&#xff08;3&…

《MySQL 简易速速上手小册》第9章:高级 MySQL 特性和技巧(2024 最新版)

文章目录 9.1 使用存储过程和触发器9.1.1 基础知识9.1.2 重点案例&#xff1a;使用 Python 调用存储过程实现用户注册9.1.3 拓展案例 1&#xff1a;利用触发器自动记录数据更改历史9.1.4 拓展案例 2&#xff1a;使用 Python 和触发器实现数据完整性检查 9.2 管理和查询 JSON 数…

VBA技术资料MF117:测试显示器大小

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套&#xff0c;分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的入门&#xff0c;到…

图论与图数据应用综述:从基础概念到知识图谱与图智能

目录 前言1 图论基础概念1.1 节点度1.2 度分布1.3 邻接矩阵 2 探索图的高级概念2.1 最短路径的关键性2.2 图的直径与平均路径的意义2.3 循环与路径类型的多样性 3 深入探讨图的广泛应用领域3.1 知识图谱的知识管理3.2 图智能在复杂决策中的应用3.3 图数据挖掘与分析的多领域应用…

[C#]winform制作仪表盘好用的表盘控件和使用方法

【仪表盘一般创建流程】 在C#中制作仪表盘文案&#xff08;通常指仪表盘上的文本、数字或指标显示&#xff09;涉及到使用图形用户界面&#xff08;GUI&#xff09;组件&#xff0c;比如Windows Forms、WPF (Windows Presentation Foundation) 或 ASP.NET 等。以下是一个使用W…

APEX开发过程中需要注意的小细节2

开发时遇到首次获取租户号失败的问题 以为是触发顺序问题&#xff0c;所以设置两个动态操作&#xff0c;一个事件是“更改”&#xff0c;另一个是“单击”&#xff0c; 但还是没有解决&#xff0c; 后来终于找到解决方法:在校验前执行取值 果然成功执行&#xff01; 动态查询年…

Zookeeper集群搭建(3台)

准备工作 1、提前安装好hadoop102、hadoop103、hadoop104三台机器&#xff0c;参照&#xff1a;CentOS7集群环境搭建&#xff08;3台&#xff09;-CSDN博客 2、提前下载好Zookeeper安装包并上传到/opt/software上、安装包&#xff0c;链接&#xff1a;https://pan.baidu.com/…

图书系统的Web实现(含源码)

源码地址https://gitee.com/an-indestructible-blade/project 注意事项&#xff1a; BorrowBooksWeb\src\main\resources路径下的application.yml文件里面的url&#xff0c;username&#xff0c;password这三个属性和自己的数据库保持一致。 浏览器访问url:http://127.0.0.1:…

软考 系统分析师系列知识点之信息系统战略规划方法(4)

接前一篇文章&#xff1a;软考 系统分析师系列知识点之信息系统战略规划方法&#xff08;3&#xff09; 所属章节&#xff1a; 第7章. 企业信息化战略与实施 第4节. 信息系统战略规划方法 7.4.2 关键成功因素法 关键成功因素&#xff08;Critical Success Factors&#xff0c…

Unity3D学习之UI系统——UGUI

文章目录 1. 前言2 六大基础组件概述3 Canvas——渲染模式的控制3.1 Canvas作用3.2 Canvas的渲染模式3.2.1 Screen Space -Overlay 覆盖模式3.2.2 Screen Space - Camera 摄像机模式3.2.3 World Space 4 CanvasScaler ——画布缩放控制器4.1 Constant Pixel Size 恒定像素模式4…

考研数据结构笔记(5)

单链表的查找 按位查找(O(n))按值查找(O(n))单链表长度(O(n))小结 基于带头结点的代码 按位查找(O(n)) 按值查找(O(n)) 单链表长度(O(n)) 小结

五官行为检测(表情基)解决方案提供商

随着人工智能技术的日益成熟&#xff0c;情感识别与行为分析在企业界的应用逐渐广泛。美摄科技作为业内领先的五官行为检测&#xff08;表情基&#xff09;解决方案提供商&#xff0c;致力于为企业提供高效、精准的情感识别与行为分析服务。 美摄科技的五官行为检测&#xff0…

【lesson47】进程通信之system V(共享内存)补充知识

文章目录 补充知识 补充知识 进行通信的key值问题&#xff0c;进程要通信的对方进程怎么能保证对方能看到&#xff0c;并且看到的就是该进程创建的共享内存的。 所以就通过key值来标识共享内存&#xff0c;key值是几不重要&#xff0c;只要在系统里是唯一的即可。 这样server和…

CentOS7集群配置免密登录

准备工作 提前开启三台虚拟机hadoop102、hadoop103,hadoop104,关于三台虚拟机的安装可以参考&#xff1a;https://mp.csdn.net/mp_blog/creation/editor/136010108 配置免密登录 一、分别修改三台机器的hosts,配置主机映射关系 vim /etc/hosts 文件中输入以下内容&#xf…

利用Pybind11封装Python版的WiringPi!

原版的WiringPi是一个用于树莓派的GPIO库&#xff0c;用C语言开发&#xff0c;仓库地址&#xff1a;https://github.com/WiringPi/WiringPi。该库允许用户以编程方式访问和控制树莓派的GPIO引脚。而随着Python在嵌入式设备上的快速发展&#xff0c;其对底层引脚的操作也变得越来…

linux系统定时任务管理

crontab使用 一、crontab简介 crontab 这个指令所设置的工作将会循环的一直进行下去&#xff01;可循环的时间为分钟、小时、每周、每月或每年等。crontab 除了可以使用指令执行外&#xff0c;亦可编辑 /etc/crontab 来支持。 至于让 crontab 可以生效的服务则是 crond 这个服…

【开源】基于JAVA+Vue+SpringBoot的假日旅社管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统介绍2.2 QA 问答 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿评论4.3 查询民宿新闻4.4 新建民宿预订单4.5 查询我的民宿预订单 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的假日旅社…

《小狗钱钱》读书笔记——如何看待金钱

目录 前言 作者 经典摘录 1、 了解致富的规律&#xff0c;一开始&#xff0c;必须明确金钱对你的意义 2、 梦想储蓄罐和梦想相册 3、认真去找机会 4、主人公吉娅的财富路径 5、注意财富积累本质 写在最后 前言 尽管[ 智慧是无法传授的], 但读书可以启发思路&#xff0…

30岁还一事无成,怎么办?

前些日子&#xff0c;知乎有一个话题&#xff0c;特别火。 原话是&#xff1a;30岁&#xff0c;如果你还没当上管理层&#xff0c;或者在某个领域取得成就&#xff0c;那你一辈子基本也就这样了。 这句话一出&#xff0c;戳中了许多人的软肋&#xff0c;一时间群情哗然。 理由是…