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唤醒
相关推荐
ABILI .1 分钟前
主动类型转换
java
奋斗的老史3 分钟前
LangChain4j 进阶实战系列
java·langchain4j·ai应用开发
橙子圆1236 分钟前
Redis知识2
java·数据库·redis
callJJ8 分钟前
Codex 联动 OpenSpec 提效方法论
java·开发语言·codex·openspec
过期动态9 分钟前
【RabbitMQ基础篇】RabbitMQ从入门到实战
java·jvm·数据库·分布式·spring·rabbitmq·intellij-idea
Gopher_HBo10 分钟前
分布式详解
后端
上弦月-编程11 分钟前
Java编程:跨平台开发利器
java·开发语言
AI人工智能+电脑小能手12 分钟前
【大白话说Java面试题】【Java基础篇】第38题:两个对象的hashCode()相同,则 equals()是否也一定为 true?
java·开发语言·后端·面试·hash-index
java1234_小锋14 分钟前
什么是可重入锁ReentrantLock?
java·开发语言