Monitor
Synchronized的实现是基于Monitor的,而Monitor是基于管程的MESA模型,ObjectMonitor数据结构,三个队列,等待唤醒机制基于Object对象中的方法
对象的内存布局
锁的状态信息是标记在对象头的Mark Word中的。
一个对象由对象头、实例数据、对齐填充三部分组成。对象头由Mark Word、Klass point、数组长度组成。
无锁状态下Mark Word存储的是对象的hash值、gc分代年龄、偏向锁标识位0、锁标识01
偏向锁存储ThreadId、epoch、gc分代年龄、偏向锁标识位1、锁标识01
轻量级锁存储指向栈中锁记录指针、锁标识位00
重量级锁存储指向创建monitor对象指针、锁标识位10
偏向锁
加锁优化机制、提升一把锁一直被一个线程占用,减少轻量级锁CAS重入校验开销
偏向锁延迟4s
新创建的对象默认是匿名偏向状态,ThreadId为0,可偏向但未偏向的状态。
当偏向锁被线程加锁后,偏向锁对象Mark Word中的ThreadId就为当前线程,需要注意的就是这个ThreadI的的不是JVM层面的线程id。当偏向锁被释放后锁对象的锁标识还是101
当线程1释放偏向锁后,线程2再加该锁,此时偏向锁会撤销,先进入到无锁状态,然后变为轻量级锁状态
当线程1还在占有锁,线程2一直竞争不到锁,此时偏向锁会升级为重量级锁
当在偏向锁为加锁时调用hashCode()方法,偏向锁撤销进入无锁状态
当在加锁时同步代码块中调用hashCode()方法,偏向锁会升级我重量级锁
当在加锁时同步代码块中调用wait()方法,偏向锁会升级为重量级锁
当在加锁时同步代码块中调用notify()方法,偏向锁会升级为轻量级锁
为了避免频繁的偏向锁撤销,还有批量重偏向和批量撤销机制。
轻量级锁
轻量级锁不存在自旋。
轻量级锁刚开始加锁时首先是无锁状态,然后拷贝锁对象Mark Word到线程栈中,生成一条锁记录,然后再CAS去修改对象头中的指针指向栈中锁记录,如果修改成功了就表示加锁成功,修改锁状态00,因为存在多线程竞争情况,如果CAS修改指针失败,就会进入到锁膨胀逻辑
如果加锁时不是无锁状态,就去判断可重入锁逻辑,如果对象头指针指向的是当前线程栈就表示可重入,在线程栈中再创建一条锁记录,存储null,修改锁对象头指针指向新的锁记录。如果不可重入,就去进行锁膨胀逻辑
释放锁是会把当前线程栈中锁记录信息恢复到对象头中,然后修改锁状态为无锁
如果要进行锁膨胀,首先会调用方法得到ObjectMonitor对象,然后去执行锁膨胀逻辑。调用方法得到ObjectMonitor对象的流程是首先判断当前锁状态是否已经是10重量级锁了,如果是就表示已经有线程创建完Monitor对象并修改了锁状态,此时直接返回已经创建好了的ObjectMonitor对象。
如果锁状态不是10,那么就去判断一个变量标识位,如果当前是其他线程正在创建Monitor对象过程中,那么就yield()15次,如果标识位还没有变化就park()阻塞
接下来就是创建Monitor对象了,主要流程就是创建并初始化ObjectMonitor对象,CAS去修改锁对象头指针指向新创建的Monitor对象,如果CAS成功就修改锁状态为10,如果CAS失败就释放Monitor对象重试。
所以比较常见的两种情况就是:
无锁 --> CAS加锁成功
已经加锁00 --> 不能重入 --> 尝试创建/获取Monitor对象 --> 锁膨胀自旋 --> 阻塞 --> 唤醒 --> 加重量级锁成功
重量级锁
重量级锁才会存在自旋
重量级锁是基于Monitor实现的,也就是管程的MESA模型。
重量级锁释放锁后会切换到无锁状态
当出现锁竞争,获取到Monitor对象后就会进入到锁膨胀逻辑
在锁膨胀逻辑中会经过CAS尝试加锁与2次自适应自旋,如果还未获取到锁就准备入队,生成一个Node对象,在循环中使用CAS入队,头插入,如果head指针修改失败就再尝试获取一次锁,再CAS ,再尝试,知道入队成功跳出循环
入队成功后就这边阻塞线程了,先进入一个死循环:尝试获取锁 --> 阻塞 等待唤醒 --> 尝试获取锁 --> 自适应自旋 --> 加内存屏障保证可见性
加锁成功才会跳出循环。
重量级锁释放锁流程:实现 _owner
为当前线程,并且重入数量也为0。再将_owner
置为null,这也是非公平锁的实现,能够让新线程先加到锁。加一个写屏障保证修改立刻生效。根据配置的策略移动_cxq
与_EntryList
队列元。释放锁,唤醒阻塞线程unpark()。
锁升级
偏向锁升级到轻量级锁
偏向锁升级到重量级锁
轻量级锁升级到重量级锁
锁粗化和锁消除机制