Java中的每一个对象都可以作为锁,具体表现为以下3种形式。
- 对于普通同步方法,锁的是当前实例对象。
- 对于静态同步方法,锁的是当前类的Class对象。
- 对于同步代码块,锁的是Synchronized括号里配置的对象。
Synchronized用的锁是存在Java对象头里的Mark Word
中的。
下面是64位虚拟机下,Mark Word
的存储结构:
锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁(自旋锁)和重量级锁。
偏向锁启用参数:-XX:+UseBiasedLocking
-
获得偏向锁
假设当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把
Mark Word
中的锁标志位设置为01
、把偏向模式设置为1
,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word
中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word
里的更新操作等)。 -
撤销偏向锁
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到为锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态,后续的同步操作就按照轻量级锁那行去执行。
当一个对象已经计算过
hashcode
后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其hashcode
请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁,在Mark Word
中记录指向该重量级锁的指针,并在重量级锁中存储原对象的hashcode
。当明确程序中的大多数的锁不会被多个不同的线程访问时,可以考虑用偏向锁;如果程序中的大多数的锁都总是被多个不同的线程访问,那使用偏向锁的多余的。
-
轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储
Lock Record
(锁记录)的空间,并将Mark Word
复制到Lock Record
中,官方称之为Displaced Mark Word
。然后线程尝试使用CAS
将Mark Word
替换为指向Lock Record
的指针。如果成功,当前线程获得锁,如果失败,表示其它线程竞争锁,当前线程便尝试使用自旋来获取锁,如果自旋超过一定次数,锁会膨胀为重量级锁,这个次数阈值默认是10次,当然还有自适应自旋锁,自适应自旋锁的自旋次数阈值是有JVM自己进行动态判断的。 -
轻量级锁解锁
轻量级锁解锁时,会使用
CAS
操作将Displaced Mark Word
替换回Mark Word
,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量本身的开销外,还额外发生了CAS操作的开销,因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
当锁膨胀成重量级锁时,JVM会通过操作系统内核申请系统锁(互斥量),将指向重量级锁的指针写入到Mark Word
中,并将Mark Word
的锁标志位更改为“10”,以后线程获取锁都需要经过操作系统内核的调度,需要经过从用户态到内核态的转变,所以重量级锁比较慢。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间;同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量;同步块执行速度较长 |
JVM实现原理实际是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数器将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
HotSpot的实现方案是当线程第一次获得锁时,会在线程栈中分配一个Displaced Mark word
为Mark Word
的Lock Record
;之后如果当前线程再次获取该锁,会在线程栈中追加分配一个Displaced Mark word
为null
的Lock Record
来表示锁重入,线程栈中的Displaced Mark word
为null
的Lock Record
个数就表示重入次数。
Synchorized和ReentrantLock都是可重入锁。
- Synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现
- Synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过
ReentrantLock#isLocked
判断 - Synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的
- Synchronized是不可以被中断的,而
ReentrantLock#lockInterruptibly
方法是可以被中断的 - 在发生异常时Synchronized会自动释放锁(由javac编译时自动实现),而ReentrantLock需要开发者在finally块中显示释放锁
- ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活
- 《深入理解Java虚拟机第3版》
- 《Java并发编程的艺术》
- 死磕Synchronized底层实现--重量级锁