醋醋百科网

Good Luck To You!

JUC系列之《深入理解synchronized:Java并发编程的基石 》

目录

  • 引言
  • 一、为什么需要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++这行代码看似简单,实际上包含了三个操作:

  1. 读取count的当前值
  2. 将值加1
  3. 将新值写回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代码块时:

  1. 尝试获取对象的Monitor(监视器锁)
  2. 如果获取成功,将Mark Word指向Monitor的指针
  3. 如果获取失败,线程进入阻塞状态

2. 字节码层面

编译器会在synchronized代码块的前后插入monitorentermonitorexit指令:

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并发编程的基石,经历了从"性能杀手"到"高效同步工具"的华丽转身。它的主要优势在于:

  1. 简单易用:语法简单,自动释放锁
  2. JVM支持:内置优化,如锁升级、锁消除等
  3. 可靠性高:不容易出现锁泄漏等问题

最佳实践

  • 优先使用同步代码块,减小锁粒度
  • 使用私有对象作为锁,避免外部干扰
  • 同步块内尽量少做耗时操作

未来展望
随着Project Loom的推进,未来可能会有更轻量的并发模型,但synchronized作为基础同步机制,仍将长期发挥重要作用。

理解synchronized不仅是为了应对面试,更是为了写出正确、高效的多线程程序。它是每个Java开发者必须掌握的基本功。

互动环节

你在使用synchronized的过程中遇到过什么有趣的问题或者坑?有没有因为锁使用不当导致过性能问题?欢迎在评论区分享你的实战经验,我们一起交流学习!

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言