阅读 25

Volatile的解析

CPU

cache模型

CPU与内存的访问的速度相差很大,可达千倍,由于速度验证不对等 为了解决CPU直接访问内存效率地下的问题,就设计了cache模型

程序运行过程中,会将所需数据从主存中复制到CPU的cache中,操作完毕后再将数据刷新到主存中。 CPU通过直接访问cache极大提高了CPU的吞吐能力

缓存一致性

由于数据需要从主存中复制到cache,然后再操作,最后刷新到主存中 单线程不会出现缓存不一致问题,但是多线程会出现

解决方法

总线加锁

一种悲观锁的实现,某个时刻只能有一个CPU能够访问到这个变量,这种方式低下, 所以有了第二种方式

缓存一致性协议

比较出名的就是Intel的MESI协议 基本思想

  1. 读取操作:不做任何处理,只是将cache中的数据读取到寄存器
  2. 写入操作:发出信号通知其他CPU将共享变量的Cache line 置为无效,所以CPU必须重新到主存中读取

JMM(Java内存模型)

JMM指定了JVM如何与主存(RAM)进行工作, 定义了线程与主存的抽象关系,具体如下

  1. 共享变量存放与主存中,每个线程都能访问
  2. 每个线程都有自己私有的工作内存或者称之为本地内存
  3. 工作内存只存储该线程对共享变量的副本
  4. 线程不能直接操作主存,只能先操作工作内存后,才能写入到主存中

具体如下图

JMM关于同步的规定

  1. 线程解锁前,必须把内存变量的值刷新回主存中

  2. 线程加锁前,必须读取主存的最新值到自己的工作内存中

  3. 加锁解锁是同一把锁

并发编程基本特性

原子性

跟数据库事务的原子性类似 指操作要么全部完成,要么全部不完成

可见性

可见性指当一个线程对共享变量进行了修改,其他线程可以立即看到修改后的最新值 代码实例

class MyData{

    //如果不加volatile修饰,那么会一致在while中遍历
    public volatile   int number = 0;

    public void add060(){
        number = 60;
    }

}

public class VolatileDemo {
    

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        MyData myData = new MyData();
        executorService.submit(()->{
           System.out.println( Thread.currentThread().getName()  + " come in");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.add060();
            System.out.println( Thread.currentThread().getName()  + " number changed");
        });
        // main线程中的判断, 如果主存中的number刷新,但是main线程的工作内存没有刷新, 那么会一致死循环下去
        while (myData.number == 0){
        }
        System.out.println( Thread.currentThread().getName()  + " mission completed");
    }
}

复制代码

有序性

有序性是指代码在执行过程中的先后顺序, 由于Java编辑器的优化,导致代码可能不严格按照编写时的顺序执行,但是它会保证最终的运算结果跟期望一致

如:

        int x = 10;
        int y = 0;
        x++;
        y = 20;
复制代码

执行过程中 y = 20 可能在 x++前执行 单线程中没有问题,但在多线程中,线程交替执行,由于编译器优化重拍的存在,两个线程中使用的变量无法保证一致性,所以结果无法预测

JMM如何保证三大特性

1. 原子性

JMM只保证基本读取和赋值的原子操作,其余的均不保证,需要借助外部工具 如:

  1. synchronized
  2. JUC中的lock
  3. 原子类

2. 可见性

Java使用以下方法来保证可见性

  1. 关键字volatile
  2. 关键字synchronized
  3. JUC中的显式锁Lock

3. 有序性

Java使用以下方法来保证有序性

  1. 关键字volatile
  2. 关键字synchronized
  3. JUC中的显式锁Lock

volatile解析

JVM提供的轻量级锁 volatile只能保证两个:

  1. 可见性
  2. 有序性

volatile

1. 保证可见性

由于JMM的特点,线程的工作内存与主存取值 所以需要使用volatile及时更新变量值,保证其他线程拿到的是最新值

2. 不能保证原子性

可以使用原子类解决, 或者加锁(下策)

3.有序性

多线程中,线程交替执行,由于编译器优化重拍的存在,两个线程中使用的变量无法保证一致性,所以结果无法预测

使用volatile后,会禁止指令重排 单例模式的懒汉模式双重检测锁就用到了volatile

// 使用volatile ,避免指令重排
    private static volatile DoubleCheck singleton;

    // 双重检测锁
    public static  DoubleCheck getInstance(){
        if(singleton == null){
            synchronized (DoubleCheck.class){
                if(singleton == null){
                    singleton = new DoubleCheck();
                }
            }
        }
        return singleton;
    }
复制代码

volatile 与 synchronized

使用区别

  1. volatile 只能用于修饰实例变量或者类变量

不能修饰方法,方法参数,局部变量,常量等

  1. synchronized可以修饰方法或代码,不能修饰变量。

效果区别

  1. 原子性

volatile不能保证原子性 synchronized可以保证原子性

  1. 可见性

都可以保证可见性

  1. 有序性

都可以保证有序性

  1. 阻塞

volatile不会导致阻塞 synchronized会导致线程进入阻塞状态