Java1.5以后对synchronized做的优化

67 阅读2分钟

1. 前置知识

不同锁下的markword存储结构

a9af1903b60d0bb2f618837456c097e0.png

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后,就会偏向锁会降为无锁,轻量级锁升级为重量级锁。

image.png

实验

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());
        }
    }
}

效果

image.png