JAVA面试题之Volatile

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
 
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
 

 

什么是Volatile?

Volatile是Java提供的轻量级同步机制

Volatile的特性有哪些?

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

可见性代码验证:

package com.lingyi.test;

class MyData{
    int count = 0;
    public void changeCount(){
        this.count = 60;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            try {
                System.out.println("线程1 start");
                //3秒后改变count的值为60
                Thread.sleep(3000);
                myData.changeCount();
                System.out.println("线程1 获取count==="+myData.count);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"线程1").start();
        while(myData.count == 0){

        }
        System.out.println("主线程获取count的值===="+myData.count);
    }
}

上面代码执行的结果如下:程序会一直执行,说明主线程获取count的值一直是0,故线程1修改后的count值没有被主程序给读取到,导致程序出现死循环。

将MyData类稍作修改,使用volatile来修饰count变量,

 volatile int count = 0;

执行后的,线程1改变的结果就会对主线程可见,这就验证了volatile的第一个特性:可见性。执行的结果如下:

不保证原子性代码验证:

MyData类中新增count自增方法: countAddOne

    /**
     * 每次执行加一
     */
    public void countAddOne(){
        this.count ++;
    }

main方法中使用20个线程来调用该方法,每个线程调用1000次,来验证volalite是否具有原子性。

 public static void main(String[] args) {
        MyData myData = new MyData();
        for(int i = 1;i <= 20;i++){
            new Thread(() -> {
                for(int j = 1;j <= 1000;j++){
                    myData.countAddOne();
                }
            },String.valueOf(i)).start();
        }
        //保证执行完上面20个线程之后再执行主线程
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t the last count is "+myData.count);
    }

调用三次之后的执行结果如下:

执行的结果都不等于20000,说明volatile并不保证数据的原子性。

解决上述原子性问题:使用JUC下的AtomicInteger类,使用的是CAS原理

AtomicInteger atomicNum = new AtomicInteger();

public void changeAtomicNum(){
    atomicNum.incrementAndGet();
}

执行结果如下所示:

  禁止指令重排:

volatile实现禁止指令重排序优化,从而避免多线环境下程序出现乱序执行的现象

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

1,保证特定操作的执行顺序

2,保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

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

public class ResortSeqDemo {
    int a =0;
    boolean flag = false;

    public void method1(){
        a = 1;
        flag = true;
    }
    public void method2(){
        if(flag){
            a = a + 5;
            System.out.println("a的值为:==="+a);
        }
    }
}

使用该demo做个简单的说明,在单线程环境中,使用线程调用method1和method2时,输出的结果一定是1,但是在多线程环境下,当有若干个线程来调用method1和method2,由于指令重排序优化,线程间的切换执行,某个线程在调用method1时,进行了指令重排序,先执行flag = true,此时由于线程的切换,另外一个线程在调用method2,获取的a的值则为初始值0,这时输出的a的值则为5。使用volatile则可以解决指令重排序问题,使得method1方法中执行的顺序一定是a = 1;flag = true.

Volatile在单例模式中的使用,先看没有使用volatile的例子,使用的DCL模式(double check lock):

public class SingletonDemo {
    private static SingletonDemo instance = null;

    public SingletonDemo() {
        System.out.println(Thread.currentThread().getName() +" SingletonDemo 构造函数被调用");
    }
    public static SingletonDemo getInstance(){
        if(instance == null){
            synchronized (SingletonDemo.class){
                if(instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for(int i = 1;i <= 20;i++){
            new Thread(() -> {
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

程序执行的结果如下:

从执行结果上看,好像并没有什么问题,但是这里涉及到指令重排序问题。

原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。

instance = new SingletonDemo();可以分为以下步骤(伪代码)

 

memory = allocate();//1,分配对象内存空间

instance(memory);//2,初始化对象

instance = memory;//3,设置instance的指向刚分配的内存地址,此时instance != null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后执行的结果在单线程中并没有改变,因此这种重排序优化是允许的。

memory = allocate();//1,分配对象内存空间

instance = memory;//3,设置instance的指向刚分配的内存地址,此时instance != null 但对象还没有初始化完

instance(memory);//2,初始化对象

但是指令重排只会保证串行语义的执行一致性(单线程)并不会关心多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题。

解决方案:使用volatile修饰

 private static volatile SingletonDemo instance = null;

下一节内容:Java面试题之CAS

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值