synchronized
约 2867 字大约 10 分钟
2025-01-22
synchronized 特点
synchronized 的特点:原子性、可见性、可重入性
原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
可见性:多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的
可重入性:单个线程可以重复拿到某个锁,锁的粒度是线程而不是调用
synchronized 同步方式
synchronized 可以修饰的地方如图所示,synchronized 上锁的资源只有两类:对象、类
- 普通的成员函数方法,属于类的实例对象
- 被 static 修饰的静态方法、静态属性,属于该类

举例:
public class Testl {
private int i=0;
private static int j=0;
private final Testl instance = new Test1();
//对成员函数加锁,必须获得该类的实例对象的锁才能进入同步块
public synchronized void add1(){
i++;
}
//对静态方法加锁,必须获得类的锁才能进入同步块
public static synchronized void add2(){
i++;
}
public void method(){
// 同步块,执行前必须获得Test1类的锁
synchronized(Testl.class){
}
//同步块,执行前必须先获得实例对象的锁
synchronized(instance){
}
}
}同步代码块
结论:依赖于 monitorenter 和 monitorexit 指令
public class Test3
private static int i=0;
public void method(){
synchronized (Test3.class){
i++;
}
}
}反编译,可得如下图:

- 由图可得,添加了 synchronized 关键字的代码块,多了两个指令monitorenter、monitorexit。即 JVM 使用 monitorenter 和 monitorexit 两个指令实现同步。
- 同步块是由 monitorenter 指令进入,然后 monitorexit 释放锁
- 在执行 monitorenter 之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加 1。当执行 monitorexit 指令时,锁的计数器也会减 1。当获取锁失败时会被阻塞,一直等待锁被释放。
- 第二个 monitorexit 是来处理异常的,仔细看反编译的字节码,正常情况下第一个 monitorexit 之后会执行
goto指令,而该指令转向的就是 23 行的return,也就是说正常情况下只会执行第一个 monitorexit 释放锁,然后返回。而如果在执行中发生了异常,第二个 monitorexit 就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。
同步方法
- 同步方法:通过方法 flags 标志
public synchronized void dosth(){
System.out.println("test Synchronized method");
}反编译,可得如下图:

由图可得,添加了 synchronized 关键字的方法,多了ACC_SYNCHRONIZED标记。即 JVM 通过在方法访问标识符(flags)中加入 ACC_SYNCHRONIZED 来实现同步功能。
synchronized 底层
synchronized 的底层实现是完全依赖 JVM 虚拟机的,谈数据在 JVM 内存的存储:Java 对象头、Monitor 对象监视器。
Java 对象
对象结构
在 JVM 中,对象是分成三部分:对象头、实例数据、对其填充,如下图
- 实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;
- 填充数据:由于虚拟机要求 对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头:是 synchronized 实现锁的基础,因为synchronized 申请锁、上锁、释放锁都与对象头有关。
- 对象头主要结构是由
Mark Word和Class Metadata Address组成 - 其中
Mark Word存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息**,** Class Metadata Address是类型指针指向对象的类元数据 ,JVM 通过该指针确定该对象是哪个类的实例
- 对象头主要结构是由

下面讲讲对象头结构中的 Mark Word 标记字段:
- Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
- 锁标志位(2bit):
- 01:该对象为无锁状态
- 00:该对象为轻量级锁,指向栈中锁记录的指针
- 10:重量级锁,指向互斥量的指针
加锁过程
JVM 一般是这样使用锁和 Mark Word 的(64 位 JVM 对象):
- 一个对象没有被当成锁时,是一个普通的对象,Mark Word 标记字段记录对象的 HashCode,锁标志位为 01,是否偏向锁位为 0
- 当对象被当做同步锁并有一个线程 A 抢到了锁时,锁标志位还是 01,但是否偏向锁那一位改成 1,前 54bit 记录抢到锁的线程 id,表示进入偏向锁状态
- 当线程 A 再次试图来获得锁时,JVM 发现同步锁对象的标志位是 01,是否偏向锁是 1,也就是偏向状态,Mark Word 中记录的线程 id 就是线程 A 自己的 id,表示线程 A 已经获得了这个偏向锁,可以执行同步锁的代码
- 当线程 B 试图获得这个锁时,JVM 发现同步锁处于偏向状态,但是 Mark Word 中的线程 id 记录的不是 B,那么线程 B 会先用 CAS 操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程 A 一般不会自动释放偏向锁。如果抢锁成功,就把 Mark Word 里的线程 id 改为线程 B 的 id,代表线程 B 获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤 5
- 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM 会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁 Mark Word 的指针,同时在对象锁 Mark Word 中保存指向这片空间的指针。上述两个保存操作都是 CAS 操作,如果保存成功,代表线程抢到了同步锁,就把 Mark Word 中的锁标志位改成 00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤 6。
- 轻量级锁抢锁失败,JVM 会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤 7。
- 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会被阻塞
对象怎么和 monitor 实现关联?
- 对象里有对象头
- 对象头里面有 Mark Word 标记字段
- Mark Word 指针指向了 ObjectMonitor
- 每个对象都与一个 monitor 相关联,线程可以占有或者释放 monitor。
ObjectMonitor
在 Java 虚拟机(HotSpot)中,Monitor(管程)是由 ObjectMonitor 实现的,其主要数据结构如下:

ObjectMonitor 中几个关键字段:
- _count:记录 owner 线程获取锁的次数
- _owner:指向持有 ObjectMonitor 对象的线程
- _WaitSet:存放处于 wait 状态的线程队列
- _EntryList:存放处于等待锁 block 状态的线程队列
- _recursions:锁的重入次数
多个线程同时访问一段同步代码执行过程:
- 首先,要获取 ObjectMonitor 的线程会进入 _EntryList 集合
- 当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程,同时 monitor 中的计数器 count 加 1
- 若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒
- 若当前线程执行完毕也将释放 monitor(锁)并复位变量的值,以便其他线程进入获取 monitor(锁)
synchronized 优化
JDK6 的时候,新增了两个锁状态:偏向锁、轻量级锁,并通过锁消除、锁粗化等方法使用各种场景,给 synchronized 性能带来了很大的提升。
无锁:
偏向锁:
- 核心思想:让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁
- 举例:如果一个线程获得了锁,那么锁就进入偏向模式,此时
Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程 ID 等于Mark Word的 ThreadID 即可,这样就省去了大量有关锁申请的操作。
轻量级锁(自旋锁):
- 核心思想:一个线程去申请一个已经被另一个线程占有的锁时,当前线程自旋申请持有锁,而不是阻塞
- 举例:当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁
重量级锁:
- 核心思想:当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,竞争不到锁的线程进入阻塞等待
- 重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。 比如下面代码的 method1 和 method2 的执行效率是一样的,因为 object 锁是私有变量,不存在所得竞争关系。

锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面 method3 经过锁粗化优化之后就和 method4 执行效率一样了。

小结
synchronized 和 Lock 区别
| synchronized | Lock | |
|---|---|---|
| 形态不同 | java 关键字、jvm 层次 | 接口 |
| 锁的释放不同 | 1.执行完同步代码,自动释放锁 2.发生异常,jvm 释放锁 | 1.手动释放锁 unlock() 2.在 finally 里必须释放,不然会死锁 |
| 锁类型不同 | 可重入、非公平 | 可重入、可公平(非公平) |
| 是否可以尝试获取锁 | 不可以 | 可以,tryLock() |
| 粒度 | 粗 | 细 |
synchronized 和 ReentrantLock 区别
| synchronized | ReentrantLock | |
|---|---|---|
| 锁类型不同 | 非公平锁 | 非公平锁、公平锁 |
| 锁的释放不同 | 1.执行完同步代码,自动释放锁 2.发生异常,jvm 释放锁 | 手动释放锁 |
| 是否可以尝试获取锁 | 不可以 | 可以,tryLock() |
| 是否可以超时获取锁 | 不支持 | 可以,tryLock(time) |
| 是否可响应中断 | 不支持,不可响应线程的 interrupt 信号 | 支持,lockInterruptibly() |
| 性能 | 较差 | 比 Synchronized 优 20% |
版权所有
版权归属:haipeng-lin