- 引言
- 一、为什么需要synchronized?
- 二、synchronized的三种使用方式
- 三、synchronized底层原理
- 四、锁优化:JDK的不断进化
- 五、synchronized vs Lock
- 六、volatile的辅助作用
- 总结与展望
- 互动环节
引言
在多线程编程中,最令人头疼的问题莫过于数据竞争和线程安全。当我们多个线程同时读写同一个共享变量时,结果往往变得不可预测,这就是线程不安全的表现。
synchornized关键字作为Java语言内置的同步锁机制,从诞生之初就肩负着解决线程安全问题的重任。它就像交通信号灯,让并发的线程变得有序,避免"交通事故"的发生。本文将带你深入理解这个Java并发编程中最基础、最重要的关键字。
一、为什么需要synchronized?
先来看一个经典的线程不安全例子:
public class UnsafeCounter { private int count = 0; public void increment() { count++; // 这行代码不是原子操作! } public int getCount() { return count; } }
count++这行代码看似简单,实际上包含了三个操作:
- 读取count的当前值
- 将值加1
- 将新值写回count
在多线程环境下,两个线程可能同时读取到相同的值,然后各自加1后写回,导致最终结果比预期少。
synchronized的作用就是保证同一时刻,只有一个线程可以执行某个方法或代码块,从而避免这种数据竞争问题。
二、synchronized的三种使用方式
1. 同步实例方法
public class SafeCounter { private int count = 0; // 同步实例方法,锁是当前对象实例(this) public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
2. 同步静态方法
public class StaticCounter { private static int count = 0; // 同步静态方法,锁是当前类的Class对象 public static synchronized void increment() { count++; } }
3. 同步代码块
public class FlexibleCounter { private int count = 0; private final Object lock = new Object(); // 专门的锁对象 public void increment() { // 一些非同步操作... synchronized (lock) { // 同步代码块,锁的是lock对象 count++; // 只有这部分需要同步 } // 其他非同步操作... } public int getCount() { synchronized (lock) { return count; } } }
同步代码块的优势:
- 更细的锁粒度,减少锁竞争
- 可以选择不同的锁对象,提高灵活性
- 性能通常比同步方法更好
三、synchronized底层原理
1. 对象头与Monitor
在JVM中,每个对象都有一个对象头,其中包含Mark Word,用于存储对象的哈希码、分代年龄和锁标志位。
当线程进入synchronized代码块时:
- 尝试获取对象的Monitor(监视器锁)
- 如果获取成功,将Mark Word指向Monitor的指针
- 如果获取失败,线程进入阻塞状态
2. 字节码层面
编译器会在synchronized代码块的前后插入monitorenter和monitorexit指令:
public void test(); Code: 0: aload_0 1: getfield #2 // 获取lock字段 4: dup 5: astore_1 6: monitorenter // 进入同步块 7: aload_0 8: dup 9: getfield #3 // 获取count字段 12: iconst_1 13: iadd 14: putfield #3 // 设置count字段 17: aload_1 18: monitorexit // 正常退出同步块 19: goto 27 22: astore_2 23: aload_1 24: monitorexit // 异常退出同步块(保证锁释放) 25: aload_2 26: athrow 27: return
四、锁优化:JDK的不断进化
早期的synchronized是"重量级锁",性能较差。经过多个JDK版本的优化,现在它的性能已经非常优秀。
1. 锁升级过程
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
// 这个示例演示了不同锁状态的变化 public class LockUpgradeDemo { private static final Object lock = new Object(); private static int count = 0; public static void main(String[] args) { // 第一阶段:无竞争,偏向锁 synchronized (lock) { count++; } // 第二阶段:轻微竞争,轻量级锁 for (int i = 0; i < 2; i++) { new Thread(() -> { synchronized (lock) { count++; } }).start(); } // 第三阶段:激烈竞争,重量级锁 for (int i = 0; i < 10; i++) { new Thread(() -> { synchronized (lock) { count++; try { Thread.sleep(100); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } }
2. 其他优化技术
- 锁消除:JVM检测到不可能存在共享数据竞争时,会消除锁
- 锁粗化:将连续的加锁解锁操作合并为一次加锁操作
- 自适应自旋:根据以往锁竞争情况动态调整自旋次数
五、synchronized vs Lock
特性 |
synchronized |
Lock(如ReentrantLock) |
实现层面 |
JVM内置,关键字 |
JDK API,接口 |
锁获取 |
自动获取和释放 |
需要手动lock()和unlock() |
灵活性 |
相对简单,功能有限 |
功能丰富,支持尝试锁、超时锁等 |
性能 |
JDK6后优化很好 |
在高竞争环境下可能更好 |
中断响应 |
不支持 |
支持锁获取中断 |
公平性 |
非公平锁 |
可选择公平或非公平 |
选择建议:
- 大多数情况下,优先选择synchronized,更简单可靠
- 需要高级功能(如超时、中断等)时,选择Lock
- 在高度竞争的场景下,可以测试两者的性能差异
六、volatile的辅助作用
虽然本文重点是synchronized,但有必要提一下它的"好搭档"——volatile。
public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }
在这个经典的双重检查锁模式中:
- synchronized保证创建实例的线程安全
- volatile防止指令重排序,保证其他线程看到的是完全初始化后的实例
volatile保证可见性和有序性,但不保证原子性,它适合用作状态标志位:
public class StoppableTask { private volatile boolean stopped = false; public void run() { while (!stopped) { // 执行任务 } } public void stop() { stopped = true; // 其他线程立即可见 } }
总结与展望
synchronized作为Java并发编程的基石,经历了从"性能杀手"到"高效同步工具"的华丽转身。它的主要优势在于:
- 简单易用:语法简单,自动释放锁
- JVM支持:内置优化,如锁升级、锁消除等
- 可靠性高:不容易出现锁泄漏等问题
最佳实践:
- 优先使用同步代码块,减小锁粒度
- 使用私有对象作为锁,避免外部干扰
- 同步块内尽量少做耗时操作
未来展望:
随着Project Loom的推进,未来可能会有更轻量的并发模型,但synchronized作为基础同步机制,仍将长期发挥重要作用。
理解synchronized不仅是为了应对面试,更是为了写出正确、高效的多线程程序。它是每个Java开发者必须掌握的基本功。