synchronized锁原理优化

参考文章

  • Synchronized原理(轻量级锁篇)
  • 偏向锁、轻量级锁、重量级锁的理解 及适用场景

轻量级锁

轻量级锁的使用场景:

  • 如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

package cn.knightzz.principle;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: SynchronizedAdvancedPrinciples
 * @projectName hm-juc-codes
 * @description: Synchronized进阶原理
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-09 14:31
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.SynchronizedAdvancedPrinciples")
public class SynchronizedAdvancedPrinciples {

    private static final Object lock = new Object();

    public static void main(String[] args) {
        method01();
    }

    private static void method02() {

        // 同步块A
        synchronized (lock) {
            method02();
        }
    }

    private static void method01() {
        // 同步块B
        synchronized (lock) {
        }
    }
}

Mark Word

Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄 Age、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。

synchronized锁原理优化_第1张图片

流程

初始状态:

synchronized锁原理优化_第2张图片

线程获取锁后, 会将 Object Ref 指向锁对象, 并且会使用 CAS 交换 锁对象 Mark Word 存储的信息与 Lock Record 的地址信息

CAS交换操作是原子性的

synchronized锁原理优化_第3张图片

替换成功的结果如下 :

synchronized锁原理优化_第4张图片

CAS 替换失败的情况 :

如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

synchronized锁原理优化_第5张图片

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重

入计数减一

synchronized锁原理优化_第6张图片

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功

  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

synchronized锁原理优化_第7张图片

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有

竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

static Object obj = new Object();
public static void method1() {
 synchronized( obj ) {
 // 同步块
 }
}

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

synchronized锁原理优化_第8张图片

这时 Thread-1 加轻量级锁失败,进入**锁膨胀流程**

  1. 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  2. 然后自己进入 Monitor 的 EntryList BLOCKED

synchronized锁原理优化_第9张图片

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,

  • 成功, 说明解锁成功
  • 如果恢复失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

一句话总结

轻量级锁流程 :

  • 互换Lock Record 的地址 和 MarkWord的内容
  • Object Ref 指向锁地址

轻量级锁没办法处理不同线程竞争锁的问题

如果加锁失败, 说明别的线程把这个锁占了 就把轻量级锁升级称重量级

  • 申请一个 Monitor 对象
  • 让原本指向锁对象的Object Reference重新指向重量级锁对象 Monitor
  • 当前线程自己进入 Monitor 的 EntryList BLOCKED

自旋优化

自旋 = 重新申请锁

因为在重试的时候, 很有有可能之前占用锁的线程退出了

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步

块,释放了锁),这时当前线程就可以避免阻塞。

自旋重试成功的情况 :

synchronized锁原理优化_第10张图片

自旋重试失败的情况

synchronized锁原理优化_第11张图片

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时, 只有当前线程在使用,每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID (Thread id) 设置到对象的 Mark Word 头,之后发现

这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

代码案例

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: TestBiasedLock01
 * @projectName hm-juc-codes
 * @description: 测试偏向锁
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-13 16:14
 */
@SuppressWarnings("all")
public class TestBiasedLock01 {

    static Object lock = new Object();

    public static void main(String[] args) {
        m1();
    }

    private static void m1() {
        synchronized (lock){
            // 同步块 A
            m2();
        }
    }

    private static void m2() {
        synchronized (lock){
            // 同步块 B
            m3();
        }
    }

    private static void m3() {
        synchronized (lock){
            // 同步块 C
        }
    }

}

synchronized锁原理优化_第12张图片

每次调用都会生成锁记录去替换, 注意 这里替换是在同一线程, 不同线程的话涉及竞争关系

synchronized锁原理优化_第13张图片

如果是偏向锁, 第一次会将锁记录地址和 MarkdWord内容进行替换

后面的话会先检查ThreadID, 如果是同一ID, 就不会替换

偏向状态

基本概念

对象头格式

synchronized锁原理优化_第14张图片

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的

    thread、epoch、age 都为 0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 :

    -XX:BiasedLockingStartupDelay=0 来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、

    age 都为 0,第一次用到 hashcode 时才会赋值

  • 第一次使用 CAS 将线程 ID (Thread id) 设置到对象的 Mark Word 头

代码案例1

https://zhuanlan.zhihu.com/p/368505776

    		<dependency>
                <groupId>org.openjdk.jolgroupId>
                <artifactId>jol-coreartifactId>
                <version>0.10version>
            dependency>
package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock02")
public class TestBiasedLock02 {

    public static void main(String[] args) throws InterruptedException {


        ClassLayout classLayout = ClassLayout.parseClass(Dog.class);

        // 打印 Dog 对象 MarkWord信息
        // 初始时偏向锁没有生效 0x0000000000000001 (non-biasable; age: 0)
        log.debug(classLayout.toPrintable(new Dog()));

        // sleep(4)
        TimeUnit.SECONDS.sleep(4);
        log.debug(classLayout.toPrintable(new Dog()));
    }
}

class Dog {
}


synchronized锁原理优化_第15张图片

结果如上 : 初始时是 001 , 偏向锁还没有开启, sleep(4) 后, 偏向锁开启

如果想避免延迟,可以加 VM 参数 : -XX:BiasedLockingStartupDelay=0 来禁用延迟

synchronized锁原理优化_第16张图片

代码案例2

添加 VM 参数 : -XX:BiasedLockingStartupDelay=0 来禁用延迟

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock03")
public class TestBiasedLock03 {

    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(dog);

        Thread t1 = new Thread(() -> {

            log.debug("synchronized 前...");
            log.debug(classLayout.toPrintable());
            synchronized (dog) {
                log.debug("synchronized 中...");
                log.debug(classLayout.toPrintable());
            }
            log.debug("synchronized 后...");
            log.debug(classLayout.toPrintable());
        }, "t1");

        t1.start();
    }
}



第一次使用 CAS 将线程 ID (Thread id) 设置到对象的 Mark Word 头 , 处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

synchronized锁原理优化_第17张图片

禁用偏向锁

在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking 禁用偏向锁

处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

synchronized锁原理优化_第18张图片

执行结果如上

synchronized锁原理优化_第19张图片

可以看到上面的, 没有偏向锁了

测试 hashCode

正常状态对象一开始是没有 hashCode 的,第一次调用才生成, 然后生成的 哈希码会被填充到对象头里面, 调用hashCode , 偏向锁就会被清除

原因是 :

image-20220714135229260

如上图 : 调用 hashCode 后, 会将对象的hashCode填入 Markword中, 但是由于没有位置存储hashCode, 所以需要清除 thread id, epoch 这些信息 ?

synchronized锁原理优化_第20张图片

撤销 - 调用对象 hashCode

调用了对象的 hashCode,但**偏向锁的对象 MarkWord 中存储的是线程 id**,如果调用 hashCode 会导致偏向锁被

撤销 :

  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode

在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking

synchronized锁原理优化_第21张图片

撤销 - 其它线程使用对象

JOL 新版本依赖

<dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.16</version>
            </dependency>

当有其它线程使用偏向锁对象时,会将偏向锁升级为重量级锁

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @author 王天赐
 * @title: TestBiasedLock04
 * @projectName hm-juc-codes
 * @description: 偏向锁撤销
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-14 14:00
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock04")
public class TestBiasedLock04 {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(dog);


        Thread t1 = new Thread(() -> {
            log.debug(classLayout.toPrintable());
            synchronized (dog) {
                log.debug(classLayout.toPrintable());
            }
            log.debug(classLayout.toPrintable());

        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {

            log.debug(classLayout.toPrintable());
            synchronized (dog) {
                log.debug(classLayout.toPrintable());
            }
            log.debug(classLayout.toPrintable());

        }, "t2");
        t2.start();

    }
}

执行结果如下 :

synchronized锁原理优化_第22张图片

撤销 - 调用 wait/notify

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * @author 王天赐
 * @title: TestBiasedLock04
 * @projectName hm-juc-codes
 * @description: 偏向锁撤销
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-14 14:00
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock04")
public class TestBiasedLock05 {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(dog);


        Thread t1 = new Thread(() -> {
            log.debug(classLayout.toPrintable());
            synchronized (dog) {
                log.debug(classLayout.toPrintable());
            }
            log.debug(classLayout.toPrintable());

            synchronized (TestBiasedLock05.class) {
                // 通知 TestBiasedLock04.class 锁上的线程
                TestBiasedLock05.class.notify();
            }

        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {

            // 等待t1线程执行完唤醒
            synchronized (TestBiasedLock05.class) {
                try {
                    TestBiasedLock05.class.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            log.debug(classLayout.toPrintable());
            synchronized (dog) {
                log.debug(classLayout.toPrintable());
            }
            log.debug(classLayout.toPrintable());

        }, "t2");
        t2.start();

    }
}

在线程t1的 synchronized 内代码执行完后释放锁, 然后唤醒 t2线程 执行, 此时就形成交错

synchronized锁原理优化_第23张图片

可以看到, 因为不存在竞争的问题, 所以 轻量级锁没有升级成重量级锁

批量重偏向

如果锁对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

启动是添加配置 -XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;

/**
 * @author 王天赐
 * @title: TestBiasedLock04
 * @projectName hm-juc-codes
 * @description: 批量重偏向
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-14 14:00
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock06")
public class TestBiasedLock06 {
    public static void main(String[] args) throws InterruptedException {
        Vector<Dog> list = new Vector<>();

        Thread t1 = new Thread(() -> {

            for (int i = 0; i < 30; i++) {
                Dog dog = new Dog();
                list.add(dog);
                synchronized (dog){
                    log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                }
            }

            synchronized (list){
                list.notify();
            }


        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {

            synchronized (list){
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            for (int i = 0; i < 30; i++) {
                // 两个线程使用的是同一个锁, 所以会涉及到撤销偏向锁 => 升级成轻量级锁
                Dog dog = list.get(i);
                log.debug("===================================================");

                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                synchronized (dog){
                    log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                }
                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
            }

        }, "t2");
        t2.start();

    }
}

synchronized锁原理优化_第24张图片

可以看到初始时是开启了偏向锁的 : biased

synchronized锁原理优化_第25张图片

synchronized锁原理优化_第26张图片

当切换到t2线程后, 偏向锁被撤销 , 因为切换线程会撤销偏向锁

synchronized锁原理优化_第27张图片

由于阈值是 20 , 前19个还是轻量级锁, 第20个就变成了偏向锁, 开始偏向p2

批量撤销

当**撤销偏向锁**阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象

都会变为不可偏向的,新建的对象也是不可偏向的

启动时添加配置 -XX:BiasedLockingStartupDelay=0 关闭偏向锁延迟

package cn.knightzz.bais;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;
import java.util.concurrent.locks.LockSupport;

/**
 * @author 王天赐
 * @title: TestBiasedLock04
 * @projectName hm-juc-codes
 * @description: 批量撤销
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-14 14:00
 */
@SuppressWarnings("all")
@Slf4j(topic = "c.TestBiasedLock07")
public class TestBiasedLock07 {

    static Thread t1, t2, t3;

    public static void main(String[] args) throws InterruptedException {
        Vector<Dog> list = new Vector<>();

        int loopNumber = 39;

        t1 = new Thread(() -> {

            for (int i = 0; i < loopNumber; i++) {
                Dog dog = new Dog();
                list.add(dog);
                synchronized (dog) {
                    log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                }
            }

            // 释放许可
            LockSupport.unpark(t2);

        }, "t1");
        t1.start();

        t2 = new Thread(() -> {

            // 获取许可证
            LockSupport.park();

            for (int i = 0; i < loopNumber; i++) {
                // 两个线程使用的是同一个锁, 所以会涉及到撤销偏向锁 => 升级成轻量级锁
                Dog dog = list.get(i);
                log.debug("===================================================");

                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                synchronized (dog) {
                    log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                }
                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
            }

            // 执行完后释放许可证
            LockSupport.unpark(t3);

        }, "t2");
        t2.start();

        t3 = new Thread(() -> {

            LockSupport.park();
            log.debug("===================================================");
            for (int i = 0; i < loopNumber; i++) {
                // 两个线程使用的是同一个锁, 所以会涉及到撤销偏向锁 => 升级成轻量级锁
                Dog dog = list.get(i);
                log.debug("===================================================");

                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                synchronized (dog) {
                    log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
                }
                log.debug(i + "\n" + ClassLayout.parseInstance(dog).toPrintable());
            }

        }, "t3");
        t3.start();

        t3.join();

        log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}

执行结果如下

synchronized锁原理优化_第28张图片

锁消除

package cn.knightzz.benchmark;

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

/**
 * @author 王天赐
 * @title: MyBenchmark
 * @projectName hm-juc-codes
 * @description: 锁消除
 * @website http://knightzz.cn/
 * @github https://github.com/knightzz1998
 * @create: 2022-07-14 21:26
 */
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyLockBenchmark {

    static int x = 0;

    @Benchmark
    public void a() throws Exception {
        x++;
    }

    @Benchmark
    public void b() throws Exception {
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }
}

synchronized锁原理优化_第29张图片

可以看到上面 a方法未加锁, 但是 b方法加了锁, 但是二者评分一致, 说明耗时一致

原因是 : 代码在执行的时候, 会被 JIT即时编译器 去优化, 会优化掉一些没有必要的代码 :

 public void b() throws Exception {
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }

可以看到上面的代码, 因为 Object 对象是局部变量, 所以不可能会被共享, 那么 synchronized (o) 就不是必须的, 将会被JIT优化掉, 相当于没有加锁,

我们可以通过 java -XX:-EliminateLocks -jar benchmarks.jar 命令来关闭 JIT 优化

synchronized锁原理优化_第30张图片

可以看到执行结果, 关闭优化后, 性能消耗要增加很多

你可能感兴趣的:(并发编程系列,Java系列,jvm,synchronized)