《白话》 jvm--锁

2020-01-13  本文已影响0人  故事细腻言不由己

参考:https://www.cnblogs.com/deltadeblog/p/9559035.html

https://time.geekbang.org/column/article/13530

https://www.jianshu.com/p/911c112e0c2f

什么是锁对象??

锁对象就是我们写代码,()里面写的哪个东西。

他存储在java对象头中。


image.png

结构一般如下:


image.png

如果实现?

代码编译后,在字节码层面,增加了monitorenter和monitorexit关键字。

这两个没换一个指令都会消耗操作数栈上的一个引用类型的元素(就是写代码的时候括号里的引用)作为要加锁的对象。

monitorenter和monitorexit的对应关系是一对多。因为monitoredxit不仅要在正常执行路劲,异常执行路劲也需要释放锁。

那么这个锁对象里面,有一个计数器和一个指向持有该锁的线程的指针。

当计数器为0的时候,来一个线程访问,那么就会把指针指向那个线程,然后计数器加1.

在计数器不是0的情况下,如果还是本线程访问,那么计数器加1,如果访问的不是本线程,该线程就等待。

重量级锁(悲观锁)

重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

这个情况下,会阻塞和唤醒线程。

对于现在大部分实现了posix接口的操作系统,以上操作,都是通过pthread的互斥量mutex实现的。需要内核态和用户态的切换。消耗比较大。

由于阻塞唤醒的成本比较大,jvm会选择不同的策略。

当根据之前经验判断很快能获得锁,就会进行自旋锁。否则就等待策略。

重量级锁是不公平锁,即无法控制先到先得的顺序。

轻量级锁(乐观锁)

什么情况适合使用轻量级锁??

多个线程在不同的时段请求同意吧锁,也就是没有锁竞争的情况。

什么是mark word??

对象头中有一段,表示mark work,最后两位用来表示锁状态。00表示轻量级锁,01表示无锁(或偏向锁),10表示重量级锁。11跟垃圾回收算法的标记有关。

轻量级锁的实现原理??

加锁过程:

当线程请求一个对象的时候,当jvm判断当前对象头中mark word,是不是重量级锁,如果不是,那么会在当前请求线程的栈帧中开辟一块空间,作为该锁的锁记录,也称作Lock Record(Lock Record中存储的mark word称作displayed mark word),然后将对象头的mark word复制到这个Lock Record中。然后,jvm会利用CAS替换当前锁对象中的mark word(假设当前锁对象的标记字段为 X…XYZ,Java 虚拟机会比较该字段是否为 X…X01。如果是,则替换为刚才分配的锁记录的地址。由于内存对齐的缘故,它的最后两位为 00。此时,该线程已成功获得这把锁,可以继续执行了。如果不是 X…X01,那么有两种可能。第一,该线程重复获取同一把锁。此时,Java 虚拟机重入会在线程栈中分配一个Displaced Mark word为null的Lock Record。第二,其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程。),让他指针指向栈的Lock Record,并且让Lock Record的owner指向锁对象的mark word。

为什么JVM选择在线程栈中添加Displaced Mark word为null的Lock Record来表示重入计数呢?

首先锁重入次数是一定要记录下来的,因为每次解锁都需要对应一次加锁,解锁次数等于加锁次数时,该锁才真正的被释放,也就是在解锁时需要用到说锁重入次数的。一个简单的方案是将锁重入次数记录在对象头的mark word中,但mark word的大小是有限的,已经存放不下该信息了。另一个方案是只创建一个Lock Record并在其中记录重入次数,Hotspot没有这样做的原因我猜是考虑到效率有影响:每次重入获得锁都需要遍历该线程的栈找到对应的Lock Record,然后修改它的值。

所以最终Hotspot选择每次获得锁都添加一个Lock Record来表示锁的重入。

解锁过程:

轻量级锁释放时需要将Displaced Mark Word替换到对象头的mark word中

(你可以将一个线程的所有锁记录想象成一个栈结构,每次加锁压入一条锁记录,解锁弹出一条锁记录,当前锁记录指的便是栈顶的锁记录)

先看当前锁记录的值是不是0,如果是,说明是重复进入同一把锁,直接弹出这个锁记录,如果不是0,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程已经成功释放这把锁。如果不是,则意味着这把锁已经被膨胀为重量级锁。此时,Java 虚拟机会进入重量级锁的释放过程,唤醒因竞争该锁而被阻塞了的线程。

上一篇下一篇

猜你喜欢

热点阅读