一. synchronized的使用
1.1 修饰实例方法(对象锁)
将 synchronized 放在普通方法的返回值前面。
- 作用范围:锁住的是当前对象实例(this)。
- 适用场景:保护某个对象的实例变量不被多个线程同时修改。
java
// 只有获取了当前对象锁的线程,才能执行这个方法
public synchronized void increment() {
count++;
}
1.2 修饰静态方法(类锁)
将 synchronized 放在 static 方法上。
- 作用范围:锁住的是当前类的 Class 对象(如 MyClass.class)。因为类对象在 JVM 中是全局唯一的,所以它能限制所有线程对该类静态变量的并发访问。
- 适用场景:保护全局的静态变量。
java
// 无论创建多少个对象,同一时刻只有一个线程能执行此方法
public static synchronized void staticIncrement() {
staticCount++;
}
1.3 修饰同步代码块(自定义锁)
将需要同步的代码包裹在 synchronized(对象) { ... } 中。
- 作用范围:锁住的是括号中显式指定的对象。
- 适用场景:当方法中只有部分代码需要保证线程安全时,使用代码块可以缩小锁的粒度,避免其他不相关的代码也被锁住,从而提升并发性能。
java
public void doSomething() {
// 这里的代码是不受保护的,多个线程可以同时执行
System.out.println("准备阶段...");
// 只有这部分代码受锁保护
synchronized (this) {
count++;
}
}
1.4 锁对象的高级用法(传入外部对象)
java
// 定义一个专门用来加锁的私有对象
private final Object lock = new Object();
public void safeMethod() {
synchronized (lock) {
// 只有拿到 lock 对象的线程才能进入这里
// 这种方式比直接锁 this 更安全,避免外部代码意外获取你的锁
}
}
二. synchronized的原理
2.1 对象在内存上的存储
对象在堆中的内存布局,指的是对象在堆中存放时的各个组成部分,主要分为以下几个部分:

对象头包含标记字段和元数据指针
标记字段 相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的,同时在64位(又分为是否开启指针压缩)、32位虚拟机中的布局都不同。以64位开启指针压缩为例:


- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
我们可以通过lock的标识,来判断是哪一种锁的等级
- 后三位是001表示无锁
- 后三位是101表示偏向锁
- 后两位是00表示轻量级锁
- 后两位是10表示重量级锁
Klass pointer元数据的指针 指向方法区中保存的InstanceKlass对象:

2.2 Monitor(监视器)
Monitor 被翻译为监视器,是由jvm提供,c++语言实现。
monitor从字节码的原理出发如下图:

- monitorenter 上锁开始的地方
- monitorexit 解锁的地方
- 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
- 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁
在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁
monitor主要就是跟这个对象产生关联图如下:

Monitor内部具体的存储结构:
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
具体的流程: - 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
- 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
- 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
- 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待
2.3 synchronized锁升级
2.3.1 CAS自旋
CAS的全称是: Compare And Swap(比较再交换 ),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。

CAS 操作包含三个操作数:
- V(内存地址):需要修改的变量的内存地址。
- E(Expected,期望值):线程认为该变量当前应该有的值。
- N(New,新值):线程希望将该变量更新为的新值。
执行逻辑:当且仅当内存地址 V 的当前值与期望值 E 相等时,才将 V 的值更新为 N。如果不相等,说明有其他线程已经修改过该变量,当前操作失败(通常会失败重试,即"自旋")。
2.3.2 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些。

2.3.3 轻量级锁
当有第二个线程尝试获取这把已经被偏向的锁时,偏向锁会被撤销,并升级为轻量级锁。
1.创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。

- 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。
如果 cas 失败,有两种情况:
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
锁膨胀:如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

2.3.4 重量级锁
如果自旋等待超过了设定的阈值(JVM 会根据历史情况自适应调整自旋次数),或者同时有多个线程(通常是3个及以上)在激烈竞争同一把锁,轻量级锁就会膨胀为重量级锁。
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。
