简介
synchronizaed关键字是JAVA阻塞同步(互斥同步)中最常用的一种方式,使用时将此关键字加到所需同步的代码块儿前即可,比如
int i = 0; synchronized (this){ i++; }
synchronizaed同步方式在JAVA中是重量级加锁方式,下面来介绍一下它的工作原理,首先写一段代码:
public class Sync { synchronized void syncMethod(){} public void add(){ synchronized (this) { } } }
我在这段代码中定义了一个同步方法,并在另一个非同步方法中定义了一个同步代码块,接下来反编译class文件看下字节码指令:
synchronized void syncMethod(); descriptor: ()V flags: ACC_SYNCHRONIZED//同步标志位! Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 4: 0 public void add(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter//同步代码块进入 4: aload_1 5: monitorexit//同步代码块退出 6: goto 14 9: astore_2 10: aload_1 11: monitorexit//同步代码块异常退出 12: aload_2 13: athrow 14: return Exception table: from to target type 4 6 9 any 9 12 9 any LineNumberTable: line 6: 0 line 8: 4 line 9: 14 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 9 locals = [ class com/Sync, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4
可以看到同步方法的同步方式是隐式的,它在方法的flags标志中添加了ACC_SYNCHRONIZED标志,程序执行中,对某方法的调用会判断这个标志位是否存在并判断是否应该同步。
同步代码块的同步方式则是显式的,它会在同步块的前后分别生成monitorenter和monitorexit指令,这两个指令是同步块的关键,他们会在进出同步块时分别通过monitor机制做标记,表示获取或者释放,这样保证了同步的进行。下面引用Java虚拟机规范的一段话来说明:
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持。
Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。无论是显式同步(有明确的monitorenter和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现的)都是如此。
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有执行其对应monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
那么monitor机制是什么呢?
monitor机制
monitor机制是synchronized关键字的具体实现方式,java对象天生就有monitor机制,个人理解monitor机制是C++编写的objectMonitor.hpp和Java对象头信息共同实现的一种同步判断机制,我们先看下objectMonitor.hpp文件:
ObjectMonitor() { _header = NULL; _count = 0;//重入机制 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
其中_WaitSet字段是 LL of threads wait()ing on the monitor,就是等待在该监视器的线程集合,其他的属性在源码中也均有注释,有兴趣可以在http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/bfac16f18d92/src/share/vm/runtime/objectMonitor.hpp中查看研究。这段代码中的字段定义了对象的占用线程,等待集合以及阻塞时的自旋等待方式等内容。
我们再来看一下Java对象头,Java对象的内存分布如下图所示:
https://www.cnblogs.com/ZoHy/p/11313155.html
Java对象中有一部分信息叫对象头,这部分信息负责存储对象自身的运行时数据和指向方法区对象类型数据的指针,其中MarkWord和Java的锁机制关系颇深,这一部分内容是可复用的,随着标志位的不同它会存储不同的内容:
当线程试图获取锁(进入同步区)时,我认为工作机制是检测对象头信息以及底层的objectMonitor字段,判断是否有被抢占,如果被占用则判断是否是线程重入,如果不是则需要阻塞等待,阻塞是通过自旋来实现的,至于获取成功以及一系列的锁优化,请见另一篇文章。
我们来看一下实际工作中线程获取锁的流程,如下图所示:
当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个外部条件在 monitor 机制中称为条件变量。
链接:https://www.jianshu.com/p/7f8a873d479c
原文地址:https://www.cnblogs.com/lybnumber6/p/12173297.html