在理解锁实现原理之前先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:
实例数据和对其填充与synchronized无关,对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
锁定实例化的对象,每个对象有其独立的对象锁,互不干扰
synchronize(Object o),每一个object相当于一把锁,不同的锁有不同的钥匙,可以减少锁竞争
synchronize放在函数签名中和synchronize(this)使用的是同一对象锁,一个钥匙多把锁
JDK6之前只有两个状态:无锁、有锁(重量级锁),
而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:
其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
Synchronized是关键字,内置语言实现
Synchronized底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令
monitorenter
monitorexit
当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
上面讲到的对象头8个字节,32位
上面介绍的四种状态,并且会因实际情况进行升级,过程如下
对象头前25位存hashCode,4位存分代年龄,1位判断是否是偏向锁(0),2位锁标志位(01)
对象头23位线程ID,2位时间戳(超时会判断线程存不存在,若不存在,变为无锁状态),4位分代年龄,(1)(01),这次拿到锁之后,下次直接拥有锁
一句话总结它的作用:减少统一鲜橙获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一鲜橙多次获得,那么此时就是偏向锁。
过程: 先说无锁—>偏向锁。锁的标志位都为01。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
要注意的是,偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
30位指向虚拟机栈执行代码的地址,(00)线程不会阻塞,一前一后去执行。自己去抢线程,如果始终得不到锁,自旋(50次)会消耗cpu
30位指向monitor地址,(10),系统分配由用户空间到了内核空间,一定会有一个人能分配到内存,竞争锁的线程会被阻塞。(多个线程排队—>objectMonitor—>wait()还想排队当notify()时再次排队,不想排队则结束)