Java多线程知识小结:Synchronized
在Java中,synchronized
关键字是实现线程同步的核心工具,用于保证同一时刻只有一个线程可以执行被修饰的代码块或方法。以下从基本原理、锁升级过程、应用场景及优化建议四个维度详细解析:
一、基本原理
1. 同步的对象
synchronized
锁的是对象而非代码:
- 修饰实例方法:锁的是当前实例对象(
this
); - 修饰静态方法:锁的是类的Class对象(如
MyClass.class
); - 修饰代码块:锁的是括号中的对象(如
synchronized(obj)
)。
2. 底层实现
- JVM层面:通过对象头中的Mark Word和**Monitor(监视器)**实现。
对象头的Mark Word包含锁状态位(如偏向锁、轻量级锁、重量级锁)和指向Monitor的指针。 - 字节码层面:
- 同步方法:通过
ACC_SYNCHRONIZED
标志位标识; - 同步代码块:通过
monitorenter
和monitorexit
指令实现。
- 同步方法:通过
3. 互斥锁语义
- 线程进入同步块前需获取Monitor的所有权(锁),退出时释放锁;
- 未获取到锁的线程会被阻塞,进入Monitor的等待队列。
二、锁升级的具体过程
JDK 6之后,synchronized
锁机制进行了优化,引入锁升级(偏向锁 → 轻量级锁 → 重量级锁)以减少性能开销。
1. 偏向锁(Biased Locking)
- 适用场景:单线程环境,无锁竞争。
- 原理:
- 首次线程访问同步块时,Mark Word存储该线程ID(偏向状态);
- 后续该线程再次进入同步块时,无需CAS操作,直接获取锁;
- 若其他线程尝试竞争锁,偏向锁撤销,升级为轻量级锁。
- 优点:无CAS操作,仅首次获取锁时有少量开销。
- 缺点:存在锁撤销(偏向锁撤销需STW),若存在多线程竞争,反而增加开销。
2. 轻量级锁(Lightweight Locking)
- 适用场景:多线程交替执行同步块(无实际竞争)。
- 原理:
- 线程进入同步块时,JVM在当前线程栈帧中创建锁记录(Lock Record);
- 通过CAS将对象头的Mark Word复制到锁记录,并将Mark Word指向锁记录地址;
- 若CAS成功,获取轻量级锁;若失败,说明存在竞争,锁膨胀为重量级锁。
- 优点:竞争不激烈时,避免线程阻塞,减少用户态/内核态切换。
- 缺点:竞争激烈时,频繁CAS导致性能下降。
3. 重量级锁(Heavyweight Lock)
- 适用场景:多线程同时竞争锁。
- 原理:
- 锁膨胀后,Mark Word指向操作系统的Monitor对象;
- 未获取到锁的线程被阻塞(进入内核态),放入Monitor的等待队列;
- 锁释放时,唤醒等待队列中的线程(需从内核态转回用户态)。
- 缺点:线程阻塞/唤醒涉及用户态与内核态切换,性能开销大。
4. 锁升级流程图
无锁状态 → 偏向锁(单线程) → 轻量级锁(多线程交替) → 重量级锁(多线程竞争)
5. 锁升级的触发条件
- 偏向锁撤销:当其他线程尝试访问偏向锁对象时,JVM在安全点(Safepoint)撤销偏向锁;
- 轻量级锁膨胀:线程尝试CAS获取轻量级锁失败时,锁膨胀为重量级锁;
- 批量重偏向/撤销:当一个类的对象频繁发生偏向锁撤销时,JVM会对该类的对象批量重偏向或撤销。
三、应用场景
1. 保护共享资源
- 示例:多线程操作同一个计数器:
public class Counter {private int count = 0;// 同步方法,锁的是thispublic synchronized void increment() {count++;} }
2. 实现原子操作
- 示例:双重检查锁定(DCL)实现单例模式:
public class Singleton {private static volatile Singleton instance; // 必须用volatile禁止指令重排public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) { // 锁的是Class对象if (instance == null) { // 第二次检查instance = new Singleton();}}}return instance;} }
3. 保证复合操作的原子性
- 示例:检查再执行(Check-Then-Act)操作:
public class Inventory {private Map<String, Integer> stock = new HashMap<>();// 复合操作:先检查库存,再扣减public synchronized boolean decreaseStock(String product, int amount) {if (stock.getOrDefault(product, 0) >= amount) {stock.put(product, stock.get(product) - amount);return true;}return false;} }
4. 线程间协作(wait/notify机制)
- 示例:生产者-消费者模型:
public class ProducerConsumer {private final Queue<Integer> queue = new LinkedList<>();private final int CAPACITY = 10;// 生产者方法public synchronized void produce(int item) throws InterruptedException {while (queue.size() == CAPACITY) {wait(); // 等待队列有空间}queue.add(item);notifyAll(); // 通知消费者有新元素}// 消费者方法public synchronized int consume() throws InterruptedException {while (queue.isEmpty()) {wait(); // 等待队列有元素}int item = queue.poll();notifyAll(); // 通知生产者有空间return item;} }
四、优化建议
1. 缩小同步块范围
- 反例:
public synchronized void process() {// 非关键代码long startTime = System.currentTimeMillis();// 关键代码(需要同步)synchronized (this) {// 操作共享资源}// 非关键代码System.out.println("耗时:" + (System.currentTimeMillis() - startTime)); }
- 正例:仅同步关键代码:
public void process() {long startTime = System.currentTimeMillis();// 仅同步关键代码synchronized (this) {// 操作共享资源}System.out.println("耗时:" + (System.currentTimeMillis() - startTime)); }
2. 优先使用同步代码块而非同步方法
- 同步方法默认锁的是
this
,可能导致锁范围过大; - 同步代码块可精确控制锁对象。
3. 使用细粒度锁替代粗粒度锁
- 反例:
public class Bank {private Map<String, Account> accounts = new HashMap<>();// 粗粒度锁:整个方法同步public synchronized void transfer(String from, String to, double amount) {// 转账逻辑} }
- 正例:
public class Bank {private Map<String, Account> accounts = new HashMap<>();// 细粒度锁:仅锁相关账户public void transfer(String from, String to, double amount) {Account fromAccount = accounts.get(from);Account toAccount = accounts.get(to);// 按顺序加锁,避免死锁Account first = fromAccount.hashCode() < toAccount.hashCode() ? fromAccount : toAccount;Account second = first == fromAccount ? toAccount : fromAccount;synchronized (first) {synchronized (second) {// 转账逻辑}}} }
4. 考虑替代方案
- 原子类(如
AtomicInteger
)替代简单计数器的synchronized
; - 读写锁(
ReentrantReadWriteLock
)替代读多写少场景的synchronized
; - 并发容器(如
ConcurrentHashMap
)替代synchronized Map
。
五、总结
synchronized
关键字的核心特点:
- 优点:
- 使用简单,无需手动释放锁;
- 锁升级机制在不同场景下有较好的性能表现;
- 支持线程间协作(wait/notify)。
- 缺点:
- 无法中断正在等待锁的线程;
- 不支持超时获取锁;
- 锁粒度较粗(要么全锁,要么全放)。
适用场景:
- 简单的同步需求(如保护共享资源、实现原子操作);
- 需配合wait/notify实现线程间协作。
在高并发场景下,若性能敏感,可考虑使用ReentrantLock
、StampedLock
等更灵活的锁机制。