Java线程安全与等待通知

news/2025/3/15 7:08:31/

目录

    • 1. 线程不安全原因
      • 1.1 引入——线程不安全的例子(抢占式执行)
      • 1.2 线程不安全的原因(5点+其他)
    • 2. 抢占式执行引起的线程不安全——synchronized
    • 3. 内存可见性引起的线程不安全——volatile
      • 3.1 例子——编译器优化误判
      • 3.2 volatile——编译器暂停优化
    • 4. 指令重排序引起的线程不安全——volatile
    • 5. 等待通知——wait和notify关键字(锁中使用)
    • 6. wait和sleep的对比(面试题)

1. 线程不安全原因

1.1 引入——线程不安全的例子(抢占式执行)

由于线程调度顺序是无序的,则让两个线程对同一个变量各自自增5w次,看变量的运行结果。

为啥会出现线程安全问题?
本质原因:线程在系统中的调度是无序的/随机的(抢占式执行)。

public class TestDemo {public static int x;public static void main(String[] args) throws InterruptedException {//两个线程对同一个变量各自自增5w次,看变量的运行结果Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {x++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {x++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(x);}
}//另一种写法
class Counter {public int count = 0;public void add() {count++;}public int getCount() {return count;}
}
public class TestDemo2 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.getCount());//实际结果和预期结果不相符,由多线程引起的bug【与线程的调度随机性密切相关】}
}

运行结果:
在这里插入图片描述

分析:有多少次是“顺序执行”,有多少次是“交错执行”是不知道的,得到的结果是啥也是变化的。

count++操作,本质上是3个cpu指令构成的(不是原子的)

  1. load把内存中的数据读取到cpu寄存器中
  2. add把寄存器中的值进行+1运算
  3. save把寄存器中的值写回到内存中
    归根结底,线程安全问题全是因为线程的无序调度导致了执行顺序不确定,结果就变化了。

1.2 线程不安全的原因(5点+其他)

①抢占式执行,随机调度(罪魁祸首,万恶之源)
线程中的代码执行到任意一行,都随时可能被切换出去
②多个线程同时修改同一个变量
注:一个线程修改同一个变量(安全)
多个线程读取同一个变量(安全)
多个线程修改不同变量(安全)
③修改操作不是原子的
注:如果某个操作对应多个cpu指令,大概率不是原子的,正是因为不是原子的,导致两个线程的指令排序存在更多的变数了。
例如++操作就不是原子的。
④内存可见性
编译器执行代码的时候对代码进行优化,有些操作下频繁读取内存,但是读取的结果不变,则优化成只从寄存器去读,不从内存中读。在这种情况下,如果另外一个线程修改了内存的值,原来读的那个线程无法感知到。【一个线程频繁读,一个线程修改】
⑤指令重排序
编译器优化产生的线程不安全。

2. 抢占式执行引起的线程不安全——synchronized

  1. 锁有2个核心操作:加锁和解锁。
    public void add() {synchronized (this) {count++;}}

①此处使用代码块的方式来表示,进入synchronized 修饰的代码块的时候,就会触发加锁,出synchronized 的代码块,就会触发解锁
②synchronized (),括号()中表示锁对象,在针对哪个对象加锁,如果2个线程针对不同对象加锁,此时不会存在锁竞争,各自获取各自的锁即可;如果2个线程针对同一个对象加锁,此时就会出现“锁竞争”,一个线程先拿到了锁,另一个线程阻塞等待。
③()里的锁对象可以是写任意一个Object对象,基本数据类型不可以,此处写了this相当于counter对象。
例如:手动指定一个锁对象,相当于吉祥物,仅仅是起到了一个标识的效果。

    private Object locker = new Object();public void add() {synchronized (locker) {count++;}}

注:如果多个线程尝试对同一个锁对象加锁,就会产生锁竞争;针对不同对象加锁,就不会有锁竞争。

  1. 给方法加锁
    synchronized public void add() {count++;}

如果直接给方法使用synchronized修饰,此时就相当于以this为锁对象。
下图2种加锁方法等价。
在这里插入图片描述
3. 给静态方法加锁

    public static void test() {synchronized (Counter.class) {}}synchronized public static void test1() {}

如果synchronized修饰静态方法(static),此时就不是给this加锁了,而是给类对象加锁。

类对象是什么?
Counter.class
类对象相当于“对象的图纸”,描述了类的方方面面的详细信息。
类对象可以用来表示.class文件的内容。

3. 内存可见性引起的线程不安全——volatile

3.1 例子——编译器优化误判

import java.util.Scanner;public class ThreadDemo1 {public static int falg = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (falg == 0) {}});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);falg = scanner.nextInt();});t1.start();t2.start();}
}

运行结果:
在这里插入图片描述
分析:理论上当输入2之后,线程1应该结束,实际上通过jconsole发现并没有结束。这是因为循环条件flag=0,编译器每次从内存中读数据发现都为真,因此编译器自动帮助我们优化了,编译器对于代码优化产生了误判。
在这里插入图片描述

内存可见性:就是多线程的环境下,编译器对于代码优化产生了误判,从而引起了bug,导致代码bug。
编译器优化:智能的调整代码执行逻辑,保证程序结果不变的前提下,通过加减语句、语句变换、一系列操作,让整个程序的执行效率大大提升。编译器对于“程序结果不变”单线程下判定是非常准确的,但是多线程就不一定了。

加了sleep循环执行速度就非常慢,当循环的次数下降了,此时load操作就不再是负担,编译器就不需要优化了。

     Thread t1 = new Thread(() -> {while (falg == 0) {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});

3.2 volatile——编译器暂停优化

volatile关键字:加上volatile关键字之后,编译器就能够保证每次都是重新从内存读取flag变量的值。

    volatile public static int falg = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);falg = scanner.nextInt();});t1.start();t2.start();}

运行结果:
在这里插入图片描述
分析:此时t2修改falg,t1就可以立即感知到了,t1就可以正确退出循环。

volatile适用的场景:一个线程读,一个线程写
sychronized适用的场景:多个线程写
volatile的效果,称为“保证内存可见性”。

4. 指令重排序引起的线程不安全——volatile

指令重排序也是编译器优化的策略,调整了代码执行的顺序,让程序更高效。
谈到优化,都得保证优化之后的结果和之前是不变的,在单线程的情况下容易保证,但是在多线程的情况下就不好说了。

class Student {}
public class ThreadDemo2 {public static Student s;public static void main(String[] args) {Thread t1 = new Thread(() -> {s = new Student();});Thread t2 = new Thread(() -> {if (s != null) {System.out.println("111");}});t1.start();t2.start();}
}

分析:s = new Student()大体可以分为3步:①申请内存空间②调用构造方法,初始化内存的数据③把对象的引用赋值给s。②和③编译器优化可对其进行调换顺序,这样上述代码就可能会因为指令重排序出现问题。

解决办法:使用volatile关键字,volatile关键字的作用主要有如下2个:

  1. 保证内存可见性:当一个线程修改一个共享变量的时候,另一个线程能读到这个修改的值。
  2. 保证有序性:禁止指令重排序,编译时jvm编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
    注意:volatile不能保证原子性。

5. 等待通知——wait和notify关键字(锁中使用)

由于线程的调度是无序的、随机的,但是在一定的需求场景下,希望线程有序执行。

  • join,算是一种控制顺序的方式,但是功效有限
  • wait,就是让某个线程先暂停下来等一等;发现条件不满足/时机不成熟,就先阻塞等待。
  • notify,就是把该线程唤醒,能够继续执行;其他线程构造了一个成熟的条件,就可以唤醒wait的线程。
  1. 分析代码的报错信息
    public static void main(String[] args) throws InterruptedException {Object o = new Object();System.out.println("wait之前");o.wait();System.out.println("wait之后");}

运行结果:
在这里插入图片描述
分析:IllegalMonitorStateException 非法锁状态异常,锁还没获取到,就尝试解锁,就会产生上述异常。【此时wait需要解锁,但是都没加上锁】

wait主要做3件事:①解锁 ②阻塞等待 ③当收到通知的时候唤醒,同时尝试重新获取锁。
因为wait必须写到synchronized代码块里面,这样才能尝试①解锁。
同理notify也是要放到synchronized中使用的。
先有wait再有notify,否则就相当于一炮打空了。
使用wait,阻塞等待会让线程进入WAITING状态。

        synchronized (o) {o.wait();}

注意:加锁的对象必须和wait的对象是同一个,这样wait操作就是在针对当前对象进行解锁。

  1. wait和notify的例子
 public static void main(String[] args) throws InterruptedException {Object object = new Object();//waitThread t1 = new Thread(() -> {System.out.println("wait之前");synchronized (object) {try {object.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("wait之后");});t1.start();Thread.sleep(1000);//notifyThread t2 = new Thread(() -> {System.out.println("notify之前");synchronized (object) {object.notify();}System.out.println("notify之后");});t2.start();}

运行结果:
在这里插入图片描述
分析:
t1先执行,执行到了wait,就阻塞了
1秒之后,t2开始执行,执行到了notify,就会通知t1线程唤醒。
注意:notify是再synchronized内部,就需要t2释放锁之后t1才能继续往下走,因为t1要重新获取锁,所有要t2释放锁。

join和wait、notify的区别
join只能让t2线程先执行完,再继续执行t1,一定是串行的。
wait notify可以让t2执行完一部分,再让t1执行,t1执行一部分,再让t2执行,非常灵活。

  1. notifyAll
    可以有多个线程,等待同一个对象。比如t1 t2 t3中都调用了object.wait,
    此时在main中调用object.notify,会随机唤醒上述的1个线程,另外2个仍然是waiting状态。
    如果调用object.notifyAll,此时就会把上述3个线程都唤醒,此时这3个线程就会重新竞争锁,然后依次执行。

6. wait和sleep的对比(面试题)

wait有一个带参数的版本,用来体现超时时间,这个时候感觉和sleep差不多。wait和sleep都能提前唤醒。
最大的区别:初心不同,设计这个东西解决的问题不同。
wait解决线程之间的顺序控制。
sleep解决让当前线程休眠一会。

使用上也有明显的区别:wait要搭配锁使用,sleep不需要。
再进一步,只是java这里的sleep和wait用法看起来比较像,其他语言的sleep和wait差别很大。


http://www.ppmy.cn/news/39397.html

相关文章

点云学习(1): 获取点云的包络框

1. 记录一些容易忘记的点云操作----后续一定补充 1.获取点云的包络框 下面的get_axis_aligned_bounding_box(),get_min_bound(),get_max_bound()等函数非常好用 import open3d as o3d import numpy as np# 读取点云数据 pcd o3d.io.read_point_cloud("input.pcd"…

SQL 子查询和链接查询

1.题目&#xff1a;现在运营想要查看所有来自浙江大学的用户题目回答明细情况&#xff0c;请你取出相应数据 question_practice_detail 答题详情表 user_profile 用户表 期望结果&#xff1a; 链表查询&#xff1a; 将question_practice_detail作为基础表&#xff0c;进行u…

【云原生】Swarm解决docker server的集群化管理和部署

一文理解Swarm解决docker server的集群化管理和部署一、简介1.1、涉及到哪些概念&#xff1f;1.2、需要注意什么&#xff1f;二、集群管理2.1、创建集群2.2、将节点加入集群2.3、查看集群状态。2.4、将节点从集群中移除2.5、更新集群2.6、锁定/解锁集群三、节点管理四、服务部署…

JSON 与 Ajax

JSON 与 Ajax AJAX 就是异步 JavaScript 和 XML&#xff0c;它是一组用于客户端的相互关联的 Web 开发技术&#xff0c;以创建异步 Web 应用程序。遵循 AJAX 模型&#xff0c;Web 应用程序可以以异步的方式发送数据以及从服务器上检索数据&#xff0c;而不影响现有页面的显示行…

044:cesium加载单个图片形成底图

第044个 点击查看专栏目录 本示例的目的是介绍如何在vue+cesium中加载单个图片形成底图. 直接复制下面的 vue+cesium源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共78行)相关API参考:专栏目标示例效果 配置方式 1)查看基础设置:https://x…

【MATLAB】一篇文章带你了解beatxbx工具箱使用

目录 一篇文章带你了解beatxbx工具箱使用 一篇文章带你了解beatxbx工具箱使用 clc;clear; tic; % step1 初始化 % 个体数量 NIND = 35; % 最大遗传代数 MAXGEN = 180; % 变量的维数 NVAR = 2; % 变量的二进制位数 % 上下界 bounds=[-10 10-10 10]; precision=0.0001; %运算精度…

67页新型智慧城市整体规划建设方案

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除 新型智慧城市总体规划智慧城市基础平台的定位对于省&#xff1a;智慧城市建设是省数字政府的核心节点和重要一环 对于市直单位&#xff1a;智慧城市基础平台是全市数据资源和公…

MongoDB 查询文档(2)

上一篇 MongoDB查询文档(1) 中介绍了MongoDB查询文档中使用比较筛选和逻辑筛选&#xff0c;这里我们继续介绍MongoDB的文档查询&#xff0c;这里我们介绍元素筛选、数组筛选。 一、元素筛选 1、判断元素是否存在&#xff08;$exists&#xff09; 语法&#xff1a;{ field: {…