Java synchronized关键字详解:从入门到原理(两课时)

文章目录

    • 第一课时:synchronized基础与使用
      • [1.1 从一个线程安全问题开始](#1.1 从一个线程安全问题开始)
      • [1.2 synchronized是什么?](#1.2 synchronized是什么?)
      • [1.3 初识synchronized的三种用法](#1.3 初识synchronized的三种用法)
        • [1.3.1 修饰实例方法](#1.3.1 修饰实例方法)
        • [1.3.2 修饰静态方法](#1.3.2 修饰静态方法)
        • [1.3.3 修饰代码块](#1.3.3 修饰代码块)
      • [1.4 深入理解锁的范围](#1.4 深入理解锁的范围)
        • [1.4.1 三种锁的对比表格](#1.4.1 三种锁的对比表格)
        • [1.4.2 常见面试题解析](#1.4.2 常见面试题解析)
      • [1.5 synchronized的核心特性](#1.5 synchronized的核心特性)
        • [1.5.1 可重入性](#1.5.1 可重入性)
        • [1.5.2 可见性保证](#1.5.2 可见性保证)
      • [1.6 第一课时小结](#1.6 第一课时小结)
    • 第二课时:synchronized原理与优化
      • [2.1 从字节码看synchronized的本质](#2.1 从字节码看synchronized的本质)
        • [2.1.1 同步代码块的字节码](#2.1.1 同步代码块的字节码)
        • [2.1.2 同步方法的字节码](#2.1.2 同步方法的字节码)
      • [2.2 Monitor机制深度剖析](#2.2 Monitor机制深度剖析)
        • [2.2.1 什么是Monitor?](#2.2.1 什么是Monitor?)
        • [2.2.2 Monitor的核心数据结构](#2.2.2 Monitor的核心数据结构)
        • [2.2.3 锁获取和释放的完整流程](#2.2.3 锁获取和释放的完整流程)
      • [2.3 锁升级:从偏向锁到重量级锁](#2.3 锁升级:从偏向锁到重量级锁)
        • [2.3.1 为什么需要锁升级?](#2.3.1 为什么需要锁升级?)
        • [2.3.2 锁的四种状态](#2.3.2 锁的四种状态)
        • [2.3.3 偏向锁(Biased Locking)](#2.3.3 偏向锁(Biased Locking))
        • [2.3.4 轻量级锁(Lightweight Locking)](#2.3.4 轻量级锁(Lightweight Locking))
        • [2.3.5 重量级锁(Heavyweight Locking)](#2.3.5 重量级锁(Heavyweight Locking))
        • [2.3.6 锁升级流程总结](#2.3.6 锁升级流程总结)
      • [2.4 synchronized与volatile的对比](#2.4 synchronized与volatile的对比)
      • [2.5 synchronized与ReentrantLock的对比](#2.5 synchronized与ReentrantLock的对比)
        • [2.5.1 对比表格](#2.5.1 对比表格)
        • [2.5.2 如何选择?](#2.5.2 如何选择?)
        • [2.5.3 ReentrantLock使用示例](#2.5.3 ReentrantLock使用示例)
      • [2.6 实战:线程安全的单例模式](#2.6 实战:线程安全的单例模式)
        • [2.6.1 饿汉式(线程安全)](#2.6.1 饿汉式(线程安全))
        • [2.6.2 懒汉式(同步方法版)](#2.6.2 懒汉式(同步方法版))
        • [2.6.3 双重检查锁(DCL)](#2.6.3 双重检查锁(DCL))
      • [2.7 第二课时小结](#2.7 第二课时小结)
    • 课程总结

适用对象

本课程适合具备Java基础语法知识,初步了解多线程概念但尚未系统学习并发编程的开发者。无论你是准备面试的求职者,还是希望提升并发编程能力的工程师,这门课程都将为你打下坚实基础。

学习目标

通过两个课时的系统学习,你将能够:

  • 掌握 synchronized的三种使用方式及其区别
  • 理解 synchronized解决线程安全问题的核心原理
  • 剖析 synchronized底层的Monitor机制和锁升级过程
  • 区分 synchronized与volatile、ReentrantLock的适用场景
  • 应对 面试中关于synchronized的高频问题

课程安排

  • 第一课时(约40分钟):基础概念、使用方式、特性详解
  • 第二课时(约50分钟):底层原理、锁升级机制、对比与实战

第一课时:synchronized基础与使用

1.1 从一个线程安全问题开始

让我们先看一个经典问题:两个线程同时对共享变量执行自增操作。

java 复制代码
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // 不是原子操作!
    }
    
    public int getCount() {
        return count;
    }
}

// 测试代码
public class Test {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) counter.increment();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果: " + counter.getCount()); // 期望20000,实际小于20000
    }
}

运行结果:多次执行,结果总是小于20000,有时甚至相差甚远。

问题根源count++看似一行代码,但在JVM中分解为三条指令:

  1. 从主内存读取count到工作内存(读)
  2. 对count进行加1操作(改)
  3. 将结果写回主内存(写)

当两个线程同时执行这三步时,可能发生交替执行,导致最后写入的值覆盖了之前的计算结果------这就是典型的竞态条件

1.2 synchronized是什么?

synchronized是Java提供的一个关键字,译为"同步"。它可以保证在同一时刻,最多只有一个线程执行被它修饰的代码段,从而解决并发环境下的线程安全问题。

从专业角度说,synchronized实现了互斥访问,它保证了三个关键特性:

特性 说明
原子性 被保护的代码块要么全部执行,要么完全不执行,不会被打断
可见性 一个线程修改共享变量后,其他线程能立即看到最新值
有序性 禁止编译器和处理器对同步块内的代码进行指令重排序

1.3 初识synchronized的三种用法

synchronized的使用非常灵活,可以根据需要选择不同的加锁对象。下面我们通过三个典型场景来理解它们的区别。

1.3.1 修饰实例方法
java 复制代码
public class SyncMethodDemo {
    private int count = 0;
    
    // synchronized修饰实例方法,锁是当前对象this
    public synchronized void increment() {
        count++;
    }
}

特点

  • 锁对象是调用该方法的实例对象(即this)
  • 同一个对象的多个synchronized实例方法,共用同一把锁
  • 不同对象的方法互不干扰

示意图

复制代码
线程A ──> obj1.increment() ──> 获取obj1锁 ──> 执行
线程B ──> obj1.increment() ──> 等待obj1锁
线程C ──> obj2.increment() ──> 获取obj2锁 ──> 执行(与A不冲突)
1.3.2 修饰静态方法
java 复制代码
public class SyncStaticDemo {
    private static int count = 0;
    
    // synchronized修饰静态方法,锁是当前类的Class对象
    public static synchronized void increment() {
        count++;
    }
}

特点

  • 锁对象是当前类的Class对象(如SyncStaticDemo.class)
  • 所有实例对象共享同一把类锁
  • 静态方法锁与实例方法锁互不干扰

思考题:如果一个类既有synchronized静态方法,又有synchronized实例方法,两个线程分别调用它们,会互斥吗?

  • 答案:不会。因为一个是类锁,一个是对象锁,两把不同的锁。
1.3.3 修饰代码块
java 复制代码
public class SyncBlockDemo {
    private Object lock = new Object();
    private int count = 0;
    
    public void increment() {
        // 同步代码块,锁是指定的lock对象
        synchronized (lock) {
            count++;
        }
    }
    
    public void decrement() {
        // 也可以锁this
        synchronized (this) {
            count--;
        }
    }
}

特点

  • 锁对象可以任意指定,灵活性最高
  • 可以精确控制同步范围,提高并发性能
  • 常用锁对象:this、自定义锁对象、类.class

1.4 深入理解锁的范围

1.4.1 三种锁的对比表格
使用形式 锁对象 作用范围 典型应用场景
修饰实例方法 当前实例对象 单个实例内 保护实例变量
修饰静态方法 当前类的Class对象 所有实例间 保护静态变量
修饰代码块 指定的任意对象 代码块内 细粒度控制
1.4.2 常见面试题解析

问题1:synchronized修饰代码块可以给类加锁吗?

当然可以!只要在括号内传入类名.class即可。例如:

java 复制代码
synchronized (SyncBlockDemo.class) {
    // 这是类锁
}

问题2:构造方法可以用synchronized修饰吗?

不可以!Java语法规定构造方法不能是同步的。实际上,构造方法本身就是线程安全的,因为JVM会保证在对象初始化完成前,其他线程无法访问该对象。

问题3:静态同步方法和非静态同步方法同时调用会互斥吗?

不会互斥。假设线程A调用实例对象的非静态同步方法,线程B同时调用该对象所属类的静态同步方法,它们持有的是两把不同的锁(对象锁 vs 类锁),因此可以并行执行。

1.5 synchronized的核心特性

1.5.1 可重入性

概念:同一个线程在持有锁的情况下,可以再次获取同一把锁,而不会被阻塞。

java 复制代码
public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("进入methodA");
        methodB(); // 直接调用,不会死锁
    }
    
    public synchronized void methodB() {
        System.out.println("进入methodB");
    }
}

为什么需要可重入?

如果没有可重入性,线程在调用methodA获取锁后,调用methodB时发现自己还要申请同一把锁,就会形成死锁。这显然不合理。

实现原理 :每个锁关联一个持有线程 和一个计数器。线程第一次获取锁时,计数器置为1;同一线程再次获取时,计数器递增;释放一次,计数器递减。直到计数器归零,锁才真正释放。

1.5.2 可见性保证

synchronized不仅能保证原子性,还能保证内存可见性。JMM(Java内存模型)规定:

  • 线程释放锁时,会将工作内存中的共享变量刷新到主内存
  • 线程获取锁时,会清空工作内存,从主内存重新读取共享变量

这就确保了:一个线程修改的共享变量,在释放锁后,其他线程能立即看到最新值。

1.6 第一课时小结

  • synchronized是Java内置的同步关键字,解决原子性、可见性、有序性问题
  • 三种使用方式:实例方法(锁this)、静态方法(锁Class对象)、代码块(锁任意对象)
  • 核心特性:可重入性、可见性、互斥性
  • 不同锁对象决定了不同的同步范围

第二课时:synchronized原理与优化

2.1 从字节码看synchronized的本质

2.1.1 同步代码块的字节码

先看一段简单的同步代码块:

java 复制代码
public class SyncCodeBlock {
    public void doSomething() {
        synchronized (this) {
            System.out.println("hello");
        }
    }
}

使用javap -c -v反编译后,关键字节码如下:

复制代码
 3: monitorenter      // 进入同步块,获取监视器锁
 4: getstatic         // 调用System.out
 7: ldc               // 加载字符串
 9: invokevirtual     // 调用println
12: aload_1
13: monitorexit       // 退出同步块,释放监视器锁
14: goto 22           // 正常结束跳转
17: astore_2          // 异常处理开始
18: aload_1
19: monitorexit       // 异常时也释放锁
20: aload_2
21: athrow            // 抛出异常
22: return

关键发现

  • 同步代码块使用monitorentermonitorexit指令
  • 有两个monitorexit:一个正常退出,一个异常退出,确保锁一定被释放
  • 这就是为什么synchronized即使抛出异常也不会死锁
2.1.2 同步方法的字节码
java 复制代码
public synchronized void syncMethod() {
    System.out.println("hello");
}

反编译结果:

复制代码
public synchronized void syncMethod();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 注意这个标志
  Code:
   0: getstatic     #2
   3: ldc           #3
   5: invokevirtual #4
   8: return

同步方法没有monitorenter/monitorexit指令,而是通过方法表flags中的ACC_SYNCHRONIZED标识。JVM根据这个标识判断是否需要获取锁。

结论:两种实现方式本质上都是获取对象的监视器锁(Monitor Lock),只是表现形式不同。

2.2 Monitor机制深度剖析

2.2.1 什么是Monitor?

Monitor(监视器)是操作系统中用于实现线程同步的机制。Java中的每个对象都与一个Monitor关联,这个关联关系存储在对象头的Mark Word中。

可以把Monitor理解为一个"接待室",里面有三个关键区域:

复制代码
                  ┌─────────────────────┐
                  │      Owner          │ ← 当前持有锁的线程
                  ├─────────────────────┤
                  │     EntryList       │ ← 等待获取锁的线程队列
                  ├─────────────────────┤
                  │     WaitSet         │ ← 调用了wait()的线程队列
                  └─────────────────────┘
2.2.2 Monitor的核心数据结构

在HotSpot虚拟机中,Monitor由C++的ObjectMonitor类实现,关键字段如下:

字段 作用
_owner 指向当前持有锁的线程
_EntryList 等待获取锁的线程队列
_WaitSet 调用了wait()的线程队列
_recursions 记录锁的重入次数
_count 记录线程获取锁的次数
2.2.3 锁获取和释放的完整流程

以两个线程T0和T1竞争锁为例:

步骤1:线程T0尝试获取锁

  • 根据对象头找到对应的ObjectMonitor
  • 检查_owner是否为null
  • 通过CAS操作将_owner设为T0,_count设为1
  • T0获取锁成功,进入同步块执行

步骤2:线程T1尝试获取锁

  • 此时_owner为T0,T1 CAS失败
  • T1会先自旋几次尝试(适应性自旋)
  • 如果T0很快释放锁,T1就成功获取
  • 如果T0未释放,T1进入_EntryList阻塞等待

步骤3:T0释放锁

  • 执行monitorexit_count减1
  • _count变为0,_owner设为null
  • 唤醒_EntryList中的线程(通常是队首线程)
  • T1被唤醒,重新竞争锁

2.3 锁升级:从偏向锁到重量级锁

早期Java的synchronized性能较差,被称为"重量级锁"。JDK 1.6之后引入了一系列优化,让synchronized的性能大幅提升。这就是著名的锁升级机制。

2.3.1 为什么需要锁升级?

想象一下这些场景:

  • 场景A:只有一个线程访问同步代码,根本不需要锁竞争
  • 场景B:两个线程交替访问,几乎没有同时竞争
  • 场景C:多个线程激烈竞争,需要操作系统级别的锁

如果用重量级锁统一处理所有场景,场景A和B会造成不必要的性能开销。锁升级就是让synchronized能够根据竞争激烈程度,动态调整锁的"重量"。

2.3.2 锁的四种状态

从低到高,锁有四种状态:

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

锁可以升级,但不能降级(除了一次GC清理)。

2.3.3 偏向锁(Biased Locking)

适用场景:只有一个线程反复获取同一把锁。

原理

  • 第一次获取锁时,通过CAS将线程ID记录到对象头
  • 之后该线程再来时,只需检查对象头中是否是自己ID
  • 如果是,直接进入,无需任何同步操作

示例代码

java 复制代码
public class BiasedLockDemo {
    private static List<Integer> list = new Vector<>(); // Vector的方法都是同步的
    
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            list.add(i); // 同一线程反复获取锁
        }
    }
}

优点 :几乎没有加锁开销,性能极高。
缺点:一旦有其他线程尝试竞争,偏向锁立即撤销并升级。

注意:从JDK 15开始,偏向锁被标记为废弃,未来可能移除。因为在实际应用中,它的性能提升有限,且维护成本高。

2.3.4 轻量级锁(Lightweight Locking)

适用场景 :两个线程交替执行同步块,没有真正的竞争。

原理

  • 线程在自己的栈帧中创建Lock Record,存储对象头的拷贝
  • 通过CAS尝试将对象头指向Lock Record
  • 如果成功,获取轻量级锁
  • 如果失败,说明有竞争,升级为重量级锁

轻量级锁的解锁

  • 通过CAS将对象头恢复为原Mark Word
  • 如果恢复成功,解锁完成
  • 如果失败,说明已经升级为重量级锁,走重量级锁解锁流程
2.3.5 重量级锁(Heavyweight Locking)

适用场景:多个线程激烈竞争。

原理

  • 线程进入_EntryList队列阻塞
  • 依赖操作系统Mutex Lock实现
  • 涉及用户态和内核态切换,开销较大
2.3.6 锁升级流程总结
复制代码
                   偏向锁
                     │
                     │ 有其他线程尝试获取
                     ↓
                 轻量级锁
                     │
                     │ 有真实竞争
                     ↓
                 重量级锁

2.4 synchronized与volatile的对比

特性 synchronized volatile
原子性 ✅ 保证 ❌ 不保证
可见性 ✅ 保证 ✅ 保证
有序性 ✅ 保证 ✅ 保证(禁止重排序)
用法 修饰方法或代码块 修饰变量
性能 较重(但有优化) 极轻
适用场景 复合操作 单一变量状态标志

volatile适用示例

java 复制代码
public class VolatileDemo {
    private volatile boolean flag = true; // 只作为状态标志
    
    public void stop() {
        flag = false; // 简单写操作
    }
    
    public void run() {
        while (flag) {
            // 执行任务
        }
    }
}

2.5 synchronized与ReentrantLock的对比

2.5.1 对比表格
对比维度 synchronized ReentrantLock
实现方式 JVM关键字 Java API(基于AQS)
锁释放 自动释放 必须手动unlock
公平性 非公平 可设置公平/非公平
可中断 不支持 支持lockInterruptibly()
超时获取 不支持 支持tryLock(timeout)
尝试获取 不支持 支持tryLock()
条件变量 一个等待集 多个Condition
锁状态查询 不支持 支持查询持有线程等
2.5.2 如何选择?

优先使用synchronized的情况:

  • 同步逻辑简单,不需要高级功能
  • 希望代码简洁不易出错
  • JVM持续优化,性能已很好

选择ReentrantLock的情况:

  • 需要公平锁
  • 需要尝试获取锁或超时获取
  • 需要可中断的锁获取
  • 需要多个条件变量(如生产者-消费者模式)
  • 高竞争场景需要更精细控制
2.5.3 ReentrantLock使用示例
java 复制代码
ReentrantLock lock = new ReentrantLock(true); // 公平锁
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 标准使用模式
lock.lock();
try {
    // 临界区代码
    while (条件不满足) {
        notEmpty.await(); // 等待
    }
    // 执行操作
    notFull.signal();
} finally {
    lock.unlock(); // 必须释放
}

2.6 实战:线程安全的单例模式

综合运用synchronized,实现几种经典的单例模式。

2.6.1 饿汉式(线程安全)
java 复制代码
public class EagerSingleton {
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}
2.6.2 懒汉式(同步方法版)
java 复制代码
public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
2.6.3 双重检查锁(DCL)
java 复制代码
public class DCLSingleton {
    // volatile保证可见性和禁止重排序
    private static volatile DCLSingleton instance;
    
    private DCLSingleton() {}
    
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

为什么需要volatile?
instance = new DCLSingleton()不是原子操作,可能发生指令重排序。如果不加volatile,其他线程可能拿到一个未初始化完成的对象。这是DCL的关键细节。

2.7 第二课时小结

  • synchronized底层基于Monitor实现,字节码层面使用monitorenter/monitorexit或ACC_SYNCHRONIZED
  • 锁升级机制(偏向锁→轻量级锁→重量级锁)大幅提升了性能
  • synchronized保证原子性、可见性、有序性,是全面的同步工具
  • 与volatile相比,synchronized更重量但功能更全
  • 与ReentrantLock相比,synchronized简单易用,ReentrantLock功能更丰富
  • 实际开发中,根据场景选择合适的同步机制

课程总结

通过两个课时的学习,我们全面掌握了synchronized关键字:

  • 第一课时:从线程安全问题出发,学习了synchronized的三种使用方式、锁的范围、可重入性等基础概念
  • 第二课时:深入底层,剖析了Monitor机制、锁升级流程,并对比了volatile和ReentrantLock

synchronized作为Java最基础的同步工具,虽然简单,但背后的原理并不简单。理解它的实现机制,不仅能帮助我们写出更正确的并发程序,还能在面试中脱颖而出。

课后思考题

  1. 如果一个线程在同步块中抛出异常,锁会自动释放吗?为什么?
  2. 偏向锁在JDK 15中被废弃的原因是什么?谈谈你的理解。
  3. 如何用synchronized实现一个阻塞队列?
  4. 为什么说synchronized是"悲观锁",而CAS是"乐观锁"?
相关推荐
油丶酸萝卜别吃2 小时前
什么是 Java 内存模型(JMM)?
java·开发语言
量子炒饭大师2 小时前
【C++入门】Cyber神经的义体插件 —— 【类与对象】内部类
java·开发语言·c++·内部类·嵌套类
Hx_Ma162 小时前
测试题(四)
java·开发语言·jvm
Never_Satisfied2 小时前
在c#中,抛出异常,并指定其message的值
java·javascript·c#
多打代码2 小时前
2026.02.11
开发语言·python
没有bug.的程序员2 小时前
IDEA 效能巅峰实战:自定义模板 Live Templates 内核、快捷键精密逻辑与研发提效深度指南
java·ide·intellij-idea·快捷键·研发提效·自定义模板
lly2024062 小时前
Scala IF...ELSE 语句详解
开发语言
追随者永远是胜利者2 小时前
(LeetCode-Hot100)22. 括号生成
java·算法·leetcode·职场和发展·go
逝水如流年轻往返染尘2 小时前
java中的泛型
java