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

简介: 本文深入解析Java中synchronized关键字的使用与原理,涵盖其三种用法、底层Monitor机制、锁升级过程及JVM优化,并对比Lock差异,结合volatile应用场景,全面掌握线程安全核心知识。
  • 引言
  • 一、为什么需要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开发者必须掌握的基本功。


相关文章
《深入理解高并发编程(第2版)》八大篇章,共433页,打包发布!!
大家好,我是冰河~~ 在 冰河技术 微信公众号中的【精通高并发系列】专题,更新了不少文章,有些读者反馈说,在公众号中刷历史文章不太方便,有时会忘记自己看到哪一篇了,当打开一篇文章时,似乎之前已经看过了,但就是不知道具体该看哪一篇了。相信很多小伙伴都会有这样的问题。那怎么办呢? 最好的解决方案就是我把这些文章整理成PDF电子书,免费分享给大家,这样,小伙伴们看起来就方便多了。
1495 0
《深入理解高并发编程(第2版)》八大篇章,共433页,打包发布!!
|
7月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
7月前
|
存储 算法 搜索推荐
《数据之美》:Java数据结构与算法精要
本系列深入探讨数据结构与算法的核心原理及Java实现,涵盖线性与非线性结构、常用算法分类、复杂度分析及集合框架应用,助你提升程序效率,掌握编程底层逻辑。
|
7月前
|
监控 Java API
JUC系列之《深入剖析LockSupport:Java并发编程的“交警”》
LockSupport是Java并发编程的底层基石,提供park()和unpark()方法实现线程阻塞与精确唤醒。基于“许可证”机制,无需同步块、调用顺序灵活、可精准控制线程,是ReentrantLock、CountDownLatch等高级同步工具的底层支撑,堪称JUC的“手术刀”。
|
7月前
|
存储 缓存 安全
JUC系列之《volatile关键字:穿透Java内存模型的可见性之剑》
本文深入解析Java中的`volatile`关键字,涵盖其核心特性(可见性与有序性)、底层原理(JMM与内存屏障)、典型使用场景(状态标志、单例模式)及局限性(不保证原子性),帮助开发者正确掌握这一轻量级同步工具,避免并发编程误区。
|
7月前
|
存储 安全 Java
《Java并发编程的“避坑”利器:ThreadLocal深度解析》
ThreadLocal通过“空间换安全”实现线程变量隔离,为每个线程提供独立副本,避免共享冲突。本文深入解析其原理、ThreadLocalMap机制、内存泄漏风险及remove()最佳实践,助你掌握上下文传递与线程封闭核心技术。
|
7月前
|
Java API 开发者
告别“线程泄露”:《聊聊如何优雅地关闭线程池》
本文深入讲解Java线程池优雅关闭的核心方法与最佳实践,通过shutdown()、awaitTermination()和shutdownNow()的组合使用,确保任务不丢失、线程不泄露,助力构建高可靠并发应用。
|
7月前
|
安全 Java API
JUC系列之《揭秘Java中的“瑞士军刀”——Unsafe类》
本文深入解析Java中的“后门”类sun.misc.Unsafe,揭秘其如何支撑AtomicInteger、AQS等并发工具的底层实现。涵盖其核心能力如CAS、内存操作、线程调度,剖析为何强大却危险,并介绍Java 9后的安全替代方案VarHandle,助你理解高性能并发背后的原理。
|
6月前
|
存储 关系型数据库 MySQL
《理解MySQL数据库》B+树索引深度解析
B+树是MySQL索引的核心数据结构,通过多路平衡树实现高效磁盘I/O。其叶节点形成双向链表,支持快速查找与范围扫描。聚簇索引按主键物理排序存储数据,查询性能优异;非聚簇索引则需回表获取完整数据。合理设计复合索引、利用覆盖索引、避免冗余索引,并定期维护,可显著提升数据库性能。
|
7月前
|
存储 算法 安全
《Java集合核心HashMap:深入剖析其原理、陷阱与性能优化》
HashMap是Java中最常用的Map实现,基于哈希表提供近乎O(1)的存取效率。其核心为“数组+链表+红黑树”结构,通过扰动哈希、&运算索引、扩容机制等实现高效操作。但线程不安全,需注意Key的不可变性与合理初始化容量。深入理解其原理,有助于写出高性能代码,避免常见陷阱。

热门文章

最新文章