接上一篇《Java并发编程系列之synchronized(一)》,这是第二篇,说的是关于并发编程的volatile元素。

Java语言规范第三版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

java 内存模型的核心是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特性来展开的,它们是多线程编程的核心。

  • 原子性(Atomicity):是指一个操作是不可中断的,即使是多个线程同时执行的情况下,一个操作一旦开始,就不会被其它线程干扰。对于基本类型的读写操作基本都具有原子性的(在32位操作系统中 long 和 double 类型数据的读写不是原子性的,因为它们有64位)。
  • 可见性(Visibility):是指在多线程环境下,当一个线程修改了某一个共享变量的值,其它线程能够立刻知道这个修改。
  • 有序性(Ordering):是指程序的执行顺序是按照代码的先后顺序执行的;对于这句话如果在单线程中所有的操作都是有序的,但是在多线程环境下,一个线程的操作相对于另外一个线程的操作是无序的。

了解volatile关键字之前需要先了解下Java内存模型,java内存模型抽象示意图如下:

Java内存模型

线程A和线程B之间若要通信的话, 必须经历下面两个步骤 :

(1)线程A和线程A本地内存中更新过的共享变量刷新到主存中去。

(2)线程B到主存中去读取线程A之前更新过的共享变量。

由此可见执行下面的语句:

int a = 100 线程必须现在自己的工作线程中对变量i所在的缓存进行赋值操作,然后再写入主存当中,而不是直接将数值100写入主存中。

特性

可见性 当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。当其他线程需要读取该值时,其他线程会去主存中读取新值。相反普通的共享变量不能保证可见性,因为普通共享变量被修改后并不会立即被写入主存,何时被写入主存也不确定。当其他线程去读取该值时,此时主存可能还是原来的旧值,这样就无法保证可见性。

有序性 java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程 执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过volatile来保证有序性,除了volatile,也可以通过synchronized和Lock来保证有序性。synchronized和Lock保证每个时刻只有一个线程执行同步代码,这相当于让线程顺序执行同步代码,从而保证了有序性。如果不考虑原子性操作的话volatile比synchronized和Lock更轻量级,成本更低。

不保障原子性 volatile关键字只能保证共享变量的可见性和有序性。如果volatile修饰并发线程中共享变量, 而该共享变量是非原子操作的话,并发中就会出现问题。比如下面代码:

publicclassHelloVolatile{publicvolatileintmNumber =0;publicstaticvoidmain(String []args){        final HelloVolatile hello =newHelloVolatile();for(inti =0; i<10; i++){newThread(){publicvoidrun(){for(intj =0; j<1000; j++){                        hello.mNumber ++;                    }                }            }.start();        }while(Thread.activeCount()>2){            Thread.yield();        }        System.out.println("number:"+hello.mNumber);    }}

这段代码预期结果是10000,可是每次执行结果都有可能不一样。这是因为自增或自减都是非原子操作。

(1) 假如mNumber此时等于100,线程1进行自增操作。

(2)线程1先读取了mNumber的值100,然后它被堵塞了。

(3)这时候线程2读取mNumber的值100,然后进行了自增操作,并写入到主存中, 这时候主存中的值为101。

(4)这时候线程1继续执行,因为此前线程1已经读取到值100,然后进行自增操作101,然后将101写入到主存中。

可以看到两个线程分别对100进行了+1操作,预期主存中的nNumber = 102,实际mNumebr = 101; 这就是因为非原子操作造成的。

使用场景

(1)并发编程中不依赖于程序中任意其状态的状态标识。可以通过关键字volatile代替synchronized, 提高程序执行效率,并简化代码。

(2)单例模式的双重检查模式DCL

publicclassDclSingleton{privatevolatilestaticDclSingleton mInstance =null;publicstaticDclSingletongetInstance(){if(mInstance==null){synchronized(DclSingleton.class){if(mInstance==null){                    mInstance =newDclSingleton();                }            }        }returnmInstance;    }}

原理浅析

将volatile修饰的变量转变成汇编代码,如下:

... lock addl $0x0,(%rsp)

通过查IA-32架构安全手册可知,Lock前缀指令在多核处理器会引发两件事。

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

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

解读 :

为了提高,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时再写回内存。如果对声明了volatile的变量进行写操作,JVM会向处理机发送一条Lock前缀指令,将这个变量所在的缓存行的数据写回到系统内存。

但是写会内存后,如果其他处理器缓存的值还是旧的,再执行计算操作就会出现问题。所以在多处理器下,为了保证各个处理器缓存是一致的,就会实现缓存一致性协议,如下图:

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的数据是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态。当处理器对这个数据进行操作的时候,就会重新从系统内存中把数据读到处理器缓存中。

文末福利:

想要了解更多并发编程知识点的,可以关注我一下,我后续也会整理更多关于并发编程这一块的知识点分享出来,另外顺便给大家推荐一个交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化以及并发编程这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多,以下学习资源都在群的共享区。

干货:Java并发编程系列之volatile(二)的更多相关文章

  1. Java并发编程系列之三十二:丢失的信号

    这里的丢失的信号是指线程必须等待一个已经为真的条件,在開始等待之前没有检查等待条件.这样的场景事实上挺好理解,假设一边烧水,一边看电视,那么在水烧开的时候.由于太投入而没有注意到水被烧开. 丢失的信号 ...

  2. Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  3. Java并发编程之三:volatile关键字解析 转载

    目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用>   volatile这个关键字可能很多朋友都听说过,或许也都用过 ...

  4. Java并发编程系列-(5) Java并发容器

    5 并发容器 5.1 Hashtable.HashMap.TreeMap.HashSet.LinkedHashMap 在介绍并发容器之前,先分析下普通的容器,以及相应的实现,方便后续的对比. Hash ...

  5. Java并发编程系列-(4) 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  6. Java并发编程系列-(1) 并发编程基础

    1.并发编程基础 1.1 基本概念 CPU核心与线程数关系 Java中通过多线程的手段来实现并发,对于单处理器机器上来讲,宏观上的多线程并行执行是通过CPU的调度来实现的,微观上CPU在某个时刻只会运 ...

  7. Java并发编程系列-(7) Java线程安全

    7. 线程安全 7.1 线程安全的定义 如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的. 类的线程安全表现为: 操作的原子性 内存的可见性 不 ...

  8. Java并发编程系列-(8) JMM和底层实现原理

    8. JMM和底层实现原理 8.1 线程间的通信与同步 线程之间的通信 线程的通信是指线程之间以何种机制来交换信息.在编程中,线程之间的通信机制有两种,共享内存和消息传递. 在共享内存的并发模型里,线 ...

  9. Java并发编程系列-(3) 原子操作与CAS

    3. 原子操作与CAS 3.1 原子操作 所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程. 为了实 ...

随机推荐

  1. 洛谷P1117 优秀的拆分【Hash】【字符串】【二分】【好难不会】

    题目描述 如果一个字符串可以被拆分为AABBAABB的形式,其中 A和 B是任意非空字符串,则我们称该字符串的这种拆分是优秀的. 例如,对于字符串aabaabaaaabaabaa,如果令 A=aabA ...

  2. poj3304 Segments【计算几何】

    C - Segments POJ - 3304 最近开始刷计算几何了 公式好多完全不会 数学不行 几何不行 记忆力不行 当机 查的题解 就当复习吧 这套专题拿来熟悉一下计算几何模板 #include ...

  3. TOP100summit:【分享实录】爆炸式增长的斗鱼架构平台的演进

    本篇文章内容来自2016年TOP100summit斗鱼数据平台部总监吴瑞城的案例分享. 编辑:Cynthia 吴瑞诚:斗鱼数据平台部总监 曾先后就职于淘宝.一号店. 从0到1搭建公司大数据平台.平台规 ...

  4. maven冲突问题

    通过配置文件解决问题: http://stamen.iteye.com/blog/2030552 1.用命令dependency:tree得到依赖关系 (或者加上Dincludes或者Dexclude ...

  5. Linux:获取当前进程的执行文件的绝对路径

    摘要:本文介绍Linux的应用程序和内核模块获取当前进程执行文件绝对路径的实现方法. 注意:使用此方法时,如果执行一个指向执行文件的链接文件,则获得的不是链接文件的绝对路径,而是执行文件的绝对路径. ...

  6. Linux的/etc/services文件的作用?

    4)端口分配 Linux系统的端口号的范围为0–65535,不同范围有不同的意义. 0 不使用 1--1023 系统保留,只能由root用户使用 1024---4999 由客户端程序自由分配 5000 ...

  7. ItunesConnect:上传完二进制文件后在构建版本中找不到

    最近经常遇到上传完二进制文件后在构建版本中找不到的情况: 环境:Xcode 8.2 (8C38) 大致有几种原因,可以按照以下步骤排查下. 排查步骤: 1.检查使用的权限,并info.plist文件中 ...

  8. Java中子类是否可以继承父类的static变量和方法而呈现多态特性

    静态方法 通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法,关于static方法,声明为static的方法有以下几条限制: 它们仅能调用其他的static 方法. 它 ...

  9. 【linux & &&命令】&后台(并行)命令 &&串行命令

    & 放在一个命令末尾,可以将这个命令放到后台执行.放到后台后主进程将继续向下执行,后台命令将与主进程并行执行. &&  放在一个命令末尾,与什么都没有单纯换行实际效果相同,等待 ...

  10. 几种常用CSS3样式

    在我们日常工作中,由于考虑到浏览器的兼容性,所以很少用CSS3样式.关于其标准,W3C 仍然在对 CSS3 规范进行开发.不过,现代浏览器已经实现了相当多的 CSS3 属性.最近学习了CSS3,发现功 ...