Synchronized的锁机制

这段时间了解了一下Java中的Synchronized和J.U.C,从中整理出一些关于的概念。

关于Synchronized的用法,在之前的这篇博客中也学习到了,现在来看看Synchronized在JVM中的实现。

Java对象在JVM中的结构

在Hotspot虚拟机当中,对象在内存中的存储布局可以分为3块区域:对象头、实例数据、对齐填充。

java对象
  • 对象头

    相当于对象的元数据部分,存储了对象自身的运行时数据和类型指针

  • 实例数据

    对象实例数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定

  • 对齐填充

    Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。这意味着如果对象头加上实例数据的长度不是8字节的整数倍,就需要加上大小合适的对齐填充进行8字节对齐

java对象头

HotSpot虚拟机的对象头包括三部分信息:

  • Mark Word

    第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中分别位32bit和64bit

  • 类型指针

    对象头的另外一部分是类型指针,即对象只想他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • 数组长度

    如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度的数据

整体结构

Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。

而上述提到的各种锁状态,需要依靠对象的Mark Word来实现。

上文提到,MarkWord用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中分别位32bit和64bit。但是,对象需要存储的运行时数据很多,已经超出了32bit和64bit结构所能记录的限度。考虑到虚拟机的空间效率,Mark Word被设计程了一个非固定的数据结构,他会根据对象的状态复用自己的存储空间。

32位虚拟机中MarkWord的结构

上图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。

偏向锁

偏向锁是JDK1.6中引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语,进一步提高程序运行的性能。

当锁对象第一次被线程获取时(无锁状态),虚拟机会把Mark Word中的锁标识位设为”01“,将是否是偏向锁标识设为”1“,然后使用CAS操作把获取到这个锁的线程ID记录在Mark Word当中,表示该线程持有了这个对象的偏向锁。

持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机只是简单的判断Mark Word当中的线程ID是否是当前线程的ID,然后直接进入临界区执行,不再进行任何的同步操作。

轻量级锁

假设对象的偏向锁已经被一个线程A持有。当有另外一个线程B尝试获取这把锁时,偏向模式宣告结束,升级为轻量级锁。

虚拟机首先将锁对象恢复为无锁状态,然后在线程A和B的栈帧中分别分配了一个名为”Lock Record“的空间,并把锁对象的Mark Word复制到”Lock Record“,叫做”Displaced Mark Word“。

虚拟机使用CAS操作尝试将对象的Mark Word更新为指向某个线程栈帧Lock Record的指针,如果这个动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word中的锁标识位更新为”00“,表示此对象处于轻量级锁定状态。

轻量级锁

而没有获取锁的线程也不会阻塞,而是通过自旋进行等待。持有轻量级锁的线程退出临界区时,需要进行解锁。通过CAS操作把线程当前栈帧中的Displaced Mark Word复制回对象的Mark Word。自旋等待的线程此时就可以以同样的方式获取对象的轻量级锁了。

很明显,如果没有竞争或轻度竞争,轻量级锁仅仅使用CAS操作和Lock Record就避免了重量级互斥锁的开销。

重量级锁

在轻量级锁中,没有获取锁的线程通过自旋进行等待。这个依据是持有锁的线程会很快的释放掉锁,倘若持有锁的线程一直不释放锁,那么自旋等待的线程就会白白的浪费CPU。为了避免这种情况,等待的线程等到一定的自旋循环次数,就会放弃等待而挂起,让出CPU。此时,轻量级锁就升级为重量级锁。如果有两条以上的线程争用同一把锁,轻量级锁也会升级为重量级锁。

重量级锁依赖于操作系统底层的互斥量(Mutex Lock)。这个重量级锁就是我们常提到的监视器锁(Monitor)。

为什么称这种锁为重量级锁?升级到重量级锁后,没有获取到锁的线程就会被阻塞。由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。

重量级锁

升级为重量级锁后,虚拟机将Mark Word锁标识位更新为10,并且将moniter对象的地址更新到Mark Word当中。

moniter对象可以理解为一种同步工具,其同步的过程类似于之前介绍到的AQS。与AQS通过state变量表示同步状态类似,moniter通过操作系统底层的互斥量来实现同步。moniter也包含了同步队列和条件队列,故Java的Object对象拥有notify、wait方法来进行线程同步。

总结

轻量级锁能提升程序同步性能的依据是”对于绝大部分的锁,在整个同步周期内都是不存在竞争的“,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,通过自旋等待,避免了线程切换的开销。

而偏向锁的目的是消除数据在无竞争情况下的同步原语,在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

但是,如果存在大量的锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在竞争频繁的情况下,轻量级锁反而比传统的重量级锁更慢。

其他

上文提到了对象的Mark Word是非固定的数据结构,其空间是可以重用的,所谓的重用就体现在在轻量级锁或重量级锁的状态下,原来无锁状态下存放hash code等数据的空间被放入了指向锁记录的指针。那么,hash code这部分数据去哪里了呢?

这是一个针对HotSpot VM的锁实现的问题。简单答案是:
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
请一定要注意,这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。

参考自当Java处在偏向锁、重量级锁状态时,hashcode值存储在哪?