锁的存储方式
在HotSpot虚拟机中,对象在内存中的存储布局,可分为三个区域:对象头(header)、实例数据(Instance Data)、对齐填充(Padding)。Synchronize的锁是存在java对象头里。
java对象头例包含两部分信息:
1)Mark Word 用于存储自身的运行时的数据,如:HashCode,GC分代年龄,锁标记、偏向锁线程ID等
2)类型指针 即对象指向它的类元信息,虚拟机通过这个指针来确定这个对象是哪个类的实例(如果java对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据)
Mark Word存储结构如图1-1 32位存储,1-2 64位存储:
1)32位虚拟机
1-1 32位MarkWord存储
2)64为虚拟机
1-2 64位MarkWord存储
在大多数情况下,锁不紧不存在多线程竞争,而且总是由同一线程多次获得,所以为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程方位加入了同步锁的代码块时,会在对象头(header)中存储当前线程ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接对比对象头(header)里面是否存储了指向当前线程的线程ID。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。
偏向锁的获取
1)首先获取锁对象头中的MarkWord,判断当前对象是否处于可偏向状态。(即当前没有对象获得偏向锁)
2)如果是可偏向状态,则通过CAS原子操作,把当前线程的ID写入到MarkWord;如果CAS成功,表示获得偏向锁成功,会将偏向锁的标记设置为1,且将当前线程的ID写入MarkWord;如果CAS失败则说明当前有其他线程获得了偏向锁,同时也说明当前环境存在锁竞争,这时候就需要将已获得偏向锁的线程中的偏向锁撤销掉,并升级为轻量级锁。(当存在竞争时就不能使用偏向锁)
3)如果当前线程是已偏向状态,需要检查MarkWord中的ThreadID是否和自己相等,如果相等则不需要再次获得锁,可以执行同步代码块;如果不相等,说明当前偏向的是其他线程,需要撤销偏向锁并升级到轻量级锁。
偏向锁的撤销
偏向锁的撤销,需要等待全局安全点(即在这个时间点上没有正在执行的字节码),然后会暂停拥有偏向锁的线程,并检查持有偏向锁的线程是否活着,主要有以下两种:
1)如果线程不处于活动状态,则将对象头设置成无锁状态。
2)如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的MarkWord要么重新偏向于其他线程(重偏向需要满足批量重偏向的条件),要么恢复到无锁活着标记对象不适合作为偏向锁。
偏向锁的撤销流程图
偏向锁注意事项
偏向锁在Java6和Java7里是默认启用的,,但是他是在应用程序启动几秒钟后才激活,如有必要可以使用JVM参数来关闭延迟 -XX:BiasedLockingStartupDelay=0,如果确定应用中所有的锁在通常情况下都处于竞争状态,可以通过JVM参数关闭偏向锁 -XX:- UseBiasedLocking=false,那么程序会默认进入轻量锁状态。
如果我们应用中大多数情况存在线程竞争,那么建议是关闭偏向锁,因为开启反而会因为偏向锁的撤销操作而引起更多的资源消耗。