栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java面试题---volatile

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java面试题---volatile

目录

1.谈谈volatile的理解

1.1 基本概念

1.2 保证可见性

1.3 不保证原子性(JMM的理解)

1.4 禁止指令重排

2.在哪些地方用过volatile

2.1单例模式DCL代码

2.2 单例模式volatile代码

3.总结


1.谈谈volatile的理解

1.1 基本概念

volatile是java虚拟机提供的轻量级的同步机制,它拥有三大特性:①保证可见性 ②不保证原子性 ③禁止指令重排

接下来对这三个特性进行一一讲述

1.2 保证可见性

先引入JMM的概念

JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。

JMM 同步规定

线程解锁前,必须把共享变量的值刷新回主内存 线程加锁前

线程加锁前,必须读取主内存的最新值到自己的工作内存

加锁解锁是同一把锁

由于 JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量的储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但线程对变量的操作(读取赋值等)必须都工作内存进行看。

首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。具体看下面的图

 程序运行过程保证可见性分为以下步骤:

1)创建线程1,2,3,分别分配工作内存

2)若线程1先将age的值改变(赋值操作)则线程2,3将"看见"age的值发生了改变 此时将不会进行下一步操作

这就是可见性的解释。用图表示如下

 代码具体实现:

import java.util.concurrent.TimeUnit;

public class VolatileDemo01 {
    public static void main(String[] args) {
        //new一个线程
        MyData myData=new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"t come in");
            try {TimeUnit.SECONDS.sleep(5);}catch (InterruptedException e){e.printStackTrace();}
                myData.addNumber();
                System.out.println(Thread.currentThread().getName()+"t update"+myData.number);
        },"线程A").start();

        //第二个线程main 若number==0 则一直循环 不往下执行
        while (myData.number==0){

        }
        //如果这行代码可以显示则证明了volatile的可见性 此时number=60,若不能执行则number=0,会在上面while一直循环
        System.out.println(Thread.currentThread().getName()+"t update"+myData.number);
    }
}
class MyData{
    volatile int number=0;
    public void addNumber(){
        this.number=60;
    }
}

验证如下:

1.3 不保证原子性

原子性是什么?

简单来说,不可分割,完整性,即某个线程正在做某个具体业务时,中间不可加塞或者分割。要么两个同时成功,要么两个同时失败。

验证代码如下:

public class VolatileDemo02 {
    public static void main(String[] args) {
        Test test=new Test();
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <=1000 ; j++) {
                    test.add();
                }
            },String.valueOf(i)).start();
        }
        //需要等待20个线程计算完
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " "+test.number);
    }
}
class Test{
    volatile int number=0;
    public void add(){
        number++;
    }
}

验证结果:

 上述代码的解释:正常情况下没有volatile时创建了20个线程,++操作执行了1000次,那么最后的值是20000,但是加了volatile之后却始终小于等于20000,说明volatile不能保证原子性

那么要怎么保证原子性呢?

1.使用synchronized关键字

2.使用AtomticInteger关键字

用synchronized可以解决,但是小题大做,所以接下来用AtomticInteger解决

代码如下:

import java.util.concurrent.atomic.AtomicInteger;


public class VolatileDemo03 {
    public static void main(String[] args) {
        Test02 test=new Test02();
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <=1000 ; j++) {
                    test.add();
                    test.atomicAdd();
                }
            },String.valueOf(i)).start();
        }
        //需要等待20个线程计算完
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("加volatile "+test.number);
        System.out.println("加AtomicInteger "+test.atomicInteger);
    }
}

class Test02{
    //使用volatile不保证原子性
    volatile int number=0;
    public void add(){
        number++;
    }

    //使用AtomicInteger可保证原子性
    AtomicInteger atomicInteger=new AtomicInteger();
    public void atomicAdd(){
        atomicInteger.getAndIncrement();
    }
}

1.4 禁止指令重排

计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种

  • 单线程环境里面确保程序最终执行的结果和代码执行的结果一致
  • 处理器在进行重排序时必须考虑指令之间的数据依赖性
  • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测

 重排指令事例1:

public void sort(){
    int x = 1; //语句1
    int y = 2; //语句2
    x=x+1;     //语句3
    y=x*x;     //语句4
}

在多线程的情况下,由于编译器优化的指令重排,程序的执行顺序有多种情况:1->2->3->4,2->1->3->4,1->3->2->4等

重排指令事例2:

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

    public void method01() {
        a = 1;           // 语句1
        // ----线程切换----
        flag = true;     // 语句2
    }

    public void method02() {
        if (flag) {     //语句3
            a = a + 3;
            System.out.println("a = " + a);
        }
    }
}

当正常执行的情况下,输出a=4,但是当多线程的情况下,由于编译器优化的指令重排,可能使得执行顺序发生改变比如先执行语句2,然后执行语句3,此时flag=true,而语句1未被执行,所以a=0,a=a+3的最终值为3

显然指令重排不是我们想要的结果,而volatile则可以禁止指令重排,加了volatile之后代码如下:

public class VolatileDemo04 {
   volatile int a = 0;//加了volatile
   volatile boolean flag = false;//加了volatile

    public void method01() {
        a = 1;           // 语句1
        // ----线程切换----
        flag = true;     // 语句2
    }

    public void method02() {
        if (flag) {     //语句3
            a = a + 3;
            System.out.println("a = " + a);
        }
    }
}

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

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

一是保证特定操作的执行顺序

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

 

 2.在哪些地方用过volatile

2.1单例模式DCL代码

首先看看不使用任何机制前代码的运行情况:

public class SingletonDemo {
    //单例模式
    private static SingletonDemo instance =null;
    private  SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
    }
    public static SingletonDemo getInstance(){
        if(instance==null){
            instance=new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        //单线程
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        //多线程
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

结果如下:

本来使用单例模式我们想要的结果是只输出一次结果,但是根据上面可以看到结果不止一次

于是我们采用加synchronized的方法解决上述问题,可以加两个地方一是:

//第一种是直接把synchronized加到方法里面 此时把方法里的成员都锁住
public static synchronized SingletonDemo getInstance(){
        if(instance==null){
            instance=new SingletonDemo();
        }
        return instance;
    }

第二种是:

public class SingletonDemo02 {
    private static SingletonDemo02 instance =null;
    private SingletonDemo02(){
        System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
    }
    public static  SingletonDemo02 getInstance(){
        //DCL Double Check Lock双端检锁机制
        if(instance==null){
            synchronized(SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo02();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) { 
        //单线程
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        //多线程
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo02.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
 public static  SingletonDemo02 getInstance(){
        //DCL Double Check Lock双端检锁机制
        if(instance==null){
            synchronized(SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo02();
                }
            }
        }
        return instance;
    }

这种称为DCL双端检锁机制,这种方法比第一种更加保险,安全,虽然这种方式安全,但是正确的概率为有可能是99%,并不能完全保证。因此继续改善加volatile!

2.2 单例模式volatile代码

代码如下:

public class SingletonDemo03 {
    private static volatile SingletonDemo03 instance =null;//注意加了volatile!
    private SingletonDemo03(){
        System.out.println(Thread.currentThread().getName()+"t 我是构造方法SingletonDemo()");
    }
    public static SingletonDemo03 getInstance(){
        //DCL Double Check Lock双端检锁机制
        if(instance==null){
            synchronized(SingletonDemo.class){
                if(instance==null){
                    instance=new SingletonDemo03();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) { 
        //单线程
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
       // System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());

        //多线程
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo03.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

小结:DCL(双端检锁)机制不一定线程安全,原因是指令重排序的存在,某一个线程执行到第一次检测,读取到的 instance 不为 null 时,instance 的引用对象可能还没有完成初始化。加入 volatile 可以禁止指令重排。instance = new Singleton() 可以分为以下三步完成。

memory = allocate(); // 1.分配对象空间

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

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

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

若在多线程情况下执行顺序可能编程1->3->2,也就是在没有初始化对象时就指向了分配的内存地址。这样会出错!

3.总结

volatile三大特性:①保证可见性 ②不保证原子性 ③禁止指令重排

volatile可用于:单例模式,使用synchronized的DCL双重检锁机制不一定能保证安全,因为多线程下有指令重排,使用volatile后可解决这个问题。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/270446.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号