Synchronized的锁升级过程是怎样的?

一、Synchronized的使用

Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

synchronized 关键字的使用方式主要有下面 3 种

  • 修饰实例方法

  • 修饰静态方法

  • 修饰代码块

1、修饰实例方法

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁

synchronized void method() {
    //业务代码
}


2、修饰静态方法

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

静态 synchronized 方法和非静态 synchronized 方法之间的调用不互斥

  • 如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。


3、修饰代码块

对括号里指定的对象/类加锁:

  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
synchronized(this) {
    //业务代码
}


4、总结:

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁;
  • synchronized 关键字加到实例方法上是给对象实例上锁;
  • 尽量不要使用 synchronized(String a) ,因为 JVM 中,字符串常量池具有缓存功能,多个线程使用相同的字符串值,实际使用的是同一个对象



二、Monitor

Java对象由三部分组成

  • 对象头
  • 对象体:对象体里放的是非静态的属性,也包括父类的所有非静态属性(private修饰的也在这里,不区分可见性修饰符),基本类型的属性存放的是具体的值,引用类型及数组类型存放的是引用指针。
  • 对齐填充

1、Java对象头

1.1 32 位虚拟机的对象头

普通对象

Mark Word :存储对象自身的运行时数据,hashCode、gc年龄以及锁信息等

Klass Word :指向Class对象

数组对象

相对于普通对象多了记录数组长度

所以对于一个int类型整数来说,它占用4字节,而一个Integer对象,在32位虚拟机中包含了8字节对象头,4字节数据,一共12字节,加上内存对齐,就是16字节



1.2 64位虚拟机的对象头

  • Markword:存储对象自身运行时数据如hashcode、gc分代年龄及锁信息等,64位系统总共占用8个字节。
  • 类型指针:对象指向类元数据地址的指针,jdk8默认开启指针压缩,64位系统占4个字节
  • 数组长度:若对象不是数组,则没有该部分,不分配空间大小,若是数组,则为4个字节长度



2、Mark Word 结构

32位虚拟机

64位虚拟机

  • 对象的hashCode占31位,重写类的hashCode方法返回int类型,只有在无锁情况下,在有调用的情况下会计算该值并写到对象头中,其他情况该值是空的。
  • 分代年龄占4位,最大值也就是15,在GC中,当survivor区中对象复制一次,年龄加1,默认是到15之后会移动到老年代。
  • 是否偏向锁占1位,无锁和偏向锁的最后两位都是01,使用这一位来标识区分是无锁还是偏向锁。
  • 锁标志位占2位,锁状态标记位,同是否偏向锁标志位标识对象处于什么锁状态。
  • 偏向线程ID占54位,只有偏向锁状态才有,这个ID是操作系统层面的线程唯一id,跟java中的线程id是不一致的


3、Moniter

Moniter称为监视器或者管程,是操作系统提供的对象

每个Java对象都可以关联一个Moniter对象,如果使用synchronized给对象上锁(重量级),该对象的Mark Word中就被设置指向Moniter对象的指针


Moniter结构

  • 刚开始Moniter中Owner为null
  • 当Thread-2执行synchronized(obj)后,就会将Moniter的所有者Owner置位Thread-2,Moniter只能有一个Owner
    • obj对象的MarkWord中最初保存的是对象的hashcode、gc年龄等信息,同时锁标志位为01,表示无锁。当获取锁后,会将这些信息保存在Moniter对象中,然后MarkWord存储的就是指向Moniter的指针,锁标志位为10(重量级锁)
  • 在Thread-2上锁的过程中,如果Thread-1、Thread-3也来执行synchronized(obj),就会进入EntryList,处于BLOCKED状态
  • Thread-2执行完同步代码块的内容后,唤醒EntryList中等待的线程来竞争锁,竞争是非公平的


4、Synchronized 字节码

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

对应的字节码为

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
              flags: ACC_PUBLIC, ACC_STATIC
              Code:
              stack=2, locals=3, args_size=1
              0: getstatic #2 // <- lock引用 (synchronized开始)
              3: dup
              4: astore_1 // lock引用 -> slot 1
              5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
              6: getstatic #3 // <- i
              9: iconst_1 // 准备常数 1
              10: iadd // +1
              11: putstatic #3 // -> i
              14: aload_1 // <- lock引用
              15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              16: goto 24
              19: astore_2 // e -> slot 2 
              20: aload_1 // <- lock引用
              21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              22: aload_2 // <- slot 2 (e)
              23: athrow // throw e
              24: return
              Exception table:
              from to target type
              6    16  19    any
              19   22  19    any
              LineNumberTable:
              line 8: 0
              line 9: 6
              line 10: 14
              line 11: 24
              LocalVariableTable:
              Start Length Slot Name Signature
              0     25     0    args [Ljava/lang/String;
              StackMapTable: number_of_entries = 2
              frame_type = 255 /* full_frame */
              offset_delta = 19
              locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
              frame_type = 250 /* chop */
              offset_delta = 4

0:拿到lock的引用

4:将lock的引用存储到 slot1 中

5:将lock对象的 MarkWord 置为 Monitor 指针,原本存储的信息就存储到 Monitor 中

6-11:执行counter++操作

14:从 slot1 中获取lock对象的引用

15:将 lock 对象的 MarkWord 重置,原本MarkWord 存储的是hashcode、gc年龄等信息,当 lock 获取锁后,将MarkWord 置位 Monitor 指针。重置就是将这些信息重新写到 MarkWord 中,同时唤醒 EntryList

16:goto 24 执行24行,退出

在Exception table中设置了监控异常的行数,如果6-16行有异常,就去执行19行

19:将异常信息 e 存储到slot2中

20:从 slot1 中获取lock对象的引用

21:将 lock对象 MarkWord 重置, 唤醒 EntryList

22:从 slot2 中获取异常信息

23:打印异常信息

注意

  • 通过异常 try-catch 机制,确保一定会被解锁
  • 方法级别的 synchronized 不会在字节码指令中有所体现


5、轻量级锁

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

轻量级锁对使用者是透明的,即语法仍然是 synchronized

如下,method1method2方法都对obj对象加锁

static final Object obj = new Object();

public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}

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

锁记录有两个属性

  • 锁记录地址,同时用00标记表示轻量级锁
  • Object referenct指向锁的对象

锁的对象obj中有对象头、对象体,对象头中Mark Word存储的是hashcode、gc年龄等信息

2、让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 ObjectMark Word,将 Mark Word 的值存入锁记录,然后将锁记录的信息存到ObjectMark Word

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

4、如果 cas 失败,有两种情况

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条 Lock Record 作为重入的计数
    • 此时锁记录中Object reference指向Object,但是由于ObjectMark World位置已经是00轻量级锁状态,因此这条锁记录存储为null

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

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

  • 成功,则解锁成功

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


6、锁膨胀

锁膨胀:轻量级锁升级为重量级锁

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


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

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

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

  • 为 Object 对象申请 Monitor 锁,让 ObjectMARK WORLD指向Moniter锁地址
  • 在堆区创建一个锁记录【Lock Record】对象,该对象包含了持有该锁的线程信息,然后ObjectMARK WORLD也会记录这个对象的地址
  • 然后 Thread-1进入 MonitorEntryList,处于BLOCKED状态

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


7、自旋优化

重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)


自旋重试成功的情况

自旋重试失败的情况

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

8、偏向锁

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

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

这里的线程id是操作系统赋予的id 和 Thread的id是不同的

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 ) {
        // 同步块 C
    }
}

没有开启偏向锁,会使用轻量级锁 ,每次重入都会执行CAS操作 开启偏向锁,每次锁重入仅判断当前ThreadID是否是自己


对象头格式

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0,不保存hashcode信息
  • 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

测试

利用 jol 第三方工具来查看对象头信息

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 仍存储于对象头中,也就是偏(心)向某个线程了


禁用偏向锁

禁用偏向锁后,创建对象后,最后3位是001,无锁状态。加锁后,变为000,轻量级锁,同时保存了锁记录地址。释放锁后,变回001无锁状态,同时清除锁记录地址

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

9、偏向锁的撤销

9.1 hashcode

Dog d = new Dog(); 后加上一句 d.hashCode();

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成
  • 调用了 hashCode() 后会撤销该对象的偏向锁
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

因为调用了hashcode(),但是默认是偏向锁,存储的是线程id,没有内存去存储hashcode,因此会撤销偏向锁,用来存储hashcode

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

9.2 其它线程使用对象

当有其它线程使用偏向锁对象时【没有发生锁竞争】,会将偏向锁升级为轻量级锁

private static void test2() 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));
        }
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        
        synchronized (TestBiased.class) {
            TestBiased.class.notify();
        }
    }, "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();
}


9.3 调用 wait/notify

重量级锁才支持 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

总结

  • 默认情况下,偏向锁是开启的,即这个锁归这个对象所拥有

  • 如果有其他线程获取锁或者调用hashcode,那么升级为轻量级锁

  • 如果发生锁竞争或者调用wait/notify,那么升级为重量级锁


10、批量重偏向、撤销

  • 对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 。当(某类型对象)撤销偏向锁超过阈值 20 次后,jvm 会在给(所有这种类型的状态为偏向锁的)对象加锁时重新偏向至新的加锁线程
  • 当撤销偏向锁阈值超过 40 次后,jvm 会将整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的
    • 例如:当前有40个锁对象,刚开始都偏向t1线程。现在t2线程获取这40个锁对象,1-19个锁对象会撤销偏向锁,第20个锁对象往后,会撤销t1的偏向锁,将偏向锁设置为t2【达到20阈值】。然后t3线程获取这40个锁对象,由于前19个锁对象已经是非偏向锁了,从第20个开始,又会撤销偏向锁,最后撤销次数达到40阈值后,会将所有的锁变为不可偏向的,即使新创建的对象也是不可偏向的。

演示批量重偏向

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] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
...
[t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - ===============> 
[t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101  // 原始轻量级锁偏向t1
[t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000  // t2获取锁,将锁升级为轻量级锁
[t2] - 0 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 // 第20次,初始轻量级锁偏向t1
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 // 第20次撤销锁,达到阈值,jvm将后边所有锁偏向t2
[t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
...
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 
[t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101

11、锁消除

锁消除 :JIT即时编译器会对字节码做进一步优化,下边代码中o是一个局部变量,不会共享,所以编译后,不会执行加锁操作,而是直接执行x++

public class MyBenchmark {
    static int x = 0;
    public void b() throws Exception {
        //这里的o是局部变量,不会被共享,JIT做热点代码优化时会做锁消除
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }
}
  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值