Java 线程同步-03:synchronized 机制

0 阅读12分钟

前言

Java的 synchronized 关键字是 Java 并发编程中最基础、最常用的线程同步机制,它的主要作用是保证多线程在访问共享资源时的线程安全。

采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

本文的重点是 synchronized 原理,如果你对其他章节不感兴趣可以直接阅读第三章节。

使用方式

synchronized 可以锁定不同的对象,主要是实例级别和类级别两个方面:

锁定对象类型语法形式锁对象作用范围
实例锁synchronized 实例方法this(当前实例)当前实例的所有同步实例方法
特定对象锁synchronized(object)任意对象(字段、局部变量等)锁定特定代码块
静态锁synchronized static 方法类对象(Class对象)类的所有实例的所有静态同步方法
类字面量锁synchronized(ClassName.class)类对象(Class对象)锁定特定代码块
字符串字面量锁synchronized("LOCK")字符串常量池中的对象全局范围的锁
参数对象锁synchronized(param)方法参数对象根据参数对象确定

代码示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * synchronized 锁定不同对象的完整示例
 * 演示6种不同的锁定方式及其并发行为
 */
public class SynchronizedDifferentLocksDemo {
    
    // ========== 各种锁对象定义 ==========
    private final Object customLock1 = new Object();   // 特定对象锁1
    private final Object customLock2 = new Object();   // 特定对象锁2
    private final String lockKey = "USER_";           // 字符串锁前缀
    private int instanceCounter = 0;                  // 实例变量
    private static int staticCounter = 0;             // 静态变量
    
    
    // ========== 1. 实例锁 ==========
    /**
     * 方式1:实例方法锁 - 锁定当前实例 (this)
     * 作用:保护实例变量,同一实例的方法调用会互斥
     */
    public synchronized void instanceMethodLock() {
        System.out.println(Thread.currentThread().getName() 
            + " 进入实例方法锁,保护实例变量: " + instanceCounter);
        try {
            instanceCounter++;
            Thread.sleep(300); // 模拟业务处理
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() 
            + " 离开实例方法锁,实例变量: " + instanceCounter);
    }
    
    /**
     * 方式2:同步代码块锁定当前实例 - 等价于实例方法锁
     */
    public void instanceBlockLock() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() 
                + " 进入实例代码块锁");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开实例代码块锁");
        }
    }
    
    
    // ========== 2. 特定对象锁 ==========
    /**
     * 方式3:锁定特定对象1 - 细粒度锁
     * 作用:减少锁竞争,提高并发度
     */
    public void customObjectLock1() {
        synchronized (customLock1) {
            System.out.println(Thread.currentThread().getName() 
                + " 进入特定对象锁1");
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开特定对象锁1");
        }
    }
    
    /**
     * 方式4:锁定特定对象2 - 与锁1不同,可以并行执行
     */
    public void customObjectLock2() {
        synchronized (customLock2) {
            System.out.println(Thread.currentThread().getName() 
                + " 进入特定对象锁2");
            try {
                Thread.sleep(250);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开特定对象锁2");
        }
    }
    
    
    // ========== 3. 静态锁 ==========
    /**
     * 方式5:静态方法锁 - 锁定类对象
     * 作用:保护静态变量,所有实例共享同一把锁
     */
    public static synchronized void staticMethodLock() {
        System.out.println(Thread.currentThread().getName() 
            + " 进入静态方法锁,保护静态变量: " + staticCounter);
        try {
            staticCounter++;
            Thread.sleep(350);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() 
            + " 离开静态方法锁,静态变量: " + staticCounter);
    }
    
    
    // ========== 4. 类字面量锁 ==========
    /**
     * 方式6:类字面量锁 - 锁定类对象,与静态锁等价
     * 作用:类级别互斥
     */
    public void classLiteralLock() {
        synchronized (SynchronizedDifferentLocksDemo.class) {
            System.out.println(Thread.currentThread().getName() 
                + " 进入类字面量锁");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开类字面量锁");
        }
    }
    
    
    // ========== 5. 字符串字面量锁 ==========
    /**
     * 方式7:字符串字面量锁(慎用)
     * 问题:字符串常量池共享,可能导致意外的锁竞争
     */
    public void stringLiteralLock(String userId) {
        // 使用字符串常量作为锁 - 不推荐!
        String lock = lockKey + userId;
        synchronized (lock.intern()) {  // intern()返回常量池中的引用
            System.out.println(Thread.currentThread().getName() 
                + " 进入字符串锁,用户: " + userId);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开字符串锁,用户: " + userId);
        }
    }
    
    
    // ========== 6. 参数对象锁 ==========
    /**
     * 方式8:参数对象锁 - 动态锁
     * 作用:根据传入对象决定锁粒度
     */
    public void parameterObjectLock(Object lockObject) {
        synchronized (lockObject) {
            System.out.println(Thread.currentThread().getName() 
                + " 进入参数对象锁,对象: " + lockObject.getClass().getSimpleName());
            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println(Thread.currentThread().getName() 
                + " 离开参数对象锁");
        }
    }
    
    
    // ========== 测试方法 ==========
    public static void main(String[] args) throws InterruptedException {
        SynchronizedDifferentLocksDemo demo1 = new SynchronizedDifferentLocksDemo();
        SynchronizedDifferentLocksDemo demo2 = new SynchronizedDifferentLocksDemo();
        
        System.out.println("=================== 测试开始 ===================\n");
        
        // 测试1:实例锁 - 不同实例可以并行
        System.out.println("测试1:实例锁 - 不同实例的实例锁不互斥");
        ExecutorService executor1 = Executors.newFixedThreadPool(2);
        executor1.execute(() -> demo1.instanceMethodLock());
        executor1.execute(() -> demo2.instanceMethodLock()); // 可以并行
        executor1.shutdown();
        executor1.awaitTermination(2, TimeUnit.SECONDS);
        
        // 测试2:实例锁 - 同一实例会互斥
        System.out.println("\n测试2:实例锁 - 同一实例的实例锁互斥");
        ExecutorService executor2 = Executors.newFixedThreadPool(2);
        executor2.execute(() -> demo1.instanceMethodLock());
        executor2.execute(() -> demo1.instanceBlockLock()); // 会互斥
        executor2.shutdown();
        executor2.awaitTermination(2, TimeUnit.SECONDS);
        
        // 测试3:不同特定对象锁可以并行
        System.out.println("\n测试3:不同特定对象锁可以并行执行");
        ExecutorService executor3 = Executors.newFixedThreadPool(3);
        executor3.execute(() -> demo1.customObjectLock1());
        executor3.execute(() -> demo1.customObjectLock2()); // 可以并行
        executor3.execute(() -> demo1.customObjectLock1()); // 会互斥
        executor3.shutdown();
        executor3.awaitTermination(2, TimeUnit.SECONDS);
        
        // 测试4:静态锁 - 所有实例共享
        System.out.println("\n测试4:静态锁 - 所有实例共享同一把锁");
        ExecutorService executor4 = Executors.newFixedThreadPool(3);
        executor4.execute(() -> staticMethodLock());
        executor4.execute(() -> SynchronizedDifferentLocksDemo.staticMethodLock());
        executor4.execute(() -> demo1.classLiteralLock()); // 与静态锁互斥
        executor4.shutdown();
        executor4.awaitTermination(3, TimeUnit.SECONDS);
        
        // 测试5:字符串字面量锁(演示问题)
        System.out.println("\n测试5:字符串字面量锁 - 相同字符串会互斥");
        ExecutorService executor5 = Executors.newFixedThreadPool(4);
        executor5.execute(() -> demo1.stringLiteralLock("001"));
        executor5.execute(() -> demo1.stringLiteralLock("002")); // 不同字符串,可以并行
        executor5.execute(() -> demo2.stringLiteralLock("001")); // 相同字符串,会互斥!
        executor5.execute(() -> demo2.stringLiteralLock("001")); // 会互斥
        executor5.shutdown();
        executor5.awaitTermination(3, TimeUnit.SECONDS);
        
        // 测试6:参数对象锁
        System.out.println("\n测试6:参数对象锁 - 根据参数对象决定");
        ExecutorService executor6 = Executors.newFixedThreadPool(4);
        Object param1 = new Object();
        Object param2 = new Object();
        
        executor6.execute(() -> demo1.parameterObjectLock(param1));
        executor6.execute(() -> demo1.parameterObjectLock(param1)); // 相同对象,会互斥
        executor6.execute(() -> demo1.parameterObjectLock(param2)); // 不同对象,可以并行
        executor6.execute(() -> demo1.parameterObjectLock(new Object())); // 新对象,可以并行
        executor6.shutdown();
        executor6.awaitTermination(2, TimeUnit.SECONDS);
        
        // 测试7:混合锁场景
        System.out.println("\n测试7:混合锁场景 - 展示各种锁的并发关系");
        ExecutorService executor7 = Executors.newFixedThreadPool(6);
        
        // 这些方法都可以并行执行,因为锁对象不同
        executor7.execute(() -> demo1.instanceMethodLock());      // 锁 this
        executor7.execute(() -> demo1.customObjectLock1());       // 锁 customLock1
        executor7.execute(() -> demo1.customObjectLock2());       // 锁 customLock2
        executor7.execute(() -> staticMethodLock());              // 锁 Class对象
        executor7.execute(() -> demo1.stringLiteralLock("003"));  // 锁字符串
        executor7.execute(() -> demo1.parameterObjectLock(new Object())); // 锁新对象
        
        executor7.shutdown();
        executor7.awaitTermination(3, TimeUnit.SECONDS);
        
        System.out.println("\n=================== 测试完成 ===================");
        System.out.println("最终实例变量: " + demo1.instanceCounter);
        System.out.println("最终静态变量: " + staticCounter);
    }
}

synchronized 原理

synchronized 是Java内置的互斥锁,基于 JVM 的 Monitor(管程)机制 实现。对于什么Monitor机制,我们在前面的【Java对象内存布局和Monitor机制】已经介绍过了,下面再给出Monitor类的结构进行回顾下:

// Monitor 内部结构
class ObjectMonitor {
    Thread owner;           // 持有锁的线程
    int count;             // 重入次数
    WaitSet waitSet;       // 等待集合(调用wait()的线程)
    EntryList entryList;   // 阻塞队列(等待锁的线程)
    
    void enter() { ... }   // 获取锁
    void exit() { ... }    // 释放锁
    void wait() { ... }    // 等待
    void notify() { ... }  // 通知
}

接下来主要介绍下在使用synchronized获取同步锁的时候,发生了什么事情。

执行过程中的三个层面

synchronized 执行过程中主要会经过如下三个层面:

synchronized 执行过程:
┌─────────────────────────────────────────────────────────────┐
│                   字节码层面 (Java 编译器生成)                 │
├─────────────────────────────────────────────────────────────┤
│ 同步方法:添加 ACC_SYNCHRONIZED 标志                           │
│ 同步块:生成 monitorenter/monitorexit 指令                     │
│ 异常表:确保异常时释放锁                                        │
└─────────────────────────────────────────────┬───────────────┘
                                              │ JIT 编译优化
┌─────────────────────────────────────────────▼───────────────┐
│                 运行时层面 (JVM 解释执行/JIT)                  │
├─────────────────────────────────────────────────────────────┤
│ 解释执行:调用 InterpreterRuntime::monitorenter/exit           │
│ JIT编译:根据锁状态生成不同机器码                                │
│ 锁优化:锁消除、锁粗化、偏向锁、轻量级锁                           │
└─────────────────────────────────────────────┬───────────────┘
                                              │ 硬件执行
┌─────────────────────────────────────────────▼───────────────┐
│                    操作系统层面 (重量级锁)                     │
├─────────────────────────────────────────────────────────────┤
│ 线程阻塞/唤醒:系统调用 pthread_mutex_lock/unlock               │
│ 上下文切换:用户态 ↔ 内核态切换                                  │
│ 调度延迟:等待队列管理                                          │
└─────────────────────────────────────────────────────────────┘

字节码生成标志

在使用synchronized 时,会在字节码层面进行增强,对于同步方法和同步对象会有不同的表现:

// 源码对比
public class SynchronizedBytecode {
    
    // 方式1:同步方法(隐式锁)
    public synchronized void syncMethod() {
        System.out.println("synchronized method");
    }
    
    // 方式2:同步代码块(显式锁)
    public void syncBlock() {
        synchronized(this) {
            System.out.println("synchronized block");
        }
    }
}

编译后的字节码:

// 编译命令:javap -v -p SynchronizedBytecode
public class SynchronizedBytecode {
  
  // syncMethod 的字节码 - 方法级同步
  public synchronized void syncMethod();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // ← 关键标志
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3  // String "synchronized method"
         5: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
  
  // syncBlock 的字节码 - 代码块级同步
  public void syncBlock();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0                           // 加载 this 引用到操作数栈
         1: dup                               // 复制栈顶值(this)
         2: astore_1                          // 存储到局部变量表 slot 1
         3: monitorenter                      // ← 关键指令:进入监视器
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #5                  // String "synchronized block"
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1                           // 加载锁对象
        13: monitorexit                       // ← 正常退出监视器
        14: goto          22                  // 跳转到方法结束
        17: astore_2                          // 异常处理开始:存储异常到 slot 2
        18: aload_1                           // 加载锁对象
        19: monitorexit                       // ← 异常退出监视器
        20: aload_2                           // 重新加载异常
        21: athrow                            // 抛出异常
        22: return
    Exception table:                          // 异常表
       from    to  target type
           4    14    17   any               // 监控4-14行,发生异常跳转到17行
          17    20    17   any               // 监控17-20行,发生异常跳转到17行(重新处理)
}

这里对实例进行同步时会增加monitorentermonitorexit 两个指令,相对比较好理解其实就是对monitor对象的获取。对方法级别进行上锁的时候会有点不一样,这里会对方法的访问标志位即acc flags增加一个ACC_SYNCHRONIZED 标志位,方法调用的时候会尝试去获取当前方法对应实例对象的monitor。

这里对实例对象上锁的时候,可以注意到存在两个monitorexit,主要是为了发生异常也能释放锁。

monitor enter&&exit

这里参考 HotSpot JVM 源码给出一个简化版的monitor获取和释放代码,仅供参考:

class InterpreterRuntime {
    
    // monitorenter 的 C++ 实现
    static void monitorenter(JavaThread* thread, BasicObjectLock* elem) {
        Handle h_obj(thread, elem->obj());
        
        // 尝试快速获取锁
        if (UseBiasedLocking) {
            // 偏向锁处理
            if (BiasedLocking::fast_enter(h_obj, elem, true, thread)) {
                return;
            }
        }
        
        // 慢速路径
        slow_enter(h_obj, elem, thread);
    }
    
    // monitorexit 的 C++ 实现
    static void monitorexit(JavaThread* thread, BasicObjectLock* elem) {
        Handle h_obj(thread, elem->obj());
        
        // 尝试快速释放锁
        if (UseBiasedLocking) {
            if (BiasedLocking::fast_exit(h_obj, elem, thread)) {
                return;
            }
        }
        
        // 慢速路径
        slow_exit(h_obj, elem, thread);
    }
    
    // 慢速进入锁的流程
    static void slow_enter(Handle obj, BasicObjectLock* lock, JavaThread* thread) {
        markOop mark = obj->mark();
        
        // 检查是否无锁
        if (mark->is_unlocked()) {
            // 尝试获取轻量级锁
            lock->set_displaced_header(mark);
            if (mark == obj()->cas_set_mark(markOopDesc::encode(lock), mark)) {
                return; // 轻量级锁获取成功
            }
        } else if (mark->has_locker() && thread->is_lock_owned((address)mark->locker())) {
            // 可重入情况
            lock->set_displaced_header(NULL);
            return;
        }
        
        // 需要膨胀为重量级锁
        inflate(thread, obj(), inflate_cause_monitor_enter)->enter(thread);
    }
}

可重入

可重入锁是指同一个线程可以多次获取同一把锁,而不会导致死锁。在Java中,synchronized本身就是可重入的,在有些场景下会需要可重入锁,下面简单举例:

场景:递归调用

public class RecursiveDemo {
    public synchronized void methodA() {
        System.out.println("进入 methodA");
        methodB();  // 递归调用,需要重入锁
        System.out.println("离开 methodA");
    }
    
    public synchronized void methodB() {
        System.out.println("进入 methodB");
        // 如果没有可重入,这里会死锁!
    }
}

场景:继承父类方法

class Parent {
    public synchronized void parentMethod() {
        System.out.println("父类方法");
    }
}

class Child extends Parent {
    @Override
    public synchronized void parentMethod() {
        super.parentMethod();  // 调用父类同步方法
        System.out.println("子类方法");
    }
}

注意点

  1. 如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。

  2. 类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。

  3. 访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。

  4. 持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。

  5. 持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。

  6. 使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。

  7. 类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。

  8. 互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。

  9. 实例锁和类锁不存在冲突,因此可以并行执行。