1. 前置知识
不同锁下的markword存储结构
2. 锁消除
JVM 在编译期间通过逃逸分析发现对象不会被多个线程访问,从而移除不必要的同步锁代码。锁消除不一定会发生,不可依赖。
3. 锁粗化
JVM 编译优化时发现一个对象的锁 被频繁连续加锁解锁,会将这些 小段代码合并,一次性加锁,从而避免反复加解锁的开销,这叫 锁粗化。
public class Main {
public static void main(String[] args) {
Object o = new Object();
for (int i = 0; i < 500; i++) {
synchronized (o) {//加锁释放锁500次
//操作
}
}
}
}
优化后变为
public class Main {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {//加锁释放锁1次
for (int i = 0; i < 500; i++) {
//操作
}
}
}
}
4. 锁升级
锁升级是 Java 虚拟机中为了提高并发性能,对 synchronized锁实现的一种分阶段优化机制。JVM 会根据 竞争程度自动在不同的锁状态之间进行升级。 JDK1.5,synchronized直接使用重量级锁,需要在用户态内核态之间切换,性能相对ReentrantLock较差。
1.5以后可以归类为四种锁
| 锁状态 | 适用场景 | 特点(性能从高到低) |
|---|---|---|
| 无锁 | 无并发访问 | 最高性能 |
| 偏向锁 | 单线程多次访问同一对象 | 加锁无需 CAS,线程指针存对象头 |
| 轻量级锁 | 多线程访问竞争很少 | 使用自旋 + CAS |
| 重量级锁 | 多线程并发 + 竞争激烈 | 使用操作系统互斥量(阻塞/唤醒) |
synchronized可以分为四个锁阶段
| 锁阶段 | 特点 |
|---|---|
| 无锁/匿名偏向锁 | 没有被作为锁资源,在一个线程获取资源时候,无锁升级为轻量级锁,匿名偏向升级为偏向锁 |
| 偏向锁 | 当前锁资源,只有一个线程获取,锁资源会存入线程指针,这个线程反复获取这把锁,当这个线程不获取这个锁资源,被其他线程获取该锁资源,那么就会更改存入的线程指针。在多个线程都获取资源时,锁会升级 |
| 轻量级锁 | 使用CAS并且自旋,JVM还会自适应自旋次数,超过自旋次数则升级为重量级锁 |
| 重量级锁 | 传统synchronized的方式,获取锁资源失败就将线程挂起 |
锁升级/降级路径:因为偏向锁和轻量级锁无法保存hashcode,所以这两种状态,在生成hashcode后,就会偏向锁会降为无锁,轻量级锁升级为重量级锁。
实验
import org.openjdk.jol.info.ClassLayout;
public class Main {
public static void main(String[] args){
Object o = new Object();
//无锁
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
// 主线程获取锁轻量级锁
System.out.println("主线程持有锁后对象内存布局:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();
// 重量级锁
System.out.println("调用hashCode()后对象内存布局:");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
效果