目录

一、synchronized 基础使用

1. 修饰实例方法

2. 修饰静态方法

3. 修饰代码块

二、synchronized 底层核心:Monitor 锁机制

1. 前置知识:Java 对象头

2. 什么是 Monitor?

3. synchronized 加锁 / 解锁完整流程

4. 底层指令:monitorenter 与 monitorexit

三、核心知识点总结


在 Java 多线程开发中,synchronized 是解决线程安全问题最基础、最核心的关键字,它能保证代码的原子性、可见性、有序性,是面试和实际开发中绕不开的重点。本文从基础使用入手,深入讲解其底层 Monitor 锁实现原理,帮你彻底理解 synchronized。

一、synchronized 基础使用

synchronized 本质是互斥锁,同一时间只允许一个线程执行被修饰的代码,它有三种使用场景:

1. 修饰实例方法

锁的是当前实例对象,同一个对象的多线程调用会互斥,不同对象互不影响。

public class SyncDemo {
    // 锁当前实例对象
    public synchronized void method() {
        // 同步代码逻辑
    }
}

注意注意:synchronized 修饰实例方法时,锁的是调用该方法的具体对象实例this),而非方法本身。

也就是说,SyncDemo 的一个实例对象同一时刻只能有一个线程进行调用,这里给出一个样例看一下

public class SyncDemo {
    // 计数器,用于直观展示执行顺序
    private int count = 0;

    // synchronized修饰实例方法,锁的是调用该方法的SyncDemo实例对象
    public synchronized void method() {
        // 模拟耗时操作,让线程执行时间更明显
        for (int i = 0; i < 3; i++) {
            count++;
            System.out.println(Thread.currentThread().getName() + 
                               " - count = " + count);
            try {
                Thread.sleep(500); // 暂停500毫秒,放大执行差异
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ========== 场景1:多线程调用【同一个对象】的同步方法 ==========
        System.out.println("===== 场景1:同一对象,多线程互斥 =====");
        SyncDemo sameObject = new SyncDemo(); // 唯一的对象实例
        
        // 线程1调用sameObject的method方法
        Thread thread1 = new Thread(() -> sameObject.method(), "线程1");
        // 线程2调用sameObject的method方法
        Thread thread2 = new Thread(() -> sameObject.method(), "线程2");
        
        thread1.start();
        thread2.start();
        
        // 等待场景1的线程执行完毕,再执行场景2
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // ========== 场景2:多线程调用【不同对象】的同步方法 ==========
        System.out.println("\n===== 场景2:不同对象,互不影响 =====");
        SyncDemo object1 = new SyncDemo(); // 第一个对象
        SyncDemo object2 = new SyncDemo(); // 第二个对象
        
        // 线程3调用object1的method方法
        Thread thread3 = new Thread(() -> object1.method(), "线程3");
        // 线程4调用object2的method方法
        Thread thread4 = new Thread(() -> object2.method(), "线程4");
        
        thread3.start();
        thread4.start();
    }
}

2. 修饰静态方法

锁的是当前类的 Class 对象,全局唯一,所有线程调用该方法都会互斥。

public class SyncDemo {
    // 锁当前类的Class对象
    public static synchronized void staticMethod() {
        // 同步代码逻辑
    }
}

有了上面的例子,其实这个很好理解了,就是所有的实例对象同一时刻只能有一个线程调用

3. 修饰代码块

灵活指定锁对象,粒度更细,推荐优先使用。

public class SyncDemo {
    private final Object lock = new Object();
    
    public void test() {
        // 锁指定对象
        synchronized (lock) {
            // 同步代码逻辑
        }
    }
}

说白了synchronized 锁的本质是锁对象,而非代码,不同锁对象互不干扰。

二、synchronized 底层核心:Monitor 锁机制

synchronized 之所以能实现互斥,底层依赖 JVM 的 Monitor(监视器锁) 机制,而 Monitor 又和 Java 对象头密切相关。

1. 前置知识:Java 对象头

Java 对象在内存中分为三部分:对象头、实例数据、对齐填充,其中对象头是实现 synchronized 的关键。

对象头主要包含:

  • Mark Word:存储对象的哈希码、分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等核心信息。
  • Klass Pointer:指向对象对应类的元数据指针。

锁状态就存在 Mark Word 中,synchronized 就是通过修改 Mark Word 实现锁升级(本文重点讲重量级锁对应的 Monitor 机制)。

重点来看一下 Mark Word

  • 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表示重量级锁

2. 什么是 Monitor?

Monitor 直译是监视器,JVM 中它是一个同步工具 / 同步原语,本质是一个对象监视器锁,每个 Java 对象都关联一个 Monitor,底层由C++语言实现

可以把 Monitor 理解为一个房间,同一时间只能有一个线程进入:

  • 进入房间 = 加锁
  • 离开房间 = 释放锁
  • 没抢到锁的线程 = 在门外等待

Monitor 核心数据结构如下:

核心包含三个关键队列 / 指针:

  • _owner:指向当前持有锁的线程,未持有锁时为 null。
  • _EntryList锁等待队列,所有竞争锁失败的线程会进入这里,处于阻塞状态。
  • _WaitSet等待队列,调用 wait() 方法的线程会进入这里,释放锁,被 notify()/notifyAll() 唤醒后重新竞争锁。

3. synchronized 加锁 / 解锁完整流程

synchronized(object) 代码块为例,底层执行流程:

  1. 线程进入同步代码块,先尝试获取对象的 Monitor 锁。
  2. 检查 _owner 是否为 null:
    • 为 null:表示锁未被持有,当前线程将 _owner 指向自己,加锁成功,执行同步代码。
    • 不为 null:表示锁已被其他线程持有,当前线程进入 _EntryList 阻塞,直到锁被释放。
  3. 持有锁的线程执行完同步代码,或调用 wait() 方法:
    • 正常执行完毕:释放锁,_owner 置为 null,唤醒 _EntryList 中的线程重新竞争。
    • 调用 wait():释放锁,线程进入 _WaitSet,等待被唤醒。
  4. 线程被 notify() 唤醒后,从 _WaitSet 移到 _EntryList,重新参与锁竞争。

注意注意:参与锁竞争只能是_EntryList里面的线程或者是新来的线程,在 _WaitSet里的线程必须先被唤醒进入阻塞队列

4. 底层指令:monitorenter 与 monitorexit

JVM 为 synchronized 提供了两个字节码指令:

  • monitorenter:进入同步代码块时执行,尝试获取对象的 Monitor 锁。
  • monitorexit:退出同步代码块时执行,释放 Monitor 锁。

借助javap命令查看class的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

注意:JVM 会保证异常也能释放锁,所以代码中会有一个 monitorenter 对应两个 monitorexit(一个正常退出,一个异常退出)。

三、核心知识点总结

  1. synchronized 是互斥锁,保证多线程下的线程安全,实现原子性、可见性、有序性。
  2. 锁的是对象,实例方法锁当前对象,静态方法锁 Class 对象,代码块锁指定对象。
  3. 底层依赖 Monitor 监视器锁,通过对象头 Mark Word 关联。
  4. Monitor 核心:_owner(持有锁线程)、_EntryList(锁等待)、_WaitSet(wait 等待)。
  5. 底层字节码:monitorenter 加锁,monitorexit 解锁,异常安全释放锁。

本文只讲解了 synchronized 基础使用和最核心的 Monitor 重量级锁原理,后续还有偏向锁、轻量级锁的锁升级机制,是 synchronized 优化的关键,下篇文章继续深入讲解。

感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐