并发编程学习---Java并发机制的底层实现原理

目录

  • 序言
  • volatile
    • 介绍
    • 如何确保可见性
    • 举例
  • synchronized
    • 应用方式
    • 实现原理
    • 锁存在哪里,存储什么信息
    • 锁的升级和比对
  • 原子操作实现原理
    • cpu层面
    • java层面
    • CAS实现原子操作存在三个问题

序言:

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令

1. volatile

1.1 介绍:

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度

1.2 如何确保可见性

java代码

volatile Singleton instance = new Singleton();

通过JIT编译器转为汇编代码

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

Lock前缀的指令在多核处理器下会引发了两件事情

  • 将当前处理器缓存行的数据写回到系统内存
  • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
    cpu使用MESI(修改、独占、共享、无效)协议维护内部缓存和其他处理器缓存的一致性

1.3 举例

public class VolatileFeaturesExample {
    // 使用volatile声明64位的long型变量
    volatile long vl = 0L;
    public void set(long l) {
        // 单个volatile变量的写
        vl = l;
    }
    public void getAndIncrement () {
        // 复合(多个)volatile变量的读/写
        vl++;
    }
    public long get() {
        // 单个volatile变量的读
        return vl; 
    }
}

等价于

public class VolatileFeaturesExample {
    // 64位的long型普通变量
    long vl = 0L;

    // 对单个的普通变量的写用同一个锁同步
    public synchronized void set(long l) {
        vl = l;
    }

    // 普通方法调用
    public void getAndIncrement() {
        // 调用已同步的读方法
        long temp = get();
        // 普通写操作
        temp += 1L;
        // 调用已同步的写方法
        set(temp);
    }

    // 对单个的普通变量的读用同一个锁同步
    public synchronized long get() {
        return vl;
    }
}

只保证可见性和防止重排序,不保证原子性

2. synchronized

2.1 应用方式

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是Synchonized括号里配置的对象

2.2 实现原理

从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁

2.3 锁存在哪里,存储什么信息

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁,那么到底存在哪里呢?锁里面会存储什么信息呢?

java对象组成:
并发编程学习---Java并发机制的底层实现原理

组成 元素
Mark Word 其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等
Class对象指针 其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址
对象实际数据 包括了对象的所有成员变量,其大小由各个成员变量的大小决定
对齐 最后一部分是对齐填充的字节,按8个字节填充

Mark Word的存储结构

并发编程学习---Java并发机制的底层实现原理

2.4 锁的升级和比对

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

不同锁的优缺点比较

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间。同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量。同步块执行速度较长

不同锁状态Mark work标识符

锁状态 标识符 标志位 存储内容
未锁定 0 01 hash code(31),年龄(4)
偏向锁 1 01 线程ID(54),时间戳(2),年龄(4)
轻量级锁 00 栈中锁记录的指针(64)
重量级锁 10 monitor的指针(64)
GC标记 11 空,不需要记录信息

3. 原子操作实现原理

3.1 cpu层面

  • 使用总线锁
  • 使用缓存锁(MESI)

3.2 java层面

  • 锁(偏向锁、轻量级锁、互斥锁)
    出了偏向锁,其他底层都是使用了循环cas实现
  • 使用循环CAS实现原子操作

3.3 CAS实现原子操作存在三个问题

  • ABA问题(解决方案:添加版本号)
  • 循环时间长开销大(解决方案:pause指令)
  • 只能保证一个共享变量的的原子操作(解决方案:使用AtomicReference类)