Java多线程中解决内存可见性问题的两种方法:synchronized、volatile 及CAS算法


前言

首先我们简要了解一下Java的内存模型,然后根据一段代码引出内存可见性问题。

  • Java内存模型
    Java内存模型规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来的)。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

  • 代码示例

public class 内存可见性问题 {
    
    
    public static void main(String[] args) {
    
    
        MyRunable0 myRunable0 = new MyRunable0();
        Thread th = new Thread(myRunable0);
        th.start();
        while (true){
    
    
            //程序没有进入,线程在其工作内存中将共享变量值修改为true,但是没能及时刷新到主存中
            if(myRunable0.isFlag()){
    
    
                System.out.println("进来了");
                break;
            }
        }

    }
}
class MyRunable0 implements Runnable{
    
    
    boolean flag=false;

    public boolean isFlag() {
    
    
        return flag;
    }

    public void setFlag(boolean flag) {
    
    
        this.flag = flag;
    }

    @Override
    public void run() {
    
    
        flag=true;
        System.out.println("线程将flag改成"+flag);

    }
}
  • 运行结果
    在这里插入图片描述
  • 可见性
    一个线程对共享变量值的修改,能够及时被其他线程看到。
  • 原子性
    原子性就是指该操作是不可再分的。无论是多核还是单核,具有原子性的量,同一时刻只能有一个线程对它进行操作。
  • 实现可见的条件
  1. 线程修改后的共享变量值能够及时从工作内存刷新到主内存中
  2. 其他线程能够及时把共享变量的最新值从主存中更新到自己的工作内存中
  • Java语言层面支持的可见性实现方式
  1. synchronized
  2. volatile

一、synchronized实现可见性

1.synchronized能够实现

  • 原子性(同步)
  • 可见性

2.JVM关于synchronized的两条规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。

3.代码

        while (true){
    
    
        //使用 synchronized实现可见性,注意锁的使用
           synchronized (myRunable0){
    
    
               if (myRunable0.isFlag()) {
    
    
                   System.out.println("进来了");
                   break;
               }
           }
        }

4.synchronized的弊端

  • 无法控制阻塞的实长
  • 阻塞不可以被中断
  • 效率变低

二、volatile实现可见性

1.volatile能够实现

  • 可见性问题
  • 不能保证原子性问题

2.实现原理

通过加入内存屏障和禁止重排序优化来实现的。

  • 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
  • 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令

通俗的来讲,volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,当该变量放生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值。

3.代码

		//在共享变量前添加volatile关键字
 volatile boolean flag=false;

4.volatile的适用场景

  • 对变量的写入操作不依赖其当前值
  • 该变量没有包含在具有其他变量的不变式中

三、CAS(Compare-And-Swep)算法

1.概述

  • CAS,Compare and Swap比较并交换。总共由三个操作数,一个内存值v,一个线程本地内存旧值a(期望操作前的值)和一个新值b,在操作期间先拿旧值a和内存值v比较有没有发生变化,如果没有发生变化,才能内存值v更新成新值b,发生了变化则不交换。

2.代码

public class CAS算法 {
    
    
    public static void main(String[] args) {
    
    
        Mrunable mrunable = new Mrunable();
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(mrunable).start();
        }

    }
}
class Mrunable implements Runnable{
    
    
    //Java提供好的原子变量
    AtomicInteger i=new AtomicInteger(1);
    
    public AtomicInteger getI() {
    
    
        return i;
    }

    @Override
    public void run() {
    
    
        while (true){
    
    
            try {
    
    
                Thread.sleep(50);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            //使用原子变量调用相应的方法完成需求
            System.out.println(i.getAndIncrement());
        }


    }
}

总结

synchronized和volatile比较

  • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
  • 从内存可见性角度讲,volatile读操作相当于加锁,volatile写操作相当于解锁
  • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。

猜你喜欢

转载自blog.csdn.net/m0_46988935/article/details/112937371