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唤醒
相关推荐
程序员小假1 天前
什么是线程池?它的工作原理?
java·后端
盖世英雄酱581361 天前
java 深度调试【第一章:堆栈分析】
java·后端
lastHertz1 天前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥1 天前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜1 天前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始1 天前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴1 天前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年1 天前
用Go实现类似WinGet风格彩色进度条
后端
蓝色汪洋1 天前
string字符集
java·开发语言
卿言卿语1 天前
CC1-二叉树的最小深度
java·数据结构·算法·leetcode·职场和发展