Back

JVM之volatile关键字

引入

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范, 用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

  1. 主内存(Main Memory)
    主内存可以简单理解为计算机当中的内存,但又不完全等同。
    主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。

  2. 工作内存(Working Memory)
    工作内存可以简单理解为计算机当中的CPU高速缓存,但又不完全等同。
    每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
    线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。
    不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。

对于一个静态变量

static int s = 0;

线程A执行如下代码:

s = 3;

通过一系列内存读写的操作指令,线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。

从单线程的角度来看,这个过程没有任何问题。这时候我们引入线程B,执行如下代码:

System.out.println("s=" + s);

有可能输出 0 或 3

可以使用synchronized同步锁(影响性能)或者使用volatile关键字修饰(轻量)

volatile的实现原理

volatile可以实现内存的可见性和防止指令重排序。

通过内存屏障技术实现的。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障指令,内存屏障效果有:

  • 禁止volatile 修饰变量指令的重排序
  • 写入数据强制刷新到主存
  • 读取数据强制从主存读取

使用原则:

  • 对变量的写操作不依赖于当前值。例如 i++ 这种就不适用。
  • 该变量没有包含在具有其他变量的不变式中。

volatile的使用场景不是很多,使用时需要仔细考虑下是否适用volatile,注意满足上面的二个原则。

volatile 之 可见性

volatile关键字具有许多特性,其中最重要的特性就是保证了用volatile修饰的变量对所有线程的可见性[当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值]。

比如用一个变量标识程序是否启动、初始化完成、是否停止等,如下:

public class MessageLoopHandler{
    private volatile boolean shutdown = false;

    //A线程执行shutdown方法
    public void shutdown(){
        shutdown = true;
    }

    //B线程检查到shutdown为true结束循环
    public void doWork(){
        while(!shutdown){
            //应用没有停止时,继续检查是否有新消息
        }
    }
}

但volatile不能保证变量的原子性即无法保证线程安全。

什么时候适合用

  1. 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
    public class VolatileTest {
        public volatile static int count = 0;
       
        public static void main(String[] args) {
            //开启 10个线程
            for (int i = 0; i < 10; i++) {
                new Thread(
                        new Runnable() {
                            public void run() {
                                try {
                                    Thread.sleep(1);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                //每个线程当中让count值自增 100次
                                for (int j = 0; j < 100; j++) {
                                    count++;
                                }
                            }
                        }
                ).start();
            }
            try {
                Thread.Weep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System, out.print("count" + count);
            //最终count的结果值未必是1000,有可能小于1000。
        }
    }
    
  2. 变量不需要与其他的状态变量共同参与不变约束。
    volatile static int start = 3;
    volatile static int end = 6;
       
    线程A执行如下代码
    while (start < end){
      //do something
    }
       
    线程B执行如下代码
    start+=3;
    end+=3;
       
    //这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成6,
    //造成了一瞬间 start == end,从而跳出while循环的可能性。
    

volatile 之 有序性 (阻止指令重排)

public class Singleton2 {
    private static Singleton2 instance;

    public static Singleton2 getInstance() {
    	//双重检测机制
        if (instance == null) {
        	//同步锁
            synchronized (Singleton2.class) {
            	//双重检测机制
                if (instance == null) {
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

已上代码存在问题,可能会得到一个没有初始化完成的对象

JVM编译器的指令重排,可能会得到一个没有初始化的对象

java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance =memory;     //3:设置instance指向刚分配的内存地址 

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate();    //1:分配对象的内存空间 
instance =memory;     //3:设置instance指向刚分配的内存地址 
ctorInstance(memory);  //2:初始化对象 

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。
此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

comments powered by Disqus