Java 并发编程:synchronized 优化原理深度解析

一、引言

synchronized 是 Java 并发编程中最基础且核心的同步机制,用于保证临界区代码的原子性、可见性和有序性。早期 synchronized 因性能开销较大被称为 "重量级锁",但 JVM 通过偏向锁、轻量级锁、重量级锁的三级锁机制进行了深度优化,在不同并发场景下自动切换锁状态,平衡了线程安全与执行效率。本文基于 Java 并发编程核心知识,结合具体代码示例与 JVM 底层实现,详细拆解 synchronized 的优化原理与实践逻辑。

二、synchronized 核心基础

2.1 核心作用

  • 互斥性:同一时刻仅允许一个线程进入临界区,避免竞态条件。
  • 可见性:线程释放锁时,会将工作内存中修改的共享变量同步至主存;线程获取锁时,会清空工作内存中共享变量的缓存,从主存重新读取。
  • 有序性:禁止指令重排,保证临界区代码按顺序执行。

2.2 底层依赖:Monitor 机制

synchronized 的同步语义依赖Java 对象头Monitor(管程) 实现:

  1. Java 对象头结构
    • 普通对象的对象头包含 Mark Word(32 位 / 64 位)和 Klass Word(类指针)。
    • Mark Word 是核心,存储对象的锁状态、哈希码、年龄代、偏向线程 ID 等信息,不同锁状态下结构不同。
  2. Monitor 原理
    • Monitor 是操作系统级别的同步原语,每个 Java 对象都关联一个 Monitor(通过对象头的 Mark Word 指向)。
    • Monitor 包含 EntryList(等待锁的线程队列)、Owner(持有锁的线程)、WaitSet(调用 wait () 后等待的线程队列)。
    • 线程获取锁时进入 EntryList 阻塞,获取锁后成为 Owner;释放锁时唤醒 EntryList 中的线程竞争锁。

2.3 基础使用代码示例

synchronized 可修饰实例方法、静态方法和代码块,以下是三种使用方式的核心示例:

java 复制代码
public class SynchronizedBasicDemo {
    // 1. 修饰实例方法(锁对象:当前实例this)
    public synchronized void instanceMethod() {
        System.out.println("实例方法同步:" + Thread.currentThread().getName());
        try {
            Thread.sleep(100); // 模拟业务耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 2. 修饰静态方法(锁对象:SynchronizedBasicDemo.class)
    public static synchronized void staticMethod() {
        System.out.println("静态方法同步:" + Thread.currentThread().getName());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 3. 修饰代码块(灵活指定锁对象)
    private final Object lock = new Object(); // 自定义锁对象
    public void codeBlockMethod() {
        synchronized (lock) { // 锁自定义对象
            System.out.println("代码块同步(自定义锁):" + Thread.currentThread().getName());
        }
        synchronized (this) { // 锁当前实例
            System.out.println("代码块同步(this锁):" + Thread.currentThread().getName());
        }
        synchronized (SynchronizedBasicDemo.class) { // 锁类对象
            System.out.println("代码块同步(类锁):" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        SynchronizedBasicDemo demo = new SynchronizedBasicDemo();
        // 测试实例方法(同一实例互斥,不同实例不互斥)
        new Thread(demo::instanceMethod, "实例线程1").start();
        new Thread(demo::instanceMethod, "实例线程2").start();
        new Thread(new SynchronizedBasicDemo()::instanceMethod, "实例线程3").start();

        // 测试静态方法(全局互斥)
        new Thread(SynchronizedBasicDemo::staticMethod, "静态线程1").start();
        new Thread(SynchronizedBasicDemo::staticMethod, "静态线程2").start();

        // 测试代码块
        new Thread(demo::codeBlockMethod, "代码块线程").start();
    }
}

关键说明

  • 实例方法锁仅对同一实例的线程互斥,不同实例互不影响;静态方法锁和类对象锁是全局唯一的,所有线程互斥。
  • 代码块锁的灵活性最高,可通过指定锁对象缩小同步范围,提升性能。

三、synchronized 三级锁优化机制

JVM 根据并发竞争强度,自动切换 synchronized 的锁状态,从低开销到高开销依次为:偏向锁 → 轻量级锁 → 重量级锁,以下结合代码示例解析各阶段特性。

3.1 偏向锁:无竞争场景优化

3.1.1 设计思想

大多数场景下,锁由同一线程多次获取,无并发竞争。偏向锁通过 "标记线程 ID" 的方式,避免每次获取锁都进行 CAS 操作,降低开销。

3.1.2 实现原理与代码示例
java 复制代码
import org.openjdk.jol.info.ClassLayout;

public class BiasLockDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // JVM默认延迟4秒启用偏向锁,此处设置延迟为0(需添加JVM参数:-XX:BiasedLockingStartupDelay=0)
        System.out.println("初始状态(无锁):");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());

        // 单线程多次获取锁,触发偏向锁
        synchronized (lock) {
            System.out.println("\n第一次加锁(偏向锁):");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }

        // 同一线程再次获取锁,偏向锁直接生效
        synchronized (lock) {
            System.out.println("\n第二次加锁(偏向锁复用):");
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

运行结果关键信息

  • 无锁状态:Mark Word 中锁标志位为 01,偏向标识位为 0。
  • 偏向锁状态:Mark Word 中写入当前线程 ID,偏向标识位变为 1,锁标志位仍为 01。
3.1.3 偏向锁撤销与批量优化

当其他线程尝试竞争偏向锁时,会触发撤销流程,代码示例如下:

java 复制代码
public class BiasLockRevokeDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 线程1先获取锁,触发偏向锁
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程1持有偏向锁");
                try {
                    Thread.sleep(1000); // 保持锁持有状态
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Thread-1");
        t1.start();
        t1.join(); // 等待线程1释放锁

        // 线程2尝试获取锁,触发偏向锁撤销
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2竞争锁,偏向锁撤销");
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }, "Thread-2");
        t2.start();
    }
}

关键说明

  • 线程 2 竞争时,JVM 会在安全点暂停线程 1,将偏向锁撤销为无锁或轻量级锁。
  • 批量重刻名:当一个类的偏向锁撤销达 20 次,JVM 会将该类所有对象批量偏向当前线程;达 40 次则批量撤销偏向锁。

3.2 轻量级锁:低并发竞争优化

3.2.1 适用场景

多个线程交替获取锁,无长时间持有锁的情况,避免重量级锁的内核态切换开销。

3.2.2 实现原理与代码示例
java 复制代码
public class LightweightLockDemo {
    private static final Object lock = new Object();
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程交替获取锁,触发轻量级锁
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (lock) {
                    count++; // 临界区代码(执行时间短)
                }
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        }, "Thread-2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("最终计数:" + count);
        System.out.println("锁状态(轻量级锁):");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
}

关键说明

  • 线程交替执行短临界区代码,竞争程度低,锁状态保持为轻量级锁。
  • 轻量级锁的 Mark Word 会存储指向线程栈帧中 "锁记录(Lock Record)" 的指针,锁标志位为 00。
3.2.3 自旋优化

轻量级锁竞争时,线程会通过自旋尝试获取锁,代码示例如下(模拟自旋场景):

java 复制代码
public class SpinLockDemo {
    private static final Object lock = new Object();
    private static int count = 0;

    public static void main(String[] args) {
        // 3个线程交替竞争锁,自旋优化生效
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 500; j++) {
                    synchronized (lock) {
                        count++;
                        // 临界区执行时间短,自旋可成功获取锁
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }, "Thread-" + i).start();
        }
    }
}

关键说明

  • 自旋次数由 JVM 自适应调整(基于 CPU 核心数和并发情况)。
  • 若临界区执行时间短,自旋可避免线程阻塞,提升效率;若执行时间长,自旋会浪费 CPU,触发锁膨胀。

3.3 重量级锁:高并发竞争场景

3.3.1 适用场景

多个线程同时竞争锁,且锁持有时间较长,自旋优化无法提升效率。

3.3.2 实现原理与代码示例
java 复制代码
public class HeavyweightLockDemo {
    private static final Object lock = new Object();
    private static int count = 0;

    public static void main(String[] args) {
        // 10个线程同时竞争锁,且临界区执行时间长,触发重量级锁
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    synchronized (lock) {
                        count++;
                        try {
                            Thread.sleep(50); // 模拟长时间持有锁
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }, "Thread-" + i).start();
        }
    }
}

运行结果关键信息

  • 锁状态升级为重量级后,Mark Word 会指向 Monitor 对象,锁标志位为 10。
  • 未获取锁的线程进入 EntryList 阻塞(内核态),避免自旋浪费 CPU。
3.3.3 开销分析
  • 重量级锁的获取和释放涉及内核态与用户态切换,开销较大。
  • 但能保证高并发场景下的线程安全,适合临界区代码执行时间长的场景。

四、锁状态转换流程

4.1 转换触发条件

锁状态转换 触发场景
无锁 → 偏向锁 线程首次获取锁,无竞争
偏向锁 → 轻量级锁 其他线程尝试竞争偏向锁
偏向锁 → 无锁 持有偏向锁的线程执行完毕,无其他线程竞争
轻量级锁 → 重量级锁 自旋失败,或多个线程同时竞争轻量级锁
轻量级锁 → 无锁 线程释放锁,无其他线程竞争
重量级锁 → 无锁 线程释放锁,无其他线程竞争

4.2 轻量级锁→重量级锁的具体膨胀流程(结合示例)

当轻量级锁竞争超过自旋阈值后,会触发锁膨胀,具体流程如下:

  1. 竞争失败触发膨胀:Thread-1 尝试获取轻量级锁失败(说明自旋失效、存在持续竞争),进入锁膨胀流程。
  2. 绑定 Monitor 锁:为 Object 对象申请对应的 Monitor 锁,将 Object 对象头的 Mark Word 替换为该 Monitor 的地址(此时锁状态标记为 "重量级锁")。
  3. 线程阻塞入队 :Thread-1 进入 Monitor 的 EntryList 队列,线程状态变为BLOCKED
  4. 重量级解锁流程
    • Thread-0 执行完临界区代码,尝试用 CAS 恢复 Object 的 Mark Word(轻量级锁的解锁逻辑),但此时 Object 头已指向 Monitor,CAS 失败。
    • Thread-0 切换至重量级解锁:通过 Object 头的 Monitor 地址找到对应的 Monitor 对象,将 Monitor 的Owner设置为null,同时唤醒 EntryList 中处于BLOCKED状态的线程(如 Thread-1)参与锁竞争。

4.3 完整转换流程图

五、其他优化:锁消除与锁粗化

5.1 锁消除

JVM 通过逃逸分析,识别出不会被多线程共享的局部对象,自动消除其 synchronized 锁。

代码示例
java 复制代码
public class LockEliminationDemo {
    // 局部对象仅当前线程使用,JVM会消除synchronized锁
    public String processString(String str) {
        // new Object()是局部对象,无逃逸,锁被消除
        synchronized (new Object()) {
            return str.toUpperCase() + "-" + System.currentTimeMillis();
        }
    }

    public static void main(String[] args) {
        LockEliminationDemo demo = new LockEliminationDemo();
        // 单线程执行,锁消除优化生效
        for (int i = 0; i < 10000; i++) {
            demo.processString("test" + i);
        }
        System.out.println("执行完成(锁消除已生效)");
    }
}

关键说明

  • 可通过 JVM 参数-XX:+EliminateLocks开启锁消除(JDK8 默认开启),-XX:-EliminateLocks禁用。
  • 示例中new Object()创建的对象仅在方法内使用,无线程共享,锁操作冗余,JVM 会自动消除。

5.2 锁粗化

将多个连续的细粒度锁合并为一个粗粒度锁,减少锁获取和释放的次数。

代码示例
java 复制代码
public class LockCoarseningDemo {
    private final Object lock = new Object();
    private StringBuilder sb = new StringBuilder();

    // 循环内多次获取锁,JVM会将锁粗化到循环外部
    public void appendStrings(String... strs) {
        for (String str : strs) {
            synchronized (lock) { // 细粒度锁
                sb.append(str);
            }
        }
    }

    public static void main(String[] args) {
        LockCoarseningDemo demo = new LockCoarseningDemo();
        demo.appendStrings("a", "b", "c", "d", "e");
        System.out.println(demo.sb.toString());
    }
}

关键说明

  • 未优化前,循环内每次 append 都要获取和释放锁,开销较大。
  • JVM 锁粗化后,会在循环开始前获取一次锁,循环结束后释放,减少锁操作次数。

六、优化效果对比

锁状态 适用场景 获取锁开销 释放锁开销 并发性能 代码特征
偏向锁 单线程重复获取 极低(仅检查线程 ID) 无(不主动释放) 最高 单线程执行同步块,无竞争
轻量级锁 低并发交替获取 低(CAS 操作) 低(CAS 操作) 多线程交替执行短临界区
重量级锁 高并发竞争 高(内核态阻塞) 高(内核态唤醒) 多线程同时执行长临界区

七、实践建议

  1. 避免在临界区执行耗时操作(如 IO、网络请求),否则会导致重量级锁长时间持有,降低并发效率。

  2. 减少锁粒度:将大临界区拆分为多个小临界区,使用不同的锁对象,提升并发度(如 ConcurrentHashMap 的分段锁思想)。

    java 复制代码
    // 优化前:一个锁控制多个逻辑
    synchronized (lock) {
        createOrder();
        updateStock();
    }
    // 优化后:细粒度锁,逻辑并行执行
    synchronized (orderLock) { createOrder(); }
    synchronized (stockLock) { updateStock(); }
  3. 利用锁优化的特性:单线程场景下 synchronized 开销极低,无需刻意替换为 Lock;高并发场景可结合 Lock(如 ReentrantLock)灵活控制锁行为。

  4. 禁用偏向锁(可选):如果程序存在大量短时间竞争的场景,可通过 JVM 参数-XX:-UseBiasedLocking禁用偏向锁,避免偏向锁撤销的开销。

相关推荐
木井巳3 分钟前
【递归算法】求根节点到叶节点数字之和
java·算法·leetcode·深度优先
爱学习的阿磊4 分钟前
使用XGBoost赢得Kaggle比赛
jvm·数据库·python
没有bug.的程序员6 分钟前
Spring Boot 事务管理:@Transactional 失效场景、底层内幕与分布式补偿实战终极指南
java·spring boot·分布式·后端·transactional·失效场景·底层内幕
智航GIS9 分钟前
ArcGIS Python零基础脚本开发教程---1.1 Describe 函数
开发语言·python·arcgis
云游云记13 分钟前
php 网络请求工具全解:cURL 与 Guzzle 总结
开发语言·网络·php
华农第一蒟蒻14 分钟前
一次服务器CPU飙升的排查与解决
java·运维·服务器·spring boot·arthas
m0_7482299922 分钟前
帝国CMS后台搭建全攻略
java·c语言·开发语言·学习
weixin_4624462328 分钟前
PaddleX 3.2 人脸识别实战:自定义人脸库 + CartoonFace 官方案例 Top-K 识别完整指南
开发语言·r语言
Testopia44 分钟前
走一遍 AI 学习之路 —— AI实例系列说明
开发语言·人工智能·python
码农娟1 小时前
Hutool XML工具-XmlUtil的使用
xml·java