同步器源码阅读准备知识:JMM、volatile、synchronized、分布式锁

第三周概念

一、Java内存模型(JMM)

全称:Java Memory Model

1、概述
  • Java模型规定了所有的变量都存储在主内存中(不是硬件的主内存,仅指虚拟机中的),出了主内存,每条线程还有自己的工作内存,工作内存保存着该线程使用到的变量的主内存副本的拷贝,线程对变量的操作都必须在工作内存中进行,包括读取和赋值,而不能直接读取主内存中的变量。不同线程之间也不能访问对方工作内存中的变量,线程之间传递必须通过主内存来完成。

    主内存、工作内存、线程关系图如下:

在这里插入图片描述

2、内存中的8种交互操作

主内存和工作内存之间的具体交互协议,Java内存模型地定义了8种具体的操作来完成

在这里插入图片描述

(1)、lock
  • 锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态;
(2)、unlock
  • 解锁,作用于主内存的变量,他把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定;
(3)、read
  • 读取,作用于主内存的变量,它把一个变量从主内存传输到工作内存中,以便后续的load操作使用;
(4)、load
  • 载入,作用于工作内存的变量,它把read操作从主内存得到的变量放入工作内存的变量副本中;
(5)、use
  • 使用,作用工作内存的变量,它把工作内存中的一个变量传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作;
(6)、assign
  • 赋值,作用于工作内存的变量,它把一个从执行引擎接受到的变量赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时使用这个操作;
(7)、store
  • 存储,使用于工作内存的变量,它把工作内存中一个变量的值传递到主内存中,以便后续的write操作使用
(8)、write
  • 写入,作用于主内存的变量,它把store操作从工作内存得到的变量的值放入到主内的变量中;
3、执行交互操作的8大基本规则

(1)、不允许read和load单独出来,不允许store和write单独出来,即不允许出现从主内存读取了而工作内存不接受,或者从工作内存回写了但主内存不接受的情况出现;

(2)、不允许一个线程丢弃它最近的assign操作,即变量在工作内存变化了必须把该变化同步回主内存;

(3)、不允许一个线程无原因地(即未发生过assign操作)把一个变量从工作内存同步回主内存;

(4)、一个新的变量必须在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load或assign)过的变量,换句话说就是对一个变量的use和store操作之前必须执行过load和assign操作;

(5)、一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁

(6)、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要从新执行load或assign操作初始化变量的值;

(7)、如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量;

(8)、对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行stroe和write操作;

注意,上面的lock和unlock是实现synchronized的基础,Java灭有把lock和unlock操作直接给用户,而是提供了两个更高层次的指令来隐式的使用者两个操作,即moniterenter和moniterexit

4、原子性、可见性、有序性

Java内存模型就是为了解决多线程环境下共享变量的一致性问题的。

一致性主要包含三大特性:原子性、可见性、有序性

(1)、原子性
  • 原子性是指一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是一个操作,也可以是多个操作。
  • 由Java内存模型来直接保证的原子性操作包括read、load、use、assign、store、write这两个操作,所以我们可以认为基本类型变量的读写是具备原子性的。
  • 如果应用需要一个更大范围的原子性,Java内存模型还提供了lock和unlock这两个操作来满足这个条件,尽管不能使用这两个操作,但是我们还可以使用他们的具体实现synchronized来实现,因此synchronized块之间的操作也是原子性的
(2)、可见性
  • 可见性,是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。
  • Java内存模型是通过在变更修改后同步回主内存,在变量读取前从主内存刷新变量值来实现的,它是依赖主内存的,无论是普通变量还是volatile变量都是如此。
  • 普通变量与volatile变量的主要区别是是否会在修改后立即同步回主内存,以及是否在每次读取前立即从主内存刷新。因此我们可以说volatile变量保证了多线程环境下变量的可见性,但是普通变量不能保证这一点。
  • 除了volatile之外,还有两个关键字也可以保证可见性,他们是synchronized和final。synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作”这条规则获取的。final的可见性是指被final修饰的字段在构造器中一旦被初始化完成,那么其它线程中就能看见这个final字段了。
(3)、有序性
  • Java程序中天然的有序性可以总结为一句话;如果在本线程中观察,所有的操作都是有序的;如果在另外一个线程中观察,所有的操作都是无序的。
  • 前半句是指线程内标新为串行的语义,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。
  • Java中给提供了volatile和synchronized两个关键字保证有序性。volatile天然就有有序性,因为其禁止重排序。synchronized的有序性是由 “一个变量同一时刻只允许一条线程对其进行lock操作” 这条规则获取的。
5、现行发生原则(Happens-Before)

从JDK 5开始,Java使用新的内存模型,使用happens-before的概念来阐述操作之间的内存可见性

如果Java内存模型的有序性都只依靠volatile和synchronized来完成,那么有一些曹组就会变得很啰嗦,但是我们在编写Java并发代码时并没有感受到,这是因为Java语音天然定义了一个 ”先行发生” 原则,这个原则非常重要,依靠这个原则我们可以很容易地判断在并发环境下两个操作是否可能存在竞争冲突问题。

先行发生,是指操作A先行发生于操作B,那么操作A产生的影响能够被操作B感知到,这种影响包括修改了共享内存中变量的值、发送了消息、调用了方法等。

Happens-Before语义:

​ 从JDK 5开始,Java使用新的内存模型,使用happens-before的概念来阐述操作之间的内存可见性。那到底什么是happens-before呢?

​ 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

Java内存模型定义的先行发生原则有哪些?

(1)、程序次序原则

  • 在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确的讲是控制流顺序而不是代码顺序,因为要考虑分支、循环等情况。

(2)、监视器锁定原则

  • 一个unlock操作先行发生于后面对同一个锁的lock操作。

(3)、volatile原则

  • 对一个volatile变量的写操作先行发生于后面对该变量的读操作。

(4)、线程启动原则

  • 对线程的start()操作先行发生于线程内的任何操作。

(5)、线程终止原则

  • 线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止。

(6)、线程中断原则

  • 对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupt()方法检测是否发生中断。

(7)、对象终结原则

  • 一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始,

(8)、传递性原则

  • 如果操作A先行操作B,操作B先行发生操作C,那么操作A先行发生于操作C。

这里的 “先行发生” 与 ”时间上的先发生” 没有必然的关系。

6、as-if-serial语义

​ 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)

  • 比如 A —> C , B ----> C
    A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
    
  • A —> B ----> C 或者 B ----> A ----> C
     as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器
    共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。asif-serial语义使 单线程 (因为在多线程环境下 重排序将会对程序造成影响,是结果不会是预期的)程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
    
7、JMM总结
  • 硬件内存架构使得我们必须建立内存模型来保证多线程环境下对共享内存的正确性;
  • java内存模型定义了保证多线程环境下共享变量一致性的规则;
  • Java内存模型提供了工作内存与内存交互的8大操作:lock、unlock、read、load、use、assign、store、write;及交互操作的使用规则;
  • Java内存模型对原子性、可见性、有序性提供了实现;
  • 先行发生的8大原则:程序次序原则、监视器锁定原则、volatile原则、线程启动原则、线程终止原则、线程中断原则、对象终结原则、传递性原则;先行发生不等于时间上的先发生;

二、volatile

1、概述

是Java虚拟机提供的最轻量级的同步机制了

语义一:可见性
  • Java内存模型规定,volatile变量的每次修改都必须立即回写到主内存中,volatile变脸搞得每次使用都必须从主内存刷新最新的值。
语义二:禁止重排序(有序)

​ java中有序性:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。

​ 前者是指线程内表现为串行的语义,后半句是指 “指令重排序” 现象和 “工作内存和主内存同步延迟” 现象。

  • volatile是禁止重排序的,他能保证安汇总代码顺序执行。
实现:内存屏障

​ volatile的重排序是怎么实现的呢?

​ 答案:内存屏障

  • 内存屏障的两个作用:
    • 阻止屏障两侧的指令重排序
    • 强制把写缓冲区/高速缓存中的数据回写到主内存,让缓存中相应数据失效。
2、场景
  • 运算的结果并不依赖于变量的当前值,或者能够保证只有单一的线程修改变量的值
  • 变量不需要与其他状态变量共同参与不变约束。
3、总结
  • 可以保证可见性
  • 可以保证有序性
  • 不可以保证原子性(见场景第一条前则)
  • 底层主要通过内存屏障来实现
  • 使用场景必须保证场景本身就是原子的。

三、synchronized

​ synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成monitorenter和monitorexit字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。

实现原理
  • 原子操作的lock和unlock没有直接给用户使用,而是提供了更高层次的字节指令monitorenter和monitorexit,而synchronized就是使用monitorenter和monitorexit两个指令来实现的

  • 根据JVM规范的要求,在执行monitorenter指令的时候首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorenter的时候会把计数器减1,当计数器减少为0时,锁就释放了

  • 测试代码

public class Test {
    public void sss() {
        synchronized (Test.class) {
            synchronized (Test.class) {

            }
        }
    }

    public static void main(String[] args) {
        new Test().sss();
    }
}
  • 字节码
Compiled from "Test.java"
public class com.enmo.dbaas.installv2.chain.factory.Test {
  public com.enmo.dbaas.installv2.chain.factory.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void sss();
    Code:
           // 加载常量池中的Test.clss类对象到操作数栈
       0: ldc   #2    // class com/enmo/dbaas/installv2/chain/factory/Test
           // 复制栈顶元素
       2: dup
           // 存储一个引用到本地变量 0 中,后面的 0 表示第几个变量
       3: astore_0
           // 调用monitorenter,它的参数变量0,也就是上面的Test.class类对象
       4: monitorenter
           // 再次加入常量池中的Test.clss类对象到操作数栈
       5: ldc   #2    // class com/enmo/dbaas/installv2/chain/factory/Test
           // 复制栈顶信息
       7: dup
           // 存储一个引用到本地变量 1 中
       8: astore_1
           // 再次调用monitorenter,它的参数是变量1,也还是Test.class类对象
       9: monitorenter
           // 从本地变量表中加载第 1 个变量
      10: aload_1
          // 调用monitorexit解锁,他的参数是上面加载的变量 1
      11: monitorexit
          // 调到第 20 行
      12: goto          20
      15: astore_2
      16: aload_1
      17: monitorexit
      18: aload_2
      19: athrow
          // 从本地变量表中加载第 0 个变量
      20: aload_0
          // 调用monitorexit解锁,它的参数是上面加载的变量 0
      21: monitorexit
          // 调到第30行
      22: goto          30
      25: astore_3
      26: aload_0
      27: monitorexit
      28: aload_3
      29: athrow
          // 方法返回,结束
      30: return
    Exception table:
       from    to  target type
          10    12    15   any
          15    18    15   any
           5    22    25   any
          25    29    25   any

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #3                  // Method sss:()V
       3: return
}
原子性、可见性、有序性

​ 内存模型主要就是来解决缓存一致性的问题的,而缓存一致性主要包括原子性、有序性、可见性。

​ synchronized关键字底层是通过monitorenter 和 monitorexit实现的,而这两个指令又是通过原子操作lock和unlock来实现的。

回顾内存模型JMM:

1、一个变量同一时刻只允许一个线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能解锁。

2、如果对一个变量执行lock操作,将会清空工作内存种此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

3、如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其他线程锁定的变量。

4、对一哥比那里执行unlock操作之前,必须先把此变量同步回主内存中,即执行sotre和write操作。

通过规则1,我们知道对于lock和unlock之间的代码,同一时刻只允许一个线程访问,所以synchronized是有序的。

通过规则1、2、4,我们知道每次lock和unlock时都会从主内存加载变量或把变量刷新回主内存,而lock和unlock之间的变量(这里是指锁定的变量)是不会被其它线程修改的,所以,synchronized是可见性的。

通过规则1、3,我们知道所有对变量的加锁都要排队进行,且其它线程不允许解锁当前线程锁定的对象,所以,synchronized是具有序性的。

所以,synchronized是可以保证有序性、可见性、原子性。

公平锁、非公平锁

1、synchronized是非公平锁

  • 开启多个线程,发起有序,但是执行顺序不是按照发起来的
synchronized锁优化

1、偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。

2、轻量级所,是指当锁偏向锁时,被另外一个线程锁访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。

3、重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量锁,重量级锁会使其他线程阻塞,性能降低。

总结

1、synchronized编译时会在同步块前后生成monitorenter和monitorexit字节码指令

2、monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以的;

3、monitorenter和monitorexit字节码指令底层是使用Java内存模型的lock和unlock指令

4、synchronized是可重入锁、非公平锁

5、synchronized可以保证 原子性、有序性、可见性

6、synchronized有三种状态:偏向锁、轻量锁、重量级锁

四、分布式锁

待补充

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值