线程通信和线程同步  

        并发编程的两个核心问题是线程通信和线程同步,其中线程通信指线程之间以何种机制交换信息。常见的通信机制有两种:共享内存(线程之间共享公共状态,通过读-写公共状态来隐式通信)、消息传递(线程之间通过发送信息来显示通信),java采用共享内存的通行机制。同步指控制不同线程之间操作发生相对顺序(互斥)的机制。本篇主要从java的内存模型角度分析java底层如何保证内存可见性从而确保线程之间能够正常通行。

 java 内存模型(JMM)         

        java线程之间的通讯由JMM控制,JMM决定一个线程对共享变量的写入何时对其他线程可见。

 happens-before关系             

     定义:如果A 操作happends-before B操作,那么A操作的结果将对B操作可见。

    判断方法

            1、 一个线程中的每个操作 happens-before 于该线程中的任意后续操作。

            2、对一个监视器的解锁 happens-before 于随后对该监视器(同一个锁)的加锁。

            3、对一个监视器的解锁 happens-before 于随后对该监视器(同一个锁)的加锁。

            4、如果A happens-before B 且 B happends-before C ,那么Ahappens-before C。

 重排序                                          

       重排序分编译器重排序和处理器重排序,编译器重排序是指编译器在不改变单线程程序语义的情况下,可以改变语句的执行顺序,达到优化的目的。处理器重排序是指如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序,达到指令并行执行的目的。  

       重排序引发多线程的内存可见性问题,JMM会禁止特定类型的编译器重排序,同时在java编译器在生成的指令序列的适当位置插入内存屏障以此来禁止特定类型的处理器从排序。           

 顺序一致性内存模型对比JMM

       顺序一致性模型是一个理想的参考模型,提供了极强的内存可见性保证,JMM参考这个模型。

       在顺序一致性模型中,一个线程中的所有操作必须按照程序的流程来执行,不管同步与否,所有线程只能看到一个单一的操作执行顺序,JMM中无此保证,JMM对于同步程序有顺序一致性的效果,未同步程序在JMM中不但整体的执行顺序是无序的,而且所有线程看到的操作顺序也可能不一致,例如:线程A 执行 a=10操作(将值写到本地缓存),在将其刷新到主存之前,此操作只对线程A可见,在其他线程看来,因为看不到a的新值,所以就以为线程A并没有执行此操作。JMM对未同步的程序只提供最小安全性,线程执行时读取的值要么是某个线程写入的值,要么是默认值(0,null,false),不会出现一些无中生有的值。

        顺序一致性模型保证对所有的内存读/写都具有原子性。JMM不保证对64位的long 、double变量的读/写操作具有原子性。从jsr-133内存模型开始(jdk5),仅仅只允许把一个64位long/double型变量的写操作拆分成两个32位的写操作来执行,任意的读操作在jsr-133中都具有原子性。

 volatile                                         

        volatile具有可见性,对一个volatile变量的读,总是能看到对这个volatile变量最后的写入。同时volaile具有原子性,对任意单个volatile变量的读/写具有原子性(对long、double类型很有意义,对v++这种复合操作不具原子性)。

        volatile 的写-读与锁的释放-获取有相同的内存效果(写==释放锁 ,读==获取锁)。

        volatile 读/写的内存语义:当写一个volatile变量时,JMM会把对应的本地内存中的共享变量刷新到主存,实际是向接下来将要读取这个变量的某个线程发出消息。当读一个voltaile变量时,JMM会把该线程对应的本地缓存置为无效,线程接下来将从主存中读取共享变量。实际是接收到了之前某个线程发出的消息。因此volatile读/写实际是两个线程之间通过主存进行通信。

       为了实现volatile的内存语义,JMM针对编译器制定了以下3条重排序规则,同时为了实现规则,JMM采取了如图1所示的内存屏障插入策略。

               1、当第二个操作是volatile写时,不管第一个操作(volatile操作,普通操作)是什么,都不能重排序。

               2、当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。

               3、当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

消息或者邮件的未读小红点 未读条数 java怎么实现 java未读消息原理_java内存模型

            图1

 final域                                        

       对于final域,编译器和处理器遵守两条重排序规则:1、在构造函数内对一个final域的写入与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作不能重排序。2、初次读一个包含final域的对象的引用与随后初次读这个final域,这两个操作之间不能重排序。JMM禁止把final域的写重排序到构造函数之外,由此可保证在对象引用为任意线程可见之前,对象final域已经被正确初始化了(普通域不具有这个保障),当然前提条件是final引用不能从构造函数内“溢出”(对象未构造完成之前就将其引用暴露出去)。编译器会在final域写之后,构造函数返回之前插入StoreStore屏障,由此便确保了final域的写不会重排序到构造函数之外。通过为final域增加读/写重排序规则,可以为java程序员提供初始化安全保证:只要对象是正确初始化的(被构造对象的引用在s构造函数中没“溢出”),那么不需要同步就可以保证任意线程都能看到final域在构造函数中被初始化的值。