synchronized介绍
java中jdk1.6之前和jdk1.6及之后synchronized完全不一样。1.6之前直接上来都是重量级锁导致java的性能很低效,而1.6及之后甲骨文公司对其进行优化,通过一个锁的升级过程从而来支持一些非复杂的场景。那么本文主要是针对synchronized的源码及一些使用进行了解。
java的内存布局和对象头及锁升级过程
请查看之前发布过的文章:对象实例化与内存布局(深入)
加锁的三种方式及锁的粒度
加到静态方法,锁是当前类对象
- public synchronized static void method(){
- System.out.println("锁的是对象");
- }
同步类方法,锁是当前方法
- public synchronized void method2(){
- System.out.println("锁的是方法!");
- }
同步代码块,锁是括号里面的对象
- public void method1(){
- synchronized (this){
- System.out.println("锁是代码块!");
- }
- }
查看锁
- public class SynchronizedLock {
- public synchronized void method(){
- System.out.println("同步锁测试!");
- }
- }
javap -c -v SynchronizedLock.class
- Classfile /Users/csh/ideaworkspace/jdk8/src/main/java/com/lock/SynchronizedLock.class
- Last modified 2022-11-20; size 512 bytes
- MD5 checksum c1a83fca01614297b7553ecf31e0b979
- Compiled from "SynchronizedLock.java"
- public class com.lock.SynchronizedLock
- minor version: 0
- major version: 52
- flags: ACC_PUBLIC, ACC_SUPER
- Constant pool:
- #1 = Methodref #7.#16 // java/lang/Object."
":()V - #2 = Fieldref #17.#18 // java/lang/System.out:Ljava/io/PrintStream;
- #3 = String #19 // 同步锁测试!
- #4 = Methodref #20.#21 // java/io/PrintStream.println:(Ljava/lang/String;)V
- #5 = String #22 // 普通方法!
- #6 = Class #23 // com/lock/SynchronizedLock
- #7 = Class #24 // java/lang/Object
- #8 = Utf8
- #9 = Utf8 ()V
- #10 = Utf8 Code
- #11 = Utf8 LineNumberTable
- #12 = Utf8 method
- #13 = Utf8 method2
- #14 = Utf8 SourceFile
- #15 = Utf8 SynchronizedLock.java
- #16 = NameAndType #8:#9 // "
":()V - #17 = Class #25 // java/lang/System
- #18 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
- #19 = Utf8 同步锁测试!
- #20 = Class #28 // java/io/PrintStream
- #21 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
- #22 = Utf8 普通方法!
- #23 = Utf8 com/lock/SynchronizedLock
- #24 = Utf8 java/lang/Object
- #25 = Utf8 java/lang/System
- #26 = Utf8 out
- #27 = Utf8 Ljava/io/PrintStream;
- #28 = Utf8 java/io/PrintStream
- #29 = Utf8 println
- #30 = Utf8 (Ljava/lang/String;)V
- {
- public com.lock.SynchronizedLock();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=1, locals=1, args_size=1
- 0: aload_0
- 1: invokespecial #1 // Method java/lang/Object."
":()V - 4: return
- LineNumberTable:
- line 8: 0
-
- public synchronized void method();
- descriptor: ()V
- //注意:ACC_SYNCHRONIZED 是用来标识是同步方法
- 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 同步锁测试!
- 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- LineNumberTable:
- line 10: 0
- line 11: 8
-
- public void method2();
- descriptor: ()V
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=1, args_size=1
- 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
- 3: ldc #5 // String 普通方法!
- 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 8: return
- LineNumberTable:
- line 14: 0
- line 15: 8
- }
从上面可以看出来普通的方法和同步方法,在字节码层面同步方法多了一个ACC_SYNCHRONIZED 从而来使jvm知道这个方法是同步方法。
什么是monitor对象?
首先monitor叫作管程或监视器,用于创建监视锁,每个对象都存在一个monitor关联的对象,包含monitorenter和monitorexit。
个人理解:这个就是监控,你干啥干啥了。
流程是这样的:
如果当进入的时候monitor为0,则代表没有被持有,会将这个monitor计数器设置为1;
如果进来的时候线程为该monitor的持有者,那么计数器加1(可重入);
如果该线程不是该monitor的持有者,进来获取若已被其它线程持有,那么获取monitor失败,进入阻塞状态,直到monitor计数器为0,其它线程退出Monitor,重新唤醒同步队列中的线程;
为什么会用两个MonitorExit指令?
第一个退出指令为正常退出,第二个为异常退出。
源码阅读
ObjectMonitor.hpp(C++代码)
- ObjectMonitor() {
- _header = NULL; //头部默认为空
- _count = 0; //记录个数
- _waiters = 0, //等待中的线程数
- _recursions = 0; //线程重入次数
- _object = NULL; //存储该monitor的对象
- _owner = NULL; //拥有该monitor的线程
- _WaitSet = NULL; //等待线程的列表
- _WaitSetLock = 0 ;
- _Responsible = NULL ;
- _succ = NULL ;
- _cxq = NULL ; //多线程竟争锁时的单向链表
- FreeNext = NULL ;
- _EntryList = NULL ; //待锁列表(入口列表)
- _SpinFreq = 0 ;
- _SpinClock = 0 ;
- OwnerIsThread = 0 ;
- _previous_owner_tid = 0; // 前一个拥有此监视器的线程 ID
- }
注意这里:_WaitSet和_EntryList是ObjectMonitor的两个主要队列,分别用来存放等待队列和正在执行的集合。
多线程时的执行流程:
先进入_EntryList集合,然后线程去获取monitor,若成功获取则进入_Owner并把当前线程设置为owner,同时Monitor中的计数器count加1(_count);
若线程执行过程中调用了wait()方法,当前线程就会释放持有的monitor,这时_owner重新被置为null,count减1,同时该线程被放入_WaitSet列表中等待被唤醒。
若线程执行完毕,那么默认会释放monitor(锁)并将count的值减1或置为0,让出这个锁并且唤醒_WaitSet队列中的线程。
hostpot中有两种解释器模板解释器和字节码解释器,其中字节码是基于汇偏于言,虚拟机早期开发的一种解释器(很慢的原因,一行一行读的),而模板是正在正在解释使用的另外一种高效快速解释器。(不懂解释器请看之前文章!)
不过模板解释器基于汇编来写的(性能高啊~)比较难读,而字节码解释器是C++代码写的。
以下来源于:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816
实际不会读到这个bytecodeInterpreter.cpp而是读到templateTable_x86_64.cpp
以下代码位置:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/cpu/x86/vm/templateTable_x86_64.cpp#l3667
这个基于汇编语言的,的确纯阅读起来,太费解了,还是需要一行一行来调试会更易理解,想想这个学汇编都十几年前的事了~,还是建议看看c++吧。
最后
由于时间问题及考虑到把所有c++或汇编源码贴出来没什么意思,建议有需要深入了解同步锁,锁升级过程的源码同学可以看看下面的参考文章,里面有些大佬已经把详细步骤及过程写出来了,可以深入再学学。关于虚拟机的汇编源码我本人是不建议看,因为代码量巨大还有耗费的时间可能需要最少长达半个月或几个月才能消化完,所以有这个时间都可以学一个新的技术点了,当然有需要深入或工作上需要的同学那建议看看下面的参考文章可以减少不少的学习弯路。
参考文章:
https://github.com/farmerjohngit/myblog/issues/13
https://blog.csdn.net/u014454538/article/details/120731549
https://tech.meituan.com/2018/11/15/java-lock.html
https://www.pdai.tech/md/java/thread/java-thread-x-key-synchronized.html
https://xie.infoq.cn/article/3ac3596347f9e914a2d2a3587
https://xiaomi-info.github.io/2020/03/24/synchronized/
https://www.cnblogs.com/trunks2008/p/14613090.html
https://xie.infoq.cn/article/3ac3596347f9e914a2d2a3587
https://zhuanlan.zhihu.com/p/33830504
c++实现源码类:
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/cpu/x86/vm/templateTable_x86_64.cpp
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/interpreterRuntime.cpp