面试官,别挂电话,Synchronized,我还能说上半小时。

偏执的太偏执、 2023-05-22 04:41 81阅读 0赞

面试官,别挂电话,Synchronized,我还能说上半小时。

Synchronized关键字,经常被用于线程同步。执行Synchronized修饰的同步代码块的线程,首先会获得“对象的锁”,如果有其他线程尝试执行同步代码块,会阻塞,直到该线程执行完同步代码,释放“对象锁”。上面的概念,肯定不陌生,但是对象锁具体是什么,或许你不太清楚,本文从其底层原理出发,详细解读Synchronized关键字

Synchronized关键字概念

synchronized是Java中关键字,是利用锁机制来实现同步,锁机制有如下两种特性

  • 互斥性(也叫原子性):同一时刻只允许一个线程获取对象锁
  • 可见性:线程获取对象锁后,会清空线程工作内存中的同步代码块使用到的共享变量

    对象释放锁前,会将线程工作内存中对共享变量的修改刷新回主内存

对象锁与类锁

在Java中,每个对象都会有一个monitor对象,monitor对象其实就是经常说的,对象锁或者称为

监视器锁。而类锁(类似使用Synchronized(Object.class)), 其实是通过对象锁实现,如果熟悉类加载机制,肯定知道,类加载的同时,会在堆中,new一个Class对象。此Class对象的monitor对象就是类锁,由此可知,每个类只有一个类锁

对象头

Java对象在内存中存储的布局可以分为三块区域: 对象头、实例数据、对齐填充。
对象头,分为两个部分,第一个部分存储对象自身的运行时数据,又称为Mark Word,32位虚拟机占32bit,64位虚拟机占64bit。如图所示,不同锁状态下,Mark Word的结构.第二部分存储类指针

format_png

对象锁原理(Monitor)

Java虚拟机中,monitor是由ObjectMonitor实现的,主要数据结构如下

  1. 1 ObjectMonitor() {
  2. 2 _header = NULL;//markOop对象头
  3. 3 _count = 0;
  4. 4 _waiters = 0,//等待线程数
  5. 5 _recursions = 0; //重入次数
  6. 6 _object = NULL;
  7. 7 _owner = NULL;//指向获得ObjectMonitor对象的线程
  8. 8 _WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
  9. 9 _WaitSetLock = 0 ;
  10. 10 _Responsible = NULL ;
  11. 11 _succ = NULL ;
  12. 12 _cxq = NULL ;
  13. 13 FreeNext = NULL ;
  14. 14 _EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
  15. 15 _SpinFreq = 0 ;
  16. 16 _SpinClock = 0 ;
  17. 17 OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
  18. 18 _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  19. 19 }

ObjectMonitor主要有几个需要关注的成员变量

  • _owner:指向获得ObjectMonitor对象的线程
  • _EntryList: 处于等待锁block状态的线程,会被加入到entry set
  • _WiatSet: 处于wait状态的线程,会被加入到wait set(调用同步对象wait方法)

多个线程同时访问一段同步代码时,首先会进入_EntryList集合,进行阻塞等待, 当线程获取到对象的monitor后进入owner区域,并把monitor中的_owner变量指向该线程,同时monitor中的计数器count自加一,若线程调用同步对象的wait()方法将释放当前持有的monitor,_owner变量重置为null,count自减一,同时该线程进入_WaitSet中等待唤醒,线程执行完同步代码块后,也将_Ownercount变量重置.
可重入原理:线程第一次获取对象锁后,会将_owner变量指向自己,并将count设置为1.下一次线程尝试去获取对象锁后,发现_owner是指向自己的,就直接将count自加1,进入同步代码块。线程每一次退出同步代码块时,会将count自减1,直到count为0时,才释放对象锁,即将_owner设置为null
format_png 1

代码块锁解锁过程

  1. public class SyncCodeBlock{
  2. public int i;
  3. public void syncTask(){
  4. synchronized(this) {
  5. i++;
  6. }
  7. }
  8. }

反编译后,得到上述代码的字节码

  1. public void syncTask();
  2. Code:
  3. 0: aload_0
  4. 1: dup
  5. 2: astore_1
  6. 3: monitorenter
  7. 4: aload_0
  8. 5: dup
  9. 6: getfield #2 // Field i:I
  10. 9: iconst_1
  11. 10: iadd
  12. 11: putfield #2 // Field i:I
  13. 14: aload_1
  14. 15: monitorexit
  15. 16: goto 24
  16. 19: astore_2
  17. 20: aload_1
  18. 21: monitorexit
  19. 22: aload_2
  20. 23: athrow
  21. 24: return

从字节码中可以看出,Synchronized底层是使用monitorenter,monitorexit指令实现线程同步.

  • 执行monitorenter指令时,线程将尝试获取同步对象的锁(即monitor对象),若monitor的count变量(记录线程进入次数)为0, 则将count设置为1,_owner设置为当前线程,``.取锁成功.如果线程已拥有该对象锁,则可以重入该锁.重入时count计算器的值也会加一
  • 指向monitorexit指令时,执行器将count计数器减一,当计数器为0时,其他线程将有机会持有

    monitor.值得注意的是,编译器会确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都会执行相应的monitorexit指令.无论此方法是正常结束还是异常结束.

    如果方法异常完成,编译器会自动产生一个异常处理器,异常处理器的目的就是用来执行monitorexit指令.从字节码中也可以看出多了一个monitorexit指令.它就是异常结束时被执行的释放monitor指令.

方法体加锁解锁过程

  1. public class Main{
  2. public int i;
  3. public synchronized void syncTask(){
  4. i++;
  5. }
  6. }

反编译上述代码后,得到如下字节码

  1. public synchronized void syncTask();
  2. descriptor: ()V
  3. flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  4. Code:
  5. stack=3, locals=1, args_size=1
  6. 0: aload_0
  7. 1: dup
  8. 2: getfield #2 // Field i:I
  9. 5: iconst_1
  10. 6: iadd
  11. 7: putfield #2 // Field i:I
  12. 10: return

方法体的同步时隐式,即无需字节码指令来控制.JVM可以从方法常量池中的方法表结构(method_info structure)中的ACC_SYNCHRONIZED访问标志来区分一个方法是否同步方法.

当调用方法时,检测方法的ACC_SYNCHRONIZED是否被设置,如果设置了,线程需先持有该对象的monitor,然后执行方法,最后方法完成时释放monitor.

如果一个同步方法执行期间抛出了异常,并且方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放.

发表评论

表情:
评论列表 (有 0 条评论,81人围观)

还没有评论,来说两句吧...

相关阅读