Java中锁的全面详解(深刻理解各种锁)

一.Monitor

1. Java对象头

以32位虚拟机位例

对于普通对象,其对象头的存储结构为

总长为64位,也就是8个字节, 存在两个部分

  • Kclass Word: 其实也就是表示我们这个对象属于什么类型,也就是哪个类的对象.
  • 而对于Mark Word.查看一下它的结构存储

64位虚拟机中

而对于数组对象,我们将会多出32位来存储数组的长度.

2. Monitor原理

Monitor也就是管程,监视器,其实也就是我们熟知的锁.

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针.(只有重量级锁才会关联Monitor)

也就是如下图所示:

  • 每个对象都拥有一把锁,假设此时锁对象为Object.class对象.此时Thread-2拿到锁,会将自己的Mark Word指向对应的Monitor.也就是像这样(JDK6以前为重量级锁).此时会将自己原有的,例如之前的年龄age,hashcode等存储到Monitor中.

  • 其他线程来抢占锁,突然发现锁的Monitor中的Owner属性不为null.则进入阻塞状态.
  • 等Thread-2中代码执行完毕,将锁释放.Monitor将会唤醒所有阻塞的线程,并把之前存储的原有Object.class中对象头中的属性重新赋值给Object.class.此时其他线程就可以来抢占锁.(不公平的抢占)

注意:

  1. synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  2. 不加 synchronized 的对象不会关联监视器,不遵从以上规则

字节码角度分析:

对于这样一段代码

我们查看对应的字节码指令,以来理解这个过程.

以上便是sybchronized过去的原理.

二.synchronized优化原理

而在从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,

如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了 synchronized的性能.

1. 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以 使用轻量级锁来优化。

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

如果 cas 失败,有两种情况

  1. 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

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

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

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

总的来说就是:

1. 进行加锁操作时,jvm会判断是否已经是重量级锁,如果不是,则会在当前线程

栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象MarkWord复制到该锁

记录中

2. 复制成功之后,jvm使用CAS操作将对象头MarkWord更新为指向锁记录的指针,

并将锁记录里的owner指针指向对象头的MarkWord。如果成功,则执行‘3’,否则

执行‘4’

3. 更新成功,则当前线程持有该对象锁,并且对象MarkWord锁标志设置为‘00’,

即表示此对象处于轻量级锁状态

4. 更新失败,jvm先检查对象MarkWord是否指向当前线程栈帧中的锁记录,如果是

则执行‘5’,否则执行‘6’

5. 表示锁重入;然后当前线程栈帧中增加一个锁记录第一部分(Displaced Mark

Word)为null,并指向Mark Word的锁对象,起到一个重入计数器的作用。

6. 表示该锁对象已经被其他线程抢占,则进行自旋等待(默认10次),等待次数

达到阈值仍未获取到锁,则升级为重量级锁(会由另一个线程为锁对象申请Monitor,也就是接下来的锁膨胀)

2.可重入锁

synchronized可重入的实现原理(优化过后)

  1. 锁对象与计数器
    • 每个Java对象都有一个与之关联的监视器(Monitor)。这个监视器内部维护了一个计数器(通常称为recursions变量),用于记录当前线程获取该对象锁的次数。
    • 当线程首次进入synchronized修饰的代码块或方法时,会尝试获取对象的锁。如果锁未被占用(即计数器为0),则线程获取锁并将计数器设置为1。(与之前再创造一个Lock Record不同)
  1. 重入机制
    • 如果当前线程已经持有了该对象的锁(即该线程已经执行了synchronized修饰的代码块或方法),当再次尝试进入另一个由相同对象锁保护的synchronized代码块或方法时,JVM会检查持有锁的线程是否为当前线程。
    • 如果是,则JVM允许该线程再次获取锁,并将计数器的值加1,而不是让线程等待锁被释放。这个过程就是synchronized的可重入性。
  1. 锁的释放
    • 当线程退出synchronized修饰的代码块或方法时,JVM会自动将对象锁的计数器减1
    • 如果计数器减至0,则表示当前线程已经释放了该对象的锁,其他等待获取该锁的线程将有机会获取锁并进入相应的synchronized代码块或方法。

2. 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁.

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

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

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

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

3. 自旋锁

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

自旋重试成功的情况

自旋重试失败的情况

注意:

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

4. 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有.可以理解为,偏向锁就是JVM认为这把锁就是属于一个线程

static final Object obj = new Object();
public static void m1() {
 synchronized( obj ) {
 // 同步块 A
 m2();
 }
}
public static void m2() {
 synchronized( obj ) {
 // 同步块 B
 m3();
 }
}
public static void m3() {
 synchronized( obj ) {
 }
}

4.1 偏向状态

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后3位,为101这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数

-XX:BiasedLockingStartupDelay=0 来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值

 

// 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 
public static void main(String[] args) throws IOException {
    Dog d = new Dog();
    ClassLayout classLayout = ClassLayout.parseInstance(d);
    new Thread(() -> {
        log.debug("synchronized 前");
        System.out.println(classLayout.toPrintableSimple(true));
        synchronized (d) {
            log.debug("synchronized 中");
            System.out.println(classLayout.toPrintableSimple(true));
        }
        log.debug("synchronized 后");
        System.out.println(classLayout.toPrintableSimple(true));
    }, "t1").start();
}

输出

11:08:58.117 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 
11:08:58.121 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101

注意:

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

测试禁用

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

输出

11:13:10.018 c.TestBiased [t1] - synchronized 前 
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
11:13:10.021 c.TestBiased [t1] - synchronized 中 
00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 
11:13:10.021 c.TestBiased [t1] - synchronized 后 
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
4.2 撤销 - 调用对象 hashCode

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

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

所以由于偏向锁没有存储空间来存储hashCode的值,只能撤销.(仔细查看偏量锁的对象头结构)

  • 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking(禁用默认偏向锁.)

输出

11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 
11:22:10.391 c.TestBiased [t1] - synchronized 前
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 
11:22:10.393 c.TestBiased [t1] - synchronized 中
00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 
11:22:10.393 c.TestBiased [t1] - synchronized 后
00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001
4.3 撤销 - 其它线程使用对象

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

private static void test2() throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    }
    synchronized (TestBiased.class) {
        TestBiased.class.notify();
    }
    // 如果不用 wait/notify 使用 join 必须打开下面的注释
    // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
    /*try {
     System.in.read();
     } catch (IOException e) {
     e.printStackTrace();
     }*/
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
    synchronized (TestBiased.class) {
        try {
            TestBiased.class.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    synchronized (d) {
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    }
    log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    }, "t2");
    t2.start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 
[t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 
[t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 
[t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
4.4 撤销 - 调用 wait/notify
public static void main(String[] args) throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            try {
                d.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (d) {
            log.debug("notify");
            d.notify();
        }
    }, "t2").start();
}

输出

[t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 
[t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 
[t2] - notify 
[t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010

4.5 批量重偏向

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

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得有可能偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程.

private static void test3() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 30; i++) {
        Dog d = new Dog();
        list.add(d);
        synchronized (d) {
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }
    synchronized (list) {
        list.notify();
    } 
    }, "t1");
    t1.start();

    Thread t2 = new Thread(() -> {
    synchronized (list) {
        try {
            list.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    log.debug("===============> ");
    for (int i = 0; i < 30; i++) {
        Dog d = list.get(i);
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
    }
    }, "t2");
    t2.start();
}

输出(只取对应了的部分)

[t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101


[t2] - ===============> (注意到19次的变化,没有撤销)
[t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 
[t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101
4.6 批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的.(第二个循环擦除了20次,第三个循环擦除了20次.这两个循环前20次都是擦除,然后锁重偏向,后19次都在锁偏向,即不会擦除)

private static void test4() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 39;
    t1 = new Thread(() -> {
    for (int i = 0; i < loopNumber; i++) {
        Dog d = new Dog();
        list.add(d);
        synchronized (d) {
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }
    LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
    LockSupport.park();
    log.debug("===============> ");
    for (int i = 0; i < loopNumber; i++) {
        Dog d = list.get(i);
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
    }
    LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
    LockSupport.park();
    log.debug("===============> ");
    for (int i = 0; i < loopNumber; i++) {
        Dog d = list.get(i);
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
    }
    }, "t3");
    t3.start();
    t3.join();
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}

5. 锁消除

锁消除,Java虚拟机中的即时编译器会自动帮我们优化,不存在锁共享的情况会直接将锁擦除.

  • 26
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值