热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

java线程volatile_多线程与高并发(四)volatile关键字

上一篇学习了synchronized的关键字,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volati

上一篇学习了synchronized的关键字,synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volatile是一个轻量级的同步机制。

前面学习了Java的内存模型,知道各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改,但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。

而volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

我们可以先简单的理解:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

一、三个特性

在分析volatile之前,我们先看下多线程的三个特性:原子性,有序性和可见性。

1.1 原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。即多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

看下面几行代码:

int a = 10; //语句1

a++; //语句2

int b=a; //语句3

a = a+1; //语句4

上面的4行代码中,只有语句1才是原子操作。

语句1直接将数值10赋值给a,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a。

语句3包含两个操作:1:读取a的值;2:再将a的值写入工作内存。

语句4与语句2类似,也是三个操作。

从这里可以看出,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

1.2 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

前面线程安全篇中学习过happens-before原则,可以去前篇看看。

1.3 可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

synchronized能够保证任一时刻只有一个线程执行该代码块,并且在释放锁之前会将对变量的修改刷新到主存当中,那么自然就不存在原子性和可见性问题了,线程的有序性当然也可以保证。

下面我们来看看volatile关键字。

二、volatile的使用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

禁止进行指令重排序。

2.1 可见性

先看下面的代码:

public classVolatileTest {private static boolean isOver = false;private static int a = 1;public static voidmain(String[] args) {

Thread thread= new Thread(newRunnable() {

@Overridepublic voidrun() {while (!isOver) {

a++;

}

}

});

thread.start();try{

Thread.sleep(500);

}catch(InterruptedException e) {

e.printStackTrace();

}

isOver= true;

}

}

这里的代码会出现死循环,原因在于虽然在主线程中改变了isOver的值,但是这个值的改变对于我们新开线程中并不可见,在线程的本地内存未被修改,所以就会出现死循环。

aae624326067422ba0ce03e9c92becc2.png

如果我们用volatile关键字来修饰变量,则不会出现此情形

private static volatile boolean isOver = false;

这说明volatile关键字实现了可见性。

2.2 有序性

再看下面代码:

public classSingleton {private volatile staticSingleton instance;privateSingleton() {

}publicSingleton getInstance() {if (instance == null) {//步骤1

synchronized (Singleton.class) {//步骤2

if (instance == null) {//步骤3

instance = new Singleton();//步骤4

}

}

}returninstance;

}

}

这个是大家很熟悉的单例模式double check,在这里看到使用了volatile字修饰,如果不使用的话,这里可能会出现重排序的情况。

因为instance = new Singleton()这条语句实际上包含了三个操作:

1.分配对象的内存空间;

2.初始化对象;

3.设置instance指向刚分配的内存地址。步骤2和步骤3可能会被重排序,流程变为1->3->2

833ad73dcec2f8dd9656d7a99afb0b76.png

如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,将会读取到一个没有初始化完成的对象。

用volatile修饰的话就可以禁止2和3操作重排序,从而避免这种情况。volatile包含禁止指令重排序的语义,其具有有序性。

2.3 原子性

看下面代码:

public classVolatileExample {private static volatile int counter &#61; 0;public static voidmain(String[] args) {for (int i &#61; 0; i <10; i&#43;&#43;) {

Thread thread&#61; new Thread(newRunnable() {

&#64;Overridepublic voidrun() {for (int i &#61; 0; i <10000; i&#43;&#43;)

counter&#43;&#43;;

}

});

thread.start();

}try{

Thread.sleep(1000);

}catch(InterruptedException e) {

e.printStackTrace();

}

System.out.println(counter);

}

}

启10个线程&#xff0c;每个线程都自加10000次&#xff0c;如果不出现线程安全的问题最终的结果应该就是&#xff1a;10*10000 &#61; 100000;可是运行多次都是小于100000的结果&#xff0c;问题在于 volatile并不能保证原子性&#xff0c;counter&#43;&#43;这并不是一个原子操作&#xff0c;包含了三个步骤&#xff1a;1.读取变量counter的值&#xff1b;2.对counter加一&#xff1b;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后&#xff0c;其他线程对这个值已经做了自增操作后&#xff0c;那么线程A的这个值自然而然就是一个过期的值&#xff0c;因此&#xff0c;总结果必然会是小于100000的。

如果让volatile保证原子性&#xff0c;必须符合以下两条规则&#xff1a;

运算结果并不依赖于变量的当前值&#xff0c;或者能够确保只有一个线程修改变量的值&#xff1b;

变量不需要与其他的状态变量共同参与不变约束

三、实现原理

上面看到了volatile的使用&#xff0c;volatile能够保证可见性和有序性&#xff0c;那它的实现原理是什么呢&#xff1f;

在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令&#xff0c;Lock前缀的指令在多核处理器下会引发了两件事情&#xff1a;

将当前处理器缓存行的数据写回到系统内存。

这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度&#xff0c;处理器不直接和内存进行通信&#xff0c;而是先将系统内存的数据读到内部缓存(L1&#xff0c;L2或其他)后再进行操作&#xff0c;但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作&#xff0c;JVM就会向处理器发送一条Lock前缀的指令&#xff0c;将这个变量所在缓存行的数据写回到系统内存。但是&#xff0c;就算写回到内存&#xff0c;如果其他处理器缓存的值还是旧的&#xff0c;再执行计算操作就会有问题。所以&#xff0c;在多处理器下&#xff0c;为了保证各个处理器的缓存是一致的&#xff0c;就会实现缓存一致性协议&#xff0c;每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了&#xff0c;当处理器发现自己缓存行对应的内存地址被修改&#xff0c;就会将当前处理器的缓存行设置成无效状态&#xff0c;当处理器对这个数据进行修改操作的时候&#xff0c;会重新从系统内存中把数据读到处理器缓存里。volatile的实现原则&#xff1a;

Lock前缀的指令会引起处理器缓存写回内存&#xff1b;

一个处理器的缓存回写到内存会导致其他处理器的缓存失效&#xff1b;

当处理器发现本地缓存失效后&#xff0c;就会从内存中重读该变量数据&#xff0c;即可以获取当前最新值。

3.1 内存语义

理解了volatile关键字的大体实现原理&#xff0c;那对内volatile的内存语义也相对好理解&#xff0c;看下面的代码&#xff1a;

public classVolatileExample2 {private int a &#61; 0;private boolean flag &#61; false;public voidwriter() {

a&#61; 1;

flag&#61; true;

}public voidreader() {if(flag) {int i &#61;a;

}

}

}

假设线程A先执行writer方法&#xff0c;线程B随后执行reader方法&#xff0c;初始时线程的本地内存中flag和a都是初始状态&#xff0c;下图是线程A执行volatile写后的状态图。

55a8966a6996155c490dea6b704c6f7b.png

如果添加了volatile变量写后&#xff0c;线程中本地内存中共享变量就会置为失效的状态&#xff0c;因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。

ae85da24ab15fbfe5b3428910847e79c.png

对volatile写和volatile读的内存语义做个总结。

线程A写一个volatile变量&#xff0c;实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。

线程B读一个volatile变量&#xff0c;实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。

线程A写一个volatile变量&#xff0c;随后线程B读这个volatile变量&#xff0c;这个过程实质上是线程A通过主内存向线程B发送消息。

70f871854208f62f8b8cac6d73c2487c.png

3.2 内存语义的实现

我们知道&#xff0c;JMM是允许编译器和处理器对指令序列进行重排序的&#xff0c;但我们也可以用一些特殊的方式组织指令阻止指令重排序&#xff0c;这个方式就是增加内存屏障。我们先来简答了解下内存屏障&#xff0c;JMM把内存屏障指令分为4类&#xff1a;

e2ec94d9820dfdc49e138da34571e3b3.png

StoreLoad Barriers是一个“全能型”的屏障&#xff0c;它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵&#xff0c;因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

了解完内存屏障后&#xff0c;我们再来看下volatile的重排序规则&#xff1a;

当第二个操作是volatile写时&#xff0c;不管第一个操作是什么&#xff0c;都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile读时&#xff0c;不管第二个操作是什么&#xff0c;都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

当第一个操作是volatile写&#xff0c;第二个操作是volatile读时&#xff0c;不能重排序。

要实现volatile的重排序规则&#xff0c;需要来增加一些内存屏障&#xff0c;为了保证在任意处理器平台都可以实现&#xff0c;内存屏障插入策略非常保守&#xff0c;主要做法如下&#xff1a;

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是&#xff1a;volatile写是在前面和后面分别插入内存屏障&#xff0c;而volatile读操作是在后面插入两个内存屏障

StoreStore屏障&#xff1a;禁止上面的普通写和下面的volatile写重排序&#xff1b;

StoreLoad屏障&#xff1a;防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障&#xff1a;禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障&#xff1a;禁止下面所有的普通写操作和上面的volatile读重排序

volatile写插入内存屏障后生成的指令序列示意图&#xff1a;

9974a66e8c11360ca983a9972611afb8.png

volatile读插入内存屏障后生成的指令序列示意图&#xff1a;

f8fada5a0e96ae8b280af08ff8aecbb3.png



推荐阅读
  • Spring Boot 中配置全局文件上传路径并实现文件上传功能
    本文介绍如何在 Spring Boot 项目中配置全局文件上传路径,并通过读取配置项实现文件上传功能。通过这种方式,可以更好地管理和维护文件路径。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • javax.mail.search.BodyTerm.matchPart()方法的使用及代码示例 ... [详细]
  • Spring – Bean Life Cycle
    Spring – Bean Life Cycle ... [详细]
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 原文网址:https:www.cnblogs.comysoceanp7476379.html目录1、AOP什么?2、需求3、解决办法1:使用静态代理4 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文介绍了Java中的com.sun.codemodel.JBlock._continue()方法,并提供了多个实际代码示例,帮助开发者更好地理解和使用该方法。 ... [详细]
  • 在JavaWeb开发中,文件上传是一个常见的需求。无论是通过表单还是其他方式上传文件,都必须使用POST请求。前端部分通常采用HTML表单来实现文件选择和提交功能。后端则利用Apache Commons FileUpload库来处理上传的文件,该库提供了强大的文件解析和存储能力,能够高效地处理各种文件类型。此外,为了提高系统的安全性和稳定性,还需要对上传文件的大小、格式等进行严格的校验和限制。 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
author-avatar
手机用户2602900871
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有