浅谈synchronized的实现原理

前言

Synchronized是Java中的重量级锁,在我刚学Java多线程编程时,我只知道它的实现和monitor有关,但是synchronized和monitor的关系,以及monitor的本质究竟是什么,我并没有尝试理解,而是选择简单的略过。在最近的一段时间,由于实际的需要,我又把这个问题翻出来,Google了很多资料,整个实现的过程总算是弄懂了,为了以防遗忘,便整理成了这篇博客。
在本篇博客中,我将以class文件为突破口,试图解释Synchronized的实现原理。

从java代码的反汇编说起

很容易的想到,可以从程序的行为来了解synchronized的实现原理。但是在源代码层面,似乎看不出synchronized的实现原理。锁与不锁的区别,似乎仅仅只是有没有被synchronized修饰。不如把目光放到更加底层的汇编上,看看能不能找到突破口。javap是官方提供的*.class文件分解器,它能帮助我们获取*.class文件的汇编代码。具体用法可参考这里。 接下来我会使用javap命令对*.class文件进行反汇编。
编写文件Test.java:

public class Test {

    private int i = 0;

    public void addI_1(){
        synchronized (this){
            i++;
        }
    }

    public synchronized  void addI_2(){
        i++;
    }
}

生成class文件,并获取对Test.class反汇编的结果:

javac Test.java
javap -v Test.class
Classfile /Users/zhangkunwei/Desktop/Test.class
  Last modified Jul 13, 2018; size 453 bytes
  MD5 checksum ada74ec8231c64230d6ae133fee5dd16
  Compiled from "Test.java"
  ... ...
  public void addI_1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
  ... ...
    public synchronized void addI_2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
   ... ...

通过反汇编结果,我们可以看到:

  • 进入被synchronized修饰的语句块时会执行monitorenter,离开时会执行monitorexit
  • 相较于被synchronized修饰的语句块,被synchronized修饰的方法中没有指令monitorentermonitorexit,且flags中多了ACC_SYNCHRONIZED标志。
    monitorentermonitorexit指令是做什么的?同步语句块和同步方法的实现原理有何不同?遇事不决查文档,看看官方文档的解释。

monitorenter

Description
The objectref must be of type reference.

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread > that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as > > follows:

  • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor > and sets its entry count to one. The thread is then the owner of the monitor.

  • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

  • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

Notes

  • A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a synchronized statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine's method invocation and return instructions, as if monitorenter and monitorexit were used.

简单翻译一下:
指令monitorenter的操作的必须是一个对象的引用,且其类型为引用。每一个对象都会有一个monitor与之关联,当且仅当monitor被(其他(线程)对象)持有时,monitor会被锁上。其执行细节是,当一个线程尝试持有某个对象的monitor时:

  • 如果该对象的monitor中的entry count==0,则将entry count置1,并令该线程为monitor的持有者。
  • 如果该线程已经是该对象的monitor的持有者,那么重新进入monitor,并使得entry count自增一次。
  • 如果其他线程已经持有该对象的monitor,则该线程将会被阻塞,直到monitor中的entry count==0,然后重新尝试持有。
    注意:
    monitorenter必须与一个以上monitorexit配合使用来实现Java中的同步语句块。而同步方法却不是这样的:同步方法不使用monitorentermonitorexit来实现。当同步方法被调用时,Monitor介入;当同步方法return时,Monitor退出。这两个操作,都是被JVM隐式的handle的,就好像这两个指令被执行了一样。

monitorexit

Description

  • The objectref must be of type reference.

  • The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

  • The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

简单翻译一下:
指令monitorenter的操作的必须是一个对象的引用,且其类型为引用。并且:

  • 执行monitorexit的线程必须是monitor的持有者。
  • 执行monitorexit的线程让monitorentry count自减一次。如果最后entry count==0,这个线程就不再是monitor的持有者,意味着其他被阻塞线程都能够尝试持有monitor

根据以上信息,上面的疑问得到了解释:

  1. monitorentermonitorexit是做什么的?
    monitorenter能“锁住”对象。当一个线程获取monitor的锁时,其他请求访问共享内存空间的线程无法取得访问权而被阻塞;monitorexit能“解锁”对象,唤醒因没有取得共享内存空间访问权而被阻塞的线程。

  2. 为什么一个monitorenter与多个monitorexit对应,是一对多,而不是一一对应?
    一对多的原因,是为了保证:执行monitorenter指令,后面一定会有一个monitorexit指令被执行。上面的例子中,程序正常执行,在离开同步语句块时执行第一个monitorexit;Runtime期间程序抛出Exception或Error,而后执行第二个monitorexit以离开同步语句块。

  3. 为什么同步语句块和同步方法的反汇编代码略有不同?
    同步语句块是使用monitorentermonitorexit实现的;而同步方法是JVM隐式处理的,效果与monitorentermonitorexit一样。并且,同步方法的flags也不一样,多了一个ACC_SYNCHRONIZED标志,这个标志是告诉JVM:这个方法是一个同步方法,可以参考这里

Monitor

在上一个部分,我们容易得出一个结论:synchronized的实现和monitor有关。monitor又是什么呢?从文档的描述可以看出,monitor类似于操作系统中的互斥量这个概念:不同对象对共享内存空间的访问是互斥的。在JVMHotspot)中,monitor是由ObjectMonitor实现,其主要的数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   //指向当前monitor的持有者 
    _WaitSet      = NULL;   //持有monitor后,调用的wait()的线程集合
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  //尝试持有monitor失败后被阻塞的线程集合
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

可以看出,我们可以

  • 通过修改_owner来指明monitor锁的拥有者;
  • 通过读取_EntryList来获取因获取锁失败而被阻塞的线程集合;
  • 通过读取_WaitSet来获取在获得锁后主动放弃锁的线程集合。

到这里,synchronized的实现原理已经基本理清楚了,但是还有一个未解决的疑问:线程是怎么知道monitor的地址的?线程只有知道它的地址,才能够访问它,然后才能与以上的分析联系上。答案是monitor的地址在Java对象头中。

Java对象头

在Java中,每一个对象的组成成分中都有一个Java对象头。通过对象头,我们可以获取对象的相关信息。
这是Java对象头的数据结构(32位虚拟机下):


Java对象头数据结构

其中的Mark Word,它是一个可变的数据结构,即它的数据结构是依情况而定的。下面是在对应的锁状态下,Mark Word的数据结构(32位虚拟机下):

Mark Word数据结构.png

synchronized是一个重量级锁,所以对应图中的重量级锁状态。其中有一个字段是:指向重量级锁的指针,共占用25+4+1=30bit,它的内容就是这个对象的引用所关联的monitor的地址。
线程可以通过Java对象头中的Mark Word字段,来获取monitor的地址,以便获得锁。

回到最初的问题

synchronized的实现原理是什么?从上面的分析来看,答案已经显而易见了。当多个线程一起访问共享内存空间时,这些线程可以通过synchronized锁住对象的对象头中,根据Mark Word字段来访问该对象所关联的monitor,并尝试获取。当一个线程成功获取monitor后,其他与之竞争monitor持有权的线程将会被阻塞,并进入EntryList。当该线程操作完毕后,释放锁,因争用monitor失败而被阻塞的线程就会被唤醒,然后重复以上步骤。

写在最后

我发现其实大部分答案都可以从文档中得到,所以以后遇到问题还是要尝试从文档中找到答案。
本人水平有限,如果本文有错误,还望指正,谢谢~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容