synchronized锁

关于synchronized锁的一些问题

什么是synchronized锁

官方:同步方法支持一种简单的策略来防止线程受到干扰和内存一致性错误;如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成

通俗点来说就是程序中用于保护线程安全的一种机制。

底层原理

HotSpot虚拟机中,对象内存布局主要分为对象头(Header)、示例数据(Instance Data)和对齐填充(Padding)。

对象头 = Mark Word + 类型指针(Klass pointer)。

类型指针(Klass pointer : 用于标识JVM通过这个指针来确定这个对象是哪个类的实例。

Mark Word : 用于储存对象自身的运行时数据,例如对象的hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程的ID、偏向时间戳等。在运行期间,Mark Word里存储的数据会随着内部锁标志位的变化而变化。

无锁状态(01)

对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01。

偏向锁状态(01)

还是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01。

轻量级锁状态(00)

开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00。

重量级锁状态(10)

30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为10。

GC标记(11)

开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。

之所以这些标志位会变化,是JVM对重量级锁的一种优化,在jdk1.6 之前,用synchronized 都是重量级锁 ,都需要底层操作系统的Mutex Lock(互斥锁)来实现,这个是涉及到系统调用的,会导致内核空间与用户空间的上下文切换,很低效。

于是jdk1.6之后为了增加性能减少操作Mutex Lock,引入了偏向锁轻量级锁:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

锁升级过程

1
2
3
4
5
6
Object lock = new Object();
// 临界区之前
synchronized(lock){
// 临界区
}
// 临界区之后

无锁

当有线程进入临界区之前(synchronized块之前),lock里的对象头的 Mark Word 结构为下面所示:

锁标志位01,无偏向。这是JVM给的初始值。

偏向锁

线程A刚进入临界区时(synchronized),发现标志位是01 ,而且无偏向。立马把当前线程ID记录到了这个Mark Word当中,修改为偏向(1)。也就是下面这样:

如果线程A,再次进入临界区时 , 判断线程id 是当前自己,就直接执行。也就是说,如果一直没有其它线程过来,线程A就跟没加synchronized一样执行,效率极高。这就是偏向锁

轻量级锁

当有其它线程B 也进入临界区了(线程A还没出临界区),就CAS自旋等待很短的时间,如果线程A此时恰好退出临界区了,CAS退出就升级为轻量级锁,否则就是重量级锁。 轻量级锁会构造一个Lock Record锁记录,如下图:

Lock Record 锁记录

Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:

Lock Record 描述
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
RcThis 表示blocked或waiting在该monitor record上的所有线程的个数;
Nest 用来实现 重入锁的计数;
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免不必要的阻塞或等待线程唤醒ngjio,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁

重量级锁

Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”

重量级锁的Mark Word结构图:

这个重量级锁的指针指向的就是 ObjectMonitor

锁升级图

图源:收割Offer:互联网大厂面经——布兜