volatile面试

156 阅读3分钟

JMM关于同步的规定:

  1. 线程解锁前,必须吧共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

volatile是java虚拟机提供的轻量级的同步机制具有以下特点:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

不保证原子性

原子性的定义

一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

volatile不保证原子性代码验证

package com.hyq.test;
public class VolatileAtomDemo {
  // volatile不保证原子性 
  // 原子性:保证数据一致性、完整性 
  volatile int number = 0;

  public void addPlusPlus() {
    number++;
  }

  public static void main(String[] args) {
    VolatileAtomDemo volatileAtomDemo = new VolatileAtomDemo();
    for (int j = 0; j < 20; j++) {
      new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          volatileAtomDemo.addPlusPlus();
        }
      }, String.valueOf(j)).start();
    } // 后台默认两个线程:一个是main线程,一个是gc线程
    while (Thread.activeCount() > 2) {
      Thread.yield();
    }
    // 如果volatile保证原子性的话,最终的结果应该是20000 
    // 但是每次程序执行结果都不等于20000 
    System.out.println(Thread.currentThread().getName() + "\t final number result = " + volatileAtomDemo.number);
  }
}

代码执行结果如下:多次执行结果证明volatile不保证原子性 image.png

解决volatile不保证原子性问题

  1. 方法前加synchronized解决
public synchronized void addPlusPlus() {
  number++;
}
  1. 加锁解决
Lock lock = new ReentrantLock();
public void addPlusPlus() {
  lock.lock();
  number++;
  lock.unlock();
}
  1. 原子类解决
package com.hyq.test;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileSolveAtomDemo {
  // 原子Integer类型,保证原子性
  private AtomicInteger atomicNumber = new AtomicInteger();

  // 底层通过CAS保证原子性
  public void addPlusPlus() {
    atomicNumber.getAndIncrement();
  }

  public static void main(String[] args) {
    VolatileSolveAtomDemo volatileSolveAtomDemo = new VolatileSolveAtomDemo();
    for (int j = 0; j < 20; j++) {
      new Thread(() -> {
        for (int i = 0; i < 1000; i++) {
          volatileSolveAtomDemo.addPlusPlus();
        }
      }, String.valueOf(j)).start();
    }
    // 后台默认两个线程:一个是main线程,一个是gc线程
    while (Thread.activeCount() > 2) {
      Thread.yield();
    }
    // 因为volatile不保证原子性,所以选择原子类AtomicInteger来解决volatile不保证原子 性问题
    // 最终每次程序执行结果都等于20000
    System.out.println(Thread.currentThread().getName() + "\tfinal number result = " + volatileSolveAtomDemo.atomicNumber.get());
  }
}

Volatile禁止指令重排

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。\
内存屏障(Memory Barrier)又称内存栅栏 ,是一个CPU指令,它的作用有两个: 保证特定操作执行的顺序性; 保证某些变量的内存可见性(利用该特性实现volatile内存可见性)

volatile实现禁止指令重排优化底层原理:

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译 器和CPU,不管什么指令都不能和这条Memory Barrier指令重排,也就是说通过插入内存屏障,就能 禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种CPU的缓存数据, 因此任何CPU上的线程都能读取到这些数据的最新版本。

  • 左边:写操作场景:先StoreStore指令,后StoreLoad指令。
  • 右边:读操作场景:先LoadLoad指令,后LoadStore指令。

image.png

LoadLoad Barriers
示例:Load1; LoadLoad; Load2
该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作

StoreStore Barriers
示例:Store1; StoreStore; Store2
该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作

LoadStore Barriers
示例:Load1; LoadStore; Store2
确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作

StoreLoad Barriers
示例:Store1; StoreLoad; Load2
该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令。

volatile使用场景

单例模式(DCL-Double Check Lock双端检锁机制)

public class DCLSingleTon {
    private DCLSingleTon(){
        System.out.println(Thread.currentThread().getName() +" 构造方法执行了");
    }
 
    private static volatile DCLSingleTon singleTon = null;
 
    public static DCLSingleTon getInstance(){
        //加锁前后都做一次判断
        if (singleTon == null){
            synchronized (DCLSingleTon.class){
                if (singleTon ==null){
                    singleTon = new DCLSingleTon();
                }
            }
        }
        return singleTon;
    }
 
    public static void main(String[] args) {
        //多线程环境下,执行多次,构造方法只执行一次;说明只有一个对象
        for(int i = 1;i<100;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DCLSingleTon.getInstance();
                }
            }).start();
        }
    }
}