目录
Synchronized是Java用于保存线程安全的一个关键字,对于它的使用在之前已经描述过,这里不再赘述。这里主要描述它底层的工作原理以及实现流程。
一段被Synchronized关键字修饰的代码进行反编译后,会发现两个指令,分别是monitorenter和monitorexit。这两个指令包裹的临界区代码表示同步。
在了解基本流程前需要了解两个关键点,分别是monitor和对象头
monitor
monitor意为监视器,由ObjectMonitor实现,在Java中每个对象中都包含一个monitor,用于线程的同步,因此Java中的每个对象都可以作为锁使用。该对象中包含两个队列_waitSet,_EntryList以及一个对象_owner。
当多线程访问同一段同步代码时,进入_EntryList中,若某线程获取到monitor使用权,则进入临界区执行。此时monitor处于锁定状态,_owner对象指向该线程,锁的计数器+1,表示当前线程持有锁,若未获取到,则处于阻塞状态。若当前线程在执行过程中调用了wait()方法,则释放锁,并进入_waitSet集合。若顺利执行结束,则释放锁,则锁的计数器-1,当计数器为0时,表示该锁为可获取状态。
对象头
Java的对象头中存储了两个对象,分别是klass Pointer(类型指针)和Mark word(标记字段),虾米那分别介绍二者的作用
(1)klass Pointer 是对象指向元数据的指针,JVM虚拟机会通过该指针确定一个对象所属的类
(2)Mark word 用于存储对象自身的运行时数据,例如哈希码,锁的状态标志,线程所持有的锁,因此Mark word是实现轻量级锁和偏向锁的关键。
Mark Word | ||
---|---|---|
状态 | 标志位 | 存储内容 |
未锁定 | 01 | 哈希码,分代年龄 |
轻量级锁定 | 00 | 指向锁的指针 |
重量级锁定 | 10 | 指向重量级锁的指针 |
可偏向 | 01 | 偏向线程ID |
锁的获取与升级
在阐述锁升级的过程前,需要先了解一下四种锁
1.无锁
表示所有线程都可以访问修改锁,但同一时刻,仅有一人成功
2.偏向锁
特点:在但线程执行的情况下,该线程在后续访问时,可以自动获取锁,降低损耗,JVM在对象头中的Mark word字段中设置线程Id表示偏向。
获取流程:
step1:访问Mark word中偏向标志是否为1,若为1,确认可偏向
step2:测试线程Id是否执行当前线程,若是,执行step3
step3:若线程Id为指向,则通过CAS操作竞争锁,竞争成功,修改线程Id,执行临界区资源
step4:失败,说明当前还存在其他线程竞争,即非但线程的情况,升级为轻量级锁
3.轻量级锁
特点:不支持并发,而是多线程串行访问资源,但是每次执行都会重新获取锁,消耗性能
获取流程:
step1:访问临界区代码块时,创建锁记录,即Lock Record对象,用于保存Mark word和对象引用
step2:尝试使用线程的mark word替换锁的mark word
step3:成功,则锁对象中的mark word为Lock Record,执行临界区代码
step4:失败,判断锁对象的mark word是否指向当前线程的栈帧,若是,则说明当前线程拥有锁,直接执行临界区代码,否则有多线程竞争,升级为重量级锁
4.重量级锁
支持并发,同一时间,仅有一个线程可以访问临界区资源,依赖操作系统的互斥锁实现,因此在实现时,需要操作系统做内核态与用户态的接环,资源消耗大。
5.自旋锁
在线程尝试获取锁,但未获取到时,不挂起而是执行一个空循环,以等待锁的释放,再次获取锁,但并不是一直执行空循环,当到达阈值的时候,线程不再等待,该阈值一般为一个上下文的时间。
流程图如下所示: