一、synchronized 关键字概述

synchronized 的作用

synchronized 是 Java 中的关键字,用于实现线程同步,确保多个线程在访问共享资源时的线程安全。其主要作用包括:

  1. 互斥性:同一时刻只允许一个线程访问被 synchronized 修饰的代码块或方法。
  2. 可见性:线程在释放锁时,会将共享变量的修改刷新到主内存,保证其他线程能立即看到最新值。

synchronized 的意义

  1. 解决线程安全问题:防止多线程环境下出现数据竞争(Race Condition)和脏读等问题。
  2. 简化并发编程:相比手动使用 Lock 接口,synchronized 语法更简洁,且自动管理锁的获取与释放。

使用场景

  1. 修饰实例方法:锁对象是当前实例(this)。

    public synchronized void method() {
        // 线程安全代码
    }
    
  2. 修饰静态方法:锁对象是当前类的 Class 对象。

    public static synchronized void staticMethod() {
        // 线程安全代码
    }
    
  3. 修饰代码块:需显式指定锁对象(可以是任意对象)。

    public void block() {
        synchronized (lockObject) {
            // 线程安全代码
        }
    }
    

注意事项

  1. 锁对象选择

    • 避免使用字符串常量或基本类型包装类(如 Integer),可能因 JVM 缓存导致意外锁冲突。
    • 推荐使用私有、不可变的成员变量作为锁对象。
  2. 性能影响

    • 过度使用 synchronized 可能导致线程阻塞,降低并发性能。
    • 细粒度锁(如代码块)通常比粗粒度锁(如整个方法)更高效。
  3. 死锁风险

    • 多个线程互相持有对方需要的锁时会导致死锁。需避免嵌套锁或按固定顺序获取锁。

示例代码

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    // 实例方法同步
    public synchronized void increment() {
        count++;
    }

    // 静态方法同步
    public static synchronized void log() {
        System.out.println("Static synchronized method");
    }

    // 代码块同步
    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }
}

synchronized 的使用场景

1. 保护共享资源
  • 定义:当多个线程同时访问共享变量或对象时,使用 synchronized 防止数据竞争(Race Condition)。
  • 示例
    public class Counter {
        private int count = 0;
        
        public synchronized void increment() {
            count++; // 线程安全操作
        }
    }
    
2. 保证方法原子性
  • 定义:确保一个方法的所有操作作为一个不可分割的单元执行。
  • 示例
    public synchronized void transfer(Account from, Account to, int amount) {
        from.withdraw(amount);
        to.deposit(amount);
    }
    
3. 实现线程间通信
  • 定义:结合 wait()notify() 方法,实现线程的等待/唤醒机制。
  • 示例
    public class MessageQueue {
        private String message;
        
        public synchronized void put(String msg) {
            while (message != null) {
                wait(); // 等待消息被消费
            }
            message = msg;
            notify(); // 唤醒消费者线程
        }
        
        public synchronized String take() {
            while (message == null) {
                wait(); // 等待消息到达
            }
            String msg = message;
            message = null;
            notify(); // 唤醒生产者线程
            return msg;
        }
    }
    
4. 单例模式的双重检查锁
  • 定义:确保懒汉式单例的线程安全。
  • 示例
    public class Singleton {
        private static volatile Singleton instance;
        
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
5. 保护代码块而非整个方法
  • 定义:仅对关键代码段加锁,减少锁的粒度以提高性能。
  • 示例
    public void updateSharedData() {
        // 非线程安全操作
        synchronized (this) {
            // 仅保护核心逻辑
        }
        // 其他非线程安全操作
    }
    
注意事项
  1. 锁对象选择:实例方法默认锁 this,静态方法锁 Class 对象,代码块需显式指定锁对象。
  2. 避免死锁:确保多个线程以相同顺序获取锁。
  3. 性能影响:过度使用会导致线程串行化,降低并发性。

synchronized 在 Java 并发编程中的地位

核心地位
  1. 基础同步机制:synchronized 是 Java 最基础的线程同步工具,自 JDK 1.0 引入,是解决并发问题的基石。
  2. 内置锁(Monitor)实现:直接基于 JVM 层实现,无需依赖外部库,是 Java 语言原生的线程安全解决方案。
关键作用
  1. 原子性保障:确保代码块或方法的执行不可分割,避免竞态条件。
  2. 可见性控制:遵循 happens-before 原则,保证锁释放前对共享变量的修改对其他线程可见。
  3. 有序性约束:阻止指令重排序优化破坏同步代码的逻辑顺序。
应用场景分层
  1. 方法级同步
    public synchronized void method() { /* 临界区 */ }
    
  2. 代码块同步
    synchronized(obj) { /* 临界区 */ }
    
  3. 静态方法同步:锁定类对象(Class 对象)
演进与优化
  1. 锁升级机制(JDK 6+):
    • 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
    • 显著减少同步开销
  2. 与 JUC 的关系
    • 作为基础锁机制,为 ReentrantLock 等高级锁提供对比基准
    • 适合简单的同步场景,复杂场景推荐使用 JUC 工具
不可替代性
  1. 语法简洁性:相比显式锁,代码更简洁
  2. 自动释放:无需手动释放锁,避免死锁风险
  3. JVM 优化支持:持续获得 HotSpot 虚拟机的针对性优化

注意:虽然 JUC 包提供了更灵活的并发工具,但 synchronized 仍是 85% 以上并发场景的首选方案(根据 Oracle 官方统计)。


二、synchronized 的基本用法

synchronized 修饰实例方法

概念定义

synchronized 修饰实例方法时,会将整个方法体作为同步代码块,锁对象是当前实例(this)。同一时刻,只有一个线程能访问该实例的同步方法。

使用场景
  1. 保护实例变量的线程安全:当多个线程操作同一个对象的实例变量时。
  2. 方法级互斥:需要确保某个方法的执行不被其他线程中断。
示例代码
public class Counter {
    private int count = 0;

    // synchronized 修饰实例方法
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
注意事项
  1. 锁对象是当前实例:不同实例的同步方法不会互斥。
    Counter c1 = new Counter();
    Counter c2 = new Counter();
    // 线程1调用c1.increment()和线程2调用c2.increment()不会阻塞
    
  2. 性能影响:同步范围是整个方法,可能降低并发性能。
  3. 继承问题:子类覆盖父类的同步方法时,不会自动继承synchronized修饰符,需显式声明。
常见误区
  • 误认为锁的是方法:实际锁的是对象实例。
  • 误用于静态方法:实例方法的锁和静态方法的锁(类对象)无关。

synchronized 修饰静态方法

概念定义

synchronized 修饰静态方法时,锁的是当前类的 Class 对象(类锁),而不是实例对象。这意味着同一时间只有一个线程可以访问该类的所有静态同步方法。

使用场景
  1. 当多个线程需要访问共享的静态资源时。
  2. 需要保证静态方法的线程安全时。
代码示例
public class Counter {
    private static int count = 0;

    // 静态同步方法
    public static synchronized void increment() {
        count++;
    }

    public static synchronized int getCount() {
        return count;
    }
}
注意事项
  1. 类锁与对象锁独立:静态同步方法和实例同步方法不会互斥,因为它们获取的是不同的锁。
  2. 影响范围:类锁会影响该类的所有静态同步方法,但不会影响非静态方法。
  3. 性能考虑:过度使用静态同步方法可能导致性能问题,因为所有线程都需要竞争同一把锁。
常见误区
  1. 误认为锁的是实例:静态同步方法锁的是类对象,不是实例对象。
  2. 与非静态方法混淆:静态同步方法和非静态同步方法不会互斥,因为它们使用不同的锁。
示例解释

在上述代码中:

  • 多个线程调用 increment()getCount() 时,会竞争 Counter.class 的锁。
  • 同一时间只有一个线程能执行这些静态同步方法。
  • 但如果有非静态同步方法,其他线程仍可以同时访问这些非静态方法。

synchronized 修饰代码块

概念定义

synchronized 修饰代码块是 Java 中实现线程同步的一种方式,它允许开发者精确控制同步范围,只锁定必要的代码片段,而不是整个方法。代码块同步需要指定一个锁对象(可以是任意对象),线程进入代码块前必须先获取该锁对象的监视器锁(Monitor)。

基本语法
synchronized(lockObject) {
    // 需要同步的代码
}
  • lockObject:作为锁的对象,可以是 this、类对象(ClassName.class)或任意实例对象。
使用场景
  1. 缩小同步范围:当只有部分代码需要同步时,避免不必要的性能损耗。
  2. 灵活选择锁对象:可根据需求选择不同的锁对象(如实例锁或类锁)。
  3. 解决竞态条件:例如多线程对共享变量的操作。
示例代码
实例锁(锁定当前对象)
public class Counter {
    private int count = 0;
    private final Object lock = new Object(); // 专用锁对象

    public void increment() {
        synchronized (lock) { // 或使用 synchronized(this)
            count++;
        }
    }
}
类锁(锁定 Class 对象)
public static void staticMethod() {
    synchronized (Counter.class) {
        // 同步静态代码块
    }
}
注意事项
  1. 锁对象选择
    • 使用 this 时,锁的是当前实例,不同实例间不会互斥。
    • 使用类对象(如 ClassName.class)时,锁对所有实例生效。
    • 建议使用私有 final 对象作为锁(如 private final Object lock),避免外部恶意锁定。
  2. 避免锁嵌套:多个锁的嵌套可能导致死锁。
  3. 性能优化:同步块应尽量短小,减少持有锁的时间。
与 synchronized 方法的区别
特性 synchronized 代码块 synchronized 方法
锁范围 自定义代码段 整个方法
锁对象 可指定任意对象 默认 this 或类对象(静态方法)
灵活性

对象锁和类锁的区别

概念定义
  1. 对象锁:作用于实例方法或代码块,锁的是当前对象实例(this)。
  2. 类锁:作用于静态方法或代码块,锁的是类的 Class 对象(如 MyClass.class)。
使用场景
  1. 对象锁:保护非静态成员变量的并发访问。
    public synchronized void method() {} // 对象锁(实例方法)
    public void block() {
        synchronized(this) {} // 对象锁(代码块)
    }
    
  2. 类锁:保护静态成员变量的并发访问或全局配置。
    public static synchronized void staticMethod() {} // 类锁(静态方法)
    public void block() {
        synchronized(MyClass.class) {} // 类锁(代码块)
    }
    
关键区别
特性 对象锁 类锁
锁目标 实例对象(this 类的 Class 对象
影响范围 同一实例的同步方法/块 所有实例的同步静态方法/块
并发性 不同实例间不互斥 全局互斥
示例说明
class Counter {
    private int count = 0;
    private static int staticCount = 0;

    // 对象锁
    public synchronized void add() { count++; }

    // 类锁
    public static synchronized void staticAdd() { staticCount++; }
}
  • 线程A操作 counter1.add() 不会阻塞线程B操作 counter2.add()(不同实例)。
  • 线程A操作 Counter.staticAdd() 会阻塞线程B操作 Counter.staticAdd()(全局互斥)。
注意事项
  1. 类锁会导致所有实例的静态方法串行执行,性能影响较大。
  2. 错误混用对象锁和类锁可能导致并发问题(如用对象锁保护静态变量)。

三、synchronized 的实现原理

对象头中的 Mark Word

概念定义

Mark Word 是 Java 对象头(Object Header)的一部分,用于存储对象的运行时元数据。它通常占用 32 位(32 位 JVM)或 64 位(64 位 JVM)空间,具体内容会根据对象的状态(如无锁、偏向锁、轻量级锁、重量级锁、GC 标记等)动态变化。

存储内容

Mark Word 在不同锁状态下存储的信息不同,主要包括:

  1. 无锁状态
    • 对象的哈希码(identity hashcode)
    • 对象的分代年龄(用于垃圾回收)
  2. 偏向锁状态
    • 偏向线程 ID
    • 偏向时间戳
    • 对象的分代年龄
  3. 轻量级锁状态
    • 指向栈中锁记录的指针
  4. 重量级锁状态
    • 指向监视器(Monitor)的指针
  5. GC 标记状态
    • 标记为可回收状态
示例代码

以下代码展示了如何通过 ClassLayout 工具(来自 jol-core 库)查看对象的 Mark Word:

import org.openjdk.jol.info.ClassLayout;

public class MarkWordExample {
    public static void main(String[] args) {
        Object obj = new Object();
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

输出示例(64 位 JVM,无锁状态):

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
注意事项
  1. 哈希码延迟计算:对象的哈希码(hashCode())在第一次调用时才会计算并存入 Mark Word。
  2. 锁升级:Mark Word 的内容会随着锁状态的变化(如从偏向锁升级为轻量级锁)而动态更新。
  3. 内存对齐:在 64 位 JVM 中,若开启指针压缩(默认开启),Mark Word 仍为 64 位,但对象头其他部分可能被压缩。

监视器锁(Monitor)机制

概念定义

监视器锁(Monitor)是一种同步机制,用于控制多个线程对共享资源的访问。在 Java 中,synchronized 关键字就是基于监视器锁实现的。每个 Java 对象都有一个内置的监视器锁(也称为内部锁或互斥锁),线程可以通过获取该锁来进入同步代码块或方法。

核心特性
  1. 互斥性:同一时刻只有一个线程可以持有监视器锁。
  2. 可重入性:同一个线程可以多次获取同一把锁(避免死锁)。
  3. 内存可见性:锁的释放会强制将工作内存中的修改同步到主内存。
实现原理

监视器锁的实现依赖于:

  • 对象头中的 Mark Word:存储锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
  • 等待队列(Entry Set):存储竞争锁的线程。
  • 条件队列(Wait Set):存储调用 wait() 的线程。
使用方式
// 同步代码块(显式指定锁对象)
synchronized (lockObject) {
    // 临界区代码
}

// 同步方法(锁是当前实例对象或类对象)
public synchronized void method() {
    // 临界区代码
}
锁升级过程
  1. 偏向锁:适用于只有一个线程访问的场景。
  2. 轻量级锁:通过 CAS 实现,适用于低竞争场景。
  3. 重量级锁:真正的监视器锁,涉及操作系统互斥量。
注意事项
  1. 锁对象不能为 null(会抛出 NullPointerException)。
  2. 避免锁粒度过大(影响并发性能)。
  3. 注意锁的嵌套使用可能导致死锁。
  4. 非公平锁特性:不保证等待线程的获取顺序。
示例代码
public class Counter {
    private int count = 0;
    private final Object lock = new Object();
    
    // 同步方法方式
    public synchronized void increment() {
        count++;
    }
    
    // 同步块方式
    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }
}

重量级锁的实现

概念定义

重量级锁是Java中synchronized关键字在竞争激烈时采用的锁机制,依赖于操作系统底层的互斥量(Mutex Lock)实现。当多个线程竞争同一锁时,未获取锁的线程会被阻塞,进入内核态等待唤醒,因此开销较大。

核心实现原理
  1. Monitor机制
    每个Java对象关联一个Monitor(监视器锁),通过ObjectMonitor(C++实现)管理:

    • _owner:指向持有锁的线程
    • _EntryList:存储阻塞中的线程
    • _WaitSet:存储调用wait()的线程
  2. 系统调用依赖
    通过pthread_mutex_lock(Linux)等系统调用实现线程阻塞/唤醒,涉及用户态到内核态的切换。

工作流程
synchronized(obj) {
    // 临界区代码
}
  1. 线程尝试通过CAS获取锁
  2. 失败后进入_EntryList,线程被挂起(OS级阻塞)
  3. 锁释放时唤醒_EntryList中的线程
性能特点
  • 优点:保证严格互斥,解决激烈竞争场景
  • 缺点
    • 上下文切换开销大(约5-10μs)
    • 不适合高并发场景
示例代码
public class HeavyLockExample {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        synchronized(lock) {  // 可能升级为重量级锁
            count++;
        }
    }
}
注意事项
  1. 重量级锁不可逆,一旦升级不会降级(JDK15前)
  2. 通过-XX:+UseHeavyMonitors可强制使用(默认自动升级)
  3. 锁争用导致线程阻塞时,会触发重量级锁

字节码层面的 monitorenter 和 monitorexit

概念定义

monitorentermonitorexit 是 Java 字节码指令,用于实现 synchronized 关键字的同步机制。

  • monitorenter:进入同步块时获取对象的监视器锁(Monitor)。
  • monitorexit:退出同步块时释放监视器锁。
底层原理
  1. 锁的获取

    • 执行 monitorenter 时,JVM 会尝试获取对象的监视器锁:
      • 如果锁未被占用,当前线程获取锁,并将锁的计数器置为 1。
      • 如果锁已被当前线程持有,计数器加 1(可重入性)。
      • 如果锁被其他线程占用,当前线程阻塞,直到锁被释放。
  2. 锁的释放

    • 执行 monitorexit 时,锁的计数器减 1。
    • 当计数器为 0 时,锁被完全释放,其他线程可以竞争锁。
字节码示例

以下代码的同步块:

public void syncMethod() {
    synchronized (this) {
        System.out.println("Hello");
    }
}

编译后的字节码如下(关键部分):

aload_0             // 加载 this 引用到操作数栈
dup                 // 复制栈顶的 this 引用
astore_1            // 存储 this 引用到局部变量表(用于 monitorexit)
monitorenter        // 获取 this 的监视器锁
...                 // 执行同步块内代码
aload_1             // 加载之前存储的 this 引用
monitorexit         // 释放锁(正常退出路径)
goto 20             // 跳过异常处理路径
...                 // 异常处理路径(也会调用 monitorexit 确保锁释放)
注意事项
  1. 隐式的 monitorexit
    • 编译器会自动在同步块结束和异常路径插入 monitorexit,确保锁必然释放。
  2. 可重入性
    • 同一线程多次获取同一锁时,计数器递增,需对应次数的 monitorexit 才能完全释放。
  3. 性能影响
    • monitorenter/monitorexit 涉及操作系统层面的互斥操作,可能引发线程阻塞和上下文切换。
与 JVM 优化的关系

现代 JVM 会针对 monitorenter/monitorexit 进行优化,如:

  • 锁膨胀:从偏向锁升级为轻量级锁、重量级锁。
  • 锁消除:逃逸分析后移除不必要的同步。

四、synchronized 的锁升级过程

无锁状态

概念定义

无锁状态是指一个对象没有被任何线程持有锁的状态。在Java中,每个对象都有一个内置锁(也称为监视器锁或互斥锁),当没有任何线程持有该锁时,对象处于无锁状态。

使用场景
  1. 对象初始化时:新创建的对象默认处于无锁状态。
  2. 锁释放后:当持有锁的线程执行完同步代码块或同步方法并释放锁后,对象会回到无锁状态。
  3. 非同步代码:在非同步代码中访问对象时,对象通常处于无锁状态。
注意事项
  1. 竞争条件:无锁状态下,多个线程可以同时访问对象的非同步方法或代码块,可能导致数据竞争和不一致。
  2. 性能考虑:无锁状态下的操作通常比同步操作性能更高,因为不需要锁的开销。
  3. 可见性问题:无锁状态下,线程可能看不到其他线程对共享变量的修改,除非使用volatile或其他同步机制。
示例代码
public class LockFreeExample {
    private int counter = 0;

    // 无锁状态下的方法
    public void increment() {
        counter++; // 非原子操作,可能导致竞争条件
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        LockFreeExample example = new LockFreeExample(); // 对象初始化为无锁状态

        // 多个线程同时访问无锁方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Counter: " + example.getCounter()); // 结果可能小于2000
    }
}
常见误区
  1. 认为无锁就是线程安全:无锁状态下的操作不一定是线程安全的,除非操作本身是原子的或使用了其他同步机制。
  2. 忽略可见性问题:即使操作是原子的,无锁状态下仍可能存在可见性问题。
  3. 过度依赖无锁:虽然无锁操作性能高,但不适合所有场景,特别是需要保证原子性和一致性的操作。

偏向锁

概念定义

偏向锁是Java 6引入的一种锁优化机制,主要用于减少无竞争情况下的同步开销。其核心思想是:如果一个线程获得了锁,那么锁会“偏向”这个线程,后续该线程再次获取锁时无需任何同步操作。

工作原理
  1. 初始状态:锁对象第一次被线程访问时,JVM会在对象头中记录线程ID(偏向模式开启)。
  2. 重入检查:同一线程再次请求锁时,只需检查对象头的线程ID是否指向自己。
  3. 竞争处理:当其他线程尝试获取锁时,偏向模式结束,可能升级为轻量级锁。
使用场景
  • 适用于只有一个线程访问同步块的场景
  • 适合绝大多数锁在应用生命周期内不会被争用的情况
  • 典型场景:单线程环境或初始化操作
示例代码
public class BiasedLockExample {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        synchronized(lock) {  // 第一次获取,启用偏向
            System.out.println("First lock");
        }
        
        synchronized(lock) {  // 同一线程重入,直接访问
            System.out.println("Second lock");
        }
    }
}
注意事项
  1. 延迟生效:JVM启动后前几秒(默认4s)不会启用偏向锁
  2. 批量重偏向:当一类锁对象被多个线程交替访问时,JVM可能批量撤销偏向
  3. 禁用方式:通过JVM参数-XX:-UseBiasedLocking关闭
  4. 对象头变化:偏向锁会修改对象头的Mark Word结构
性能影响
  • 优势:无竞争时完全消除同步开销
  • 劣势:存在锁撤销的开销(约20个额外时钟周期)
  • 适用性:在明确单线程访问的场景下效果最佳
锁升级路径

偏向锁 → 轻量级锁(出现竞争时)→ 重量级锁(竞争激烈时)


轻量级锁(Lightweight Lock)

概念定义

轻量级锁是Java虚拟机(JVM)针对多线程竞争不激烈的场景设计的一种锁优化机制。它通过CAS(Compare-And-Swap)操作和**线程栈帧的锁记录(Lock Record)**实现,避免了传统重量级锁(如操作系统互斥锁)的性能开销。

核心原理
  1. 加锁过程

    • 当线程访问同步代码块时,JVM会在当前线程的栈帧中创建Lock Record
    • 通过CAS尝试将对象头的Mark Word替换为指向Lock Record的指针。
    • 若成功,线程获得锁;若失败(其他线程已持有锁),则升级为重量级锁
  2. 解锁过程

    • 使用CAS将Mark Word还原回对象头。
    • 若还原失败(锁已膨胀为重量级锁),则唤醒等待线程。
使用场景
  • 低竞争环境:多个线程交替执行同步代码块,而非真正并发竞争。
  • 短时间持有锁:锁的持有时间极短(如简单计算或赋值操作)。
示例代码
public class LightweightLockExample {
    private final Object lock = new Object();

    public void doTask() {
        synchronized (lock) {  // 可能触发轻量级锁
            // 快速操作(如计数器递增)
        }
    }
}
注意事项
  1. 锁膨胀:若多线程竞争加剧,轻量级锁会升级为重量级锁(不可逆)。
  2. 适应性:JVM会根据运行时竞争情况自动选择锁策略(偏向锁→轻量级锁→重量级锁)。
  3. 性能对比
    • 优点:无系统调用,CAS操作在用户态完成。
    • 缺点:CAS自旋可能消耗CPU(竞争激烈时)。
底层结构
  • 对象头Mark Word(64位JVM示例):
    | 锁状态   | 存储内容                           |
    |----------|----------------------------------|
    | 轻量级锁 | 指向线程栈中Lock Record的指针      |
    

重量级锁

概念定义

重量级锁是 Java 中 synchronized 关键字在竞争激烈时采用的锁机制,依赖于操作系统的互斥量(Mutex Lock)实现。当多个线程竞争同一锁时,未获取锁的线程会被操作系统挂起(进入阻塞状态),等待锁释放后被唤醒。这种锁的获取和释放涉及用户态和内核态的切换,开销较大,因此称为“重量级锁”。

使用场景
  1. 高竞争环境:当多个线程频繁争抢同一锁时,偏向锁和轻量级锁会升级为重量级锁。
  2. 长时间持有锁:如果线程持有锁的时间较长(如执行耗时操作),适合直接使用重量级锁避免频繁自旋消耗 CPU。
核心特点
  1. 阻塞等待:未获取锁的线程会被操作系统挂起,不消耗 CPU。
  2. 内核态介入:依赖操作系统提供的互斥量,涉及用户态到内核态的切换。
  3. 开销大:线程阻塞和唤醒需要上下文切换,性能损耗较高。
示例代码
public class HeavyweightLockExample {
    private final Object lock = new Object();

    public void doTask() {
        synchronized (lock) {  // 竞争激烈时升级为重量级锁
            System.out.println(Thread.currentThread().getName() + " 持有锁");
            try {
                Thread.sleep(1000); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        HeavyweightLockExample example = new HeavyweightLockExample();
        // 多个线程竞争同一锁
        for (int i = 0; i < 5; i++) {
            new Thread(example::doTask).start();
        }
    }
}
注意事项
  1. 避免过度竞争:尽量减少锁的持有时间,避免在高并发场景下频繁触发重量级锁。
  2. 锁升级不可逆:一旦升级为重量级锁,即使竞争消失,锁也不会降级(直到下次无竞争时重新偏向)。
  3. 替代方案:在高并发场景下,可考虑 ReentrantLock 或并发容器(如 ConcurrentHashMap)。

锁膨胀的过程

锁膨胀是指 Java 中 synchronized偏向锁逐步升级为轻量级锁,最终可能升级为重量级锁的过程。这一机制旨在优化多线程竞争下的性能。

1. 无锁状态
  • 对象刚创建时,没有任何线程持有锁,处于无锁状态。
2. 偏向锁
  • 触发条件:第一个线程访问同步代码块时,JVM 会启用偏向锁(通过 Mark Word 记录线程 ID)。
  • 特点:无竞争时,同一线程多次获取锁无需额外开销。
  • 升级条件:当另一个线程尝试获取锁时,偏向锁会撤销(Revoke)。
3. 轻量级锁(自旋锁)
  • 触发条件:偏向锁撤销后,线程通过CAS竞争锁(将 Mark Word 替换为指向线程栈中锁记录的指针)。
  • 特点:线程通过自旋等待锁释放,避免直接阻塞。
  • 升级条件:自旋超过一定次数(默认 10 次,或自适应自旋调整)或竞争加剧时,升级为重量级锁。
4. 重量级锁
  • 触发条件:轻量级锁竞争失败后,JVM 会调用操作系统互斥量(Mutex)阻塞线程。
  • 特点:线程进入阻塞队列,由操作系统调度,开销较大。
示例代码
public class LockEscalation {
    private final Object lock = new Object();

    public void doTask() {
        synchronized (lock) { // 锁可能经历偏向→轻量级→重量级
            System.out.println(Thread.currentThread().getName() + " 持有锁");
        }
    }
}
注意事项
  1. 锁不可降级:一旦升级为重量级锁,无法回退。
  2. 偏向锁延迟:JVM 默认延迟 4s 启用偏向锁(通过 -XX:BiasedLockingStartupDelay=0 可关闭)。
  3. 适应性自旋:JVM 会根据历史自旋成功率动态调整自旋次数。

五、synchronized 的优化机制

偏向锁的概念

偏向锁(Biased Locking)是 Java 6 引入的一种锁优化机制,旨在减少无竞争情况下的同步开销。其核心思想是:

  • 假设锁总是由同一线程持有,消除无竞争时的 CAS 操作。
  • 适用于单线程重复访问同步块的场景。

偏向锁的工作原理

  1. 初始状态:锁对象的 Mark Word 记录偏向线程 ID(默认为空)。
  2. 首次加锁:通过 CAS 将线程 ID 写入 Mark Word,后续该线程可直接进入同步块。
  3. 竞争发生:当其他线程尝试获取锁时,撤销偏向锁,升级为轻量级锁。

偏向锁的优化场景

  • 单线程独占:如单例对象的初始化、线程封闭的局部变量。
  • 低竞争环境:如大部分时间仅一个线程访问的缓存。

示例代码

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

    public static void main(String[] args) {
        synchronized (lock) { // 首次加锁,启用偏向锁
            System.out.println("Thread holds biased lock");
        }
        // 同一线程重复获取锁,直接进入同步块
        synchronized (lock) {
            System.out.println("Re-entering with biased lock");
        }
    }
}

注意事项

  1. 延迟启用:JVM 默认在启动后 4 秒才启用偏向锁(通过 -XX:BiasedLockingStartupDelay=0 禁用延迟)。
  2. 锁撤销开销:竞争发生时,撤销偏向锁会触发 STW(Stop-The-World),影响性能。
  3. 禁用场景:高竞争环境(如并发计数器)应通过 -XX:-UseBiasedLocking 关闭偏向锁。

与其他锁的关系

  • 轻量级锁:偏向锁竞争失败后升级为轻量级锁,通过 CAS 自旋尝试获取。
  • 重量级锁:轻量级锁自旋失败后进一步升级为重量级锁,涉及操作系统互斥量。

偏向锁通过减少无竞争时的同步开销,提升了单线程场景下的性能,但需结合实际竞争情况权衡使用。


轻量级锁的自旋优化

概念定义

轻量级锁的自旋优化是 JVM 针对多线程竞争锁时的一种性能优化策略。当线程尝试获取已被占用的轻量级锁时,不会立即阻塞,而是通过**自旋(循环尝试)**等待锁释放,避免线程切换的开销。

使用场景
  1. 锁竞争时间短:适用于锁持有时间非常短的场景(如几十纳秒到微秒级别)。
  2. 多核CPU环境:自旋会占用CPU资源,单核CPU无实际意义。
  3. 轻量级锁状态:仅当锁处于轻量级锁状态时触发(偏向锁升级后)。
实现原理
  1. 自旋次数控制

    • JDK 1.6 前:固定次数(默认10次,可通过 -XX:PreBlockSpin 调整)。
    • JDK 1.6+:自适应自旋(JVM动态调整自旋次数,基于前一次自旋成功率)。
  2. 失败处理

    • 自旋成功:直接获取锁继续执行。
    • 自旋失败:升级为重量级锁,线程进入阻塞状态。
示例代码(伪逻辑)
while (锁未被释放 && 自旋次数未耗尽) {
    // 空循环或短暂停顿(如Thread.yield())
    continue;
}
if (锁未被释放) {
    // 升级为重量级锁
}
注意事项
  1. 避免长时间自旋:若锁持有时间长,自旋会浪费CPU资源。
  2. 适应性权衡:自适应自旋可能在高竞争场景下退化为直接阻塞。
  3. 配置参数
    • -XX:+UseSpinning:启用自旋(JDK 1.6 后默认开启)。
    • -XX:PreBlockSpin:历史版本中调整默认自旋次数。

适应性自旋

概念定义

适应性自旋(Adaptive Spinning)是 Java 中 synchronized 锁优化的一种策略,主要用于减少线程因竞争锁而频繁挂起和唤醒的开销。它基于历史数据动态调整自旋次数,避免盲目自旋浪费 CPU 资源。

工作原理
  1. 自旋:线程在获取锁失败时,不立即挂起,而是循环尝试(自旋)获取锁。
  2. 自适应:JVM 根据锁的持有时间和竞争情况动态调整自旋次数:
    • 若锁近期被成功获取过,则增加自旋次数。
    • 若自旋很少成功,则减少自旋甚至直接挂起线程。
使用场景
  • 轻量级锁竞争:适用于锁持有时间短、竞争不激烈的场景。
  • 多核 CPU 环境:自旋能充分利用 CPU 资源,减少线程切换开销。
示例代码
public class AdaptiveSpinDemo {
    private final Object lock = new Object();

    public void doWork() {
        synchronized (lock) { // JVM 可能在此处触发适应性自旋
            // 临界区代码
        }
    }
}
注意事项
  1. 避免滥用:长时间自旋会浪费 CPU,适用于短任务。
  2. JVM 控制:开发者无法手动配置,由 JVM 自动管理。
  3. 与锁升级关系:适应性自旋通常发生在轻量级锁阶段,若失败会升级为重量级锁。

锁消除(Lock Elimination)

概念定义

锁消除是JVM在即时编译(JIT)时进行的一种优化技术。它通过逃逸分析(Escape Analysis)检测到某些同步代码块的锁对象不可能被其他线程访问时,会直接移除这些无意义的锁操作,从而减少同步开销。

使用场景
  1. 局部变量同步:锁对象是方法内部的局部变量,且未逃逸出当前线程。
    public void foo() {
        Object lock = new Object(); // 锁对象未逃逸
        synchronized(lock) {       // 锁会被消除
            System.out.println("Hello");
        }
    }
    
  2. 线程封闭对象:如StringBuffer在单线程中被使用时,其内置的synchronized会被消除。
技术原理
  1. 逃逸分析:判断对象的作用域是否仅限于当前线程。
  2. JIT优化:在编译为本地代码时,直接移除锁相关的字节码指令。
注意事项
  1. 依赖JVM实现:需开启-XX:+DoEscapeAnalysis(默认开启)。
  2. 不适用于共享数据:若锁对象可能被其他线程访问(如作为返回值或存入静态字段),则无法消除。
  3. 性能对比:在单线程场景下,StringBuilder(无锁)仍比StringBuffer(锁消除后)稍快,因后者有额外的锁检查逻辑。
示例验证
public class LockEliminationDemo {
    // 对比锁消除前后的性能差异
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            createBuffer(); // 锁会被消除
        }
        System.out.println("Time: " + (System.currentTimeMillis() - start));
    }

    private static String createBuffer() {
        StringBuffer sb = new StringBuffer(); // 局部变量未逃逸
        sb.append("Lock");
        sb.append("Elimination");
        return sb.toString();
    }
}

通过-XX:+PrintCompilation可观察JIT优化日志。


锁粗化(Lock Coarsening)

概念定义

锁粗化是 JVM 对 synchronized 同步块的一种优化技术。当编译器或运行时检测到连续多次对同一对象加锁/解锁,且这些操作之间没有其他重要代码时,会自动将多个细粒度的锁合并为一个粗粒度的锁,从而减少锁的开销。

使用场景

典型场景包括:

  1. 循环体内频繁加锁:例如在循环中反复对同一对象加锁。
  2. 相邻同步块:多个连续的 synchronized 块之间无复杂逻辑。
示例代码
// 优化前(可能触发锁粗化)
for (int i = 0; i < 100; i++) {
    synchronized(this) {
        // 简单操作
    }
}

// 优化后(JVM 可能合并为单个锁)
synchronized(this) {
    for (int i = 0; i < 100; i++) {
        // 简单操作
    }
}
注意事项
  1. 非绝对优化:JVM 会根据代码实际执行情况决定是否粗化。
  2. 锁持有时间:粗化后锁持有时间变长,可能影响并发性能。
  3. 与锁消除区别:锁消除是直接去掉不必要的锁,而锁粗化是合并锁范围。
底层原理

通过逃逸分析确认锁对象无竞争时,JIT 编译器会将相邻锁区域合并,减少 monitorenter/monitorexit 指令的调用次数。


六、synchronized 的性能分析

synchronized 与性能开销

概念定义

synchronized 是 Java 中实现线程同步的关键字,用于保证多线程环境下代码块的原子性和可见性。由于它涉及锁机制(如监视器锁),在高并发场景下可能带来显著的性能开销。

性能开销来源
  1. 锁竞争

    • 多个线程争抢同一把锁时,未抢到锁的线程会进入阻塞状态(BLOCKED),导致上下文切换(Context Switching),消耗 CPU 资源。
    • 示例:线程 A 持有锁时,线程 B 和 C 需要等待,触发线程调度。
  2. 锁升级过程

    • 从无锁 → 偏向锁 → 轻量级锁 → 重量级锁的升级过程(JDK 优化后)会消耗额外资源。
    • 重量级锁依赖操作系统互斥量(Mutex),涉及用户态到内核态的切换,开销最大。
  3. 内存屏障(Memory Barrier)

    • synchronized 会插入内存屏障指令,保证可见性,但会限制编译器和 CPU 的指令重排序优化。
优化建议
  1. 减小同步范围
    仅同步必要的代码块(而非整个方法),减少锁持有时间。

    // 不推荐
    public synchronized void method() { /* 全部代码 */ }
    
    // 推荐
    public void method() {
        synchronized(this) { /* 仅同步关键部分 */ }
    }
    
  2. 降低锁粒度
    使用细粒度锁(如 ConcurrentHashMap 的分段锁)替代全局锁。

  3. 避免嵌套锁
    嵌套锁易引发死锁,且增加竞争概率。

  4. 考虑替代方案

    • 读多写少场景:使用 ReentrantReadWriteLock
    • 高并发场景:使用 java.util.concurrent 包中的无锁结构(如 AtomicInteger)。
误区与注意事项
  • 误区:认为 synchronized 一定比 ReentrantLock 慢。
    • JDK 6 后 synchronized 已优化(如锁消除、锁粗化),性能差距缩小。
  • 注意:过度优化可能导致代码复杂度上升,需权衡可维护性与性能。

synchronized 与 ReentrantLock 的性能对比

性能差异概述
  1. 早期版本(Java 5 之前)

    • synchronized 是重量级锁,依赖 JVM 的 Monitor 实现,性能较差。
    • ReentrantLock 基于 AQS(AbstractQueuedSynchronizer),通过 CAS 操作优化竞争,性能更优。
  2. Java 6 及之后

    • JVM 对 synchronized 进行了优化(如偏向锁、轻量级锁、适应性自旋等),性能差距显著缩小。
    • 在低竞争场景下,两者性能接近;高竞争时 ReentrantLock 仍可能略优。
关键性能指标对比
特性 synchronized ReentrantLock
获取锁的方式 JVM 隐式管理 显式调用 lock()/unlock()
吞吐量 低竞争时优,高竞争时较差 高竞争时更稳定(可配置公平/非公平)
中断响应 不支持 支持 lockInterruptibly()
超时机制 不支持 支持 tryLock(timeout)
条件变量 仅通过 wait()/notify() 支持多个 Condition 对象
适用场景建议
  1. 优先选择 synchronized

    • 简单同步场景(如单方法内的互斥)。
    • 需要减少代码复杂度时(自动释放锁)。
  2. 选择 ReentrantLock

    • 需要高级功能(如超时、中断、公平锁)。
    • 高竞争场景(如线程池任务队列)。
示例代码对比
// synchronized 实现
public class SyncExample {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}

// ReentrantLock 实现
public class LockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
注意事项
  1. synchronized 可能因优化导致锁升级(无锁→偏向锁→轻量级锁→重量级锁)。
  2. ReentrantLock 必须手动释放锁(否则会导致死锁),建议在 finally 中调用 unlock()

synchronized 锁状态性能差异

Java中的synchronized锁有四种状态:无锁、偏向锁、轻量级锁和重量级锁。这些状态会随着竞争情况升级,性能表现各不相同。

无锁状态
  • 定义:对象未被任何线程锁定
  • 性能:最高,无同步开销
  • 场景:单线程访问或只读操作
偏向锁
  • 定义:假设只有一个线程会访问同步块
  • 性能:极低开销(仅CAS设置线程ID)
  • 场景:单线程重复访问同步块
  • 特点:第一次获取锁需要CAS,之后只需检查线程ID

示例代码:

synchronized(obj) { // 第一次进入:偏向锁获取
    // 同一线程再次进入:直接访问
}
轻量级锁
  • 定义:多线程交替访问但无竞争
  • 性能:中等开销(CAS+自旋)
  • 场景:低并发、短时间同步块
  • 特点
    • 通过CAS将对象头替换为锁记录指针
    • 失败时会自旋尝试(自适应自旋)
重量级锁
  • 定义:多线程激烈竞争
  • 性能:高开销(OS层面线程阻塞)
  • 场景:高并发、长时间同步块
  • 特点
    • 未获取锁的线程会进入阻塞状态
    • 涉及用户态到内核态的切换
性能对比表
锁状态 适用场景 性能开销 升级条件
无锁 单线程 N/A
偏向锁 单线程重复访问 极低 出现第二个线程竞争
轻量级锁 多线程交替访问 中等 自旋超过阈值或第三个线程竞争
重量级锁 多线程激烈竞争 已升级
优化建议
  1. 减少同步块范围
  2. 避免在长时间操作(如IO)上加锁
  3. 对于高并发场景考虑使用显式锁(如ReentrantLock)

synchronized 在高并发场景下的性能表现

概念定义

synchronized 是 Java 中的内置锁(监视器锁),用于实现线程同步。在高并发场景下,它的性能表现直接影响系统的吞吐量和响应时间。

性能影响因素
  1. 锁竞争

    • 当多个线程同时竞争同一把锁时,未获取锁的线程会进入阻塞状态,导致上下文切换开销。
    • 竞争越激烈,性能下降越明显。
  2. 锁粒度

    • 粗粒度锁:锁住整个方法或大段代码,减少锁竞争频率,但并发度低。
    • 细粒度锁:只锁必要部分(如代码块或特定对象),提高并发度,但可能增加锁开销。
  3. 锁升级(JDK 6+优化):

    • 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。
    • 高并发时可能升级为重量级锁,导致性能下降。
优化建议
  1. 减少锁持有时间

    • 只锁必要的代码块,避免在锁内执行耗时操作(如 I/O)。
    // 不推荐
    public synchronized void process() {
        // 耗时操作
    }
    
    // 推荐
    public void process() {
        synchronized(this) {
            // 仅同步必要部分
        }
        // 其他操作
    }
    
  2. 降低锁粒度

    • 使用多个锁对象代替单个锁。
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void methodA() {
        synchronized(lock1) { /* ... */ }
    }
    
    public void methodB() {
        synchronized(lock2) { /* ... */ }
    }
    
  3. 读写分离

    • 读多写少场景,使用 ReadWriteLock 替代 synchronized
  4. 无锁编程

    • 考虑 ConcurrentHashMapAtomicInteger 等并发工具类。
性能对比(示例)
  • 低竞争场景synchronized 性能接近无锁(偏向锁优化)。
  • 高竞争场景:性能显著下降,可能低于 ReentrantLock(因后者支持公平锁、条件变量等)。
注意事项
  1. 避免锁嵌套(死锁风险)。
  2. 锁对象通常声明为 final,防止意外修改。
  3. JDK 6+ 已对 synchronized 做了大量优化,无需过度排斥。

七、synchronized 的注意事项

死锁问题及预防

死锁定义

死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。

死锁产生的四个必要条件
  1. 互斥条件:资源一次只能被一个线程占用。
  2. 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源。
  3. 不可剥夺条件:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺。
  4. 循环等待条件:存在一个线程等待的循环链,每个线程都在等待下一个线程所占用的资源。
死锁示例代码
public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread1 holds lock1");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("Thread1 holds lock2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread2 holds lock2");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("Thread2 holds lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}
死锁预防方法
  1. 破坏互斥条件:尽可能使用共享资源(如读写锁),但某些资源必须互斥访问(如打印机)。
  2. 破坏占有并等待
    • 一次性申请所有需要的资源(避免部分持有)。
    • 使用tryLock()尝试获取锁,失败则释放已持有的锁。
  3. 破坏不可剥夺条件:允许系统强制回收资源(Java中较难实现)。
  4. 破坏循环等待条件
    • 按固定顺序获取锁(如统一先获取lock1再获取lock2)。
    • 使用资源排序策略。
实际开发建议
  1. 锁顺序:统一锁的获取顺序(如按hashCode排序)。
  2. 超时机制:使用Lock.tryLock(timeout)避免无限等待。
  3. 减少锁粒度:缩小同步代码块范围。
  4. 避免嵌套锁:尽量避免在持有锁时再获取其他锁。
检测工具
  • 使用jstackVisualVM分析线程转储(Thread Dump),查找BLOCKED状态和锁持有关系。

锁粒度的选择

锁粒度是指锁定的范围大小,通常分为粗粒度锁细粒度锁。选择合适的锁粒度是优化并发性能的关键。

粗粒度锁
  • 定义:锁定整个对象或方法,例如直接使用 synchronized 修饰方法。
  • 优点:实现简单,不易出现死锁。
  • 缺点:并发性能差,所有线程必须串行访问。
  • 适用场景:临界区代码简单或竞争不激烈时。

示例:

public synchronized void update() {
    // 方法体
}
细粒度锁
  • 定义:只锁定必要的部分,例如锁定特定变量或代码块。
  • 优点:提高并发性能,允许多个线程同时访问不同部分。
  • 缺点:实现复杂,可能引发死锁。
  • 适用场景:临界区代码复杂或竞争激烈时。

示例:

private final Object lock = new Object();

public void update() {
    synchronized(lock) {
        // 临界区代码
    }
}
选择原则
  1. 尽量减小锁范围:只在必要的地方加锁。
  2. 避免锁嵌套:容易导致死锁。
  3. 权衡性能与复杂度:细粒度锁性能更好但实现更复杂。
常见误区
  • 过度同步:滥用 synchronized 导致性能下降。
  • 锁对象选择不当:例如使用可变对象作为锁。
  • 忽略死锁风险:多个锁的获取顺序不一致。
最佳实践
  • 优先使用 java.util.concurrent 包中的并发工具。
  • 对读写分离场景使用 ReadWriteLock
  • 使用 volatile 或原子类替代简单场景的锁。

避免锁的过度使用

锁的过度使用问题

过度使用锁会导致性能下降、死锁风险增加以及代码复杂度上升。在高并发场景下,锁的争用会成为系统瓶颈。

常见过度使用场景
  1. 方法级同步:直接在方法上使用synchronized,导致整个方法串行执行。
  2. 锁粒度太大:使用一个大锁保护多个不相关的资源。
  3. 嵌套锁:在已持有锁的情况下又申请其他锁,容易导致死锁。
优化策略
  1. 减小锁粒度
// 反例:粗粒度锁
synchronized(this) {
    // 操作多个独立资源
}

// 正例:细粒度锁
Object lock1 = new Object();
Object lock2 = new Object();

synchronized(lock1) {
    // 操作资源1
}
synchronized(lock2) {
    // 操作资源2
}
  1. 使用读写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

void readData() {
    rwLock.readLock().lock();
    try {
        // 读操作
    } finally {
        rwLock.readLock().unlock();
    }
}
  1. 使用并发容器
// 替代手动同步的集合
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
  1. 无锁编程
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子操作无需锁
注意事项
  1. 不要为了"安全"而盲目加锁
  2. 优先考虑线程封闭(ThreadLocal)或不可变对象
  3. 锁的范围应只包含必要的临界区代码
  4. 避免在锁内执行耗时操作(如IO)

锁的可重入性

概念定义

锁的可重入性(Reentrancy)指的是同一个线程可以多次获取同一把锁而不会导致死锁。简单来说,如果一个线程已经持有了某个锁,那么它可以再次请求该锁而不会被阻塞。

使用场景
  1. 递归调用:在递归方法中,如果方法内部使用了synchronized,可重入性可以确保递归调用不会因为锁的重复获取而阻塞。
  2. 嵌套锁:当一个synchronized方法调用另一个synchronized方法时,可重入性可以避免线程因重复获取同一把锁而阻塞。
示例代码
public class ReentrantExample {
    public synchronized void outer() {
        System.out.println("Outer method");
        inner(); // 调用另一个synchronized方法
    }

    public synchronized void inner() {
        System.out.println("Inner method");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.outer(); // 不会发生死锁
    }
}
常见误区或注意事项
  1. 锁的释放次数必须与获取次数匹配:每次重入锁都需要对应一次释放操作,否则可能导致其他线程无法获取锁。
  2. 避免过度嵌套:虽然可重入性提供了便利,但过度嵌套可能导致代码逻辑复杂,增加调试难度。
  3. 非可重入锁的风险:如果使用不可重入锁(如某些自定义锁),在递归或嵌套调用时会导致死锁。
实现原理

Java中的synchronized关键字实现的锁是可重入的,通过以下机制实现:

  1. 锁计数器:每个锁关联一个计数器,记录锁被同一线程获取的次数。
  2. 线程标识:锁会记录持有它的线程,当同一线程再次请求锁时,计数器递增;释放锁时,计数器递减,直到为0时完全释放锁。

锁的公平性

什么是锁的公平性

锁的公平性指的是多个线程竞争同一把锁时,获取锁的顺序是否按照线程请求锁的顺序来分配。公平锁保证先请求的线程先获取锁,而非公平锁则允许"插队"行为。

公平锁 vs 非公平锁
  • 公平锁

    • 严格按照FIFO顺序获取锁
    • 优点:避免线程饥饿
    • 缺点:性能开销较大
  • 非公平锁

    • 允许新请求的线程插队获取锁
    • 优点:吞吐量更高
    • 缺点:可能导致某些线程长时间等待
synchronized的公平性

synchronized关键字实现的锁是非公平锁。当一个线程释放锁时,任何等待的线程都有机会获取锁,不保证先等待的线程先获取。

公平性影响示例
public class FairnessDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Thread-" + i).start();
            // 确保线程按顺序启动
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出可能显示后启动的线程先获取锁,证明synchronized的非公平性。

何时需要公平锁
  • 当线程等待时间差异很重要时
  • 需要避免线程饥饿的场景
  • 对吞吐量要求不高的场景
实现公平锁的替代方案

如果需要公平锁,可以使用ReentrantLock的公平模式:

ReentrantLock fairLock = new ReentrantLock(true);  // true表示公平锁
性能考虑

在大多数情况下,非公平锁的性能优于公平锁,因为减少了线程切换的开销。只有在确实需要保证顺序时才使用公平锁。


八、synchronized 与其他同步机制对比

synchronized 与 volatile 的区别

1. 功能定位
  • synchronized:用于实现线程的互斥同步,保证代码块或方法的原子性、可见性和有序性。
  • volatile:仅保证变量的可见性有序性(禁止指令重排序),但不保证原子性。
2. 原子性
  • synchronized:能保证复合操作(如 i++)的原子性。
  • volatile:不保证原子性,仅适用于单次读/写操作(如 flag = true)。
3. 使用范围
  • synchronized:可修饰代码块或方法。
  • volatile:仅能修饰变量。
4. 性能开销
  • synchronized:涉及线程阻塞和上下文切换,开销较大。
  • volatile:无阻塞,仅通过内存屏障实现,性能更高。
5. 适用场景
  • synchronized:需要互斥访问的复合操作(如计数器递增)。
  • volatile:状态标志(如 while (!stop))或单次赋值的共享变量。
6. 示例代码
// synchronized 示例
public class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++; // 原子性保证
    }
}

// volatile 示例
public class Flag {
    private volatile boolean stop = false;
    public void shutdown() {
        stop = true; // 可见性保证
    }
}
7. 常见误区
  • 误用 volatile 替代锁:如对 volatile int i 执行 i++ 仍存在线程安全问题。
  • 过度使用 synchronized:简单状态标志应优先考虑 volatile。
8. 底层实现
  • synchronized:通过 Monitor 锁(JVM 层)实现。
  • volatile:通过内存屏障(CPU 指令)禁止重排序并强制刷新主内存。

synchronized 与 ReentrantLock 的对比

基本特性
  1. 锁的实现机制

    • synchronized:JVM 内置锁,基于监视器(Monitor)实现,无需手动释放锁。
    • ReentrantLock:JDK 提供的显式锁,基于 AQS(AbstractQueuedSynchronizer)实现,需手动调用 lock()unlock()
  2. 可重入性
    两者均为可重入锁(同一线程可重复获取同一把锁)。

  3. 公平性

    • synchronized:非公平锁(无法指定公平策略)。
    • ReentrantLock:可通过构造函数指定公平锁(new ReentrantLock(true))或非公平锁。
功能对比
  1. 锁的获取方式

    • synchronized:隐式获取/释放锁,代码块或方法级别。
    • ReentrantLock:显式调用 lock()unlock(),支持尝试获取锁(tryLock())和超时机制(tryLock(long timeout, TimeUnit unit))。
  2. 中断响应

    • synchronized:线程阻塞时无法响应中断(Interrupt)。
    • ReentrantLock:支持中断等待锁的线程(lockInterruptibly())。
  3. 条件变量(Condition)

    • synchronized:通过 wait()/notify() 实现单一等待队列。
    • ReentrantLock:支持多个条件变量(newCondition()),可精细化控制线程唤醒。
性能差异
  • JDK 1.6 后,synchronized 经过优化(偏向锁、轻量级锁等),性能与 ReentrantLock 接近。
  • 高竞争场景下,ReentrantLock 的吞吐量可能更高(因可定制化策略)。
示例代码
synchronized 使用
public class SyncExample {
    private final Object lock = new Object();

    public void doSomething() {
        synchronized (lock) {
            // 临界区代码
        }
    }
}
ReentrantLock 使用
public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        lock.lock();
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    }
}
使用场景建议
  • 优先选择 synchronized:简单同步需求(如单机低竞争场景)。
  • 选择 ReentrantLock:需要高级功能(如公平锁、可中断、超时、多条件变量)或高并发优化场景。

synchronized 与 ReadWriteLock 的对比

概念定义
  • synchronized:Java 内置关键字,用于实现线程同步,保证同一时刻只有一个线程访问共享资源(独占锁)。
  • ReadWriteLock:接口(如 ReentrantReadWriteLock),提供读写分离锁,允许多个读线程并发访问,但写线程独占资源。
使用场景
  • synchronized:适合写多读少强一致性场景(如计数器、共享配置更新)。
  • ReadWriteLock:适合读多写少场景(如缓存系统、频繁查询但低频更新的数据)。
性能差异
  • synchronized:所有操作(读/写)互斥,高并发下性能较差。
  • ReadWriteLock:读操作可并发,写操作仍互斥,读多时吞吐量更高。
示例代码
synchronized 实现
public class SynchronizedCounter {
    private int count;
    
    public synchronized void increment() { // 写操作加锁
        count++;
    }
    
    public synchronized int getCount() { // 读操作也加锁
        return count;
    }
}
ReadWriteLock 实现
public class ReadWriteLockCounter {
    private int count;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void increment() {
        lock.writeLock().lock(); // 写锁独占
        try {
            count++;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public int getCount() {
        lock.readLock().lock(); // 读锁共享
        try {
            return count;
        } finally {
            lock.readLock().unlock();
        }
    }
}
注意事项
  1. 锁升级问题ReadWriteLock 不支持从读锁升级到写锁(可能导致死锁)。
  2. 公平性ReentrantReadWriteLock 可配置公平/非公平模式,而 synchronized 是非公平的。
  3. 锁粒度synchronized 锁粒度较粗(方法或代码块),ReadWriteLock 可更灵活控制读写分离。

synchronized 与 StampedLock 的对比

基本概念
  • synchronized:Java 内置的互斥锁,基于 JVM 实现,支持可重入、自动加锁/解锁,适用于简单的同步场景。
  • StampedLock:JDK 8 引入的乐观读锁机制,提供三种模式(写锁、悲观读锁、乐观读),适合读多写少的场景。
性能差异
  • synchronized
    • 悲观锁,竞争激烈时性能下降明显(线程阻塞/唤醒开销大)。
    • 无锁优化(偏向锁→轻量级锁→重量级锁),但高并发下可能直接升级为重量级锁。
  • StampedLock
    • 乐观读锁(无阻塞)在高读低写场景性能更优。
    • 写锁和悲观读锁仍会阻塞,但通过 tryOptimisticRead() 减少竞争。
功能对比
特性 synchronized StampedLock
锁类型 悲观锁 乐观读锁 + 悲观写锁
可重入 支持 不支持
锁升级 支持(JVM 优化) 不支持
条件变量 支持(wait/notify 不支持
中断响应 不支持 支持(tryWriteLock等超时方法)
代码示例
// synchronized 实现计数器
class SyncCounter {
    private int value;
    public synchronized void increment() { value++; }
    public synchronized int get() { return value; }
}

// StampedLock 实现计数器
class StampedLockCounter {
    private int value;
    private final StampedLock lock = new StampedLock();
    
    public void increment() {
        long stamp = lock.writeLock();
        try { value++; } 
        finally { lock.unlockWrite(stamp); }
    }
    
    public int get() {
        long stamp = lock.tryOptimisticRead(); // 乐观读
        int current = value;
        if (!lock.validate(stamp)) { // 检查是否被修改
            stamp = lock.readLock(); // 退化为悲观读
            try { current = value; } 
            finally { lock.unlockRead(stamp); }
        }
        return current;
    }
}
使用场景
  • 优先选 synchronized
    • 简单同步逻辑(如单方法同步)
    • 需要条件变量或可重入特性
  • 优先选 StampedLock
    • 读多写少且性能敏感(如缓存)
    • 需要尝试获取锁(tryXXX方法)
注意事项
  1. StampedLock
    • 不是可重入锁,重复获取会死锁。
    • 必须手动释放锁(建议用 try-finally)。
    • 乐观读后需验证数据一致性(validate())。
  2. synchronized
    • 无法中断等待中的线程。
    • 锁粒度较粗时可能成为性能瓶颈。

九、synchronized 的常见面试题

synchronized 的实现原理

基本概念

synchronized 是 Java 中用于实现线程同步的关键字,主要用于解决多线程环境下的数据竞争问题。它可以修饰方法或代码块,确保同一时间只有一个线程能够访问被保护的资源。

实现机制

synchronized 的实现依赖于 JVM 内置锁(Monitor),具体实现包括以下两种方式:

  1. 修饰代码块
    使用 monitorentermonitorexit 字节码指令实现:

    • monitorenter:尝试获取对象的 Monitor 锁。
    • monitorexit:释放 Monitor 锁(包括正常退出和异常退出)。
    public void method() {
        synchronized (this) { // monitorenter
            // 临界区代码
        } // monitorexit
    }
    
  2. 修饰方法
    方法的访问标志位 ACC_SYNCHRONIZED 会被设置,JVM 在调用方法时自动获取锁。

    public synchronized void method() {
        // 临界区代码
    }
    
锁的升级过程

JVM 对 synchronized 进行了优化,引入了 锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),以减少性能开销:

  1. 偏向锁

    • 适用于单线程场景,通过标记线程 ID 避免重复加锁。
    • 如果发生竞争,升级为轻量级锁。
  2. 轻量级锁

    • 通过 CAS(Compare-And-Swap)尝试获取锁。
    • 如果竞争激烈(自旋失败),升级为重量级锁。
  3. 重量级锁

    • 直接使用操作系统级别的互斥量(Mutex),线程会进入阻塞状态。
底层数据结构

锁信息存储在对象头的 Mark Word 中,包含以下关键字段:

  • 锁标志位(2 bit):标识当前锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
  • 偏向线程 ID:记录持有偏向锁的线程。
  • 锁记录指针:轻量级锁指向栈中的锁记录。
  • Monitor 指针:重量级锁指向 Monitor 对象。
注意事项
  1. 锁的粒度
    • 尽量减小同步代码块的范围,避免不必要的性能损耗。
  2. 锁的对象
    • 静态方法锁的是类对象(Class 对象),非静态方法锁的是当前实例(this)。
  3. 避免死锁
    • 确保锁的获取顺序一致,防止循环等待。
示例代码
public class Counter {
    private int count = 0;
    
    // 同步方法
    public synchronized void increment() {
        count++;
    }
    
    // 同步代码块
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}

synchronized 和 ReentrantLock 的区别

1. 基本概念
  • synchronized:Java 内置的关键字,用于实现线程同步,基于 JVM 层面的锁机制。
  • ReentrantLock:Java 并发包(java.util.concurrent.locks)提供的显式锁,基于 API 实现。
2. 锁的获取与释放
  • synchronized
    • 自动获取和释放锁(进入同步代码块获取锁,退出时释放锁)。
    • 无需手动管理,减少编码错误。
  • ReentrantLock
    • 需要显式调用 lock() 获取锁,unlock() 释放锁。
    • 必须在 finally 块中释放锁,否则可能导致死锁。
3. 锁的特性
  • 可重入性
    • 两者都支持可重入(同一线程可多次获取同一锁)。
  • 公平性
    • synchronized:非公平锁(不保证等待线程的获取顺序)。
    • ReentrantLock:可通过构造函数选择公平锁或非公平锁(默认非公平)。
  • 中断响应
    • synchronized:不支持中断等待。
    • ReentrantLock:支持 lockInterruptibly(),可响应中断。
  • 条件变量
    • synchronized:通过 wait()notify()notifyAll() 实现条件等待。
    • ReentrantLock:可通过 Condition 对象实现更灵活的条件等待(支持多个条件队列)。
4. 性能
  • 低竞争场景
    • synchronized:性能优于 ReentrantLock(JVM 优化)。
  • 高竞争场景
    • ReentrantLock:性能更好(支持更细粒度的控制)。
5. 使用场景
  • synchronized
    • 简单的同步需求(如方法或代码块同步)。
    • 不需要高级功能(如公平锁、条件变量)。
  • ReentrantLock
    • 需要高级功能(如可中断、公平锁、多个条件变量)。
    • 需要更灵活的锁控制(如尝试获取锁 tryLock())。
6. 示例代码
synchronized 示例
public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
ReentrantLock 示例
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
7. 注意事项
  • synchronized
    • 无法手动释放锁,可能因异常导致锁无法释放(需谨慎设计)。
  • ReentrantLock
    • 必须显式释放锁,否则会导致死锁。
    • 适合复杂场景,但代码复杂度较高。

synchronized 的锁升级过程

锁升级的背景

Java 中的 synchronized 关键字在早期版本中性能较差,因为每次加锁和解锁都需要进行操作系统级别的互斥操作(重量级锁)。为了优化性能,JVM 引入了锁升级机制,根据竞争情况动态调整锁的级别。

锁的四种状态
  1. 无锁(No Lock):对象未被任何线程锁定。
  2. 偏向锁(Biased Lock):适用于只有一个线程访问同步块的场景,避免 CAS 操作。
  3. 轻量级锁(Lightweight Lock):适用于多个线程交替访问同步块,通过 CAS 竞争锁。
  4. 重量级锁(Heavyweight Lock):适用于高竞争场景,线程会进入阻塞状态,由操作系统管理。
锁升级的步骤
  1. 初始状态(无锁)
    对象刚创建时,没有任何线程访问,处于无锁状态。

  2. 偏向锁

    • 当第一个线程访问同步块时,JVM 会将锁标记为偏向锁,并在对象头中记录该线程的 ID。
    • 后续该线程再次进入同步块时,只需检查对象头的线程 ID 是否匹配,无需额外操作。
    • 触发条件:没有竞争或只有一个线程访问。
  3. 轻量级锁

    • 当另一个线程尝试获取锁时,JVM 会撤销偏向锁,升级为轻量级锁
    • 轻量级锁通过 CAS(Compare-And-Swap)操作竞争锁:
      • 成功:线程获取锁。
      • 失败:锁膨胀为重量级锁。
    • 触发条件:多个线程交替访问,但没有高竞争。
  4. 重量级锁

    • 当多个线程竞争激烈(CAS 失败多次),轻量级锁会升级为重量级锁
    • 竞争失败的线程会被阻塞,进入操作系统的等待队列,由操作系统调度。
    • 触发条件:高并发竞争。
锁降级

锁升级是单向的(偏向锁 → 轻量级锁 → 重量级锁),但在某些 JVM 实现中,重量级锁在竞争减少时可能会降级为轻量级锁(非标准行为)。

示例代码
public class LockUpgradeExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 偏向锁阶段(单线程)
        synchronized (lock) {
            System.out.println("偏向锁:第一个线程获取锁");
        }

        // 轻量级锁阶段(多线程交替)
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("轻量级锁:第二个线程获取锁");
            }
        }).start();

        // 重量级锁阶段(高竞争)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    System.out.println("重量级锁:高竞争场景");
                }
            }).start();
        }
    }
}
注意事项
  1. 锁升级是 JVM 自动完成的,开发者无法手动干预。
  2. 偏向锁在 JDK 15 后默认禁用(可通过 -XX:+UseBiasedLocking 启用)。
  3. 高竞争场景下,直接使用重量级锁可能更高效(如 ReentrantLock)。

synchronized 与可见性

概念定义

synchronized 关键字不仅能保证原子性,还能保证可见性。可见性指的是一个线程对共享变量的修改,能够立即被其他线程看到。

实现原理
  1. 进入同步块前:JVM 会强制清空工作内存(线程私有缓存),从主内存重新加载共享变量。
  2. 退出同步块后:JVM 会强制将工作内存中的修改刷新到主内存。
示例代码
public class VisibilityDemo {
    private boolean flag = false;
    
    public synchronized void setFlag() {
        flag = true; // 修改对其它线程可见
    }
    
    public synchronized boolean getFlag() {
        return flag; // 能读取到最新值
    }
}
注意事项
  1. 必须对同一把锁(对象监视器)的同步块才能保证可见性
  2. 非同步方法无法保证能看到同步块内的修改
  3. volatile 的区别:
    • synchronized 保证代码块的原子性和可见性
    • volatile 只保证单次读/写的可见性
典型使用场景

当需要保证多线程环境下共享变量的修改对其他线程立即可见时,应使用 synchronized 同步访问方法。


synchronized 的可重入性

什么是可重入性

可重入性指的是同一个线程可以多次获取同一个锁而不会导致死锁。在 Java 中,synchronized可重入锁,即线程可以重复进入由它自己持有的同步代码块或方法。

为什么 synchronized 是可重入的
  1. 避免死锁:如果一个线程已经持有锁,再次尝试获取同一把锁时,如果不可重入,会导致线程自己阻塞自己。
  2. 设计初衷synchronized 的设计考虑了递归调用和嵌套同步的场景。
示例代码
public class ReentrantDemo {
    public synchronized void method1() {
        System.out.println("method1");
        method2(); // 调用另一个同步方法
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ReentrantDemo demo = new ReentrantDemo();
        demo.method1(); // 同一线程可重入
    }
}

输出:

method1
method2
实现原理

JVM 会为每个锁关联一个计数器持有线程

  1. 线程首次获取锁时,计数器置为 1。
  2. 同一线程再次获取锁时,计数器递增。
  3. 退出同步块时计数器递减,归零时释放锁。
注意事项
  1. 可重入次数必须与释放次数匹配,否则可能导致其他线程无法获取锁。
  2. 重入不适用于不同线程(仍需竞争锁)。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐