`

从volatile关键字谈下并发问题的个人理解

 
阅读更多
 

    先来看一篇详细分析volatile语义的文章。

    http://www.infoq.com/cn/articles/java-memory-model-4

 

    若问及volatile关键字的含义,一般都会得到如下答案。

    (1)对volatile修饰的变量,各线程间的写操作将立即对其他线程可见。

    (2)volatile修饰的变量的操作是原子性的,并且如果volatile的赋值操作依赖于他的前一个值,则会失去原子性。 

 

      其实,(1)有着更深层次的含义,即对可见性的理解。(2)volatile本身和原子性并无直接关系,(2)中所说的原子性其实并不是真正意义上的原子性,其实是可见性的另一种表现形式。

  

      那何为可见性问题呢,对于同一段代码,执行线程和观察线程所见到的执行顺序不一定是一致的。听起来有点拗口,看如下例子。

   乱序执行导致的可见性问题

         以下文伪代码为例,解释了何谓乱序执行导致的可见性问题。

          线程1:

                       Resource resource=init();

                       boolean hasInit=true;

                       afterOperation();

          线程2:

                      if(hasInit){

                        getResource();

                       more operation;

                       }

          上述程序粗看没问题,但当线程2中hasInit=true时,resource一定初始化好了吗。答案是否定的。

          JAVA为了执行效率,代码在执行时候会有一定程度上的指令乱序。但对同一个线程内的程序来说,乱

序执行不影响每一句代码对之前执行的代码的可见性。对上述程序来说,当线程1中的afterOperation()获取到flag值为true,resource一定是初始化完成了的。 但对线程2来说,resource有可能未初始化完成时,就获取了为true的flag值。

         回到之前volatile的语义,如下:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

        简单来说,当上述的flag变量设置为volatile时,对线程2来说,resource的初始化一定是在volatile的写之前执行。即禁止flag的写语句重排到定义的位置之前。

       那对线程2来说,线程1中的afterOperation()被线程2观察到时,是否flag一定已经是true了呢?答案是否定的。具体原因么,可参考volatile的内存含义的第二条。即需要volatile的读来禁止这种情况。

      以上便是文章开头所说的 “可见性”的真正所指。另外,乱序执行是对编译后的命令来说的,一句JAVA命令被编译后的执行命令,也可以被乱序执行。这在下文提到的单例模型中会讲到。

     顺便插一句,如何快速地判断多线程情况下的代码可见性呢,实际应用中可参考happens-before原则

 

   以上其实便是可见性问题。实际应用中,除了可见性问题,更常见的便是原子性问题

 

   原子性操作被破坏

        先从最简单的变量自增操作说起。众所周知,JAVA的自增操作不是原子性的。一句JAVA命令,会被编译器分解为多条命令执行。以自增操作为例 ,  a++  执行时的操作时的字节码如下

       public void getNext(); Code: 0:   aload_0 //加载局部变量表index为0的变量,在这里是this
       1:   dup                 //将当前栈顶的对象引用复制一份
       2:   getfield        #2; //Field id:I,获取id的值,并将其值压入栈顶
       5:   iconst_1            //将int型的值1压入栈顶
       6:   iadd                //将栈顶两个int类型的元素相加,并将其值压入栈顶
       7:   putfield        #2; //Field id:I,将栈顶的值赋值给id
       10:  return }

     若在执行自增操作的时候,同时有其他线程对同一变量a进行写操作或者读操作,则操作的对象可能是执行过程中状态异常的变量,并导致获取期望之外的结果。 对于具体的自增操作的多线程测试,网上很多。

      原子性的概念,简单来说,就是进行一些列操作的时候,操作中的动作要么不执行,要么全部执行完后才让执行结果对其他线程可见。

      上述概念一样可以运用于多条语句,举例来说,在一个方法中需要执行如下write操作。

       boolean flag=true;

       Date date=new Date();

      这两个变量的状态必须一起更新,若当flag=true执行完毕后就立刻被其他线程读取,则其他线程中可能会获得错误的date变量。即上文说的“执行过程中状态异常的变量”。

     解决方式,便是上述write操作和read操作用同一个锁进行synchronized。这样可以保证,在write操作时,不正确的值不会被其他线程读取。

 

   原子性问题有时会和可见性问题同时出现,典型代表便是著名的基于双加锁的单例模式。

  

private Resource resource;
public static Resource getInstance(){  
        if( resource == null ){   //(1)   
        synchronized(lock){      
             if( resource == null ){    
                   resource = new Resource();  //(4) 
             }  
         }  
     } 

 

 

     该写法中的resource变量为竞争资源,其中标注为4的语句可能会引起失败。原因就在于resource=new Resouce()这句话至少包含2个步骤,一是是调用resource对象的构造函数并初始化,二是将新分配的Resource对象的内存引用赋值给resource变量。 当另一个线程中执行语句1时,由于并未将resource设置为volatile,则可能导致获取到不为空但尚未初始化完成的resource对象。这就是上文所说的乱序执行导致的问题。

     

  

       总结:      

               并发问题主要分为资源竞争时原子操作的被破坏、和多线程情况下乱序执行导致的线程可见性。

              凡是在并发过程中遇到上述两种问题,都需要考虑解决方式。

              常见的解决方式是通过加锁来实现。锁的获取也具有volatile读的内存语义,锁的释放具有volatile写的内存语义。如果功力不够,在多线程编程时则少用volatile变量来解决并发问题。用锁则稳妥些(效率牺牲)。

           

 

 

 

 

分享到:
评论

相关推荐

    Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解...只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁

    详细分析java并发之volatile关键字

    主要介绍了java并发之volatile关键字的的相关资料,文中代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下

    Java并发编程volatile关键字的作用

    主要介绍了Java并发编程volatile关键字的作用,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下

    java并发编程理论基础精讲

    本资源为您提供了关于 Java ...volatile 关键字: 解释 volatile 关键字的作用,探讨如何使用 volatile 实现线程间的可见性和有序性。 通过这份资源,将为您提供实用的指导,帮助您构建更稳定、可靠的多线程应用程序。

    Java 关键字 volatile 的理解与正确使用

    本文主要介绍 volatile 的使用准则,以及使用过程中需注意的地方,感兴趣的朋友一起看看吧

    Java 多线程编程面试集锦20道问题解答Java多线程编程高难度面试题及解析

    在当今高并发的应用场景下,对多线程编程的理解和应用是评估面试者的重要指标。通过这些高难度问题,您将全面掌握Java多线程编程的核心概念、技术和最佳实践。 每个问题都包含了深入的答案解析,涵盖了多线程编程的...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第二阶段05讲、一个解释volatile关键字作用最好的例子.mp4 │ 高并发编程第二阶段06讲、Java内存模型以及CPU缓存不一致问题的引入.mp4 │ 高并发编程第二阶段07讲、CPU以及CPU缓存的结构,解决高速...

    java并发编程

    , 这里,读者将通过使用java.lang.thread类、synchronized和volatile关键字,以及wait、notify和notifyall方法,学习如何初始化、控制和协调并发操作。此外,本书还提供了有关并发编程的全方位的详细内容,例如限制...

    93个netty高并发教学视频下载.txt

    83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析;84_Netty引用计数注意事项与内存泄露检测方式;85_Netty编解码器剖析与入站出站处理器详解;86_Netty自定义编解码器与TCP粘包拆包问题;87_Netty编解码...

    深入理解Java并发之synchronized实现原理.docx

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当...

    精通并发与netty视频教程(2018)视频教程

    83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析 84_Netty引用计数注意事项与内存泄露检测方式 85_Netty编解码器剖析与入站出站处理器详解 86_Netty自定义编解码器与TCP粘包拆包问题 87_Netty编解码器...

    在线 Java 硕士加薪课程 Term-05 班 (10.68G)

    1.Day01-jdk8新特性 ---【课时1】01-JDK8简介.mp4 ---【课时2】02-JDK8接口新特性应用.mp4 ---【课时3】03-JDK8中lambda表达式应用.mp4 ---【课时4】04-JDK8中的方法...---【课时6】06-volatile关键字应用分析.mp4 -

    精通并发与netty 无加密视频

    第83讲:AtomicIntegerFieldUpdater实例演练与volatile关键字分析 第84讲:Netty引用计数注意事项与内存泄露检测方式 第85讲:Netty编解码器剖析与入站出站处理器详解 第86讲:Netty自定义编解码器与TCP粘包拆包...

    Java面试题.docx

    49、synchronized 和volatile 关键字的区别 51-58题 51、ReentrantLock 、synchronized和volatile比较 53、死锁的四个必要条件? 56、什么是线程池,如何使用? 56、什么是线程池,如何使用? 58、有三个线程T1...

    精通并发与 netty 视频教程(2018)视频教程

    Netty引用计数的实现机制与自旋锁的使用技巧 82_Netty引用计数原子更新揭秘与AtomicIntegerFieldUpdater深度剖析 83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析 84_Netty引用计数注意事项与内存泄露...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    【集合】HashMap在并发场景下的问题和解决方案 67 多线程put后可能导致get死循环 67 多线程put的时候可能导致元素丢失 68 解决方案 68 【集合】ConcurrentHashMap的get(),put(),又是如何实现的?ConcurrentHashMap...

    c#学习笔记.txt

    volatile指示字段可由操作系统、硬件或并发执行的线程等在程序中进行修改。 9,语句 语句是程序指令。除非特别说明,语句都按顺序执行。C# 具有下列类别的语句。 类别C# 关键字 选择语句if, else, switch, case 迭代...

    javaSE代码实例

    16.6 volatile关键字的含义与使用 372 16.7 小结 373 第17章 高级线程开发 374 17.1 线程池的使用 374 17.1.1 线程池的基本思想 374 17.1.2 JavaSE 5.0中固定尺寸线程池的基本知识 374 17.1.3 自定义...

    宋劲彬的嵌入式C语言一站式编程

    5. 深入理解函数 1. return语句 2. 增量式开发 3. 递归 6. 循环语句 1. while语句 2. do/while语句 3. for语句 4. break和continue语句 5. 嵌套循环 6. goto语句和标号 7. 结构体 1. 复合类型与结构体 2. 数据抽象 3...

Global site tag (gtag.js) - Google Analytics