volatile 面试,java中volatile什么意思

  volatile 面试,java中volatile什么意思

  00-1010 1.volatile保证可见性1.1。什么是JMM模式?1.2.易变保证可见性的代码验证1.2.1,不可见性的代码验证1.2.1,易变保证可见性的验证2。volatile不保证原子性2.1什么是原子性?2.2不保证原子性的代码验证2.3不保证原子性的volatile的解决方案2.3.1方法1:使用synchronized2.3.2方法1:使用JUC包下的atomic integer 3 2.3 volatile禁止指令重排3.1什么是指令重排?3.2单线程singleton模式3.3多线程singleton模式3.4多线程singleton模式改进:DCL3.5多线程singleton模式改进,DCL3.5版本存在的问题volatile保证禁止指令重排的原理总结Q:请说说你对volatile的理解?

  答:volatile是Java虚拟机提供的轻量级同步机制。它有三个特点:

  1)保证可见性

  2)不保证原子性

  3)禁止指令重排

  刚学完java基础,如果有人问你什么是volatile?如果说它有什么作用的话,我相信它一定很蠢…

  可能看了答案,我根本不明白。同步机制是什么?什么是能见度?什么是原子性?什么是指令重排?

  

目录

 

  要了解什么是可见性,你必须先了解JMM。

  JMM(Java内存模型)本身是一个抽象的概念,并不真正存在。它描述了一组规则或规范,通过这些规则或规范来确定程序中各种变量的访问方法。JMM关于同步的规定:

  1)在线程解锁之前,必须将共享变量的值刷新回主存;

  2)在线程锁定之前,它必须将主存的最新值读入自己的工作内存;

  3)上锁和开锁是同一个锁;

  由于JVM运行程序的实体是线程,当每个线程被创建时,JMM会为它创建一个工作内存(有些地方称为堆栈空间)。工作内存是每个线程的私有数据区。

  Java内存模型规定所有变量都存储在主存中,主存是所有线程都可以访问的共享内存区域。

  但是,操作(阅读、赋值等。)的变量必须在工作内存中执行。所以我们要先把变量从主存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再把变量写入主存。

  看一下对JMM的介绍,可能还是有一些好处的。接下来,以一个售票系统为例:

  1)如下图所示,售票系统后端只剩下一张票,已经读入主存:ticketNum=1。

  2)此时网络上有多个用户在抢票,所以此时多个线程都在同时进行购票服务。假设这三个线程都已经读取了当前的票数:ticketNum=1,那么接下来它们就会买票。

  3)假设线程1先抢占cpu资源,先买票,在自己的工作内存中把ticketNum的值改为0: ticketnum=0,再写回主存。

  此时线程1的用户已经买了票,那么此时线程2和线程3应该不能继续买票,所以系统需要通知线程2和线程3此时ticketNum等于0:ticket num=0。如果有这样的通知操作,可以理解为可见性。

  通过上面JMM的介绍和例子,我们可以简单的总结一下。

  JMM内存模型的可见性是指当多线程访问主存中的某个资源时,如果某个线程修改了自己工作内存中的资源并写回主存,那么JMM内存模型应该通知其他线程从新的来源获取最新的资源,以保证最新资源的可见性。

  1.1中00-1010,可见性的含义已经基本理解。接下来,用代码验证一下。volatile确实可以保证可见性。

  00-1010首先验证不使用volatile是否没有可见性。

  包com。koping。测试;导入Java。util。并发。时间单位;类my data { int number=0;public void add 10(){ this。数字=10;} }公共类volatilevibilitydemo { public static void main(String[]args){ my data my data=new my data();//启动一个线程修改对象的号,将数字的值加10个新线程(()-g

  t; { System.out.println("线程" + Thread.currentThread().getName()+"t 正在执行"); try{ TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); } myData.add10(); System.out.println("线程" + Thread.currentThread().getName()+"t 更新后,number的值为" + myData.number); } ).start(); // 看一下主线程能否保持可见性 while (myData.number == 0) { // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环; // 如果没有可见性的话,就会一直在循环里执行 } System.out.println("具有可见性!"); }}运行结果如下图,可以看到虽然线程0已经将number的值改为了10,但是主线程还是在循环中,因为此时number不具有可见性,系统不会主动通知。

  

 

  

 

  

1.2.1、volatile保证可见性验证

在上面代码的第7行给变量number添加volatile后再次测试,如下图,此时主线程成功退出了循环,因为JMM主动通知了主线程更新number的值了,number已经不为0了。

 

  

 

  

 

  

2、volatile不保证原子性

 

  

2.1 什么是原子性?

理解了上面说的可见性之后,再来理解下什么叫原子性?

 

  原子性是指不可分隔,完整性,即某个线程正在做某个业务时,中间不能被分割。要么同时成功,要么同时失败。

  还是有点抽象,接下来举个例子。

  如下图,创建了一个测试原子性的类:TestPragma。在add方法中将n加1,通过查看编译后的代码可以看到,n++被拆分为3个指令进行执行。

  因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。

  

 

  

 

  

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

 

  首先给MyData类添加一个add方法

  

package com.koping.test;class MyData { volatile int number = 0; public void add() { number++; }}

然后创建测试原子性的类:TestPragmaDemo。测试下20个线程给number各加1000次之后,number的值是否是20000。

 

  

package com.koping.test;public class TestPragmaDemo { public static void main(String[] args) { MyData myData = new MyData(); // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000 for (int i=0; i<20; i++) { new Thread(() -> { for (int j=0; j<1000; j++) { myData.add(); } }).start(); } // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待 while (Thread.activeCount()>2){ Thread.yield(); } System.out.println("number值加了20000次,此时number的实际值是:" + myData.number); }}

运行结果如下图,最终number的值仅为18410。

 

  可以看到即使加了volatile,依然不保证有原子性。

  

 

  

 

  

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

 

  

 

  

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

 

  结果如下图,最终确实可以使number的值为20000,保证了原子性。

  但是,实际业务逻辑方法中不可能只有只有number++这1行代码,上面可能还有n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

  

package com.koping.test;class MyData { volatile int number = 0; public synchronized void add() { // 在n++上面可能还有n行代码进行逻辑处理 number++; }}

 

  

 

  

2.3.2 方法1:使用JUC包下的AtomicInteger

给MyData新曾一个原子整型类型的变量num,初始值为0。

 

  

package com.koping.test;import java.util.concurrent.atomic.AtomicInteger;class MyData { volatile int number = 0; volatile AtomicInteger num = new AtomicInteger(); public void add() { // 在n++上面可能还有n行代码进行逻辑处理 number++; num.getAndIncrement(); }}

让num也同步加20000次。结果如下图,可以看到,使用原子整型的num可以保证原子性,也就是number++的时候不会被抢断。

 

  

package com.koping.test;public class TestPragmaDemo { public static void main(String[] args) { MyData myData = new MyData(); // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000 for (int i=0; i<20; i++) { new Thread(() -> { for (int j=0; j<1000; j++) { myData.add(); } }).start(); } // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待 while (Thread.activeCount()>2){ Thread.yield(); } System.out.println("number值加了20000次,此时number的实际值是:" + myData.number); System.out.println("num值加了20000次,此时number的实际值是:" + myData.num); }}

 

  

 

  

3、volatile禁止指令重排

 

  

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

 

  计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

  处理器在进行重排时,必须要考虑指令之间的数据依赖性。

  单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

  但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

  看了上面的文字性表达,然后看一个很简单的例子。

  比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:

  1)1234

  2)2134

  3)1324

  以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

  

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

 

  

3.2 单线程单例模式

看完指令重排的简单介绍后,然后来看下单例模式的代码。

 

  

package com.koping.test;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("单线程的情况测试开始"); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); System.out.println("单线程的情况测试结束n"); }}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。

 

  

 

  

 

  

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

 

  

package com.koping.test;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(); } // DCL(Double Check Lock双端检索机制)// if (instance == null) {// synchronized (SingletonDemo.class) {// if (instance == null) {// instance = new SingletonDemo();// }// }// } return instance; } public static void main(String[] args) { // 单线程测试// System.out.println("单线程的情况测试开始");// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());// System.out.println("单线程的情况测试结束n"); // 多线程测试 System.out.println("多线程的情况测试开始"); for (int i=1; i<=10; i++) { new Thread(() -> { SingletonDemo.getInstance(); }, String.valueOf(i)).start(); } }}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。

 

  

 

  

 

  

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

 

  

package com.koping.test;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();// } // DCL(Double Check Lock双端检锁机制) if (instance == null) { // a行 synchronized (SingletonDemo.class) { if (instance == null) { // b行 instance = new SingletonDemo(); // c行 } } } return instance; } public static void main(String[] args) { // 单线程测试// System.out.println("单线程的情况测试开始");// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());// System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());// System.out.println("单线程的情况测试结束n"); // 多线程测试 System.out.println("多线程的情况测试开始"); for (int i=1; i<=10; i++) { new Thread(() -> { SingletonDemo.getInstance(); }, String.valueOf(i)).start(); } }}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。

 

  

 

  

 

  

3.5 多线程单例模式改进,DCL版存在的问题

需要注意的是3.4中的DCL版的单例模式依然不是100%准确的!!!

 

  是不是不太明白为什么3.4DCL版单例模式不是100%准确的原因

  是不是不太明白在3.1讲完指令重排的简单理解后,为什么突然要讲多线程的单例模式

  因为3.4DCL版单例模式可能会由于指令重排而导致问题,虽然该问题出现的可能性可能是千万分之一,但是该代码依然不是100%准确的。如果要保证100%准确,那么需要添加volatile关键字,添加volatile可以禁止指令重排

  接下来分析下,为什么3.4DCL版单例模式不是100%准确?

  查看instance = new SingletonDemo();编译后的指令,可以分为以下3步:

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

  2)初始化对象:instance(memory);

  3)设置instance指向分配的内存地址:instance = memory;

  由于步骤2和步骤3不存在数据依赖关系,因此可能出现执行132步骤的情况。

  比如线程1执行了步骤13,还没有执行步骤2,此时instance!=null,但是对象还没有初始化完成;

  如果此时线程2抢占到cpu,然后发现instance!=null,然后直接返回使用,就会发现instance为空,就会出现异常。

  这就是指令重排可能导致的问题,因此要想保证程序100%正确就需要加volatile禁止指令重排。

  

 

  

3.6 volatile保证禁止指令重排的原理

在3.1中简单介绍了下执行重排的含义,然后通过3.2-3.5,借助单例模式来举例说明多线程情况下,为什么要使用volatile的原因,因为可能存在指令重排导致程序异常。

 

  接下来就介绍下volatile能保证禁止指令重排的原理。

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

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

  2)保证某些变量的内存可见性;

  由于编译器和处理器都能执行指令重排。如果在指令之间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说,通过插入内存屏障,禁止在内存屏障前后的指令执行重排需优化

  内存屏障的另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

  

 

  

 

  

总结

到此这篇关于Java基础面试题之volatile详解的文章就介绍到这了,更多相关Javavolatile详解内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

 

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: