锁的实现原理
锁具体表现的三种形式
1、对于普通的同步方法,锁是当前的实例对象。
2、对于静态同步方法,锁是当前类的Class对象。
3、对于同步方法块,锁是Synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先要得到锁,退出或者抛出异常的时候必须释放锁。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令来实现的,方法同步虽然与代码块同步的在细节方面有所不同,但是也可以用monitorenter和monitorexit来解释:
monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法的结束初和异常处,JVM要保证每个monitorenter都要有一个monitorexit与之相对应。任何对象都要有一个monitor与之相关联,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会获取对象所对应的monitor的所有权,即尝试获取对象的锁。
注意:Synchronized用的锁是存在Java的对象头里的。
偏向锁与轻量级锁
锁的级别:
无锁状态<偏向锁状态<轻量级锁状态<重量级锁状态
注意:锁只能升级,不能降级
1、偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程的竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试对象头的Mark Work里是否存储着指向当前线程的偏向锁。如果测试成功则表示已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前时偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
(1)偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程试图竞争偏向锁的时候,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(这个时间点没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的进程是否还活着,如果线程不处于活动的状态,则将对象头设置成无锁的状态;如果线程仍然活着,则拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么回复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
(2)关闭偏向锁
偏向锁在Java6和Java7中默认时启用的,但是它是在应用程序启动几秒之后才激活,如果有必要,可以通过JVM参数来关闭延迟。如果能确定应用程序的锁通常都处于一种竞争的状态,可以通过JVM来关闭偏向锁。
2、轻量级锁
(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word 替换回对象头,如果成功。则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成为重量级所。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得的锁被阻塞住了),一旦锁升级成重量锁,就不会再会恢复到轻量级的锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后就会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。