前言
Java的 synchronized 关键字是 Java 并发编程中最基础、最常用的线程同步机制,它的主要作用是保证多线程在访问共享资源时的线程安全。
采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。
本文的重点是 synchronized 原理,如果你对其他章节不感兴趣可以直接阅读第三章节。
使用方式
synchronized 可以锁定不同的对象,主要是实例级别和类级别两个方面:
| 锁定对象类型 | 语法形式 | 锁对象 | 作用范围 |
|---|---|---|---|
| 实例锁 | synchronized 实例方法 |
this(当前实例) |
当前实例的所有同步实例方法 |
| 特定对象锁 | synchronized(object) |
任意对象(字段、局部变量等) | 锁定特定代码块 |
| 静态锁 | synchronized static 方法 |
类对象(Class对象) |
类的所有实例的所有静态同步方法 |
| 类字面量锁 | synchronized(ClassName.class) |
类对象(Class对象) |
锁定特定代码块 |
| 字符串字面量锁 | synchronized("LOCK") |
字符串常量池中的对象 | 全局范围的锁 |
| 参数对象锁 | synchronized(param) |
方法参数对象 | 根据参数对象确定 |
代码示例:
java
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类的结构进行回顾下:
java
// Monitor 内部结构
class ObjectMonitor {
Thread owner; // 持有锁的线程
int count; // 重入次数
WaitSet waitSet; // 等待集合(调用wait()的线程)
EntryList entryList; // 阻塞队列(等待锁的线程)
void enter() { ... } // 获取锁
void exit() { ... } // 释放锁
void wait() { ... } // 等待
void notify() { ... } // 通知
}
接下来主要介绍下在使用synchronized获取同步锁的时候,发生了什么事情。
执行过程中的三个层面
synchronized 执行过程中主要会经过如下三个层面:
text
synchronized 执行过程:
┌─────────────────────────────────────────────────────────────┐
│ 字节码层面 (Java 编译器生成) │
├─────────────────────────────────────────────────────────────┤
│ 同步方法:添加 ACC_SYNCHRONIZED 标志 │
│ 同步块:生成 monitorenter/monitorexit 指令 │
│ 异常表:确保异常时释放锁 │
└─────────────────────────────────────────────┬───────────────┘
│ JIT 编译优化
┌─────────────────────────────────────────────▼───────────────┐
│ 运行时层面 (JVM 解释执行/JIT) │
├─────────────────────────────────────────────────────────────┤
│ 解释执行:调用 InterpreterRuntime::monitorenter/exit │
│ JIT编译:根据锁状态生成不同机器码 │
│ 锁优化:锁消除、锁粗化、偏向锁、轻量级锁 │
└─────────────────────────────────────────────┬───────────────┘
│ 硬件执行
┌─────────────────────────────────────────────▼───────────────┐
│ 操作系统层面 (重量级锁) │
├─────────────────────────────────────────────────────────────┤
│ 线程阻塞/唤醒:系统调用 pthread_mutex_lock/unlock │
│ 上下文切换:用户态 ↔ 内核态切换 │
│ 调度延迟:等待队列管理 │
└─────────────────────────────────────────────────────────────┘
字节码生成标志
在使用synchronized 时,会在字节码层面进行增强,对于同步方法和同步对象会有不同的表现:
java
// 源码对比
public class SynchronizedBytecode {
// 方式1:同步方法(隐式锁)
public synchronized void syncMethod() {
System.out.println("synchronized method");
}
// 方式2:同步代码块(显式锁)
public void syncBlock() {
synchronized(this) {
System.out.println("synchronized block");
}
}
}
编译后的字节码:
java
// 编译命令: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行(重新处理)
}
这里对实例进行同步时会增加monitorenter 和monitorexit 两个指令,相对比较好理解其实就是对monitor对象的获取。对方法级别进行上锁的时候会有点不一样,这里会对方法的访问标志位即acc flags增加一个ACC_SYNCHRONIZED 标志位,方法调用的时候会尝试去获取当前方法对应实例对象的monitor。
这里对实例对象上锁的时候,可以注意到存在两个monitorexit,主要是为了发生异常也能释放锁。
monitor enter&&exit
这里参考 HotSpot JVM 源码给出一个简化版的monitor获取和释放代码,仅供参考:
cpp
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本身就是可重入的,在有些场景下会需要可重入锁,下面简单举例:
场景:递归调用
java
public class RecursiveDemo {
public synchronized void methodA() {
System.out.println("进入 methodA");
methodB(); // 递归调用,需要重入锁
System.out.println("离开 methodA");
}
public synchronized void methodB() {
System.out.println("进入 methodB");
// 如果没有可重入,这里会死锁!
}
}
场景:继承父类方法
java
class Parent {
public synchronized void parentMethod() {
System.out.println("父类方法");
}
}
class Child extends Parent {
@Override
public synchronized void parentMethod() {
super.parentMethod(); // 调用父类同步方法
System.out.println("子类方法");
}
}
注意点
-
如果同一个方法内同时有两个或更多线程,则每个线程有自己的局部变量拷贝。
-
类的每个实例都有自己的对象级别锁。当一个线程访问实例对象中的synchronized同步代码块或同步方法时,该线程便获取了该实例的对象级别锁,其他线程这时如果要访问synchronized同步代码块或同步方法,便需要阻塞等待,直到前面的线程从同步代码块或方法中退出,释放掉了该对象级别锁。
-
访问同一个类的不同实例对象中的同步代码块,不存在阻塞等待获取对象锁的问题,因为它们获取的是各自实例的对象级别锁,相互之间没有影响。
-
持有一个对象级别锁不会阻止该线程被交换出来,也不会阻塞其他线程访问同一示例对象中的非synchronized代码。当一个线程A持有一个对象级别锁(即进入了synchronized修饰的代码块或方法中)时,线程也有可能被交换出去,此时线程B有可能获取执行该对象中代码的时间,但它只能执行非同步代码(没有用synchronized修饰),当执行到同步代码时,便会被阻塞,此时可能线程规划器又让A线程运行,A线程继续持有对象级别锁,当A线程退出同步代码时(即释放了对象级别锁),如果B线程此时再运行,便会获得该对象级别锁,从而执行synchronized中的代码。
-
持有对象级别锁的线程会让其他线程阻塞在所有的synchronized代码外。例如,在一个类中有三个synchronized方法a,b,c,当线程A正在执行一个实例对象M中的方法a时,它便获得了该对象级别锁,那么其他的线程在执行同一实例对象(即对象M)中的代码时,便会在所有的synchronized方法处阻塞,即在方法a,b,c处都要被阻塞,等线程A释放掉对象级别锁时,其他的线程才可以去执行方法a,b或者c中的代码,从而获得该对象级别锁。
-
使用synchronized(obj)同步语句块,可以获取指定对象上的对象级别锁。obj为对象的引用,如果获取了obj对象上的对象级别锁,在并发访问obj对象时时,便会在其synchronized代码处阻塞等待,直到获取到该obj对象的对象级别锁。当obj为this时,便是获取当前对象的对象级别锁。
-
类级别锁被特定类的所有示例共享,它用于控制对static成员变量以及static方法的并发访问。具体用法与对象级别锁相似。
-
互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果获得了锁,把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁便被释放了。由于synchronized同步块对同一个线程是可重入的,因此一个线程可以多次获得同一个对象的互斥锁,同样,要释放相应次数的该互斥锁,才能最终释放掉该锁。
-
实例锁和类锁不存在冲突,因此可以并行执行。