请谈谈你对volatile的理解

1.volitale是Java虚拟机提供的一种轻量级的同步机制

三大特性1.1保证可见性 1.2不保证原子性 1.3禁止指令重排

首先保证可见性

1.1 可见性

概念:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

package com.yuxue.juc.volatileTest;

/**
* 1验证volatile的可见性
* 1.1 如果int num = 0,number变量没有添加volatile关键字修饰
* 1.2 添加了volatile,可以解决可见性
*/
class VolatileDemo1 { //自定义的类
public static class MyTest{
//类的内部成员变量num
public int num = 0;
//numTo60 方法,让num值为60
public void numTo60(){
num = 60;
}
} public static void main(String[] args) { MyTest myTest = new MyTest();
//第一个线程
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
Thread.sleep(3000);
myTest.numTo60();
System.out.println(Thread.currentThread().getName() + "\t update value:" + myTest.num);
} catch (InterruptedException e) {
e.printStackTrace();
}
} ,"thread1").start();; //主线程判断num值
while (myTest.num == 0){
//如果myData的num一直为零,main线程一直在这里循环
}
System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myTest.num);
}
}

如上代码是没有保证可见性的,可见性存在于JMM当中即java内存模型当中的,可见性主要是指当一个线程改变其内部的工作内存当中的变量后,其他线程是否可以观察到,因为不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,因为此处没有添加volatile指令,导致其中thread1对num值变量进行更改时,main线程无法感知到num值发生更改,导致在while处无限循环,读不到新的num值,会发生死循环

此时修改类中代码为

/**
* volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
*/
public static class MyTest{
//类的内部成员变量num
public volatile int num = 0;
//numTo60 方法,让num值为60
public void numTo60(){
num = 60;
}
}

此时volatile就可以保证内存的可见性,此时运行代码就可以发现

1.2 不保证原子性

原子性概念:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败

类代码为:

//自定义的类
public static class MyTest {
//类的内部成员变量num
public volatile int num = 0; public void numPlusPlus() {
num++;
}
}

主方法为

public static void main(String[] args) {
MyTest myTest = new MyTest();
/**
* 10个线程创建出来,每个线程执行2000次num++操作
* 我们知道,在字节码及底层,i++被抽象为三个操作
* 即先取值,再自加,再赋值操作
*/
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
for (int j = 0; j < 2000; j++) {
myTest.numPlusPlus();
}
}, "Thread" + i).start();
}
//这里规定线程数大于2,一般有GC线程以及main主线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t finally num value is " + myTest.num);
}

代码如上所示,如果volatile保证原子性,那么10个线程分别执行自加2000次操作,那么最终结果一定是20000,但是执行三次结果如下

//第一次
main finally num value is 19003
//第二次
main finally num value is 18694
//第三次
main finally num value is 19552

可以发现,我们num的值每次都不相同,且最后的值都没有达到20000,这是为什么呢?

为什么会出现这种情况?

首先,我们要考虑到这种情况,假如线程A执行到第11行即myTest.numPlusPlus();方法时

线程进入方法执行numPlusPlus方法后,num的值不管是多少,线程A将num的值首先初始化为0(假如主存中num的值为0),之后num的值自增为1,之后线程A挂起,线程B此时也将主存中的num值读到自己的工作内存中值为0,之后num的值自增1,之后线程B挂起,线程A继续运行将num的值写回主存,但是因为volatile关键字保证可见性,但是在很短的时间内,线程B也将num的值写回主存,此时num的值就少加了一次,所以最后总数基本上少于20000

如何解决

但是JUC有线程的原子类为AtomicInteger类,此时,将类代码更改为

public static class MyTest {
//类的内部成员变量num
public volatile int num = 0;
AtomicInteger atomicInteger = new AtomicInteger(); //numTo60 方法,让num值为60
public void numTo60() {
num = 60;
} public void numPlusPlus() {
num++;
}
public void myAtomPlus(){
atomicInteger.getAndIncrement();
}
}

共同测试num和atomicInteger,此时执行主函数,三次结果为

//第一次
main finally num value is 19217
main finally atomicInteger value is 20000
//第二次
main finally num value is 19605
main finally atomicInteger value is 20000
//第三次
main finally num value is 18614
main finally atomicInteger value is 20000

我们发现volatile关键字并没有保证我们的变量的原子性,但是JUC内部的AtomicInteger类保证了我们变量相关的原子性,AtomicInteger底层用到了CAS,CAS不了解的话可以参考这篇文章

1.3 禁止指令重排

有序性的概念:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排顺序是必须要考虑指令之间的数据依赖性

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

重排代码实例: 声明变量: int a,b,x,y=0

线程A 线程B
x=a; y=b;
b=1; a=2;
执行结果 x=0,y=0

如果编译器对这段程序代码执行重排优化后,可能出现如下情况:

线程A 线程B
b=1; a=2;
x=a; y=b;
执行结果 x=2,y=1

这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的

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

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

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

2.JMM(java内存模型)

为什么提到JMM?JMM当中规定了可见性、原子性、以及有序性的问题,在多线程中只要保证了以上问题的正确性,那么基本上不会发生多线程当中存在数据安全问题

JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁时同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是贡献内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先概要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:

JMM的三大特性

2.1可见性

2.2原子性

2.3有序性

所以JMM当中的2.1和2.3在volatile当中都有很好的体现,volatile关键字并不能保证多线程当中的原子性,但是volatile是轻量级的同步机制,不想synchronized锁一样粒度太大

3.你在那些地方用过volatile?结合实际谈论一下?

当普通单例模式在多线程情况下:

/**
* 普通单例模式
* */
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(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//并发多线程后,构造方法会在一些情况下执行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}

此时会出现两个线程运行了SingletonDemo的构造方法

此时就违反了单例模式的规定,其构造方法在一些情况下会被执行多次

解决方式:

  1. 单例模式DCL代码

DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断

public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}

不仅两次判空让程序执行更有效率,同时对代码块加锁,保证了线程的安全性

但是!还存在问题!

什么问题?

大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次

DCL(双端检锁)机制不一定线程安全,原因时有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance=new SingleDemo();可以被分为一下三步(伪代码):

memory = allocate();//1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance执行刚分配的内存地址,此时instance!=null

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

所以如果3步骤提前于步骤2,但是instance还没有初始化完成指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。

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

此时加上volatile后就不会出现线程安全问题

private static volatile SingletonDemo instance = null;

因为volatile禁止了指令重排序的问题

对volatile的理解--从JMM以及单例模式剖析的更多相关文章

  1. java面试-谈谈你对volatile的理解

    一.volatile特性: volatile是Java虚拟机提供的轻量级的同步机制.主要有三大特性: 保证可见性 不保证原子性 禁止指令重排序 1.保证可见性 1)代码演示 AAA线程修改变量numb ...

  2. Java线程工作内存与主内存变量交换过程及volatile关键字理解

    Java线程工作内存与主内存变量交换过程及volatile关键字理解 1. Java内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行.此处的所谓内存模 ...

  3. volatile变量理解 via《Java并发编程实战》

    第3章:对象的共享 volatile关键字的理解 volatile变量,用来确保将变量的更行操作通知到其他线程.当变量申明为volatile类型后,编译器与运行时都会注意带这个变量时共享的,因此不会将 ...

  4. 谈谈你对volatile的理解

    1.volatile是Java虚拟机提供的轻量级的同步机制 1.1保证可见性1.2不保证原子性1.3禁止指令重排 JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象 ...

  5. volatile的理解

    用法解释 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其 ...

  6. 你说一下对Java中的volatile的理解吧

    前言 volatile相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性:禁止指令重排.这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用 ...

  7. volatile禁止重排使用场景与单例模式的Double Check Lock

    普通单例模式Demo public class Demo{ private static Demo INSTANCE; private Demo(){} public static Demo getI ...

  8. 对于volatile的理解

    哎.要学的东西太多,时间太少.一周的工作下来要总结的东西太多,还处理不完,越积越多.大周末的好想出去玩啊.... 得嘞,废话止于此. 无聊时候乱看网页发现了volatile的一篇文章,以前曾经对vol ...

  9. volatile的理解和使用

    package thread; /** * Created by Administrator on 2017/1/15. */ public class Counter { public volati ...

随机推荐

  1. micro 从cli的tag中获取配置

    官方文档: https://micro.mu/docs/go-config.html https://github.com/micro/go-micro/tree/master/config/sour ...

  2. [bug] HDFS:hdfs missing blocks. The following files may be corrupted

    原因 HDFS数据块丢失,需要删除丢失块的元信息 bin/hadoop fsck / -delete 参考 https://blog.csdn.net/lixgjob/article/details/ ...

  3. curl: (35) SSL connect error

    curl: (35) SSL connect error weixin_34212762 2018-02-23 20:16:23  230  收藏 文章标签: 运维 版权 阿里云的机器,昨晚githu ...

  4. 笔记本用HDMI转VGA 使用双屏办公 听语音

    笔记本用HDMI转VGA 使用双屏办公 听语音 原创 | 浏览:1212 | 更新:2019-11-12 12:16 1 2 3 4 5 6 7 分步阅读 笔记本使用 转接头扩展出一块屏幕.使用多屏办 ...

  5. STM32的VDD与VDDA

    http://bbs.21ic.com/icview-1651072-1-1.html VDD VSS 就是平常的电源与地.后面带A的都是模拟量的电源.

  6. 10.10 traceroute:追踪数据传输路由状况

    traceroute命令 用于显示网络数据包传输到指定主机的路径信息,追踪数据传输路由状况.默认数据包大小是60字节(IPv4)或80字节(IPv6),用户可另行设置.它与Windows下的trace ...

  7. JAVA并发(4)-并发队列ConcurrentLinkedQueue

    本文开始介绍并发队列,为后面介绍线程池打下基础.并发队列莫非也是出队.入队操作,还有一个比较重要的点就是如何保证其线程安全性,有些并发队列保证线程安全是通过lock,有些是通过CAS. 我们从Conc ...

  8. gin使用validator库参数校验若干实用技巧

    validator库参数校验若干实用技巧 本文介绍了使用validator库做参数校验的一些十分实用的使用技巧,包括翻译校验错误提示信息.自定义提示信息的字段名称.自定义校验方法等. validato ...

  9. Python+Selenium学习笔记16 - unittest单元测试框架

    unittest单元测试框架包括 Test Case,  Test Suite, Test Runner, Test Fixture Test Cases 组成Test Suite, Test Run ...

  10. TVMNN编译Compiler栈

    TVMNN编译Compiler栈 内容纲要 前言 调研目标 TVM介绍 TVM源码架构 FrontEnd Relay BackEnd VTA实现原理及设计思想提炼 整体结构 VTA Hardware ...