Synchronized实现原理

一个对象在内存中分为对象头,实例变量,和对其填充。
实例变量是用来存本对象的属性信息,对其填充使该对象保持占用8字节的整数倍。

那对象头具体是什么呢?

对象头分为三部分:MarkWord,指向类的指针,数组长度(只有数组对象才有)。

那么问题就来了,MarkWord是什么?
MarkWord存了对象跟锁有关的信息。这就是今天我们要讲的内容。

具体见下图,64位虚拟机时。
在这里插入图片描述

首先我们看重量级锁,标志为是10.储存了指向监视器monitor对象的指针,monitor对象是有C+实现的,结构如下:

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }

首先,所有并发获取锁的线程id会被放入_EntryList里,当一个线程获取到锁时,将自己的对象头里的重量级锁指向该monitor对象,并将monitor对象里_owner指向自己,将monitor里的_count加1.
这时如果该对象调用wait()方法,则将monitor中_count 减1,将_owner置为null。并将该线程ID放入_waitSet里,等待唤醒。
当线程执行完毕也进入waiting状态,也释放monitor对象,同上。

那么锁是升级的过程是怎样的?MarkWord是怎么变化的?

1.无锁状态:首先,当对象没有被当初一个锁时,此时MarkWord记录着对象的hashcode,锁标志为为01,是否偏向为0。

2.偏向锁状态:当线程A使用该对象加锁时,MarkWord中将记录该线程的ID,并将是否偏向改为1。即此时使用偏向锁。

当某线程再次获取该锁时,会比较线程ID和MarkWord中的线程ID是否一致,如一致则再次获取锁,若不一致说明有竞争,不过由于偏向锁并不会主动释放锁,所以会使用CAS获取锁,获取成功则该线程获取到锁,将MarkWord中的线程ID改为自己ID。锁CAS获取锁失败,进入步骤3.

3.轻量级锁状态:进入此步骤说明,有多个线程竞争同一个锁,那么将在锁升级为轻量级锁。
升级过程是,在线程中开辟锁记录空间(Lock Record),用户储存MarkWord的拷贝,并将MarkWord和Lock Record相互指向,此操作为CAS操作。将MarkWord锁标志置为00。
如果CAS成功,说明此线程获取到轻量级锁。若失败,进入步骤4.

4.自旋锁:自旋锁不是一种锁状态,而是一种策略。由于此时需要阻塞没有拿到锁的线程。不过阻塞线程需要CPU从用户态转为内核态,开销较大。如果下一刻就可以拿到锁了,一没拿到锁就进入内核态很显然是不合适的。所以,此时使用自旋来减少开销。自旋一段时间成功获得锁,则仍在轻量级锁状态。否则轻量级锁膨胀为重量级锁。即步骤5.

5.重量级锁状态:将锁标志为置为10,将MarkWord中指针指向重量级所monitor。并阻塞所有没有获取到锁的线程。

总结:

锁的升级过程是对锁的开销进行的尽可能的优化。首先无锁状态和偏向锁使用简单的标志位来判断锁的持有,避免CAS的开销。当已经使用偏向锁时,才使用CAS获取锁。当CAS失败时,进行生成栈中的锁记录空间,与MarkWord的拷贝与相互指向,升级成轻量级锁,若该CAS操作失败,开始使用自旋避免CPU频繁的上下文切换的开销。自旋失败,才升级为重量级锁。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐