Optimus_7

博客园 首页 新随笔 联系 订阅 管理

1. JMM规定CPU执行的(线程执行的)一些交互操作(应该并不是指令名称,只是抽象动作概念):

    每条指令都是原子的(指令内部的操作们粘在一起的,不可分开的,要么都执行要么都不执行)

    (JMM规定每条指令都是原子的,但是对double和long的操作除外)

 

    lock:作用于主内存的变量, 将该变量被唯一的线程独占。对于一个变量而言,会变成单核CPU。

    unlock:作用于主内存的变量,与lock相反。

 

    read:作用于主内存的变量,将变量的值拿在手里。

    load:作用于工作内存的变量,将read操作拿着的变量的值,放入工作内存的变量的拷贝空间中。

 

    store:作用于工作内存的变量,将变量的值拿在手里。

    write:作用于主内存的变量。将store操作操作拿着的变量的值,放入主内存中。

 

    use:作用于工作内存的变量,将变量提供给CPU。

    assign:作用于工作内存的变量,接收CPU的指令,将一个值赋值给变量。

 

    JMM对以上指令的规定:

      1. read-load和store-write,必须成对出现,且每对内部按顺序执行。(但是可以不连续执行)

      2. 如果一个线程执行了assign,则后面必须store-write。即 assign+(store-write)

      3. 如果一个线程没执行assign,则不允许store-write。即 no assign,no(store-write)

      4. 一个线程使用变量(执行use或者store-write)之前,这个变量必须load,(如果load之后变量为null的话还需要assign初始化)

      5. 一个变量只能被同一个线程lock,但是可以lock多次。记得unlock时也要unlock那么多次。

      6. 一个线程执行lock变量,会清空这个变量的在工作内存的值,所以需要lock之后执行load和assign。

      7. 一个线程只能unlock当前线程已经lock了的变量。

      8. 一个线程执行unlock,之前必须store-write同步会主内存。unlock ‘before = (store-write)

 

    JMM允许对double和long的操作可以不需要满足原子性:

      因为double和long是64位,又因为32位CPU的缓存行是32位,所以有时不可能实现原子性。

      所以JMM允许double和long的操作可以不需要满足原子性。

      但是现在的商用JVM都已经把double和long的操作实现为原子了。

2. Volatile

  2.1 Volatile语义1:(可见性语义) 

    Volatile变量改变时会直接写入主内存。但是只保证可见性,并不保证原子性。

    意思是,只保证数据显示的值是最新的,但是不保证我用这条数据的时候别人不用,如果大家都用,可能会造成某些人的操作被覆盖而看不出来。

    例子:以下场景不适合用Volatile:

    新建20个线程,对同一个Volatile变量1万次++,但是结果小于20万。

    解决方式:加锁 或者 CAS的AtomInterger

  2.2 Volatile语义1的推论:

    满足以下两个条件时,适合用Volatile。如果不满足的话还是用Synchronized或者JUC吧:

    1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。(场景比如:不并发进行++)

    2. 变量不需要与其他的状态变量共同参与不变约束。(场景比如:用Volatile变量作为开关(且只用这一个Volatile变量),控制某个方法是否执行)

  2.3 Volatile语义2:(有序性语义)

    禁止指令重排序。

    例子:以下场景适合用Volatile:

      Volatile+DCL单例模式。

  2.4 Volatile的实现方式为LOCK指令,相当于内存屏障:

    当变量为Volatile,在改变Volatile变量的值以后,比普通变量时多出一条汇编代码 ,注意是汇编代码而不是字节码。

    lock addl $0x0,(%esp)

    这句话的关键是lock,

    加入了lock前缀之后就变成了如下操作:

    1. 对CPU总线加锁(不过后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大)

    2. 这时其他CPU的读写请求都会被阻塞,直到锁释放。

    3. 当前CPU改变当前高速缓存中的Volatile变量的值,强制做了一次store+write操作,写入主内存。

    4. 锁释放,同时清空其他CPU的相应的缓存行(也有人说是设置为无效Invalid)。

    5. 当其他CPU读取那个Volatile变量时,发现空行或者无效,那怎么办呢?根据MESI,会从主存获取。  

    

    使语义1生效的原因:

      即操作12345

    使语义2生效的原因:

      即操作12345,其实整个操作相当于在写Volatile变量加了全屏障。

 

    ps.

      Volatile只可以修饰成员变量(静态不静态都可以)但是不可以修饰局部变量(idea不允许)。因为局部变量只存活于方法栈中(工作内存中)所以不存在主存可见性的问题。

      加了Volatile的成员变量的字节码的区别是:变量多了一个ACC_VOLATILE的flag而已。

    

    public class test {
        static int i;
        // static Volatile int i;
        void hy () {
            i = 20;
        }
    }

    

 

3. 内存屏障

  作用:

    1. 保证数据的可见性

    2. 防止指令之间的重排序

  x86的内存屏障分为三类及其作用:

    内存屏障其实是Intel提供的硬件指令:sfence (Store Barrier)、lfence(Load Barrier)、mfence (Full Barrier)

    Intel还提供了一个lock指令前缀,这个前缀是专门用于加在指令(比如add)之前的,表示当前指令操作的内存只能由当前CPU使用,而且自带Full Barrior效果;(就是Volatile的实现)

      1. 读屏障Load Barrier(lfence + 内存地址):先使缓存行失效,然后触发强制从主存获取数据的动作。

        保证的是,Load Barrier之前的load和之后的load不会被重排序。

      2. 写屏障Store Barrier(sfence + 内存地址):触发强制写入主存的动作,对其他CPU可见。

        保证的是,Store Barrier之前的store和之后的store不会被重排序。

      3. 全屏障 Full Barrier(mfence + 内存地址):强制从主存读取以及强制向主存写。

        保证的是,Full Barrier上面的不能下去,同时,Full Barrier下面的不能上去。

        (全屏障是一个原子效果,并不是等于写屏障+读屏障,因为写屏障+读屏障这个组合也可能出现重排(因为x86允许Store-Load 重排序))

   X86下仅支持一种指令重排:Store-Load ,

    即读操作可能会重排到写操作前面,同时不同线程的写操作并没有保证全局可见,例子见《Intel® 64 and IA-32 Architectures Software Developer’s Manual》手册8.6.1、8.2.3.7节。

    要注意的是这个问题只能用mfence(或者是Lock前缀)解决,不能靠组合sfence和lfence解决。

    (用sfence+lfence组合仅可以解决重排问题,但不能解决全局可见性问题,简单理解不如视为sfence和lfence本身也能乱序重拍)

   JMM的内存屏障分为四类:

    Java编译器会在适当位置插入以下内存屏障来禁止重排序

    1. LoadLoad Barrier:相当于x86的 Load Barrier

    2. LoadStore Barrier:相当于x86的读屏障+写屏障的原子组合。

    3. StoreLoad Barrier:相当于x86的 Full Barrier(即写屏障+读屏障的原子组合)

    4. StoreStore Barrier:相当于x86的 Store Barrier

    

 

4. Final

  如果final修饰一个基本数据类型,表示该基本数据类型的值一旦在初始化后便不能发生变化;

  如果final修饰一个引用类型,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。

  (其实本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。)

  final修饰的属性必须要初始化后才能返回这个类的实例对象,如果不初始化赋值会编译失败。可以在变量声明的时候初始化 + 也可以在构造函数中对这个变量赋初值。

 

  Final的实现内存屏障:

  1. 对某个类的final域的初始化后,加入一个Store-load屏障,然后才能使用这个对象。(如果不加屏障就可能会重排序了,就会可能拿到的对象的final域是没有初始化的)(盲猜也是Lock前缀,全屏障)

  2. (没明白,可以不用说)初次读这个对象的final域之前,加入一个load-Store屏障。(如果不加屏障就可能会重排序了,就会可能拿到的对象的final域是没有初始化的)

 

  题外话,需要注意Final的使用方法: 

    Final的成员变量的注意事项:

     1. 

      该成员变量必须在创建对象之前进行赋值,否则编译失败。

      即定义成员变量的时候手动赋值 或者 利用构造器对成员变量进行赋值

     2. 

      final变量是属于对象的,意思是同一个类的两个对象的各自final变量可以是不相同的。

5. 硬件层面需要解决两个问题之一:缓存一致性问题:(CPU之间横向层面,多核操作同一变量的问题)

  问题产生原因:CPU1缓存与CPU2缓存之间数据不同步的问题。

  解决方式1(不推荐):通过在总线加LOCK#锁的方式(因为会变成单核CPU,不推荐)

  解决方式2(采用):MESI(缓存一致性协议)+Store Buffer(缓冲区)+Invalidate Queue

 

  MESI协议:

    Store Buffer & Invalidate Queue为MESI 提供了异步解决方案,强调的是异步。

 

    Store Buffer:读写时的更高级缓存

    Invalidate Queue:而当一个CPU核收到Invalid消息时,会把消息写入自身的Invalidate Queue中,随后异步将其设为Invalid状态(具体什么时候未知)。

    Invalid状态:非法

    Share状态 :正在被共享

    Exclusive状态:正在被独占

    Modified状态:缓存行已被修改,但是还未写入主存。

 

    CPU向缓存写数据时,

    先写入Store-Buffer,

    如果该缓存行是Invalid,则从主存中获取并刷新缓存先,再把自己缓存行设置为Share。

    如果该缓存行是Share ,就把其他CPU的这条缓存行都设置为Invalid,然后自己缓存行变为Exclusive。

    如果该缓存行是Exclusive,就把该缓存行设置为Modified,写入缓存行,然后异步写入主存(具体什么时候未知),然后把自己缓存行设置为Exclusive。

    如果该缓存行是Modified,就先等待异步写入主存后(具体什么时候未知),再变为Exclusive先。

 

    CPU从缓存读数据时,

    先扫描Store-Buffer,如果没找到就去找缓存,

    如果该缓存行是Invalid,则从主存中获取并刷新缓存,变为Share 。

    如果该缓存行是Share ,则直接读。

    如果该缓存行是Exclusive ,则直接读。

    如果该缓存行是Modified,需要等待变成Exclusive才可以读。

 

    缺点1:当一个CPU核收到Invalid消息时,会把消息写入自身的Invalidate Queue中,随后异步将其设为Invalid状态(具体什么时候未知)。这个期间可能就会发生读取数据的操作,而此时的数据是脏数据。

    缺点2:当CPU向缓存写数据时,在Modified之后&Exclusive之前,异步写入主存(具体什么时候未知),而此时别人可能从主存读取了脏数据。

 

6. 硬件层面需要解决两个问题之二: 指令重排问题:(时间纵向层面,多核操作同一变量的问题)

  问题原因之编译器重排:

    由于编译器只需要满足JMM的as-if-serial规则,

    即,在单线程下不能改变结果。

    所以默认允许了指令重排序。

    但是会导致多线程下出现问题。

  问题原因之处理器重排:

    为了优化CPU运算效率,CPU在保证运算结果不变的情况下,允许指令重排。

    X86默认只允许Store-Load形式的指令重排。不允许Load-Load,Load-Store,Store-Load形式的指令重排。

    但是仍会导致多线程下出现问题。

 

  解决方式1:Volatile语义2,即内存屏障。(见上文)

    应用举例:Volatile+DCL

 

  解决方式2:Final语义。(见上文)

 

  解决方式3:Synchronized(lock同理)

                但是有局限性。

    synchronized(lock同理)只是把多线程执行一个块(lock的trycatch块)变为对于单线程执行一个块(lock的trycatch块)。

    synchronized(lock同理)保证的是线程1块对线程2的同一个块(lock的trycatch块)不重排,但是块内部的逻辑就不能保证不重排了。

    

  解决方式4:java自带的happenbefore原则

 

7. 硬件层面需要解决两个问题之总结:

  MESI(CPU之间横向层面,多核操作同一变量的问题,解决多核操作同一变量的问题)+内存屏障(时间纵向层面,解决多核操作同一变量的问题)组成了x86的解决方式。

  其中MESI是必然发生的,而内存屏障是可以我们手动加上的(Volatile)。

  Synchronized和Lock是JVM的解决方式

8. JMM层面要求代码需要满足三个特性之一:原子性

  解决方式1:Synchronized修饰(lock同理)

    每一个synchronized块(lock的trycatch块),都是一整个原子操作。

  解决方式2:CAS

    cmpxchg一个指令完成了比较和交换两个操作。

  解决方式3:AQS(内部使用了Volatile+CAS维护State的改变+同步队列的节点状态、节点前后、头尾节点等)

9. JMM层面要求代码需要满足三个特性之二:可见性

  解决方式1:Volatile

    Volatile语义1.(见上文)

  解决方式2:Synchronized(lock同理)

    但是有局限性。

    synchronized(lock同理)只是把多线程执行一个块(lock的trycatch块)变为对于单线程执行一个块(lock的trycatch块)

    synchronized(lock同理)保证的是线程1块对线程2的同一个块可见。不能保证块内的第一行对第二行可见。

  解决方式3:final

    final语义(见上文)

  解决方式4:AQS(内部使用了Volatile+CAS维护State的改变+同步队列的节点状态、节点前后、头尾节点等)

10. JMM层面要求代码需要满足三个特性之三:有序性

  解决方式1:Volatile语义2,即内存屏障。(见上文)

    应用举例:Volatile+DCL

  解决方式2:Final语义。(见上文)

  解决方式3:Synchronized(lock同理)

                但是有局限性。

    synchronized(lock同理)只是把多线程执行一个块(lock的trycatch块)变为对于单线程执行一个块(lock的trycatch块)。

    synchronized(lock同理)保证的是线程1块对线程2的同一个块(lock的trycatch块)不重排,但是块内部的逻辑就不能保证不重排了。

  解决方式4:java自带的happens-before原则

  解决方式5:AQS(内部使用了Volatile+CAS维护State的改变+同步队列的节点状态、节点前后、头尾节点等)

 

11. Happens-Before规则: 

  为了实现有序性,我们可以使用Volatile或Synchronized或final,但是这样对程序员不友好,因为代码会很烦琐。为了辅助Volatile或Synchronized或final,所以Java语言自带了Happens-Before规则:。

 

  Happens-Before规则的意思是,java天生自带了某些情况下的两个操作的前后可见,

  即假如A happens- before B,则A对于B是可见的。但是并不表示A必须在B之前执行。

  意思是,可以重排,但是不能影响我们两者的有序性,所以其实也一定程度地约束了不允许重排。

  至于Java是怎么保证AB有序性的(或者叫A对于B的可见性),盲猜是使用了JMM的四种内存屏障吧。

 

  程序顺序规则:在一个线程内,

    前:前面的操作,后:后面的操作。

  Synchronize规则:对于一个monitor锁,

    前:unlock,后:lock。

  Volatile规则:对于一个 volatile变量,

    前:写,后:读。

  线程启动规则:对于一个Thread对象,

    前:Thread.start,后:Thread的内部方法被调用。

  线程中断规则:对于一个Thread对象,

    前:Thread.interrupt,后:interrupt被检测到

  线程终止规则:对于一个Thread对象t1,还有一个线程t2在t1内部运行t2.join,

    前:t2的所有操作,后:t2.join结束后,t1恢复

  对象终结原则:对于一个对象,

    前:对象初始化,后:对象执行finalize

  传递性:对于操作A先于操作B,并且,操作B先于操作C,则操作A先于操作C

  

 

 

posted on 2020-08-10 20:44  Optimus_7  阅读(617)  评论(0编辑  收藏  举报