从三个层面解析synchronized原理

前言:在上一篇博客说完了synchronized锁的到底是什么,以及基本的用法,作用,算是synchronized的预备知识。但是学东西要知其然也要知其所以然,所以这篇准备深入的分析,synchronized的原理,synchronized到底是怎么实现同步,保证线程安全的。

Java代码层面

Java代码层面比较简单,就是通过synchronized关键字,通过给对象上锁,对于普通方法锁住的是调用的那个实例对象,对于静态方法锁住的是当前类的class对象。这个可以在上一篇博客上看详情。在这里就不多说了

锁信息的记录位置

上面我这篇文章,说了相当多的synchronized的用法,我们知道了synchronized是锁当前对象,那么问题来了,对象把这把锁的信息记录在对象的哪个位置呢?看代码

可以在对象布局里面看到,对一个对象加锁和不加锁,他们改变的地方就是第一个对象头,因此我们可以得出结论,锁信息是被记录在对象的对象头信息中。mark word

字节码层面

可以通过命令 javap -v xxx.class来查看编译后的class文件字节码命令。

synchronized代码块

首先我们来看下编译后的同步代码块的字节码,具体关键代码长这样。我用箭头标注出了Java代码在字节码所体现的部分。可以看到,对于被synchronized修饰的代码块,在字节码层面是通过monitorenter和monitorexit这两个命令来实现加锁的,看命令的意思就是,监视器入口,监视器出口,在这两个命令之间的代码,都是经过同步处理的,此外监视器还不止一个出口,还有一个是异常的出口,当synchronized代码块里的代码,出现了异常就会走这个锁出口,如果是正常执行,走的则是第一个monitorexit,然后会goto到synchronized代码块之后的下一行代码。虽然不能完全看懂这个字节码,但是初步分析执行的过程应当是这样。

monitor命令的详解:关于monitor命令,这是属于字节码命令,因此直接查阅JVM文档就可以了,专门上网搜了一下这个命令的作用。

  • monitorenter :

    ​ 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  • monitorexit:

    ​ 执行monitorexit的线程必须是objectref所对应的monitor的所有者。

    1. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

    2. Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

synchronized方法

对于synchronized修饰的方法,字节码层面就和synchronized代码块不一样。看看编译过的字节码

对于synchronized修饰的方法实现同步是这样实现的,对synchronized修饰的方法设置一个ACC_SYNCHRONIZED常量,在调用这个方法之前,jvm会检查是否有ACC_SYNCHRONIZED这个常量。如果有,就对当前线程获取一个monitor锁,这个锁上会标志执行当前获取锁的那个线程,锁的范围是整个方法体,然后再执行方法体里的内容。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,如果期间有其他的线程想进入这个方法,会被检测到该线程不是当前方法体获取monitor的那个线程,然后就会被阻塞。直到拥有monitor的那个对象执行完方法,释放自己那个monitor。

其实这个方式本质上和代码块的没有区别,其实用monitorenter结合monitorexit也可以实现方法体的同步,只需要把范围扩大到整个方法就可以了,像这样:

monitorenter
    ......整个方法体
monitorexit

所以这两者其实都可以实现对方法的同步。

汇编层面

对于汇编层面的实现,是通过lock指令来实现的。底层的汇编实现 lock cmpxchg 指令 这个指令在对一块区域的值进行修改的时候,其他的线程和cpu不许打断这个指令,cas的一致性就是基于这条命令。lock指令非常重要,后面的synchronized都是基于这个lock指令,确保在执行lock指令的时候,直接锁住总线,不允许任何线程打断这个指令。这也涉及到java中的原子性问题。具体的我在之前的文章中有提到,可以来看看我这篇博客

总结

总结一下:

  • 在java代码层面,synchronized通过锁不同的对象来实现同步,普通方法和代码块锁当前实例对象,静态方法锁当前类的class对象,当线程要通过a对象访问synchronized方法的时候,就会获得a对象的锁,而此时别的线程也想通过这个a对象来访问synchronized方法的时候,就会被阻塞。
  • 在字节码层面,对于代码块,主要是通过monitorenter和monitorexit结合来实现同步的,只允许一个线程进入到monitorenter和monitorexit的范围。而对于方法,则是通过ACC_SYNCHRONIZED常量,进入方法前先检测是否有这个常量,有的话则获得monitor锁,并且记录线程信息。在释放之前不允许其他线程再进入
  • 在汇编层面是通过lock指令来实现的,在执行lock指令的时候,直接锁住总线,不允许任何线程打断这个指令
  • 对于加锁的范围,一般来说是,同步的范围越小越好,有利于提高性能
posted @ 2020-05-17 17:14  穿黑风衣的牛奶  阅读(539)  评论(3编辑  收藏  举报