【Synchronized】不同的使用场景和案例
【Synchronized】不同的使用场景和案例
- 【一】锁的作用范围与锁对象
- 【1】实例方法(对象锁)
- 【2】静态方法(类锁)
- 【3】代码块(显式指定锁对象)
- 【4】类锁(通过Class对象显式锁定)
- 【二】核心区别总结
- 【三】关键注意事项
- 【1】内存可见性
- 【2】避免死锁
- 【3】锁粒度过大问题
- 【四】测试案例补充
- 【五】原理
- 【1】Synchronized锁原理
- (1)对象头
- (2)监视器(Monitor)
- (3)字节码层面的实现
- 【2】锁升级原理
- (1)无锁状态
- (2)偏向锁
- (3)轻量级锁
- (4)重量级锁
- 【3】总结
【一】锁的作用范围与锁对象
【1】实例方法(对象锁)
(1)锁对象:当前实例(this)。
(2)作用范围:同一实例的多个线程访问同步方法时互斥;不同实例的同步方法互不影响。
(3)测试案例:
public class BankAccount {
private int balance = 1000;
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
}
}
}
// 测试代码
BankAccount account1 = new BankAccount();
BankAccount account2 = new BankAccount();
// 线程1操作account1的withdraw方法(互斥)
new Thread(() -> account1.withdraw(500)).start();
// 线程2操作account1的withdraw方法(被阻塞)
new Thread(() -> account1.withdraw(500)).start();
// 线程3操作account2的withdraw方法(不受影响)
new Thread(() -> account2.withdraw(500)).start();
(4)结果:线程1和线程2对account1的操作互斥;线程3与account1的操作无关
【2】静态方法(类锁)
(1)锁对象:类的Class对象(如BankAccount.class)。
(2)作用范围:所有实例调用静态同步方法时互斥。
(3)测试案例:
public class OrderService {
private static int orderCount = 0;
public static synchronized void generateOrder() {
orderCount++;
}
}
// 测试代码
OrderService instanceA = new OrderService();
OrderService instanceB = new OrderService();
// 线程1调用静态方法(互斥)
new Thread(() -> OrderService.generateOrder()).start();
// 线程2调用静态方法(被阻塞)
new Thread(() -> OrderService.generateOrder()).start();
// 线程3通过实例调用静态方法(同样被阻塞)
new Thread(() -> instanceA.generateOrder()).start();
(4)结果:所有线程调用generateOrder()都会竞争同一把类锁,无论通过类名还是实例调用。
【3】代码块(显式指定锁对象)
(1)锁对象:任意显式指定的对象(如实例变量、类对象)。
(2)作用范围:仅同步代码块内的操作,锁粒度更细。
(3)测试案例:
public class CacheManager {
private final Object lock = new Object();
private int cacheSize = 0;
public void updateCache() {
// 非同步代码
System.out.println("非同步操作");
synchronized(lock) { // 显式锁定lock对象
cacheSize++;
}
}
}
// 测试代码
CacheManager manager = new CacheManager();
// 线程1和线程2竞争lock对象的锁
new Thread(() -> manager.updateCache()).start();
new Thread(() -> manager.updateCache()).start();
(4)结果:仅代码块内的cacheSize++操作互斥,其他代码可并发执行347。
【4】类锁(通过Class对象显式锁定)
(1)锁对象:类的Class对象(如ClassName.class)。
(2)作用范围:与静态方法锁效果一致,但可用于非静态方法中。
(3)测试案例:
public class LockDemo {
public void classLockBlock() {
synchronized(LockDemo.class) { // 显式锁定类对象
// 同步代码
}
}
}
// 线程1和线程2调用classLockBlock方法时互斥
LockDemo obj1 = new LockDemo();
LockDemo obj2 = new LockDemo();
new Thread(() -> obj1.classLockBlock()).start();
new Thread(() -> obj2.classLockBlock()).start();
(4)结果:不同实例调用classLockBlock时仍互斥,因为锁的是LockDemo.class
【二】核心区别总结
锁类型 锁对象 作用范围 适用场景
实例方法(对象锁) 当前实例(this) 同一实例的同步方法 保护实例变量(如账户余额)
静态方法(类锁) 类的Class对象 所有实例的静态方法 保护静态变量(如全局计数器)
显式对象锁 指定对象(如lock) 锁定指定对象的同步代码块 细粒度控制(如缓存更新)
显式类锁 ClassName.class 跨实例同步(与静态方法锁等效) 需要全局同步的非静态方法逻辑
【三】关键注意事项
【1】内存可见性
实例锁仅保证同一实例内的变量可见性。
类锁保证所有实例间的变量可见性(因Class对象在方法区共享)。
【2】避免死锁
按固定顺序获取锁(如先锁A再锁B)。
避免嵌套锁(如synchronized方法内调用另一个synchronized方法时需谨慎)。
【3】锁粒度过大问题
错误示例:将耗时操作(如IO)放在同步方法内。
优化方案:仅同步关键代码块
【四】测试案例补充
场景:对象锁与类锁的互斥性测试
public class MixedLockDemo {
public synchronized void instanceMethod() {
// 实例锁
}
public static synchronized void staticMethod() {
// 类锁
}
}
// 线程1调用实例方法,线程2调用静态方法
MixedLockDemo obj = new MixedLockDemo();
new Thread(() -> obj.instanceMethod()).start(); // 锁obj实例
new Thread(() -> MixedLockDemo.staticMethod()).start(); // 锁MixedLockDemo.class
结果:两个线程不会互斥,因为实例锁和类锁是独立的
【五】原理
【1】Synchronized锁原理
(1)对象头
在 Java 中,每个对象都有一个对象头(Object Header),对象头中包含了一些与对象自身相关的信息,如哈希码、分代年龄等,同时也包含了锁的相关信息。对象头的结构在不同的 JVM 实现中可能会有所不同,但通常会包含一个 Mark Word 字段,这个字段用于存储对象的锁状态、哈希码等信息。
(2)监视器(Monitor)
synchronized 关键字的底层实现依赖于监视器(Monitor)。监视器是 Java 中实现同步的基础,它是一个同步工具,相当于一个特殊的房间,线程进入这个房间就相当于获得了锁,其他线程则需要等待。每个 Java 对象都可以关联一个监视器,当一个线程试图访问被 synchronized 修饰的代码块或方法时,它会首先尝试获取该对象的监视器。
(3)字节码层面的实现
当使用 synchronized 修饰代码块时,编译后的字节码会包含 monitorenter 和 monitorexit 指令。例如:
public class SynchronizedExample {
public void test() {
synchronized (this) {
// 同步代码块
}
}
}
编译后的字节码中会包含如下指令:
monitorenter
// 同步代码块的字节码
monitorexit
(1)monitorenter 指令
当线程执行到 monitorenter 指令时,它会尝试获取对象的监视器。如果监视器的进入计数器为 0,表示该监视器没有被其他线程持有,当前线程可以获取该监视器,并将进入计数器加 1。如果监视器已经被其他线程持有,当前线程会被阻塞,直到该监视器被释放。
(2)monitorexit 指令
当线程执行到 monitorexit 指令时,它会将监视器的进入计数器减 1。当进入计数器为 0 时,表示当前线程已经释放了该监视器,其他线程可以尝试获取该监视器。
当使用 synchronized 修饰方法时,编译后的字节码会在方法的访问标志中设置 ACC_SYNCHRONIZED 标志。当线程调用该方法时,会自动检查该标志,如果设置了该标志,线程会首先尝试获取该方法所属对象的监视器,然后再执行方法体。
【2】锁升级原理
在 Java 6 之前,synchronized 是一个重量级锁,性能较低。为了提高性能,Java 6 引入了锁升级机制,使得 synchronized 的性能有了显著提升。锁升级的过程是一个逐步升级的过程,从无锁状态开始,经过偏向锁、轻量级锁,最终升级为重量级锁。
(1)无锁状态
对象刚被创建时,处于无锁状态,Mark Word 中存储的是对象的哈希码、分代年龄等信息。
(2)偏向锁
(1)原理:偏向锁是为了在没有竞争的情况下减少锁的获取和释放带来的性能开销。当一个线程第一次访问被 synchronized 修饰的代码块或方法时,会在对象头的 Mark Word 中记录该线程的 ID,这个过程称为偏向锁的获取。以后该线程再次进入该同步代码块时,无需进行任何同步操作,直接进入代码块执行,这样可以避免频繁的锁获取和释放操作,提高性能。
(2)升级条件:当有其他线程尝试竞争该偏向锁时,偏向锁会升级为轻量级锁。
(3)轻量级锁
(1)原理:当线程获取轻量级锁时,会在当前线程的栈帧中创建一个锁记录(Lock Record),并将对象头的 Mark Word 复制到锁记录中。然后,线程尝试使用 CAS(Compare-And-Swap)操作将对象头的 Mark Word 替换为指向锁记录的指针。如果替换成功,说明该线程成功获取了轻量级锁;如果替换失败,说明有其他线程正在竞争该锁,当前线程会尝试自旋等待锁的释放。
(2)升级条件:如果自旋次数达到一定阈值(通常由 JVM 动态调整),或者有多个线程同时竞争该锁,轻量级锁会升级为重量级锁。
(4)重量级锁
原理:重量级锁依赖于操作系统的互斥量(Mutex)来实现,当线程获取重量级锁失败时,会被阻塞,进入等待队列,直到锁被释放。重量级锁的性能开销较大,因为线程的阻塞和唤醒需要操作系统进行上下文切换,这会消耗较多的 CPU 时间。
【3】总结
synchronized 锁的实现原理基于对象头和监视器,通过 monitorenter 和 monitorexit 指令或 ACC_SYNCHRONIZED 标志来实现线程同步。锁升级原理是为了在不同的竞争情况下选择合适的锁状态,以提高性能。在无竞争的情况下,使用偏向锁;在有少量竞争的情况下,使用轻量级锁;在竞争激烈的情况下,使用重量级锁。