JVM
规范中Synchronized
代码块使用monitorenter
和monitorexit
指令实现。Synchronized
方法使用另一种方法实现,细节未说明,但同样适用这两个指令实现。
同步代码中,monitorenter
被插入开始位置,monitorexit
插入到结束处和异常处。传统锁情况下,线程执行到monitorenter
时会尝试获取monitor
所有权,即尝试获得对象锁,而在monitorexit
时解锁。
JSE1.6为了减少获得锁和释放锁的消耗,引入偏向锁和轻量级锁,于是Synchronized
对应4种锁状态: 无锁,偏向锁,轻量级锁,重量级锁。偏向锁和轻量级锁Synchronized
对应的锁放在Java
对象头里1个字长的Mark Word
中(相比获取monitor
轻量了)。
Mark Word
32位和64位Java
的Mark Word
长度不一样,但结构基本一致。Mark Word
中最后2bit为锁标志位。
锁标志位11 代表GC
标记,标志对象将被清理。
锁标志位01 代表无锁状态/偏向锁,倒数第三位为是否偏向锁标志(1bit),用于区分这两种状态。偏向锁标志为0时为无锁,Mark Word
中存放对象的HashCode
,分代年龄;偏向锁标志位1时为偏向锁,Mark Word
中HashCode
被替换为持有锁线程的id。
锁标志位00 代表轻量级锁,Mark Word
中内容被替换为指向锁记录的指针。
锁标志位10 代表重量级锁,Mark Word
中内容被替换为指向互斥量(monitor/mutex)的指针。
由于锁占用了垃圾回收要用的Mark Word
位,推测轻量/重量锁对象并不参与垃圾回收。
锁记录
线程进入同步块时,JVM
会在当前线程栈桢中创建存储锁记录的空间,并将对象头Mark Word
复制到锁记录(如果重入可能填0),这里存储的Mark Word
官方称为Displaced Mark Word
。线程获取锁时,就会用Displaced Mark Word
来进行CAS
。线程退出同步块时,栈桢出栈,锁记录消失。
偏向锁
加锁
CAS
修改HashCode
为当前线程id
,成功则获取偏向锁,在栈中添加锁记录,失败则CAS
自旋。若
CAS
失败,检查偏向锁中线程id
是否就是当前线程id
,如果是则线程已经获取锁。不是则申请撤销偏向锁。撤销锁
只有竞争出现才会撤销偏向锁,撤销执行时机是在全局安全点。
先检查持有锁的线程是否
active
,不是active
直接撤销。当前线程是active则遍历偏向对象的在线程栈中的锁记录,如果当前没有锁记录则撤销,如果有则升级为轻量级锁。
特点
偏向锁的引入依据是大多数情况下锁并不存在多线程竞争,偏向锁对同一线程多次锁非常友好,但是对多线程竞争的情况,撤销锁的性能消耗很大。偏向锁默认开启,在应用程序启动几秒之后才激活。
关闭偏向锁:-XX: UseBiasedLocking = false
关闭启动延时:-XX: BiasedLocingStartupDelay = 0
轻量级锁
加锁
CAS
修改Mark Word
指向锁记录指针,成功则获得锁,失败表示竞争失败。若
CAS
失败,则进入CAS
自旋。如果有两条或以上线程竞争锁,则膨胀锁为重量级锁。解锁
CAS
将Displaced Mark Word
替换到对象头。如果锁已经被其他线程膨胀为重量级锁,这时Mark Word
指向重量级锁指针,导致CAS
失败,然后线程会获取重量级锁,并解锁。特点
轻量锁竞争中,竞争锁线程使用自旋而不阻塞,响应速度快。适合每个同步任务速度快的场景。
重量级锁
加锁
如果
Mark Word
不指向一个monitor
对象(当前不是重量级锁),获取或创建一个monitor
对象,设置monitor
的header
字段为Displaced Mark Word
,owner
字段为Mark Word
指向的锁记录指针(轻量锁膨胀时,如果是无锁直接到重量级锁,则为null
),obj
字段指向锁对象,之后将Mark Word
指向monitor
。- 通过
CAS
尝试修改monitor
的onwer
字段为锁记录指针,成功则获得锁。否则阻塞,阻塞后加入一个队列中等待解锁唤醒。解锁
将monitor
的owner
设置为null
,如果等待队列中有其他线程则唤醒。特点
线程竞争不使用自旋,不会消耗CPU
,吞吐量优先时使用。参考资料
《Java并发编程的艺术》
死磕Synchronized底层实现—重量级锁
JVM锁简介:偏向锁、轻量级锁和重量级锁