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唤醒
相关推荐
考虑考虑4 小时前
cmd局部设置java变量
运维·后端·自动化运维
benbenAItalk4 小时前
数字人口播视频的批量生产实践:素材规范、任务编排与质量验收
java·前端·音视频
sunshine22 girl4 小时前
Java学习二 基本语法2, 算术运算符
java·学习
niucloud-admin4 小时前
JAVA V6 多商户商城 开发文档——插件目录结构
java·开发语言
2601_962885724 小时前
如何用 Python 扫描 A 股跳空缺口并统计缺口回补概率?
java·前端·python
滕州市燕猫虎计算机科技工作室个体工商户4 小时前
Java锁
java·开发语言
shehuiyuelaiyuehao6 小时前
1计算机是如何工作的
java·计算机网络
陈随易6 小时前
在Finch用了62亿词元,我认为这是新一代Agent工具之神
前端·人工智能·后端
letisgo57 小时前
JAVA 高级进阶07篇《@Transactional失效的8个场景:代理机制到传播行为》
java·面试·transactional·spring事务·aop动态代理
水域安全老周7 小时前
水趣钓鱼救生衣专利拆解:两级锁紧如何解决落水人衣分离
java·前端·网络