Synchronized 关键字
Synchronized 是 Java 中的一种锁的方式,是在 JVM 层面一种锁。在 jdk 1.6以前是一种重量级锁,在经历过优化后 Synchronized 锁已经没有那么“重”了。
Synchronized 有 3 种使用方式:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的Class对象
- 同步代码块,锁是Synchonized括号里配置的对象
1 | private static int i; |
当一个线程试图访问同步代码块时,首先必须获得锁,那么这个锁的位置到底在哪呢?锁到底包含了那些信息?
要想弄清楚上面两个问题,需要引入一个对象头的概念。
1. java 对象头
首先看下Java 的对象结构,如图 1:
Java 对象分为 3 个部分,对象头
,实例数据
和对齐空间
。我们重点关注的对象头,其他的两项对于我们 synchronized
没有影响。
synchronized 用的锁是存在 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于 4 字节,即32bit。
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 |
32/32bit | ArrayLength | 这个标记一般没有,除非锁定的对象是数组,这个表示是数组的长度 |
其中 Mark Word 在默认情况下存储着对象的 HashCode、分代年龄、锁标记位等以下是32位 JVM 的 Mark Word 默认存储结构
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象HashCode | 对象分代年龄 | 0 | 01 |
在运行期间,Mark Word 里存储的数据会随着锁标志位的变化而变化。Mark Word 可能变化为存储以下 4 种数据,如图2 :
2. 锁膨胀
上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:
无锁可偏向——>偏向锁——>轻量级锁——>重量级锁
无锁可偏向——>无锁不可偏向——>轻量级锁——>重量级锁
并且膨胀方向不可逆(某些苛刻的条件是可逆的)。
2.1 偏向锁
如果想升级偏向锁,需要有两个条件:
- 打开偏向锁开关(-XX:BiasedLockingStartupDelay=0 ),1.8 版本是默认 JVM 启动 40s 后开启
- 不能运行 hashcode 方法,因为偏向锁需要 java 对象头存储线程的 ID ,如果计算了 hashcode 对象头的空间会被 hashcode 占用。
一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
核心思想:
- 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
- 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行。
- 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
- 执行同步代码。
2.2 轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
实现逻辑
轻量级锁的实现是在每一个线程中产生一个 lock record,在竞争锁的时候,采用 CAS 的方式把 Mark Word 的指针指向当前 Lock Record 的地址。
2.3 重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
实现逻辑:
从上面的分析中,我们可以得知,Synchronized 关键字的锁存在 Java 对象头中,通过判断对象头的标记位来判断当前锁的状态,如果线程拿不到锁会一直等待,这个是怎么实现的呢?
在查询 JVM 规范可以得到 Synchronized 执行对应了两个 JVM 指令,MonitorEnter 和 MonitorExit 两个指令。同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的
首先我们先了解下 monitor (管程) 这个对象,在 Java 中所有对象都有一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
在Java虚拟机 (HotSpot) 中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件:
1 | ObjectMonitor() { |
ObjectMonitor中有两个队列,_WaitSet
和 _EntryList
,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner
指向持有 ObjectMonitor
对象的线程,
当多个线程同时访问一段同步代码时,首先会进入
_EntryList
集合当线程获取到对象的
monitor
后进入_Owner
区域并把monitor
中的owner
变量设置为当前线程,同时monitor 中的计数器 count 加 1若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。
若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁) 。
2.4 总结下锁升级的过程
3. 其他特性
3.1 锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。
1 | private void syncMethod1() { |
3.2 锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。
1 | private void syncMethod2() { |
3.3 适应性自旋(Adaptive Spinning)
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。