导读:synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面:
语义上来讲,synchronized主要有三种用法:
1. 实现原理
synchronized 同步代码块的语义底层是基于对象内部的监视器锁(monitor),分别是使用 monitorenter 和 monitorexit 指令完成。其实 wait/notify 也依赖于 monitor 对象,所以其一般要在 synchronized 同步的方法或代码块内使用。monitorenter 指令在编译为字节码后插入到同步代码块的开始位置,monitorexit 指令在编译为字节码后插入到方法结束处和异常处。JVM 要保证每个 monitorenter 必须有对应的 moniorexit。
monitorenter:每个对象都有一个监视器锁(monitor),当 monitor 被某个线程占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获得 monitor 的所有权,即尝试获取对象的锁。过程如下:
monitorexit:执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。执行指令时,monitor 的进入数减1,如果减1后进入数为0,则线程退出 monitor,不再是这个 monitor 的所有者,其他被这个 monitor 阻塞的线程可以尝试获取这个 monitor 的所有权。
在 HotSpot JVM 中,monitor 由 ObjectMonitor 实现,其主要数据结构如下:
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表(每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程。
过程如下图所示:
在 JDK1.6 之后,出现了各种锁优化技术,如轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除等,这些技术都是为了在线程间更高效的解决竞争问题,从而提升程序的执行效率。
通过引入轻量级锁和偏向锁来减少重量级锁的使用。锁的状态总共分四种:无锁状态、偏向锁、轻量级锁和重量级锁。锁随着竞争情况可以升级,但锁升级后不能降级,意味着不能从轻量级锁状态降级为偏向锁状态,也不能从重量级锁状态降级为轻量级锁状态。
无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁
要理解轻量级锁和偏向锁的运行机制,还要从了解对象头(Object Header)开始。对象头分为两部分:
monitor 监视器锁本质上是依赖操作系统的 Mutex Lock 互斥量 来实现的,我们一般称之为重量级锁
。因为 OS 实现线程间的切换需要从用户态转换到核心态,这个转换过程成本较高,耗时相对较长,因此 synchronized 效率会比较低。
重量级锁的锁标志位为'10',指针指向的是 monitor 对象的起始地址,关于 monitor 的实现原理上文已经描述了。
轻量级锁
是相对基于OS的互斥量实现的重量级锁而言的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用OS的互斥量而带来的性能消耗。
轻量级锁提升性能的经验依据是:对于绝大部分锁,在整个同步周期内都是不存在竞争的
。如果没有竞争,轻量级锁就可以使用 CAS 操作避免互斥量的开销,从而提升效率。
轻量级锁的加锁过程:
1、线程在进入到同步代码块的时候,JVM 会先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如图所示:
2、JVM 使用 CAS 操作尝试将对象头中的 Mark Word 更新为指向 Lock Record 的指针。如果更新成功,则执行步骤3;更新失败,则执行步骤4
3、如果更新成功,那么这个线程就拥有了该对象的锁,对象的 Mark Word 的锁状态为轻量级锁(标志位转变为'00')。此时线程堆栈与对象头的状态如图所示:
4、如果更新失败,JVM 首先检查对象的 Mark Word 是否指向当前线程的栈帧
轻量级锁
就要升级为重量级锁
(锁的标志位转变为'10'),Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也就进入阻塞状态轻量级锁的解锁过程:
1、通过 CAS 操作用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word
2、如果替换成功,整个同步过程就完成了
3、如果替换失败,说明有其他线程尝试过获取该锁,那就在释放锁的同时,唤醒被挂起的线程
轻量级锁
是在无多线程竞争的情况下,使用 CAS 操作去消除互斥量;偏向锁
是在无多线程竞争的情况下,将这个同步都消除掉。
偏向锁提升性能的经验依据是:对于绝大部分锁,在整个同步周期内不仅不存在竞争,而且总由同一线程多次获得
。偏向锁会偏向第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程不需要再进行同步。这使得线程获取锁的代价更低。
偏向锁的获取过程:
偏向锁的释放过程:
偏向锁、轻量级锁、重量级锁之间的状态转换如图所示(概括上文描述的锁获取和释放的内容):
下面是这几种锁的比较: