synchronized用的锁是存在于Java对象头里的。
上篇文章开始说了每个java对象都有一个与之相关联的monitor对象,当一个monitor被持有后,对象就处于锁定状态。那他们是怎么相关联的呢,其实 monitor对象(引用)是存在于java对象的对象头 的Mark word中的。
还有我们经常说synchronize关键字给对象上锁进行同步,那么怎么才是给对象上锁了呢,其实就是改变Java对象头里的信息,来加不同的锁,从而来表示无锁,偏向锁,轻量锁,重量锁。
说对象头之前,先说一下对象的结构:
Java对象在JVM内存中分为三块区域:对象头,实例数据和对齐填充。
1.对象头
对象头包含以下部分:Mark Word, 类型指针,如果对象是数组的话,还会存在一个数据数组长度,用来记录数据长度。
1.Mark Word 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。
2.类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 。
2.实例数据
实例数据存储的是真正的有效数据,即各个字段的值。无论是子类中定义的,还是从父类继承下来的都需要记录。这部分数据的存储顺序受到虚拟机的分配策略以及字段在类中的定义顺序的影响。
3.对齐填充
这部分数据不是必然存在的,因为对象的大小总是8字节的整数倍,该数据仅用于补齐实例数据部分不足整数倍的部分,充当占位符的作用。
64位JVM对象头的结构:
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
JDK对synchronized关键字的优化都是基于对象头中的Mark Word进行优化的,当对象被不同数量线程去竞争时,这时对象的锁状态会发生改变,来标识这时对象处于一个无锁,偏向锁,轻量级锁和重量级锁状态。
1、无锁
也就是对象的Monitor对象没有被线程所持有,代表的是对象处于无锁状态。
Java对象头的信息:
2、偏向锁
偏向锁是JDK1.6后面引起的一项锁优化技术,在无锁竞争的情况下,一个线程通过一次CAS操作来尝试将对象头中的Thread ID字段设置为自己的线程号,偏向锁的标识是否设置成1,锁标志位是否为01,如果设置成功,则获得锁。
偏向锁升级为轻量级锁的条件:
当有第二个线程竞争锁时,偏向锁升级为轻量级锁。
具体过程:当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,说明存在锁的竞争,当到达全局安全点(safepoint,在这个时间点上没有字节码正在执行)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
偏向锁指的是这个锁会偏向于第一个获得它的线程。
偏向锁的获取和撤销:
https://blog.csdn.net/tongdanping/article/details/79647337
偏向锁是解决的问题是:大多数时候,锁总是由同一线程多次获得,不存在多线程竞争,为了减少获取锁的代价,设置了偏向锁。 因此偏向锁不用被释放,下次该线程继续访问的时候无需再获取偏向锁了。
3.轻量级锁(自旋锁)
表示线程通过一定数量的CAS操作(JDK1.6后默认10次)完成加锁和解锁操作,如果锁获取失败,会通过自旋来获取,竞争的线程不会阻塞,如果通过一定数量的CAS操作还是获取失败,表示此时存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
轻量级锁解决的问题是:少量线程竞争同一个资源并且他们的操作时间比较短,因此不需要将线程阻塞(因为阻塞的代价比较大),没有竞争到锁的线程会轮询固定的次数来获取轻量级锁。
轻量级锁升级为重量级锁的条件:
当自旋超过一定的次数(10次),或者一个线程在持有锁,一个在自旋,又有第三个来访时(发生了多个线程竞争锁),轻量级锁升级为重量级锁。
轻量级锁的加锁过程
- 当代码进入同步代码块的时候,如果同步对象锁状态是无锁状态(锁标志位是01,是否为偏向锁是0),虚拟机首先在当前的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图一所示。
- 拷贝对象头中的Mark Word复制到锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图二所示。
- 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
图1 — 轻量级锁CAS操作之前堆栈与对象的状态
图2— 轻量级锁CAS操作之后堆栈与对象的状态
轻量级锁的解锁过程:
- 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
- 如果替换成功,整个同步过程就完成了。
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀,Mark Word存的是指向重量级锁的指针),那就要在释放锁的同时,唤醒被挂起的线程。
4.重量级锁
当一个锁被两条或两条以上的线程竞争的时候,这时候轻量级锁就会演变成重量级锁。
重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
jdk1.6之前使用的是重量级锁,需要操作系统调用函数来使线程阻塞。
阻塞或唤醒一个Java线程需要操作系统切换CPU状态(内核态,用户态)来完成,这种状态转换比较耗时。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
三种锁的优缺点比较
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |