活像个孤独患者自我拉扯,外向的孤独患者有何不可?
鸽了一段时间,继续开更。
1.同步器的存在意义
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是: 对象、变量、文件等。由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。两种同步器的本质都是加锁实现互斥访问。
加锁目的:
序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问) 不过有一点需要区别的是:
当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的 私有栈中,因此不具有共享性,不会导致线程安全问题。
2.synchronized底层原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码 块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、 偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置 与结束位置。(如下图)
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
2.1什么是monitor?
可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象 是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把 看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的 是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于 HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成 ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当 前线程,同时monitor中的计数器count加1;
2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒;
3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式 获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须 在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问 数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
2.2Monitor监视器锁
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和 MonitorExit指令来实现。(可以这么理解,每当new Object(),这个object都有一个monitor来监视这个object)
1.monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下:
a:如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者;
b:如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
c:如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
2.monitorexit:执行monitorexit的线程必须是object所对应的monitor的所有者。
指令执行时,monitor的进入数减 1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。(_count被减到0,标致着当前获取monitor的线程退出,)
其他被这个monitor阻塞的线程可以尝试去 获取这个 monitor 的所有权。(别个阻塞队列[_EntryList]里面的线程出列尝试去获取锁)
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来 完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则 会抛出java.lang.IllegalMonitorStateException的异常的原因。
2.3从字节码分析synchronized关键字
我们来看下面代码:
public class SynchronizedClass { public synchronized void method() { System.out.println("Hello World!"); } public static void main(String[] args) { new SynchronizedClass().method(); } }
执行javap -v,生成的字节码如下:
public class com.yg.edu.SynchronizedClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #8.#23 // java/lang/Object."<init>":()V #2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #26 // Hello World! #4 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #29 // com/yg/edu/SynchronizedClass #6 = Methodref #5.#23 // com/yg/edu/SynchronizedClass."<init>":()V #7 = Methodref #5.#30 // com/yg/edu/SynchronizedClass.method:()V #8 = Class #31 // java/lang/Object #9 = Utf8 <init> #10 = Utf8 ()V #11 = Utf8 Code #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 this #15 = Utf8 Lcom/yg/edu/SynchronizedClass; #16 = Utf8 method #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 args #20 = Utf8 [Ljava/lang/String; #21 = Utf8 SourceFile #22 = Utf8 SynchronizedClass.java #23 = NameAndType #9:#10 // "<init>":()V #24 = Class #32 // java/lang/System #25 = NameAndType #33:#34 // out:Ljava/io/PrintStream; #26 = Utf8 Hello World! #27 = Class #35 // java/io/PrintStream #28 = NameAndType #36:#37 // println:(Ljava/lang/String;)V #29 = Utf8 com/yg/edu/SynchronizedClass #30 = NameAndType #16:#10 // method:()V #31 = Utf8 java/lang/Object #32 = Utf8 java/lang/System #33 = Utf8 out #34 = Utf8 Ljava/io/PrintStream; #35 = Utf8 java/io/PrintStream #36 = Utf8 println #37 = Utf8 (Ljava/lang/String;)V { public com.yg.edu.SynchronizedClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 18: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/yg/edu/SynchronizedClass; public synchronized void method(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 21: 0 line 22: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/yg/edu/SynchronizedClass; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: new #5 // class com/yg/edu/SynchronizedClass 3: dup 4: invokespecial #6 // Method "<init>":()V 7: invokevirtual #7 // Method method:()V 10: return LineNumberTable: line 25: 0 line 26: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 args [Ljava/lang/String; } SourceFile: "SynchronizedClass.java"
从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的: 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通 过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切 换,对性能有较大影响。
从方法上加锁来看,实际上jvm底层操作monitor是通过 ACC_PUBLIC指令和ACC_SYNCHRONIZED来实现synchronized关键字的语意。
我们在看加锁在方法内的同步代码块:
public class SynchronizedClass { private static Object object = new Object(); public void method() { synchronized (object) { System.out.println("Hello World!"); } } public static void main(String[] args) { new SynchronizedClass().method(); } }
执行javap -v,生成的字节码如下:
public class com.yg.edu.SynchronizedClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #9.#31 // java/lang/Object."<init>":()V #2 = Fieldref #6.#32 // com/yg/edu/SynchronizedClass.object:Ljava/lang/Object; #3 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream; #4 = String #35 // Hello World! #5 = Methodref #36.#37 // java/io/PrintStream.println:(Ljava/lang/String;)V #6 = Class #38 // com/yg/edu/SynchronizedClass #7 = Methodref #6.#31 // com/yg/edu/SynchronizedClass."<init>":()V #8 = Methodref #6.#39 // com/yg/edu/SynchronizedClass.method:()V #9 = Class #40 // java/lang/Object #10 = Utf8 object #11 = Utf8 Ljava/lang/Object; #12 = Utf8 <init> #13 = Utf8 ()V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 LocalVariableTable #17 = Utf8 this #18 = Utf8 Lcom/yg/edu/SynchronizedClass; #19 = Utf8 method #20 = Utf8 StackMapTable #21 = Class #38 // com/yg/edu/SynchronizedClass #22 = Class #40 // java/lang/Object #23 = Class #41 // java/lang/Throwable #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Utf8 args #27 = Utf8 [Ljava/lang/String; #28 = Utf8 <clinit> #29 = Utf8 SourceFile #30 = Utf8 SynchronizedClass.java #31 = NameAndType #12:#13 // "<init>":()V #32 = NameAndType #10:#11 // object:Ljava/lang/Object; #33 = Class #42 // java/lang/System #34 = NameAndType #43:#44 // out:Ljava/io/PrintStream; #35 = Utf8 Hello World! #36 = Class #45 // java/io/PrintStream #37 = NameAndType #46:#47 // println:(Ljava/lang/String;)V #38 = Utf8 com/yg/edu/SynchronizedClass #39 = NameAndType #19:#13 // method:()V #40 = Utf8 java/lang/Object #41 = Utf8 java/lang/Throwable #42 = Utf8 java/lang/System #43 = Utf8 out #44 = Utf8 Ljava/io/PrintStream; #45 = Utf8 java/io/PrintStream #46 = Utf8 println #47 = Utf8 (Ljava/lang/String;)V { public com.yg.edu.SynchronizedClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 18: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/yg/edu/SynchronizedClass; public void method(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // Field object:Ljava/lang/Object; 3: dup 4: astore_1 5: monitorenter 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #4 // String Hello World! 11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: aload_1 15: monitorexit 16: goto 24 19: astore_2 20: aload_1 21: monitorexit 22: aload_2 23: athrow 24: return Exception table: from to target type 6 16 19 any 19 22 19 any LineNumberTable: line 23: 0 line 24: 6 line 25: 14 line 26: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 this Lcom/yg/edu/SynchronizedClass; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 19 locals = [ class com/yg/edu/SynchronizedClass, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: new #6 // class com/yg/edu/SynchronizedClass 3: dup 4: invokespecial #7 // Method "<init>":()V 7: invokevirtual #8 // Method method:()V 10: return LineNumberTable: line 29: 0 line 30: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 args [Ljava/lang/String; static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: new #9 // class java/lang/Object 3: dup 4: invokespecial #1 // Method java/lang/Object."<init>":()V 7: putstatic #2 // Field object:Ljava/lang/Object; 10: return LineNumberTable: line 20: 0 } SourceFile: "SynchronizedClass.java"
monitorexit,指令如果会出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;(同步块指令码上是monitorenter和monitorexit)
3.对象的内存布局
通过上面描述,我们已经知道synchronized关键字加锁是加在对象上面,对象是如何记录锁状态的呢?我们这里需要引入一个概念:对象的内存布局。
对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象) 等。Java对象头一般占有2个机器码(在32位虚拟机中,
1个机器码等于4字节也就是32bit,在64位虚拟机中,1个机器码 是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,
因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
实例数据:存放类的属性数据信息,包括父类的属性信息。
对齐填充:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。(但是对齐可以大大提高程序效率,对于会被生成很多次对象需要做这个操作)
对象组成我们看下图:
我们重点来关注对象头里面的markword,里面是我们对象锁的锁状态的记录区域:
由于64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的,我们来看32位虚拟机的markword分布。(32位虚拟机markword大小为4byte,32bit)
我们先来看看对象的对象头信息,使用opjdk提供的工具包:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
来看看下面代码:
public static void main(String[] args) throws InterruptedException { // TimeUnit.SECONDS.sleep(5); Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); // synchronized (o){ // System.out.println(ClassLayout.parseInstance(o).toPrintable()); // } }
执行一下,控制台输出:
java.lang.Object 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) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
这里有3行,我们找第一行markword,括号里面的32位数字,里面就是markword的标志位:00000001 00000000 00000000 00000000
但是我们这么看最后两位是00,对应的是轻量级锁,但是这个时候对象并没有被加锁。那是因为我们的操作系统分为大端模式和小端模式,我们一般的计算机都是小端模式,需要把高位的放到左侧去。
实际上我们打印的markword是00000000 00000000 00000000 00000001,我们看后3位是001,这就对应了上面表中的无锁状态。
我们在看一个拓展的问题,这个时候为什么hashcode没有打印呢?那是因为Object.hashCode方法类似于spring里面的懒加载,调用的时候对象的markword里面才会有对象的hashcode信息,
具体可以参考这篇文章:https://www.jianshu.com/p/be943b4958f4
然后我们继续,把代码的同步块位置注释掉:
public static void main(String[] args) throws InterruptedException {
// TimeUnit.SECONDS.sleep(5);
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
然后继续执行程序,观察对象的锁状态,控制台输出:
java.lang.Object 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) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) e8 f6 f9 02 (11101000 11110110 11111001 00000010) (49936104) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们来观察同步块里面的32位markword,11101000 11110110 11111001 00000010;转换以后是00000010 11111001 11110110 11101000,后面两位是00,对应的是轻量级锁状态,
这个就有点不对了,因为现在没有别个线程去竞争这个o对象,讲道理应该是偏向锁;这个是因为jvm默认会去延迟加载偏向锁,大概是4s左右,(这块是jvm启动的时候会有些许线程,
核心包里面的一些类里面也有synchronized同步块,多个线程竞争肯定是会从无锁升级到偏向锁再到轻量级锁,在往后;所以jvm延迟了偏向锁的加载,启动的时候直接让这些类从无锁到
轻量级锁,加快jvm的加载效率)。
我们给程序暂停5s,第一行注释放开:
public static void main(String[] args) throws InterruptedException { TimeUnit.SECONDS.sleep(5); Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }
继续运行程序,控制台输出:
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 40 19 03 (00000101 01000000 00011001 00000011) (51986437) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们继续来观察同步块里面的32位markword,11100101 00000001 00000000 00100000;转换以后是00100000 00000000 00000001 11100101,后面三位是101,对应的是偏向状态,
对应的线程id前面23位是00100000 00000000 0000000ok,没有问题
但是,我们在回过头来看上面本来应该是无锁状态的时候,我们再来看这个markword,00000101 00000000 00000000 00000000;转换以后是00000000 00000000 00000000 00000101 ;
本来应该是无锁状态的o,这个时候确实偏向锁状态,这又是为什么呢?
我们引入一个概念,无锁状态对象的匿名偏向,开启偏向锁之后,新的对象就会是偏向锁状态,但是我们看前面23位,00000000 00000000 0000000,却是没有任何的线程id记录(此时
对象是处于一种可偏向的状态)
下面来看这一段代码:
public static void main(String[] args) { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } Object o = new Object(); log.info(ClassLayout.parseInstance(o).toPrintable()); new Thread(()->{ synchronized (o){ log.info(ClassLayout.parseInstance(o).toPrintable()); } }).start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(ClassLayout.parseInstance(o).toPrintable()); new Thread(()->{ synchronized (o){ log.info(ClassLayout.parseInstance(o).toPrintable()); } }).start(); }
运行结果:
16:52:47.019 [main] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 16:52:47.066 [Thread-0] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 b8 7f 1a (00000101 10111000 01111111 00011010) (444577797) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 16:52:49.081 [main] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 b8 7f 1a (00000101 10111000 01111111 00011010) (444577797) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 16:52:49.081 [Thread-1] INFO com.yg.edu.T0_BasicLock - java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) c0 f3 ce 1a (11000000 11110011 11001110 00011010) (449770432) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们来分析上面的控制台输出。
上面图中,我们可以清晰的看出:
1:如果只有一个线程对这个对象做一个同步,那么其他线程中,对象的锁状态还是不会改变(具体看第三次打印对象的markword位置)
2:如果有多个线程对这个对象做一个同步,对象的锁状态就会做一个升级(从偏向锁升级到轻量级锁)
继续,来分析下面代码:
public static void main(String[] args) throws InterruptedException { Thread.sleep(5000); Object a = new Object(); Thread thread1 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread1 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); try { //让线程晚点儿死亡,造成锁的竞争 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread2 = new Thread(){ @Override public void run() { synchronized (a){ System.out.println("thread2 locking"); System.out.println(ClassLayout.parseInstance(a).toPrintable()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread1.start(); thread2.start(); }
执行代码,控制台输出:
thread1 locking java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a 8a cf 02 (01001010 10001010 11001111 00000010) (47155786) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total thread2 locking java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a 8a cf 02 (01001010 10001010 11001111 00000010) (47155786) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
thread1打印的对象markword:01001010 10001010 11001111 00000010;转换后为00000010 11001111 10001010 01001010,对应的是重量级锁。
thread2打印的对象markword:01001010 10001010 11001111 00000010;转换后为00000010 11001111 10001010 01001010,对应的是重量级锁。
我们可以看出,在线程竞争激烈的情况下,匿名偏向状态会直接转为重量级锁(上面程序中,不管是哪个线程拿到了对象锁,里面的sleep 2s,都会让另一个线程在无限自旋,
出现线程自旋等待,锁状态就会做出一个向上升级的动作)
上面整了这么多活,我们在来点阴间的东西吧。
看下面代码:
public static void main(String[] args) throws InterruptedException { // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。 // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动 // 偏向锁,会出现很多没有必要的锁撤销 Thread.sleep(5000); T t = new T(); //未出现任何获取锁的时候 System.out.println(ClassLayout.parseInstance(t).toPrintable()); synchronized (t){ // 获取一次锁之后 System.out.println(ClassLayout.parseInstance(t).toPrintable()); } // 输出hashcode // System.out.println(t.hashCode()); // 计算了hashcode之后,将导致锁的升级 System.out.println(ClassLayout.parseInstance(t).toPrintable()); synchronized (t){ // 再次获取锁 System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }
控制台输出:
com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 48 e7 02 (00000101 01001000 11100111 00000010) (48711685) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
控制台输出都是正常的偏向锁状态(没有多个线程竞争激烈情况,都是偏向锁状态)
我们把打印hashcode这行注释放开:
public static void main(String[] args) throws InterruptedException { // 需要sleep一段时间,因为java对于偏向锁的启动是在启动几秒之后才激活。 // 因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动 // 偏向锁,会出现很多没有必要的锁撤销 Thread.sleep(5000); T t = new T(); //未出现任何获取锁的时候 System.out.println(ClassLayout.parseInstance(t).toPrintable()); synchronized (t){ // 获取一次锁之后 System.out.println(ClassLayout.parseInstance(t).toPrintable()); } // 输出hashcode System.out.println(t.hashCode()); // 计算了hashcode之后,将导致锁的升级 System.out.println(ClassLayout.parseInstance(t).toPrintable()); synchronized (t){ // 再次获取锁 System.out.println(ClassLayout.parseInstance(t).toPrintable()); } }
控制台输出:
com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 48 69 02 (00000101 01001000 01101001 00000010) (40454149) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 1731722639 com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 8f fd 37 (00000001 10001111 11111101 00110111) (939364097) 4 4 (object header) 67 00 00 00 (01100111 00000000 00000000 00000000) (103) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total com.yg.edu.T object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 20 f3 4f 02 (00100000 11110011 01001111 00000010) (38794016) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 92 c3 00 20 (10010010 11000011 00000000 00100000) (536920978) 12 4 int T.i 0 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我们可以看最后两次打印:
第三次打印:00000001 10001111 11111101 00110111;转换过后是:00110111 11111101 10001111 00000001,最后三位是001,对应的状态是无锁状态。(这就很奇怪了)
第四次打印:00100000 11110011 01001111 00000010;转换过后是:00000010 01001111 11110011 00100000,最后两位是00,对应的状态是轻量级锁。(锁状态升级了,纳尼?)
我们这里单独对这个hashcode来分析:
我们往上翻一翻表格,偏向锁状态下,对象的markword是没地方去存储hashcode,但是,轻量级锁是有地方可以去存储这个对象的hashcode的信息,这里也可以解释的通,
上面说的,对象的hashcode的获取是一种类似于spring的懒加载的类型。那么轻量级锁状态下,对象的hashcode又是放在哪个位置的呢?
轻量级锁状态下,对象的hashcode会存储在线程栈中的一块空间中(Lock Record,可以理解成markword副本)过程看下图:
4.锁的膨胀升级过程
锁的膨胀升级是不可逆的,锁的膨胀升级是不可逆的,锁的膨胀升级是不可逆的。
上面的的markword的描述,已经大概可以知道锁的膨胀升级过程了:
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重 量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁 的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。
5.自旋锁,对象锁的消除和粗化
5.1自旋锁
自旋锁会出现在轻量级锁加锁失败的情况下,为了不直接去升级成重量级锁(升级重量级锁涉及到用户态和内核态的切换,是一个比较重的操作,具体看前面的博文)而做出来的一种优化方式。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线 程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是 自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
5.2对象锁的粗化
我们看下面代码:
private static Object object = new Object(); public static void main(String[] args) { // new SynchronizedClass().method(); synchronized (object) { System.out.println(""); } synchronized (object) { System.out.println(""); } synchronized (object) { System.out.println(""); } }
对于上面这种代码,在同一个方法中对同一个对象进行多次加锁,性能上面会有一个消耗,通过一个线程的逃逸分析(这个会专门开一篇博客来讲这个逃逸分析),
jvm会做一个锁的粗化来优化上面的代码,底层优化好以后就想当于是下面的代码:
private static Object object = new Object(); public static void main(String[] args) { // new SynchronizedClass().method(); synchronized (object) { System.out.println(""); System.out.println(""); System.out.println(""); } }
这就是一个锁的粗化的过程。
5.3锁的消除
我们看下面代码:
public static void main(String[] args) { // new SynchronizedClass().method(); Object object1 = new Object(); synchronized (object1) { System.out.println(""); } }
这个object1并不会被其他线程访问到,也就是说,这个object1不是一个临界资源,当程序对一个不是临界资源的对象加锁的时候,实际上是不生效的,这就是JVM对于这些代码做的一个优化,锁的消除。
over~
人类赞歌是勇气的赞歌,人类的伟大是勇气的伟大!!!