Java线程安全:volatile与wait/notify详解

目录

[一. volatile](#一. volatile)

1.1内存可见性

[1.2 volatile的使用](#1.2 volatile的使用)

[二. wait & notify](#二. wait & notify)

[2.1 wait()](#2.1 wait())

[2.2 notify()](#2.2 notify())

[2.3 notifyAll()](#2.3 notifyAll())

[2.4 具体流程](#2.4 具体流程)


一. volatile

volatile 是Java中的一个关键字,只要是解决线程安全问题中内存可见性的问题

在了解volatile之前我们要先了解一下什么是内存可见性

1.1内存可见性

内存可见性是 多线程环境下,一个线程修改共享变量后,其他线程能否"立刻看到"这个修改结果 的特性。

下面来看一个经典的例子:

java 复制代码
public class Text {
    public static int flog=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Scanner sc=new Scanner(System.in);
            flog=sc.nextInt();
        });

        Thread t2=new Thread(()->{
            while (flog==0){
               //什么也不做
            }
        });
        t1.start();
        t2.start();

    }
}

在这个代码中就会出现由于内存可见性而导致输入1后仍然不能退出循环

原因:

站在CPU指令的角度:

1.load操作会从内存中读取flog的值,到寄存器中

2.cmp操作会将flog与寄存器中的值进行比较并且判断跳转

虽然在另一个线程中有flog值的修改,但是编译器无法分析出另一个线程的执行时机,并且load操作的开销远远大于cmp的开销,所以编译器做出了一个大胆的判定:将load操作进行优化,优化为复用寄存器/缓存中的旧值

所以当我们输入1时,而flog读取的仍然是存放在寄存器/缓存中的旧值0,而导致一直陷入循环,不能退出

1.2 volatile的使用

volatile是Java中的关键字,直接修饰可能会触发内存可见性问题的变量后即可

java 复制代码
public class Text {
    public  volatile static int flog=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Scanner sc=new Scanner(System.in);
            flog=sc.nextInt();
        });

        Thread t2=new Thread(()->{
            while (flog==0){

            }
        });
        t1.start();
        t2.start();

    }
}

注意:

  • volatile只能解决内存可见性导致的线程安全问题,并不能保证原子性
  • volatile只能应对一个线程读一个线程写的操作,不能应对两个线程写(主要是原子性的问题,而锁不仅能保证原子性还能顺便解决内存可见性问题)

问题:若是在while中加入sleep()能阻止内存可见性问题吗?

答:能 ,因为 内存可见性问题本质上是由编译器优化带来的,而由于sleep的引入会抑制编译器对load的优化,从而解决了内存可见性问题(不是sleep影响内存可见性,而是影响编译器的优化)

我们还要知道的一点:在循环体中的各种复杂操作,都可能会引其上述的优化失效

java 复制代码
public class Text {
    public   static int flog=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            Scanner sc=new Scanner(System.in);
            flog=sc.nextInt();
        });

        Thread t2=new Thread(()->{
            while (flog==0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();

    }
}

加入sleep后就不存在线程安全问题了

二. wait & notify

由于线程之间的执行是抢占式的,我们难以预料线程执行的先后顺序。但是在实际开发中我们希望合理的协调多线程之间的执行顺序。就比如假期出行,我们要先订票支付后,平台才会给我们安排座位位置。

在Java中就涉及三个方法能够帮助我们完成整个流程

2.1 wait()

wait()要做的三件事

  1. 执行到wait后释放当前线程的锁
  2. 等待其他线程的通知
  3. 当通知到达后,线程会先从等待状态进入阻塞状态(去竞争锁),等成功获取锁后,才会进入就绪状态

上述的1和2是原子的

wait()被唤醒的条件

  1. 执行到notify(),被notify唤醒
  2. wait等待超时【(long timeout)/wait(long timeout,int nanos)】---设置超时时间
  3. 当其它线程调用该方法的interrupt方法,wait会抛出异常使其退出等待状态进入阻塞状态

注意:

  • wait必须要在synchronized中使用
  • wait()是Object类 的方法

2.2 notify()

notify是唤醒等待的线程

注意:

  • notify唤醒的是当前对象锁上,处于wait 等待状态的其中一个线程(随机唤醒其中一个)
  • notify必须要在synchronized中使用
  • notify()是Object类 的方法

2.3 notifyAll()

与notify()不同的是它唤醒处于wait 等待状态的所有线程,其余与notify一致

2.4 具体流程

注意:

  • synchronized 锁定的对象、调用 wait() 方法的对象、调用 notify() / notifyAll() 方法的对象,必须是同一个对象。要配套使用,否则会通知无效
  • wait要保证在notify之前执行,不然执行到wait时会出现死等状态(没有notify再唤醒它)

2.5 wait 与 sleep 的区别

  1. wait 要释放锁,而sleep 只是让线程陷入休眠,不会释放锁
  2. wait 必须要搭配锁使用,而 sleep 不需要
  3. 虽然 wait 和 sleep 都能被 interrupt 唤醒,但是 wait 设计的初衷是更希望被notify唤醒
相关推荐
IT_陈寒2 小时前
Vite 5年迭代揭秘:3个核心优化让你的项目构建速度提升200%
前端·人工智能·后端
无敌最俊朗@3 小时前
MQTT 关键特性详解
java·前端·物联网
JAVA学习通3 小时前
微服务项目->在线oj系统(Java-Spring)----[前端]
java·开发语言·前端
拾贰_C3 小时前
【SpringBoot】前后端联动实现条件查询操作
java·spring boot·后端
catchadmin5 小时前
PHP 快速集成 ChatGPT 用 AI 让你的应用更聪明
人工智能·后端·chatgpt·php
GUIQU.5 小时前
【QT】嵌入式开发:从零开始,让硬件“活”起来的魔法之旅
java·数据库·c++·qt
callJJ9 小时前
从 0 开始理解 Spring 的核心思想 —— IoC 和 DI(2)
java·开发语言·后端·spring·ioc·di
wangjialelele9 小时前
Linux中的线程
java·linux·jvm·c++
谷咕咕9 小时前
windows下python3,LLaMA-Factory部署以及微调大模型,ollama运行对话,开放api,java,springboot项目调用
java·windows·语言模型·llama