在处理HotSpot上的“瘦锁”时,我在理解锁记录的生命周期方面遇到了一些困难。
我的理解是:
当线程T
第一次尝试获取对象o
上的锁时,它触发“瘦锁”创建--在当前帧F
上,在T
的堆栈上创建lock record
,和mark work
的副本(现在将被称为displaced header
)加上对o
的引用被存储在F
上。s标头引用锁记录(最后两位设置为00
以将此对象标记为瘦锁定!)。
但是,CAS操作失败的原因有多种:
- 另一个线程更快地获取锁,我们需要将此瘦锁转换为一个完整的监视器;
- CAS失败了,但是可以看到对锁记录的引用属于
T
的堆栈,所以我们必须尝试重新输入相同的锁,这是正确的。在这种情况下,当前堆栈帧的锁记录保持为空。
有鉴于此,我有几个问题:
1.为什么每次尝试输入一个锁时都要创建一个新的锁记录呢?为每个对象o
只保留一个锁记录不是更好吗?
1.当离开同步块时,我不明白VM如何知道我们是否应该释放锁,或者我们是否仍然在从递归锁中“解开”。
有人能解释一下吗?
参考文献
- https://blogs.oracle.com/dave/lets-say-youre-interested-in-using-hotspot-as-a-vehicle-for-synchronization-research
- https://wiki.openjdk.java.net/display/HotSpot/Synchronization
- http://www.diva-portal.org/smash/get/diva2:754541/FULLTEXT01.pdf
- https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf
让我引用最后一个链接中的一段话:
每当对象被监视器入口字节码轻量级锁定时,就会在执行锁获取操作的线程的堆栈上隐式或显式分配一个锁记录。锁记录保存对象标记字的原始值,还包含标识哪个对象被锁定所需的元数据。在锁获取期间,将标记字复制到锁记录中,(这样的拷贝被称为置换标记字),以及原子比较和交换(CAS)操作以尝试使对象的标记字指向锁记录。如果CAS成功,该锁被膨胀,在该操作期间,OS互斥体和条件变量与该对象相关联。在膨胀过程期间,该对象的标记字用CAS更新,以指向包含指向互斥体和条件变量的指针的数据结构。在解锁操作期间,尝试CAS标记字,该标记字仍应指向锁记录,如果CAS成功,则不存在对监视器的争用,轻量级锁定仍然有效;如果失败,该锁在被持有时被争用,并且采用慢速路径来正确地释放该锁并通知等待获取该锁的其他线程。递归锁定以直接的方式被处理。如果在轻量级锁获取期间,确定当前线程已经凭借对象的标记字指向其堆栈而拥有该锁,则将零而不是对象的标记字的当前值存储到堆栈上锁记录中。如果在解锁操作期间在锁记录中看到零,该对象被当前线程递归锁定,并且该对象的标记字没有更新。此类锁记录的数量隐式记录了监视器递归计数。据我们所知,这是大多数其他JVM无法实现的一个重要属性。
谢谢
2条答案
按热度按时间jmo0nnb31#
为什么我们每次尝试输入锁时都要创建一个新的锁记录?为每个对象o只保留一个锁记录不是更好吗?
看起来你错过了锁记录的要点。锁记录不是一些 * 每个对象 * 实体,而是 * 每个锁站点 *。例如,如果一个方法有3个
synchronized
块,它的堆栈帧可能有多达3个锁记录,无论它是3个不同的锁定对象,还是同一个对象递归锁定3次。锁定记录(实际上,在HotSpot源代码中并不这样称呼它们;它们通常被称为“监视器”、“监视器槽”、“监视器块”等)有助于维护堆栈帧和其锁定监视器之间的Map。2特别是,当堆栈帧由于异常而被删除时,所有的锁定都需要自动释放。3因此,可以将监视器槽看作类似于局部变量槽的东西,它可以保存对相同或不同对象的引用。2像局部变量一样,监视器与给定的堆栈帧相关联。3它们保存对锁定对象的引用,但它们本身不是“锁”。
当离开同步块时,我不明白VM如何知道我们是否应该释放锁,或者我们是否仍然在从递归锁中“解开”。
锁定记录(监视器插槽)包含两个内容:一个对锁定对象的引用和一个所谓的“置换头”。2置换头是对象头的先前(未锁定)值,如果是递归锁,则为零。
正如我上面解释的,如果我们锁定一个对象3次,将有3个锁记录。只有第一个保存了实际的非零置换头,其他两个将为零。这意味着,前两个
monitorexit
指令将用零弹出锁记录,意识到这是一个递归锁,因此不会更新该对象。当最后一个锁记录被删除时,JVM在被置换的头部中看到非零值,并将其存储回真实的对象头部,从而将其标记为未锁定。dm7nw8vv2#
请注意,这是我最初试图回答这个问题的尝试。我很清楚,上面链接的文档可以自己回答所有问题。
对于第一个问题,新的锁记录是在堆栈上创建的,因为这比在堆上分配要便宜得多。在许多情况下,监视器永远不会被争用,所以这可能是一个巨大的胜利。堆栈分配/释放有时候是如此便宜,甚至不值得考虑。
第二个问题可以通过注意doc引用当前帧
F
来回答。总有一个帧指针寄存器,因此monitorexit指令可以简单地检查当前帧指针是否与瘦锁记录的地址匹配。如果匹配,则它知道它是最后一个出局的。一个关键的方面是monitorenter/exit指令必须被正确地平衡,JVM试图证明这一点。否则,需要一个ref计数来检测何时到达最后一个monitorexit指令。看起来,如果monitorenter/exit指令不平衡,HotSpot就不会费心编译代码或优化监视器的获取。