美文网首页
volatile关键字

volatile关键字

作者: BugBean | 来源:发表于2019-08-05 16:09 被阅读0次

    volatile是java虚拟机提供的一种轻量级的同步机制,那么volatile到底是怎么实现轻量级同步的?

    可见性

    什么是可见性?这个得从java内存模型说起

    java内存模型

    JMM把内存条的内存定义为主存,CPU的高速缓存定义为工作内存,线程在运算前会把主存的共享数据拷贝一份副本到自己的工作内存,完成运算后再把数据更新到主存,但每个线程间的工作内存的数据是不共享的,也就是线程1修改了数据再回写到主存,线程2是不知道的

    代码说话

    public class VolatileVisibilityTest {
        public static void main(String[] args) {
            Data data = new Data();
            //线程T更新number的值
            new Thread(() -> {
                try {
                    //先睡3秒,让main线程读取到number的原始值
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                data.setNumber(100);
                System.out.println(Thread.currentThread().getName() + ": 修改number的值为" + data.getNumber());
            }, "T").start();
            //线程main读取number的值,如果线程main能感知到线程T修改了number的值,将会结束循环,否则一直在死循环中
            while (data.getNumber() == 0) {}
            System.out.println(Thread.currentThread().getName() + ": 结束循环,number的值已变为" + data.getNumber());
        }
    }
    
    class Data {
        private /*volatile*/ int number = 0;
        public int getNumber() {
            return number;
        }
        public void setNumber(int number) {
            this.number = number;
        }
    }
    

    大家试着运行一下volatile注释前和注释后的区别

    在注释volatile前,main线程是能感知到T线程对number的值做出了修改的

    在注释volatile后,main线程无感知T线程对number的值做出的修改,一直在循环中

    不保证原子性

    注意,volatile是轻量级的同步机制,所以不保证原子性,先上代码

    public class VolatileAtomicityTest {
        public static void main(String[] args) throws InterruptedException {
            Data data = new Data();
            //开启20个线程,每条线程执行1000次number++,理论上最后的结果应该是20000
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++) {
                        data.increment();
                    }
                }, "Thread--" + i).start();
            }
            //当只有main线程和GC线程存活才结束循环,否则放弃执行权
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println(data.getNumber());
        }
    }
    
    public class Data {
        private volatile int number = 0;
        public void increment() {
            number++;
        }
        public int getNumber() {
            return number;
        }
        public void setNumber(int number) {
            this.number = number;
        }
    }
    

    这段代码执行多次,只有偶尔的结果是20000,其他结果都是小于20000.但是若是给increment方法加上synchronized同步锁,则执行多次结果都是20000,这证明volatile不保证原子性

    我们都知道number++不是原子性的,对应的字节码如下:

    getfield  //从主存拷贝到工作内存
    iadd  //自增
    putfield  //回写到主存
    

    假设主存内的number=0,当线程1执行了字节码getfield和iadd后,准备执行putfield时被挂起,线程2执行getfield得到number的值仍然为0,并执行iadd,这个时候切又回线程1,虽然number加了volatile关键字,线程2修改了number的值线程1是能感知到的,但线程1已经开始执行putfield了,所以回写主存number=1,线程2也回写主存number=1,这时候就发生了写丢失.

    所以单靠volatile是无法保证原子性的

    有序性

    先来说说啥是指令重排,编译器和虚拟机会重新排列无数据依赖的语句来优化程序,也就是说,你写的代码虚拟机不一定是按顺序执行的

    public class TestRearrangement {
    
        private int a = 0;
        private boolean flag = false;
    
        public void method1() {
            a = 1;  //语句1
            flag = true;  //语句2
        }
    
        public void method2() {
            if (flag) {
                a = a + 5;
                System.out.println("a=" + a);
            }
        }
    }
    

    代码中的语句1和语句2,因为它们之间没有数据依赖,在指令重排时,很有可能是先执行语句2再执行语句1.这在单线程环境下没毛病,但在多线程环境下,语句1和语句2的顺序很可能会影响method2方法执行的结果.例如线程1先执行语句2然后挂起,线程2执行method2,那么a的值就会是5,如果线程1先执行语句1,那么线程2执行完method2,a的值为6

    若给a和flag都加上volatile关键字,编译器就不会对相关代码进行重排优化

    应用场景

    懒汉式单例

    首先来看看懒汉式单例的写法

    public class Singleton {
        private volatile static Singleton instance;
        private Singleton() {
        }
        public static Singleton getInstance() {
            if (instance == null) {  //第6行
                synchronized (Singleton.class) {
                    if (instance == null) {  //第8行
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    为什么加了synchronized还要加volatile呢?关键点在于instance = new Singleton()这行代码

    instance = new Singleton(); 
    
    // 可以分解为以下三个步骤
    1 memory=allocate();// 分配内存 相当于c的malloc
    2 ctorInstanc(memory) //初始化对象
    3 s=memory //设置s指向刚分配的地址
    
    // 上述三个步骤可能会被重排序为 1-3-2,也就是:
    1 memory=allocate();// 分配内存 相当于c的malloc
    3 s=memory //设置s指向刚分配的地址
    2 ctorInstanc(memory) //初始化对象
    

    因为synchronized不能保证有序,所以instance = new Singleton()的指令有可能会被重新排序,当重新排序为1-3-2,线程1执行完指令3时,instance已经被分配了内存地址,所以instance!=null;这个时候线程1挂起,线程2访问到第6行代码if (instance == null),因为instance!=null所以直接return instance,但此时instance对象还没创建好,因为线程1的指令2还没执行完,所以此时的instance只是具有内存地址但却是空对象

    所以,单例的懒汉式写法要加上volatile关键字

    相关文章

      网友评论

          本文标题:volatile关键字

          本文链接:https://www.haomeiwen.com/subject/yphxlctx.html