Java中 synchronized 和 volatile 详解

Java中 synchronized 和 volatile 详解

文章目录

概述

Java 并发编程中的两个核心关键字:synchronized 和 volatile。它们都是为了解决多线程环境下的数据一致性问题,但在作用机制、保证的特性以及适用场景上有着本质的区别。

简单来说:
synchronized 是一把“重量级的锁”,它通过互斥访问来保证原子性、可见性和有序性。
volatile 是一个“轻量级的同步机制”,它主要保证可见性和有序性,但不保证原子性。

1. synchronized 关键字详解

synchronized 是 Java 中最基础、最常用的同步机制,它通过获取和释放对象的“监视器锁”(Monitor Lock)来实现线程间的互斥访问。

1.1 作用与核心特性

  • 互斥性 (Mutual Exclusion)
    这是 synchronized 最核心的作用。它确保在同一时刻,只有一个线程能够执行被 synchronized 保护的代码块或方法。其他试图进入的线程会被阻塞,直到当前线程释放锁。
  • 原子性 (Atomicity)
    由于互斥性,被 synchronized 保护的代码块被视为一个不可分割的整体。线程要么执行完整个代码块,要么完全不执行,不会被其他线程打断。这保证了复合操作(如 i++)的原子性。
  • 可见性 (Visibility)
    synchronized 不仅提供互斥,还保证了内存可见性。根据 Java 内存模型 (JMM) 的规定:
    进入 synchronized 块时:线程会清空其工作内存中共享变量的副本,强制从主内存重新加载最新的值。退出 synchronized 块时:线程会将其工作内存中对共享变量的修改强制刷新回主内存。
    这样,一个线程在临界区内对变量的修改,对下一个进入该临界区的线程是立即可见的。
  • 有序性 (Ordering)
    synchronized 通过“一个变量在同一时刻只允许一个线程对其进行 lock 操作”的规则,天然地禁止了指令重排序。在 synchronized 块内部,代码的执行顺序与程序的书写顺序一致。

1.2. 使用方式

synchronized 可以修饰方法或代码块,锁定的对象不同,其作用范围也不同。

1.2.1 修饰实例方法 (非静态方法)

publicclassCounter{privateint count =0;// 锁定的是当前对象实例 (this)publicsynchronizedvoidincrement(){ count++;// 这个操作是原子的}publicsynchronizedintgetCount(){return count;}}

锁对象 当前对象实例 (this)。
作用范围 同一个对象实例的多个 synchronized 实例方法之间是互斥的。不同对象实例的 synchronized 方法可以并发执行。

1.2.2 修饰静态方法

publicclassGlobalCounter{privatestaticint globalCount =0;// 锁定的是当前类的 Class 对象 (GlobalCounter.class)publicstaticsynchronizedvoidincrementGlobal(){ globalCount++;}publicstaticsynchronizedintgetGlobalCount(){return globalCount;}}

锁对象 该类的 Class 对象。
作用范围 无论创建多少个类的实例,所有线程在调用该类的 synchronized 静态方法时,都会竞争同一把锁,实现全局互斥。

1.2.3 修饰代码块 (Synchronized Block)

publicclassFineGrainedCounter{privateint countA =0;privateint countB =0;privatefinalObject lockA =newObject();privatefinalObject lockB =newObject();// 只锁定操作 countA 的部分,提高并发度publicvoidincrementA(){synchronized(lockA){// 锁定指定的对象 lockA countA++;}}// 只锁定操作 countB 的部分publicvoidincrementB(){synchronized(lockB){// 锁定指定的对象 lockB countB++;}}// 锁定当前对象实例publicvoiddoSomething(){synchronized(this){// ... 临界区代码}}}

锁对象 synchronized 括号内指定的任意对象。
作用范围 灵活性最高。可以精确控制需要同步的代码范围,避免将整个方法都锁定,从而减少锁的竞争,提高并发性能。

1.3. 实现原理

JVM 通过对象内部的“监视器锁”(Monitor)来实现 synchronized。在字节码层面:

  • 进入 synchronized 代码块时,会执行 monitorenter 指令。
  • 退出 synchronized 代码块(正常退出或发生异常)时,会执行 monitorexit 指令。

为了优化性能,JDK 1.6 引入了锁升级机制:

  • 无锁状态
  • 偏向锁 (Biased Locking)
    针对只有一个线程访问同步块的场景,将锁偏向于该线程,减少不必要的 CAS 操作。
  • 轻量级锁 (Lightweight Locking)
    当有第二个线程竞争时,升级为轻量级锁,通过自旋 CAS 尝试获取锁,避免线程阻塞。
  • 重量级锁 (Heavyweight Locking)
    当自旋一定次数后仍未获取到锁,或有多个线程竞争时,升级为重量级锁,线程会被挂起,进入阻塞状态。

1.4. 优缺点

  • 优点
    功能强大,能同时保证原子性、可见性和有序性。
    使用简单,是解决并发问题的首选方案。
    支持重入,同一个线程可以多次获取同一把锁。
  • 缺点
    性能开销: 获取和释放锁需要操作系统介入,可能导致线程上下文切换,带来性能损耗。
    可能导致死锁: 如果多个线程以不同的顺序获取多个锁,可能会发生死锁。
    阻塞: 未获取到锁的线程会被阻塞,无法做其他事情。

1.5. 适用场景

适用于需要对共享资源进行复杂操作、保证操作原子性的场景,例如:

  • 银行转账(需要保证扣款和加款两个操作的原子性)。
  • 计数器的递增 (i++)。
  • 对集合进行增删改查操作。

2. volatile 关键字详解

volatile 是一个变量修饰符,它不提供任何互斥机制,而是通过内存屏障(Memory Barrier)来保证变量的可见性和禁止指令重排序。

2.1 作用与核心特性

  • 可见性 (Visibility): 这是 volatile 最主要的作用。
    当一个线程修改了 volatile 变量的值,这个新值会立即被写入主内存。
    当其他线程读取这个 volatile 变量时,会强制从主内存中读取最新的值,而不是使用自己工作内存中的缓存副本。
    这样就保证了所有线程看到的都是该变量的最新值。
  • 有序性 (Ordering) volatile 通过插入内存屏障来禁止指令重排序。
    在写一个 volatile 变量之前,JVM 会插入一个 StoreStore 屏障,确保之前的普通写操作都已完成。
    在写一个 volatile 变量之后,JVM 会插入一个 StoreLoad 屏障,确保写操作对其他处理器可见。
    在读一个 volatile 变量之前,JVM 会插入一个 LoadLoad 屏障,确保读取到的是最新值。
    在读一个 volatile 变量之后,JVM 会插入一个 LoadStore 屏障,确保后续的普通写操作不会被重排序到读操作之前。
    这保证了 volatile 变量的读写操作不会被重排序,并且建立了“happens-before”关系。
  • 不保证原子性 (No Atomicity)
    volatile 无法保证复合操作的原子性。例如,volatile int count = 0; 语句 count++ 看起来是一条语句,但在底层是“读取-修改-写入”三个步骤。即使 count 是 volatile 的,多个线程同时执行 count++ 时,依然可能出现竞态条件,导致最终结果小于预期。

2.2. 使用方式

volatile 只能用来修饰变量。

publicclassVolatileExample{// 修饰一个布尔标志位,用于线程间通信privatevolatileboolean shutdownRequested =false;// 修饰一个对象引用privatevolatileConfig config;// 线程A:设置标志位publicvoidshutdown(){ shutdownRequested =true;// 写操作,会立即刷新到主内存}// 线程B:检查标志位publicvoiddoWork(){while(!shutdownRequested){// 读操作,每次都从主内存读取最新值// ... 执行任务}// 收到关闭请求,优雅退出}// 注意:以下操作不是原子的!privatevolatileint counter =0;publicvoidunsafeIncrement(){ counter++;// 读-改-写,非原子操作,多线程下结果可能错误}}

2.3 实现原理

volatile 的实现主要依赖于 CPU 的缓存一致性协议(如 MESI)和 JVM 插入的内存屏障指令。它告诉 JVM 和 CPU,这个变量是“易变的”,不能对其进行激进的优化(如缓存、重排序)。

2.4. 优缺点

  • 优点
    轻量级: 相比 synchronized,开销非常小,不会引起线程阻塞。
    保证可见性和有序性: 适用于简单的状态标志传递。
  • 缺点
    不保证原子性: 无法用于需要原子操作的场景。
    功能有限: 只能用于变量,不能用于方法或代码块。

2.5. 适用场景

适用于“一个线程写,多个线程读”,且写操作是原子的(通常是直接赋值)的场景:

  • 状态标志位 如上面例子中的 shutdownRequested,用于通知其他线程停止工作。
  • 一次性安全发布 (Safe Publication) 在对象构造完成后,通过 volatile 引用发布,可以保证其他线程看到的是完全构造好的对象。
  • 双重检查锁定 (DCL) 的单例模式 在单例模式中,volatile 用于防止指令重排序导致其他线程拿到一个未完全初始化的对象。
publicclassSingleton{// volatile 防止 instance = new Singleton() 指令重排序privatestaticvolatileSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null){// 第一次检查synchronized(Singleton.class){if(instance ==null){// 第二次检查 instance =newSingleton();// 可能发生重排序}}}return instance;}}

3 总结

3.1 synchronized 与 volatile 的核心区别

特性synchronizedvolatile
作用对象方法、代码块变量
核心机制互斥锁 (Monitor)内存屏障 (Memory Barrier)
原子性保证 (通过互斥实现)不保证 (仅保证单次读/写原子)
可见性保证 (进出同步块时刷新主内存)保证 (强制读写主内存)
有序性保证 (通过互斥和禁止重排序)保证 (通过内存屏障禁止重排序)
线程阻塞会阻塞 (未获取锁的线程进入阻塞状态)不会阻塞 (线程可以继续执行)
性能开销较大 (涉及操作系统,可能上下文切换)较小 (主要是内存屏障开销)
适用场景复杂的原子操作、临界区保护 简单的状态标志、一次性安全发布、DCL单例模式

3.2 适用场景

3.2.1 状态标志控制 使用volatile

仅需保证可见性进需要操作是原子的 (如 flag = true): 优先使用 volatile,因为它更轻量。

classTaskRunner{privatevolatileboolean stopped =false;// 线程安全的状态标志publicvoidrun(){while(!stopped){/* 执行任务 */}}publicvoidstop(){ stopped =true;}// 修改立即可见}

3.2.2 单例模式(双重检查锁定)synchronized+volatile

volatile防止new Singleton()的分解步骤重排序,避免返回未初始化的对象

classSingleton{privatestaticvolatileSingleton instance;// 禁止指令重排序publicstaticSingletongetInstance(){if(instance ==null){synchronized(Singleton.class){if(instance ==null){ instance =newSingleton();// 禁止重排序:分配内存→初始化→赋值引用}}}return instance;}}

3.2.3 临界区保护 使用synchronized

强制原子性,适合需要互斥访问的复合操作(如读写共享变量)。

classBankAccount{privatedouble balance;publicsynchronizedvoiddeposit(double amount){// 整个方法同步 balance += amount;}publicvoidwithdraw(double amount){synchronized(this){// 代码块同步 balance -= amount;}}}

3.2.4 线程协作(等待/通知机制)

synchronized提供锁的获取/释放机制,配合wait()/notifyAll()实现线程间协作。

classProducerConsumer{privatefinalObject lock =newObject();privateboolean isProduced =false;publicvoidproduce(){synchronized(lock){while(isProduced){ lock.wait();}// 等待消费// 生产数据... isProduced =true; lock.notifyAll();// 通知消费者}}publicvoidconsume(){synchronized(lock){while(!isProduced){ lock.wait();}// 等待生产// 消费数据... isProduced =false; lock.notifyAll();// 通知生产者}}}

Read more

金仓数据库 MongoDB 兼容:多模融合下的架构之道与实战体验

金仓数据库 MongoDB 兼容:多模融合下的架构之道与实战体验

引言:从“平替”到“超越”的技术跨越 在国产化替代(信创)浪潮下,选择数据库不再只是考量“能否使用”,更多关注其“好用与否”,还要看是否能做到“无缝切换”。提到 MongoDB,想必大家都不生疏,作为 NoSQL 领域的佼佼者,凭借自身灵活的数据架构和飞快的读写效率,斩获诸多互联网及物联网项目,不过须要诚实地表明,一旦关乎到企业核心业务,譬如要确保数据完全一致,执行繁杂的关联查询或者实施统一运作管理时,MongoDB 就常常会有些力不从心。 电科金仓(Kingbase)所给出的多模融合数据库方案颇具趣味,该方案并非仅仅创建一层适配层来博取眼球,其实在架构层面上执行了“降维打击”,经由内核级别的 MongoDB 协议适配 并结合自主研发的 OSON 存储引擎,金仓把“关系型数据库稳定的基础”与“NoSQL 灵活的特性”融合起来,现在,让我们一起探究金仓数据库(KingbaseES,

By Ne0inhk
# Flutter三方库适配OpenHarmony【flutter_libphonenumber】——联合插件(Federated Plugin)架构解析

# Flutter三方库适配OpenHarmony【flutter_libphonenumber】——联合插件(Federated Plugin)架构解析

前言 欢迎来到 Flutter三方库适配OpenHarmony 系列文章!本系列围绕 flutter_libphonenumber 这个 电话号码处理库 的鸿蒙平台适配,进行全面深入的技术分享。 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 上一篇我们从全局视角介绍了 flutter_libphonenumber 的功能定位和鸿蒙适配成果。本篇将深入解析该库采用的 联合插件(Federated Plugin)架构,这是理解整个适配工作的基础。我们将逐层拆解 platform_interface、MethodChannel、各平台实现包的协作关系,并详细分析鸿蒙平台是如何 无缝接入 这套架构的。 理解联合插件架构是进行任何 Flutter 三方库鸿蒙适配的 第一步,掌握了这套模式,你就能举一反三地适配其他库。 一、什么是联合插件(Federated Plugin) 1.1 传统插件的局限性 在 Flutter 早期,

By Ne0inhk

SpringBoot 拦截器

拦截器的概念 拦截器,顾名思义,就是在请求到达目标接口之前 “拦一下”,做完我们指定的操作后,再决定是放它继续走,还是直接把它拦下。 举个生活中的例子:我们去银行办理业务,进门后不会直接找柜员,而是要先取号、安检 —— 这两步就相当于 “拦截器”。 * 如果带了身份证、取号成功,就放行,去窗口办业务(对应接口正常执行); * 如果没带身份证,取号失败,就直接被拦下,没法办业务(对应请求被拒绝); * 等业务办完后,还能给柜员评价,这就是请求执行完后的拦截操作。 放到 SpringBoot 项目里,拦截器就是在请求到达 Controller 接口前后,执行我们预先写好的代码,比如登录校验、日志记录、参数校验这些通用操作,不用在每个接口里重复写,大大减少冗余代码。 拦截器的核心执行时机:三个关键方法 拦截器的核心是实现Spring的HandlerInterceptor接口,里面有三个核心方法,对应请求的不同阶段 1. preHandle:请求到达接口前执行(最常用) 这是拦截器最核心的方法,

By Ne0inhk
Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案

Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案 前言 在鸿蒙(OpenHarmony)生态的分布式协作编辑器、多端同步的即时通讯资产库以及需要实现“本地优先(Local-first)”架构的各类大型数字化政务应用开发中,“数据一致性的最终收敛”是系统稳定性的灵魂。面对由 5 台鸿蒙设备在不同地点、不同弱网环境下同时对同一份 JSON 资产执行的交叉修改。如果依然采用基于“锁”或“版本号覆盖”的传统同步逻辑。不仅会导致频繁出现的由于并发冲突引发的“保存失败”报错,更会因为无法处理跨设备的时序漂移,引发严重的资产状态错乱。 我们需要一种“逻辑守恒、冲突自愈”的存储艺术。 postgres_crdt 是一套专注于将 PostgreSQL 生态的严谨性与无冲突复制数据类型(

By Ne0inhk