并发编程学习---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对象组成:
组成 | 元素 |
---|---|
Mark Word | 其内容是一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等 |
Class对象指针 | 其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址 |
对象实际数据 | 包括了对象的所有成员变量,其大小由各个成员变量的大小决定 |
对齐 | 最后一部分是对齐填充的字节,按8个字节填充 |
Mark Word的存储结构
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类)