Synchronized 是 Java 中的一种锁的方式,是在 JVM 层面一种锁。在 jdk 1.6以前是一种重量级锁,在经历过优化后 Synchronized 锁已经没有那么“重”了。
Synchronized 有 3 种使用方式:
private static int i;
//1. 修饰同步方法
public synchronized void add() {
i++;
}
//2. 修饰同步静态方法
public static synchronized void addStatic() {
i++;
}
//3. 修饰同步代码块,此处锁的方式和 1 加锁方式相同
public void addCodePieceInstance() {
synchronized (this) {
i++;
}
}
//4. 修饰同步代码块,此处锁的方式和 2 加锁方式相同
public void addCodePiece() {
synchronized (SynDemo.class) {
i++;
}
}
//5. 锁定同步代码块
public void addSyncCode(){
synchronized (obj) {
}
}
当一个线程试图访问同步代码块时,首先必须获得锁,那么这个锁的位置到底在哪呢?锁到底包含了那些信息?
要想弄清楚上面两个问题,需要引入一个对象头的概念。
首先看下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 :
上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:
无锁可偏向——>偏向锁——>轻量级锁——>重量级锁
无锁可偏向——>无锁不可偏向——>轻量级锁——>重量级锁
并且膨胀方向不可逆(某些苛刻的条件是可逆的)。
如果想升级偏向锁,需要有两个条件:
一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word
的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word
的锁标记位为偏向锁以及当前线程ID等于 Mark Word
的 ThreadID 即可,这样就省去了大量有关锁申请的操作。
底层通过调用 faster_enter 和 slow_enter
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
实现逻辑
轻量级锁的实现是在每一个线程中产生一个 lock record,在竞争锁的时候,采用 CAS 的方式把 Mark Word 的指针指向当前 Lock Record 的地址。
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
实现逻辑:
从上面的分析中,我们可以得知,Synchronized 关键字的锁存在 Java 对象头中,通过判断对象头的标记位来判断当前锁的状态,如果线程拿不到锁会一直等待,这个是怎么实现的呢?
在查询 JVM 规范可以得到 Synchronized 执行对应了两个 JVM 指令,MonitorEnter 和 MonitorExit 两个指令。同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的
首先我们先了解下 monitor (管程) 这个对象,在 Java 中所有对象都有一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
在Java虚拟机 (HotSpot) 中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet
和 _EntryList
,用来保存 ObjectWaiter 对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner
指向持有 ObjectMonitor
对象的线程,
_EntryList
集合
monitor
后进入_Owner
区域并把 monitor
中的 owner
变量设置为当前线程,同时monitor 中的计数器 count 加 1
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。
private void syncMethod1() {
Object obj = new Object();
//无效的锁,会自动消除
synchronized (obj) {
//....
}
}
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。
private void syncMethod2() {
for (int i = 0; i < 100; i++) {
synchronized (Object.class) {
//....
}
}
//锁粗化会自动改成这种方式,提高性能
synchronized (Object.class) {
for (int i = 0; i < 100; i++) {
//....
}
}
}