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电子书,免费分享给大家,这样,小伙伴们看起来就方便多了。
1353 0
《深入理解高并发编程(第2版)》八大篇章,共433页,打包发布!!
|
1月前
|
Web App开发 安全 Java
并发编程之《彻底搞懂Java线程》
本文系统讲解Java并发编程核心知识,涵盖线程概念、创建方式、线程安全、JUC工具集(线程池、并发集合、同步辅助类)及原子类原理,帮助开发者构建完整的并发知识体系。
|
7天前
|
安全 Java Android开发
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
崩溃堆栈全是 a.b.c?Native 错误查不到行号?本文详解 Android 崩溃采集全链路原理,教你如何把“天书”变“说明书”。RUM SDK 已支持一键接入。
531 206
|
1月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
29天前
|
人工智能 开发框架 安全
浅谈 Agent 开发工具链演进历程
模型带来了意识和自主性,但在输出结果的确定性和一致性上降低了。无论是基础大模型厂商,还是提供开发工具链和运行保障的厂家,本质都是希望提升输出的可靠性,只是不同的团队基因和行业判断,提供了不同的实现路径。本文按四个阶段,通过串联一些知名的开发工具,来回顾 Agent 开发工具链的演进历程。
352 44
|
1月前
|
Java API 开发者
告别“线程泄露”:《聊聊如何优雅地关闭线程池》
本文深入讲解Java线程池优雅关闭的核心方法与最佳实践,通过shutdown()、awaitTermination()和shutdownNow()的组合使用,确保任务不丢失、线程不泄露,助力构建高可靠并发应用。
|
1月前
|
XML Java 数据格式
《深入理解Spring》:AOP面向切面编程深度解析
Spring AOP通过代理模式实现面向切面编程,将日志、事务等横切关注点与业务逻辑分离。支持注解、XML和编程式配置,提供五种通知类型及丰富切点表达式,助力构建高内聚、低耦合的可维护系统。
|
1月前
|
前端开发 Java 应用服务中间件
《深入理解Spring》 Spring Boot——约定优于配置的革命者
Spring Boot基于“约定优于配置”理念,通过自动配置、起步依赖、嵌入式容器和Actuator四大特性,简化Spring应用的开发与部署,提升效率,降低门槛,成为现代Java开发的事实标准。
|
1月前
|
监控 算法 Java
深入理解JVM《垃圾收集(GC)机制与算法 - 宇宙的清洁工》
Java通过垃圾收集(GC)实现自动内存管理,避免手动释放内存导致的泄漏或崩溃。主流JVM采用可达性分析算法判断对象生死,结合分代收集理论,使用标记-清除、复制、标记-整理等算法回收内存。G1、ZGC等现代收集器进一步提升性能与停顿控制。
下一篇
oss云网关配置