热门标签 | 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



推荐阅读
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 基于事件驱动的并发编程及其消息通信机制的同步与异步、阻塞与非阻塞、IO模型的分类
    本文介绍了基于事件驱动的并发编程中的消息通信机制,包括同步和异步的概念及其区别,阻塞和非阻塞的状态,以及IO模型的分类。同步阻塞IO、同步非阻塞IO、异步阻塞IO和异步非阻塞IO等不同的IO模型被详细解释。这些概念和模型对于理解并发编程中的消息通信和IO操作具有重要意义。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 关于CMS收集器的知识介绍和优缺点分析
    本文介绍了CMS收集器的概念、运行过程和优缺点,并解释了垃圾回收器的作用和实践。CMS收集器是一种基于标记-清除算法的垃圾回收器,适用于互联网站和B/S系统等对响应速度和停顿时间有较高要求的应用。同时,还提供了其他垃圾回收器的参考资料。 ... [详细]
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社区 版权所有