Volatile 介绍

23 篇文章 1 订阅


本文主要参考 《Java并发编程的艺术》以及一些博客

1 介绍

如果一个变量用了volatile修饰,那么这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

2 Java 内存模型 JMM

在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享 (本章用“共享变量”这个术语代指实例域,静态域和数组元素)。

Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享 变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽 象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的 一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意如图所示。

在这里插入图片描述

3 特性

可见性指当一个线程修改了某一个共享变量的值,其他的线程是否能够立即知道这个修改。

有序性指对于一个线程的执行代码是依次执行的。在单线程中,无论怎么重排序都不会改变结果,但是在并发时,程序的执行可能会出现乱序。Java 为了提高性能,会有三种重排,编译器重排,指令并行重排,内存系统重排。

原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

3.1 可见性

运行下面的程序,无法结束,在 idea 中可以 ctrl +F2 结束。这是因为某些线程 t2 中的 isStop 读的一直是自己本地的,即使对应的 t1 修改了也没有用。

如果有 volatile ,t1 修改了,t2 再读的时候,一定是从主内存中读到的,很快会结束。

下面的Thread.activeCount()>2是 idea 的情况,如果是 eclipse 将 2 改为 1。

public class B  {
    //volatile
    boolean isStop = false;
    public void test() {
        Thread t1 = new Thread(){
            public void run() {
                isStop = true;
            }
        };
        Thread t2 = new Thread(){
            public void run() {
                while (!isStop);
            }
    	};
        t2.start();
        t1.start();
    }
    public static void main(String args[]) throws InterruptedException {
        int num = 10;// 1个很可能没有效果
        for (int i=0;i<num;i++){
            new B().test();
        }
        while(Thread.activeCount()>2){// 在 idea 中启动时,run 会有两个线程,debug 会有1个
            System.out.println(Thread.activeCount()-2);
        }
    }
}

3.2 有序性

单例模式的线程安全的懒汉式写法,称为 DCL(Double Check Lock)。

对于下面可能出现问题的初始化语句,具体分三步:

  1. 分配内存空间

  2. 实例化对象instance

  3. 把instance引用指向已分配的内存空间,此时instance有了内存地址

如果 instance 没有指定为 volatile 的,可能执行顺序会重排序为1-3-2,如果一个线程 A 只执行了 1-3,没有执行2,则会产生错误,加上 volatile 会保证结果正确。

public class Singleton {
    private volatile static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {
            synchronized (Singleton.class) {
                if(null == instance) {
                    instance = new Singleton();// 出现问题的语句
                }
            }
        }

        return instance;

    }
}

实现有序的方法是禁止指令重排,如下表所示。

在这里插入图片描述

具体操作是插入 4 个内存屏障。

·在每个volatile写操作的前面插入一个StoreStore屏障。

·在每个volatile写操作的后面插入一个StoreLoad屏障。

·在每个volatile读操作的后面插入一个LoadLoad屏障。

·在每个volatile读操作的后面插入一个LoadStore屏障。

**LoadLoad屏障:**对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
**StoreStore屏障:**对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
**LoadStore屏障:**对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
**StoreLoad屏障:**对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

3.3 不保证原子性

对基本类型的变量的读取和赋值操作是原子性的。对于 64 位的 long/double 在 32位系统上,不是原子性的,会分两次处理。使用volatile关键字可以使得long和double具有原子性。

a = 5;//原子性
a = b;//非原子性,先加载b,然后赋值给a。
i++; ++i; i = i+1;//都是三步

以 i++ 为例,i++的操作分三步:
1. 栈中取出i
2. i + 1
3. 将i存到栈

在下面的程序中,有100 个线程,执行 10000 次加法,结果并不是 1 百万(如果没有效果,将数字继续增大即可)。

如果有两个线程 A 和 B,A 先执行第一步,切换到 B 执行第一步,这两个线程都取出 0,然后 A 执行后续的操作,将结果 1 存入主内存,B 执行后面的操作,也将结果 1 存入,这个时候,就会丢失掉一次加法操作。

public class C{
    
    public int i = 0;
    
    public void increase() {
        i++;
        System.out.println(i);
    }
    public static void main(String[] args) {
        final C c = new C();
        for(int k=0;k<100;k++){
            new Thread(){
                public void run() {
                    for(int j=0;j<10000;j++)
                        c.increase();
                };
            }.start();
        }
    }
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值