Java中synchronized的底层实现原理

开发 前端
通过synchronized进行加锁,就是通过对象头的Mark Word关联起来的,里面记录着锁状态和占有锁的线程地址指针。

一、对象头、Mark Word、monitor、synchronized怎么关联起来

(1)首先java里面每个对象JVM底层都会为它创建一个监视器monitor,这个是JVM层次为我们保证的。这个监视器就类似一个锁,哪个线程持有这个monitor的操作权,就相当于获取到了锁

(2)其次synchronized 修饰的代码或者方法,底层会生成两条指令分别为monitorenter、monitorexit。

(3)进入synchronized的代码块之前会执行monitorenter指令,去申请monitor监视器的操作权,如果申请成功了,就相当于获取到了锁。如果已经有别的线程申请成功monitor了,这个时候它就得等着,等别的线程执行完synchronized里面的代码之后就会执行monitorexit指令释放monitor监视器,这样其它在等待的线程就可以再次申请获取monitor监视器了。

monitor又是个啥东西?为什么monitor能当做锁?首先既然你知道每个对象都有一个monitor监视器,那你知道每个对象是怎么和它的monitor监视器关联起来的不?

通过synchronized进行加锁,就是通过对象头的Mark Word关联起来的,里面记录着锁状态和占有锁的线程地址指针。

当Mark Word中最后两位的锁标志位是10的时候,Mark Word的前面是monitor监视器的地址,我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图(32位):

二、monitor内部结构

monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的。

其实monitor在C++底层也是某个类的对象,那个类就是ObjectMonitor,它拥有的属性也字段如下:

//结构体如下
ObjectMonitor::ObjectMonitor() {
_header;
_count ; // 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示加锁的次数
_waiters;
_recursions;
_owner; // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁
_waitset; // wait线程的集合,在synchorized代码块中调用wait()方法的线程会被加入到此集合中沉睡,等待别人叫醒它
_waitsetLock;
_responsiable;
_succ;
_cxq;
_freenext;
_entrylist; // 非常重要,等待队列,加锁失败的线程会被加入到这个等待队列中,等待再次争抢锁
_spinFreq; // 获取锁之前的自旋的次数
_spinclock; // 获取之前每次锁自旋的时间
ownerIsThread;
}

3.1、monitor加锁原理

_count : 这个属性非常重要,直接表示有没有被加锁,如果没被线程加锁则 _count=0,如果_count大于0则说明被加锁了

_owner:这个属性也非常重要,直接指向加锁的线程,比如线程A获取锁成功了,则_owner = 线程A;当_owner = null的时候表示没线程加锁

_waitset:当持有锁的线程调用wait()方法的时候,那个线程就会释放锁,然后线程被加入到monitor的waitset集合中等待,然后线程就会被挂起。只有有别的线程调用notify将它唤醒。_entrylist:这个就是等待队列,当线程加锁失败的时候被block住,然后线程会被加入到这个entrylist队列中,等待获取锁。

_spinFreq:获取锁失败前自旋的次数;JDK1.6之后对synchronized进行优化;原先JDK1.6以前,只要线程获取锁失败,线程立马被挂起,线程醒来的时候再去竞争锁,这样会导致频繁的上下文切换,性能太差了。JDK1.6后优化了这个问题,就是线程获取锁失败之后,不会被立马挂起,而是每个一段时间都会重试去争抢一次,这个_spinFreq就是最大的重试次数,也就是自旋的次数,如果超过了这个次数抢不到,那线程只能沉睡了。_spinClock:上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。

下面通过图文展示加锁过程:

(1)首先呢,没有线程对monitor进行加锁的时候是这样的:

说明:_count = 0 表示加锁次数是0,也就是没线程加锁;_owner 指向null,也就是没线程加锁

(2)然后呢,这个时候线程A、线程B来竞争加锁了,如下图所示:

(3)线程A竞争到锁,将_count 修改为1,表示加锁次数为1,将_owner = 线程A,也就是指向自己,表示线程A获取到了锁。在_count = 0,_owner = null的时候,表示monitor没人加锁,这个时候线程A和线程B同时请求加锁,也就是竞争将_count改为1。由于线程A这哥们动作比较快,它将_count改为1,获取锁成功了。它还嘚瑟了一下,同时将_onwer = 线程A,表示自己获取了锁,告诉线程B,兄弟不好意思了,是我获取了锁,我先去操作了。

既然加锁就是将_count 设置为1,同时将_owner 指向自己。那反过来推测,释放锁的时候是不是将_count 设置为 0 , 将 _owner 设置为 null 就 OK了?是的,释放锁的过程就是这么简单:

加锁和释放锁说完了,我们接下来将的是

_spinFreq、_spinclock、_entrylist

这几个东西:

上面解释字段属性的时候说_spinFreq是等待锁期间自旋的次数、_spinclock是自旋的周期也就是每次自旋多久时间、_entrylist这个就是自旋次数用完了还没获取锁,只能放到_entrylist等待队列挂起了。

让我们继续接着图来讲:

(1)首先线程B获取锁的时候发现monitor已经被线程A加锁了(2)然后monitor里面记录的_spinFreq 、spinclock 信息告诉线程B,你可以每隔50ms来尝试加锁一次,总共可以尝试10次(3)如果线程B在10次尝试加锁期间,获取锁成功了,那线程B将_count 设置为 1,_owner 指向自己表示自己获取锁成功了(4)如果10次尝试获取锁此时都用完了,那没辙了,它只能放到等待队列里面先睡觉去了,也就是线程B被挂起了

_spinFreq和_spinclock 这两个monitor的属性主要是让线程自旋的时候使用的吧。

entryList作用是当线程自旋次数都用完了之后,只能进入等待队列进行休眠了。

4.6、轻量级锁

轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁。就好比下面的图,线程A获取了轻量级锁,锁记录存在线程A的虚拟机栈中,然后Mark Word的前面30位存储锁记录的地址。

了解了轻量级加锁的原理之后,我们继续,来讲讲偏向锁升级为轻量级锁的过程:

(1)首先线程A持有偏向锁,然后正在执行synchronized块中的代码

(2)这个时候线程B来竞争锁,发现有人加了偏向锁并且正在执行synchronized块中的代码,为了避免上述说的线程A一直持有锁不释放的情况,需要对锁进行升级,升级为轻量级锁

(3)先将线程A暂停,为线程A创建一个锁记录Lock Record,将Mark Word的数据复制到锁记录中;然后将锁记录放入线程A的虚拟机栈中

(4)然后将Mark Word中的前30位指向线程A中锁记录的地址,将线程A唤醒,线程A就知道自己持有了轻量级锁

4.6.2、在轻量级锁模式下,多线程是怎么竞争锁和释放锁的?

(1)线程A和线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中

(2)同时执行CAS操作,将Mark Word前30位设置为自己锁记录的地址,谁设置成功了,锁就获取到锁

上面讲了加锁的过程,轻量级锁的释放很简单,就将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。

Java synchronized偏向锁后hashcode存在哪里?

jdk8偏向锁是默认开启,但是是有延时的,可通过参数: -XX:BiasedLockingStartupDelay=0关闭延时。

hashcode是懒加载,在调用hashCode方法后才会保存在对象头中。

当对象头中没有hashcode时,对象头锁的状态是 可偏向( biasable,101,且无线程id)。

如果在同步代码块之前调用hashCode方法,则对象头中会有hashcode,且锁状态是 不可偏向(0 01),这时候再执行同步代码块,锁直接是 轻量级锁(thin lock,00)。

如果是在同步代码块中执行hashcode,则锁是从 偏向锁 直接膨胀为 重量级锁。

责任编辑:武晓燕 来源: 今日头条
相关推荐

2021-01-08 08:34:09

Synchronize线程开发技术

2024-03-15 15:12:27

关键字底层代码

2022-10-28 10:23:27

Java多线程底层

2017-12-06 16:28:48

Synchronize实现原理

2022-04-13 14:43:05

JVM同步锁Monitor 监视

2024-03-07 07:47:04

代码块Monitor

2019-05-27 08:11:13

高并发Synchronize底层

2023-01-04 07:54:03

HashMap底层JDK

2023-07-11 08:00:00

2017-02-27 10:43:07

Javasynchronize

2022-12-19 08:00:00

SpringBootWeb开发

2017-10-23 10:13:18

IO底层虚拟

2020-08-23 10:03:51

SynchronizeJava

2021-07-04 08:01:30

Synchronize线程安全并发编程

2021-10-26 13:18:52

Go底层函数

2024-01-29 08:00:00

架构微服务开发

2023-07-17 08:02:44

ZuulIO反应式

2024-03-14 14:56:22

反射Java数据库连接

2019-10-31 10:08:15

Synchronize面试线程

2024-02-29 16:49:20

volatileJava并发编程
点赞
收藏

51CTO技术栈公众号