java并发编程2-Synchronized关键字底层实现原理详解

发布时间:2022-07-05 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了java并发编程2-Synchronized关键字底层实现原理详解脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

前言:

上一篇分析了Synchronized关键字的用法详解,本篇则对Synchronized关键字对底层实现原理进行详细分析。

Synchronized关键字是并发编程中的元老级别角色,因此被人们习惯性称为“重量级锁”,随着Java SE1.6的优化,Synchronized引入了偏向锁和轻量级锁,则显得没有那么重了。 

上文提到Synchronized共有三种用法,而锁的是每种用法对应的对象:

1.修饰非静态方法,锁的是当前对象

2.修饰静态方法,锁的是当前类的Class对象

3.修饰代码块,锁的是括号内传入的对象

那么Synchronized关键字到底是怎么将这些对象锁起来的呢?又是怎么解锁的呢?

一、锁的本质

Java中每个对象都会有一个MonITor对象与之关联,当获取到这个monitor对象后,与之对应的对象就被锁住。Synchronized关键字在修饰代码块时,编译之后,会在代码块指令的前后分别加上monitorenter和monitorexit指令,这两个指令的意思分别就是进入和退出Monitor对象。也就是说monitorenter就是获取锁,monitorexit就是释放锁。而修饰方法的时候是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。接下来就以一段代码为例,看下Synchronized编译之后的样子。

Demo代码如下:

 1 public class SynchronizedDemo {
 2 
 3     public static Integer i = 0;
 4 
 5     public  synchronized void test1(){
 6         System.out.PRintln("这是修饰非静态方法");
 7     }
 8 
 9 
10     public  synchronized static void test2(){
11         System.out.println("这是修饰静态方法");
12     }
13 
14     public void test3(){
15         synchronized (i){
16             System.out.println("这是修身代码块");
17         }
18     }
19 }

定义了三个方法,分别使用了三种用法的Synchronized关键字,接下来再看下编译后的字节码是什么样子的,如下:

 1 Compiled From "SynchronizedDemo.java"
 2 public class com.lucky.study.concurrent.SynchronizedDemo {
 3   public static java.lang.Integer i;
 4 
 5   public com.lucky.study.concurrent.SynchronizedDemo();
 6     Code:
 7        0: aload_0
 8        1: invokesPEcial #1                  // Method java/lang/Object."<init>":()V
 9        4: return
10 
11   public synchronized void test1();
12     Code:
13        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
14        3: ldc           #3                  // String 这是修饰非静态方法
15        5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16        8: return
17 
18   public static synchronized void test2();
19     Code:
20        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
21        3: ldc           #5                  // String 这是修饰静态方法
22        5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23        8: return
24 
25   public void test3();
26     Code:
27        0: getstatic     #6                  // Fiel
d i:Ljava/lang/Integer;
28        3: dup
29        4: aStore_1
30        5: monitorenter
31        6: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
32        9: ldc           #7                  // String 这是修身代码块
33       11: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34       14: aload_1
35       15: monitorexit
36       16: goto          24
37       19: astore_2
38       20: aload_1
39       21: monitorexit
40       22: aload_2
41       23: athrow
42       24: return
43     Exception table:
44        from    to  target type
45            6    16    19   any
46           19    22    19   any
47 
48   static {};
49     Code:
50        0: iconst_0
51        1: invokestatic  #8                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
52        4: putstatic     #6                  // Field i:Ljava/lang/Integer;
53        7: return
54 }

二、对象头和Monitor对象

在JVM中,每个对象在内存中主要分为三个部分:对象头、实例数据和填充数据,而Synchronized关键字使用的锁对象是存放在对象的对象头中。JVM会用2个字来存储对象头,如果对象是数组类型,则用3个字来存,多出的1个字用来单独存数组的长度。1个字宽相当于4个字节,相当于32bit。

长度 内容 备注
32/64bit Mark Word 存储对象的hashCode、锁信息和分代信息等
32/64bit Class Metadata Address 对象类型指针,指向当前对象是哪个类
32/64bit Array length 数组长度 (非数组对象则没有)

 对象头的默认存储结构如下:

锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
无锁状态 对象的hashcode 对象分代年龄 0 01

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构。

java并发编程2-Synchronized关键字底层实现原理详解

对象头除了存储Mark Word之外还会存储Klass pointer,也就是类的Class对象的指针。

Mark Word存储同步状态、标识、hashCode、GC状态等;Klass pointer指向对象的类元信息

以64位为例,各种情况下对象头的结构分别如下:

|--------------------------------------------------------------------------------------------------------------|
|                                              Object Header (128 bits)                                        |
|--------------------------------------------------------------------------------------------------------------|
|                        Mark Word (64 bits)                                    |      Klass Pointer (64 bits)    |       
|--------------------------------------------------------------------------------------------------------------|
|  unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  无锁
|----------------------------------------------------------------------|--------|------------------------------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 |     OOP to metadata object   |  偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_lock_record:62                            | lock:2 |     OOP to metadata object   |  轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
|                     ptr_to_heavyweight_monitor:62                    | lock:2 |     OOP to metadata object   |  重量锁
|----------------------------------------------------------------------|--------|------------------------------|
|                                                                      | lock:2 |     OOP to metadata object   |    GC
|--------------------------------------------------------------------------------------------------------------|
 
其中Mark Word 和 Klass Pointer分别占用64位存储对象数据

各个属性含义如下:

identify_hashCode:对象标识Hash码,采用延迟加载技。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中,占31位

age:对象GC分代年龄,占4位

thread:占54位,持有偏向锁的线程信息,和JVM分配的线程ID不是同一个概念

epoch:占2位,偏向时间戳

ptr_to_lock_record:占62位,指向栈中锁记录的指针

ptr_to_heavyweight_monitor:占62位,指向线程Monitor的指针

biased_lock:偏向锁标记,占1位,值为1是表示当前是偏向锁,值为0表示不是偏向锁

lock:锁状态标记,占2位,lock和biased_lock可以判断当前锁是什么类型,对应值如下

biased_lock lock 锁类型
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

 

使用JOL工具打印对象头信息

Maven依赖如下:

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

 

打印代码如下:

System.out.println(ClassLayout.parseInstance(SyncTest.class).toPrintable());

 

新建User类,包含一个Long userId字段,新建User类实例,打印对象头

public static void main(String[] args){
        User user = new User();
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }

 

结果如下:

# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.lucky.test.jvm.User object internals:
 OFFSET  SIZE             TYPE DESCRIPTION                               VALUE
      0     4                  (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                  (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                  (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4   java.lang.Long User.userId                               null
Instance Size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

 

第一行:00000001 依次表示 unused占1位0,age占4位0,biased_lock占1位0,lock占2位01,此时表示无锁状态

第三行:表示User.class对象的指针

第四行:表示User对象的userId属性,值为空

第五行:对象的大小,占用16个字节,其中Mark Word占用8个字节,Class Pointer经过压缩占用4个字节,long类型属性占有4个字节,共16个字节

 

tips:如果将User类的userId属性去掉会占有多个字节呢?会是12个字节码?答案也是16个字节

结果如下:

com.lucky.test.jvm.User object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 

虽然没有了userId字段,但是会进行字节填充4个字节,因为Java给对象分配内存是是以8个字节为单位的,所以如果对象实际占有不足8的倍数,就是进行字节填充到8的倍数,而对象头占有12个字节,所以即使是空对象也至少需要占有16个字节。

言归正传,当对这个对象添加偏向锁时,对象头部就会发生变化,结果如下:

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        User user = new User();
        synchronized (user) {
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    }

 

com.lucky.test.jvm.User object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 d0 00 1e (00000101 11010000 00000000 00011110) (503369733)
      4     4        (object header)                           e6 7f 00 00 (11100110 01111111 00000000 00000000) (32742)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 

第一行:00000101 表示biased_lock标记为1,lock标记为01,此时对象的状态为偏向锁,后面存储的是持有偏向锁的线程信息

 

轻量级锁

代码如下:

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        User user = new User();
        Thread thread = new Thread(new Runnable() {
            @override
            public void run() {
                synchronized (user) {
                    System.out.println("子线程偏向锁");
                    System.out.println(ClassLayout.parseInstance(user).toPrintable());
                }
            }
        });
        thread.start();
        thread.join();
        Thread.sleep(10000L);
        synchronized (user) {
            System.out.println("主线程轻量级锁");
            System.out.println(ClassLayout.parseInstance(user).toPrintable());
        }
    }

 

打印结果如下:

 1 子线程偏向锁
 2 # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
 3 com.lucky.test.jvm.User object internals:
 4  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
 5       0     4        (object header)                           05 e8 83 c0 (00000101 11101000 10000011 11000000) (-1065097211)
 6       4     4        (object header)                           c1 7f 00 00 (11000001 01111111 00000000 00000000) (32705)
 7       8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
 8      12     4        (loss due to the next object alignment)
 9 Instance size: 16 bytes
10 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11 
12 主线程轻量级锁
13 com.lucky.test.jvm.User object internals:
14  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
15       0     4        (object header)                           c8 98 ff 0b (11001000 10011000 11111111 00001011) (201300168)
16       4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
17       8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
18      12     4        (loss due to the next object alignment)
19 Instance size: 16 bytes
20 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 

可以看出子进程获取到锁的时候是偏向锁,主进程等子进程全部执行完成后获取锁就变成了轻量级锁,即使此时已经没有锁竞争情况了,还是会将锁升级为轻量级锁

重量级锁

代码如下:

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        User user = new User();
        Thread thread1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (user) {
                    System.out.println("子线程1获取锁");
                    System.out.println(ClassLayout.parseInstance(user).toPrintable());
                    Thread.sleep(2000L);
                    System.out.println(ClassLayout.parseInstance(user).toPrintable());
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (user) {
                    System.out.println("子线程2获取锁");
                    System.out.println(ClassLayout.parseInstance(user).toPrintable());
                }
            }
        });
        thread1.start();
        Thread.sleep(1000L);
        thread2.start();
    }

 

流程为线程1先获取锁,此时为偏向锁,然后线程2尝试获取锁和线程1存在竞争关系,将锁升级为重量级锁,所以线程1睡眠2000之后就会打印重量级锁信息,打印结果如下:

 1 子线程1获取锁
 2 # WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
 3 com.lucky.test.jvm.User object internals:
 4  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
 5       0     4        (object header)                           05 18 24 7b (00000101 00011000 00100100 01111011) (2065963013)
 6       4     4        (object header)                           a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
 7       8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
 8      12     4        (loss due to the next object alignment)
 9 Instance size: 16 bytes
10 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11 
12 com.lucky.test.jvm.User object internals:
13  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
14       0     4        (object header)                           ea 91 01 7d (11101010 10010001 00000001 01111101) (2097254890)
15       4     4        (object header)                           a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
16       8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
17      12     4        (loss due to the next object alignment)
18 Instance size: 16 bytes
19 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
20 
21 子线程2获取锁
22 com.lucky.test.jvm.User object internals:
23  OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
24       0     4        (object header)                           ea 91 01 7d (11101010 10010001 00000001 01111101) (2097254890)
25       4     4        (object header)                           a2 7f 00 00 (10100010 01111111 00000000 00000000) (32674)
26       8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
27      12     4        (loss due to the next object alignment)
28 Instance size: 16 bytes
29 Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 

线程1第一次打印为偏向锁,第二次打印和线程2打印的锁信息均是重量级锁。

三、JVM对Synchronized锁的优化

Java SE1.6为了减轻获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,也就有了从低到高为:无锁、偏向锁、轻量级锁和重量级锁四种锁的状态。锁可以从低到高升级,但是不能降级。

偏向锁:

大多数情况下,锁不存在多线程竞争,而是由同一个线程多次获得,而同一个线程对于同一个对象需要频繁的获取锁和释放锁,无疑是浪费了很多资,因此就有了偏向锁的引入。

偏向锁的核心思想是如果一个线程获取到了锁,那么锁就有了偏向性,对象头中的Mark Word就变为偏向锁结构。当这个线程再次获取锁的时候,就不需要再通过CAS操作来争取锁,而可以直接获取锁。也就省去了大量的申请锁的过程。

所以针对锁竞争不严重,经常由同一个线程获取锁的情况下,偏向锁有了很明显的优化效果。但是如果锁竞争比较严重的情况,每次获取锁的线程都是不同的情况,偏向锁则失去了意义,这时候偏向锁会进行升级,升级成轻量级锁。

偏向锁撤销:偏向锁一旦被一个线程获取之后,对象头中就会记录锁偏向的线程信息,即使线程执行完成且锁已经被释放了也不会清除对象头中的线程信息。而一旦有其他线程来竞争锁时,偏向锁就需要进行撤销。撤销的过程需要等待全局安全点,也就是当前持有偏向锁的线程在这个时间点上没有字节码在执行的时机。它会先暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否还存活,如果线程已经不存活了,那么就将对象头置为无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

轻量级锁:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

重量级锁:

通过对象内部监视器(monitor)实现,monitor本质前面也提到了是基于操作系统互斥(mutex)实现的,操作系统实现线程之间切换需要从用户态到内核态切换,成本非常高 

为什么只能升级而不能降级呢?

 因为一旦锁升级就表示存在锁竞争,而存在锁竞争的情况下,轻量级锁和偏向锁的效率并不高,偏向锁需要频繁修改对象头,轻量级锁需要一直自旋来尝试获取锁,都没有直接阻塞线程对CPU更加友好。

锁消除:

锁消除是指JVM会根据一个对象是否存在同步情况,如果不存在同步情况,那么就会去除对象的锁来提升性能,通常会通过对象逃逸分析来判断

锁粗化

锁粗化是指JVM会判断一系列操作是否都是对同一个对象进行加锁,如果是的话那么话将锁进行加粗处理,这样就避免来对同一个锁多次加锁解锁的操作
 

脚本宝典总结

以上是脚本宝典为你收集整理的java并发编程2-Synchronized关键字底层实现原理详解全部内容,希望文章能够帮你解决java并发编程2-Synchronized关键字底层实现原理详解所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。