java volatile原理总结

语义

        被volatile修饰的变量保证了此变量对所有线程的可见性,即当一个线程修改了该变量,该变量会立即写到主内存,且使其他线程的该变量都失效,保证了该变量肯定是对所有线程可见的,都是被修改后的正确值。

Java代码如下:

Singleton volatile instance = new Singleton(); // instance是volatile变量

转变成汇编代码,如下:

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: Lock addl $0×0,(%esp);

对被volatile修饰的变量进行写操作后,jvm会自动的加上Lock指令,Lock指令做了两件事:

  • 将当前处理器缓存刷新到系统内存
  • 使其他CPU里缓存了该内存地址的数据无效

       

public class Demo {
    volatile long v1 = 0L;
    public void set(long l) {
        v1 = l;
    }
    public long get() {
        return v1;
    }
    public void getAndIncrement() {
        v1++;
    }
}

如上一段程序,在语义上等价于:

public class Demo {
    long v1 = 0L;
    public synchronized void set(long l) {
        v1 = l;
    }
    public synchronized long get() {
        return v1;
    }
    public void getAndIncrement() {
        long temp = get();
        temp += 1;
        set(temp);
    }
}

        理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。从内存语义的角度来说:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义——这使得volatile变量的写-读可以实现线程之间的通信。

volatile变量自身具有下列特性:

  • 可见性:对一个volatie变量的读,总是能看到任意线程对这个变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于++这种复合操作不具备原子性

synchronized可以保证可见性、原子性和有序性。

volatile写读内存语义

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;
    public void writer() {
        a = 1;             //1
        flag = true;       //2
    }
    public void reader() {
        if (flag) {        //3
            int i = a;      //4
          ……
        }
    }
}

如上一段程序,线程A执行writer(),线程B执行reader(),根据happens-before规则:

1happens-before2、3happens-before4、2happens-before3,所以1happens-before4。

 当线程A写flag变量后,线程A的本地内存A中的两个共享变量的值都会刷新到主内存,所以线程B读取能读取到正确值,注意不是只有volatile的共享变量会刷新,而是所以的写缓冲区的都会刷新。

 volatile写 - 读的内存语义:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。 

  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 

  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

内存屏障禁止指令重排序

volatile关键字本身就包含了禁止指令重排序的语义。重排序可能会导致多线程程序出现内存可见性问题。

 volatile禁止指令重排序语义的实现:

为了实现volatile写/读内存语义,JMM针对了不同编译器的volatile重排序规则表:

图片

  1. 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。 
  2. 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。 
  3. 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障,对应第1个规则。 
  • 在每个volatile写操作的后面插入一个StoreLoad屏障,对应第3个规则。 
  • 在每个volatile读操作的后面插入一个LoadLoad屏障,对应第2个规则。 
  • 在每个volatile读操作的后面插入一个LoadStore屏障,对应第2个规则。

从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义(内存可见性),这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

图片

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。 

如下一段代码:

public class VolatileExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;//第一个volatile读
        int j = v2;//第二个volatile读
        a = i + j;//普通写
        v1 = i + 1;//第一个volatile写
        v2 = j * 2;//第二个volatile写
    }
}

 编译器在生成字节码时可以做如下的优化

 此优化是针对所以平台的,看如下图:

常用的x86处理器仅会对写读进行重排序,因此在x86处理器会省略掉其他三类的存储屏障。在x86系统中只要保留StoreLoad屏障就能够正确实现volatile写-读内存语义

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

根据这条规则:StoreLoad是volatile写后才会有的屏障,所以x86系统中仅仅volatile的写会在后面插入StoreLoad屏障,所以在x86处理器中volatile写的开销比volatile读的开销大很多。

双重检查锁

经典的双重检查锁如下:

public class DoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {  //第一次检查
            synchronized (DoubleCheckedLocking.class) {
                if (instance == null) { //第二次检查
                    instance = new Instance(); //实例化
                }
            }
        }
        return instance;
    }
}

本篇文章只讲解为什么需要在实例上加volatile关键字:

instance = new Instance(); 这一行代码可以分解为:

memory = allocate();//1.分配对象所需的内存空间
initInstance();//2.初始化对象
instance = memory;//3.设置instance指向分配的内存空间

上面的代码第2和第3没有数据依赖关系,在某些处理器上是可能被重排序的,排序之后代码如下:

memory = allocate();//1.分配对象所需的内存空间
instance = memory;//3.设置instance指向分配的内存空间
initInstance();//2.初始化对象

此时可能如果有两个线程都来getInstance的话,可能会出现如下结果:

 线程B访问对象时,此时线程A还未完全初始化完对象,所以线程B可能访问到的对象是一个不完全体,导致程序的错误。

给instance变量加上volatile后,就是防止了该变量的重排序,就不会发生这种使用半个对象的情况了。

除了使用双重检查锁设置单例模式,还有一种方式就是类的方式:

public class InstanceFactory {
    //静态内部类
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }
    public static Instance getInstance(){
        return InstanceHolder.instance; //这里会导致InstanceHolder的初始化
    }
}

因为我们第一次使用静态内部类的时候,会初始化该类,初始化阶段JVM会去初始化一个锁,这个锁可以同步多个线程对同一个类的初始化。所以这种方式时依靠底层的同步方式来实现单例模式的。

退不出的循环

如下一段程序:该程序会陷入死循环

public class VolatileExample {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (true) {
                 if (!run){
                     break;
                 }
            }
        },"A").start();
        Thread.sleep(1000);
        run=false;
    }
}

因为主线程修改了run的值后刷新到仅仅主内存,A线程优化后可能每次只会从它的工作内存中取出run值,所以并不能感知到1s后主线程对run的修改动作,也就是缺少了对A的可见性。

解决方法

①给run加上volatile关键字,这样,线程A每次读取run的值都会去主内存取值。

②加上synchronized关键字,如下

public class VolatileExample {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (true) {
                 synchronized (VolatileExample.class){
                     if (!run){
                         break;
                     }
                 }
            }
        }).start();
        Thread.sleep(1000);
        synchronized (VolatileExample.class) {
            run = false;
        }
    }
}

并不是一定是给run加volatile和synchronized,如下代码:给b变量加上volatile,当线程A去访问b时,不仅是需要从主内存获取b的值,而且会刷新线程A的所有工作内存里的共享变量比如run,所以也就间接的让run得到了可见性。synchronized同理,如上的代码就是给其他不想关的对象加锁,当主线程获取到锁并修改了run的值后,释放锁的时候会把主线程中工作内存中所有写的值都刷新到主内存,当线程A得到锁后,又会刷新线程A中所有的共享变量的值,得到最新值。

public class VolatileExample {
    static boolean run = true;
    volatile static boolean b = true;//关键字在b上

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (b) {
                if (!run) {
                    break;
                }
            }
        },"A").start();
        Thread.sleep(1000);
        run = false;
    }
}

总结

volatile特点:

  • 通过使用Lock前缀的指令禁止变量在线程工作内存中缓存来保证volatile变量的内存可见性、通过插入内存屏障禁止会影响变量内存可见性的指令重排序

  • 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

volatile仅仅保证单个volatile变量的读/写具有原子性,而锁的互斥执行特性可以保证所以临界区代码的执行具有原子性。功能上volatile更强大,在可伸缩性和执行性能上,volatile更具优势。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值