17_synchronized关键字深度解析

synchronized关键字深度解析 ------ 从对象锁到锁升级

文章目录

前言

在Java并发编程中,synchronized关键字是解决线程安全问题最基础、最常用的手段。几乎所有Java面试都会问到它的底层原理、锁升级过程、以及与Lock的区别。

很多人对synchronized的认知停留在"给代码块加锁"的层面,但它的底层机制远比表面复杂。synchronized从JDK 1.0就存在了,但在JDK 1.6之前,它确实被称为"重量级锁"------每次加锁都需要操作系统级别的monitor操作,性能很差。JDK 1.6进行了"synchronized性能革命",引入了偏向锁、轻量级锁、自适应自旋、锁消除、锁粗化等一系列优化,使得synchronized在大多数场景下的性能已经不输于ReentrantLock。这也是为什么JDK 1.8的ConcurrentHashMap放弃了JDK 1.7的分段锁(Segment + ReentrantLock),转而使用synchronized + CAS的组合。

本文将带你深入理解synchronized的方方面面:三种使用方式、底层Monitor机制、Mark Word在对象头中的变化、以及从偏向锁到轻量级锁再到重量级锁的完整升级过程。

一、线程安全问题回顾

1.1 并发问题的经典场景

java 复制代码
public class ThreadSafetyIssue {
    private static int counter = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter++;  // 非原子操作
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter++;
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("期望值: 20000, 实际值: " + counter);
        // 每次运行结果可能不同,典型输出:17650、18432等
    }
}

counter++看似一行代码,实际是三个步骤:读取 → 加1 → 写回。多线程交替执行这三个步骤,导致结果不确定。

这种问题被称为竞态条件(Race Condition) ------多个线程同时访问共享数据,且至少有一个线程在修改数据,最终结果取决于线程执行的精确时序。这种bug的特点是非常难以复现和调试:你可能本地测试100次都正常,上了生产环境偶尔出现一次数据错误。这也是为什么理解线程安全原理比"能用synchronized"重要得多------你得知道什么场景下需要加锁、加在什么地方、加多大范围

1.2 synchronized的解决方案

java 复制代码
public class SynchronizedSolution {
    private static int counter = 0;
    private static final Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (lock) {
                    counter++;
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (lock) {
                    counter++;
                }
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("结果: " + counter);  // 始终是 20000
    }
}

二、synchronized的三种使用方式

synchronized可以修饰三种目标:实例方法、静态方法、代码块 。理解"锁的是什么"是使用synchronized的第一课------很多人写出了加锁代码但线程安全问题依然存在,原因就是锁错了对象

核心记忆法则:

  • 修饰实例方法 → 锁是this(当前实例对象),不同实例各锁各的,互不影响
  • 修饰静态方法 → 锁是类名.class(Class对象),所有实例共享同一把锁
  • 修饰代码块 → 锁是括号中指定的任意对象,灵活但容易出错

2.1 修饰实例方法

锁住的是当前实例对象(this)。不同实例的锁互不影响。

java 复制代码
public class SyncOnInstanceMethod {
    private int count = 0;
    
    // 等价于 synchronized(this) 包裹整个方法体
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SyncOnInstanceMethod demo = new SyncOnInstanceMethod();
        
        // 多个线程操作同一个对象
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) demo.increment();
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(demo.getCount());  // 20000
    }
}

2.2 修饰静态方法

锁住的是类的Class对象。同一个类的所有实例共享同一把锁。

java 复制代码
public class SyncOnStaticMethod {
    private static int count = 0;
    
    // 等价于 synchronized(SyncOnStaticMethod.class)
    public static synchronized void increment() {
        count++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) increment();
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(count);  // 20000
    }
}

2.3 修饰代码块

锁住的是指定的对象。这是最灵活的方式,可以精确控制锁的粒度。

java 复制代码
public class SyncBlockDemo {
    private final Object readLock = new Object();
    private final Object writeLock = new Object();
    private StringBuilder data = new StringBuilder();
    
    public void read() {
        synchronized (readLock) {
            System.out.println(Thread.currentThread().getName() 
                    + " 读取数据: " + data);
        }
    }
    
    public void write(String content) {
        synchronized (writeLock) {
            data.append(content);
            System.out.println(Thread.currentThread().getName() 
                    + " 写入数据: " + content);
        }
    }
    
    public static void main(String[] args) {
        SyncBlockDemo demo = new SyncBlockDemo();
        
        // 读和写使用不同的锁,互不影响,提高并发度
        new Thread(() -> demo.read(), "读线程").start();
        new Thread(() -> demo.write("Hello"), "写线程").start();
        new Thread(() -> demo.read(), "读线程2").start();
    }
}

锁定对象与本身方法锁互斥的陷阱

java 复制代码
// 实例方法锁和锁this是同一把锁,会互斥
// 静态方法锁和锁XXXX.class是同一把锁,会互斥
// 实例锁和类锁是两把不同的锁,不会互斥

public class LockInteraction {
    public synchronized void method1() {
        System.out.println("实例方法锁");
    }
    
    public void method2() {
        synchronized (this) {
            System.out.println("锁this");
        }
    }
    // method1和method2是同一把锁,会互斥
    
    public static synchronized void staticMethod() {
        System.out.println("静态方法锁");
    }
    // staticMethod和上面method1/method2是不同的锁,不互斥
}

三、synchronized的底层原理

3.1 对象头中的Mark Word

每个Java对象在JVM中都有一个对象头 ,其中Mark Word记录了锁状态、GC分代年龄、HashCode等信息。

复制代码
Mark Word 在不同锁状态下的结构(64位JVM):

无锁状态:  [unused:25][hashcode:31][unused:1][age:4][biased_lock:1][lock:2]
           其中: lock=01 表示无锁

偏向锁:    [thread:54][epoch:2][unused:1][age:4][biased_lock:1][lock:2]
           其中: biased_lock=1, lock=01 表示偏向锁

轻量级锁:  [ptr_to_lock_record:62][lock:2]
           其中: lock=00 表示轻量级锁

重量级锁:  [ptr_to_monitor:62][lock:2]
           其中: lock=10 表示重量级锁

3.2 Monitor机制(重量级锁)

重量级锁 基于操作系统的Monitor(管程)机制实现,依赖于操作系统的Mutex Lock(互斥锁)

java 复制代码
// synchronized底层会生成 monitorenter 和 monitorexit 指令
// 对应 C++ 层面的 ObjectMonitor 对象

// 伪代码表示:
// monitorenter: 尝试获取对象的monitor
//   如果monitor的计数器为0,获取成功,计数器+1
//   如果当前线程已经持有monitor,计数器再+1(可重入)
//   否则,线程阻塞,等待monitor被释放

// monitorexit: 释放monitor
//   计数器-1
//   如果减到0,唤醒等待的线程

3.3 可重入性验证

synchronized是可重入锁------同一线程可以多次获取同一把锁:

java 复制代码
public class ReentrantDemo {
    public synchronized void methodA() {
        System.out.println("进入方法A");
        methodB();  // 在持有锁的情况下再次加锁------可重入
        System.out.println("离开方法A");
    }
    
    public synchronized void methodB() {
        System.out.println("进入方法B");
        // 嵌套加锁同样支持
        synchronized (this) {
            System.out.println("进入方法B的同步块");
        }
    }
    
    public static void main(String[] args) {
        new ReentrantDemo().methodA();
    }
}

/* 输出:
   进入方法A
   进入方法B
   进入方法B的同步块
   离开方法A
*/

四、锁升级过程(锁膨胀)

JDK 1.6之后,synchronized做了重大优化,锁的状态会随着竞争情况自动升级。这是synchronized面试题的重中之重。整个升级过程体现了JVM对"大部分锁竞争不会发生"这一假设的利用------先以最低成本的锁(偏向锁)处理最乐观的情况,随着竞争加剧逐步升级为成本更高的锁。

锁升级的核心思想:先用最轻的锁,不行再加码。就像你去图书馆占座------如果只有你一个人去,贴张纸条(偏向锁)就够了;偶尔有人跟你抢,你站旁边等一下(轻量级锁自旋);天天有人跟你抢,就得去前台登记排队了(重量级锁)。

复制代码
无锁 ──→ 偏向锁 ──→ 轻量级锁 ──→ 重量级锁
   (逐渐升级,不可降级)

注意:锁升级是单向的、不可逆的。一旦升级到轻量级锁或重量级锁,即使后续竞争消失,也不会降回偏向锁。但有一个例外------重量级锁在GC的STW(Stop The World)阶段可能会被降级。

4.1 偏向锁(Biased Locking)

偏向锁 认为大多数情况下锁不仅不存在竞争,而且总是由同一个线程多次获取。当线程第一次获取锁时,Mark Word中记录偏向线程ID。此后该线程再次进入同步块时,只需检查Mark Word中的线程ID是否是自己,如果是则直接进入------不需要CAS操作,也不需要monitor。

注意:偏向锁并非"免费"的------如果锁确实存在多线程竞争,撤销偏向锁本身也需要开销(需要到达安全点,即STW)。这也解释了为什么JDK 15+默认关闭了偏向锁:在现代高并发应用中,锁竞争比过去更常见,偏向锁带来的收益不如其撤销开销。在实际项目中,如果你的应用确实存在大量单线程使用的锁,可以通过JVM参数手动开启。

java 复制代码
public class BiasedLockDemo {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        
        // JVM默认偏向锁会延迟4秒开启
        Thread.sleep(5000);
        
        // 第一次获取锁,升级为偏向锁
        synchronized (lock) {
            System.out.println("线程1持有偏向锁");
            System.out.println("Mark Word: " + ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}

JVM参数控制

  • -XX:+UseBiasedLocking:启用偏向锁(JDK 15后默认关闭)
  • -XX:BiasedLockingStartupDelay=0:关闭偏向锁启动延迟

4.2 轻量级锁(Lightweight Locking)

当有第二个线程尝试获取偏向锁时,偏向锁会撤销升级为轻量级锁。轻量级锁采用**CAS(Compare And Swap)**自旋尝试获取锁:

java 复制代码
public class LightweightLockDemo {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t1 获取锁(偏向锁)");
                try { Thread.sleep(2000); } catch (InterruptedException e) { }
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("t2 获取锁(升级为轻量级锁)");
                // t2在等待期间会自旋尝试,不会立即阻塞
            }
        });
        
        t1.start();
        Thread.sleep(100);  // 确保t1先获取锁
        t2.start();
        
        t1.join();
        t2.join();
    }
}

轻量级锁的特点

  • 适用于线程交替执行同步块的场景
  • 自旋会消耗CPU,但避免了线程切换的开销
  • 如果自旋超过一定次数(默认10次,JDK 6改为自适应),升级为重量级锁

4.3 自适应自旋锁

JDK 6引入了自适应自旋,自旋时间不再固定,而是根据上次在同一锁上的自旋时间及锁持有者的状态动态调整:

java 复制代码
// 自适应自旋策略:
// - 如果之前自旋成功获取过锁,JVM会让自旋时间更长
// - 如果之前自旋很少成功,JVM会减少自旋甚至直接阻塞
// 让JVM自己根据实际情况来决定,开发者无需手动调优

4.4 锁消除

JIT编译时,JVM通过逃逸分析判断某些锁不可能发生竞争,直接将其消除:

java 复制代码
public class LockElimination {
    // StringBuffer的append()方法加了synchronized
    // 但sb对象不会逃逸出方法,JVM会消除锁
    public String concat() {
        StringBuffer sb = new StringBuffer();
        sb.append("Hello ");
        sb.append("World");
        return sb.toString();
    }
    // 等效于使用 StringBuilder(线程不安全)------ 无锁
}

4.5 锁粗化

JVM会将连续的加锁解锁合并为范围更大的锁,减少加解锁次数:

java 复制代码
public class LockCoarsening {
    private final Object lock = new Object();
    
    // 优化前:反复加锁解锁
    public void beforeOptimize() {
        for (int i = 0; i < 100; i++) {
            synchronized (lock) {
                // 小操作
            }
        }
    }
    
    // JVM自动粗化为:
    public void afterOptimize() {
        synchronized (lock) {
            for (int i = 0; i < 100; i++) {
                // 小操作
            }
        }
    }
}

五、synchronized vs Lock

对比维度 synchronized Lock
实现层面 JVM关键字,C++ native JDK纯Java实现
锁获取 隐式获取释放 显式手动 lock()/unlock()
可中断 不支持 支持 lockInterruptibly()
超时获取 不支持 支持 tryLock(timeout)
公平性 非公平 可选择公平/非公平
多条件 单个 (wait/notify) 多个Condition
性能 JDK 6后差距很小
易用性 简单 需手动释放

六、经典案例:多线程卖票

java 复制代码
public class TicketSelling {
    private int tickets = 100;
    
    public synchronized boolean sellTicket() {
        if (tickets > 0) {
            // 模拟出票耗时
            try { Thread.sleep(10); } catch (InterruptedException e) { }
            System.out.println(Thread.currentThread().getName() 
                    + " 售出第 " + (tickets--) + " 张票");
            return true;
        }
        return false;
    }
    
    public static void main(String[] args) {
        TicketSelling station = new TicketSelling();
        
        for (int i = 1; i <= 4; i++) {
            final String windowName = "窗口" + i;
            new Thread(() -> {
                while (station.sellTicket()) {
                    // 持续卖票直到售罄
                }
            }, windowName).start();
        }
    }
}

总结

synchronized从JDK 1.6开始已经不再是那个"重量级"的代名词。偏向锁、轻量级锁、自适应自旋、锁粗化、锁消除等一系列优化,让synchronized的性能在许多场景下已经不输于ReentrantLock。

理解锁升级的完整过程(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)是面试中的核心考点。记住这个口诀:单线程用偏向锁,交替执行用轻量级锁,激烈竞争才用重量级锁。JVM通过这些优化策略,在保证线程安全的同时,尽可能地减少了锁带来的性能开销。

核心知识回顾:

  • 三种使用方式:实例方法锁(锁this)、静态方法锁(锁Class对象)、代码块锁(锁指定对象)。实例锁和类锁互不影响------这是容易混淆的设计点。
  • Monitor机制 :每个对象都有一个关联的Monitor对象,synchronized的加锁解锁底层就是monitorenter/monitorexit指令。Monitor内部有计数器实现可重入性。
  • Mark Word:对象头中的核心字段,不同锁状态对应不同的bit位结构。了解Mark Word是理解锁升级的前提。
  • 锁优化:自适应自旋(根据历史决定自旋时长)、锁消除(逃逸分析)、锁粗化(合并连续加锁)、偏向锁延迟(默认4秒后开启)

最后提醒:在JDK 1.8及之后的版本中,对于大多数场景,首选synchronized------它语法简洁、自动释放(不会忘记unlock)、JVM持续优化。只有在需要可中断的锁获取 (lockInterruptibly)、超时尝试 (tryLock)或公平锁等特殊需求时,才使用ReentrantLock。

✅ 亮点总结

  • synchronized的三种使用方式(实例方法/静态方法/代码块)及各自锁对象的精确辨析,实例锁与类锁互不影响
  • Mark Word在无锁、偏向锁、轻量级锁、重量级锁四种状态下的64位结构变化图解
  • 锁升级全链路机制:单线程偏向锁→交替执行轻量级锁(CAS自旋)→激烈竞争重量级锁(monitor/Mutex)
  • JVM的五种锁优化策略:自适应自旋、锁消除(逃逸分析)、锁粗化、偏向锁延迟、轻量级锁CAS
  • synchronized与Lock的全面对比:可中断性、超时获取、公平性、多条件队列等方面的差异

适用场景

  • 多线程售票系统、库存扣减等需要原子性保护的经典并发场景
  • 单例模式的线程安全实现(懒汉式synchronized方法或DCL + volatile)
  • 对代码侵入性要求低、追求简洁可靠性的业务同步场景

扩展方向

  • 深入学习ReentrantLock的高级特性(可中断lockInterruptibly()、超时tryLock()、公平锁)
  • 研究JUC原子类(AtomicInteger、LongAdder)的无锁CAS并发方案
  • 推荐阅读:18_Java中的Lock锁机制

下一篇:18_Java中的Lock锁机制

相关推荐
z落落1 小时前
C# 泛型接口和泛型类+泛型约束
开发语言·c#
阿正的梦工坊1 小时前
【Rust】02-变量、不可变性与基础类型
开发语言·后端·rust
阿正的梦工坊1 小时前
【Rust】08-集合类型、字符串与迭代器入门
开发语言·rust·c#
FuckPatience2 小时前
C# 使用泛型协变将派生类类型替换为基类类型
开发语言·c#
张忠琳2 小时前
【Go 1.26.4】(Part 1) Go 1.26.4 超深度源码分析 — 总体架构与模块全景
开发语言·golang
guygg882 小时前
C# 生成中间带 Logo 头像的二维码
开发语言·c#
闪电悠米2 小时前
黑马点评-Redis 消息队列-03_stream_consumer_group
开发语言·数据库·redis·分布式·缓存·junit·lua
8125035332 小时前
第 9 篇:子网掩码:如何划分“小区”
开发语言·php
Jun6262 小时前
QT(12)-制作lib库
开发语言·qt