Java中 synchronized 和 volatile的核心区别解析

 更新时间:2025年09月16日 10:39:18   作者:木易 士心  
本文给大家介绍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 修饰实例方法 (非静态方法)

public class Counter {
    private int count = 0;
    // 锁定的是当前对象实例 (this)
    public synchronized void increment() {
        count++; // 这个操作是原子的
    }
    public synchronized int getCount() {
        return count;
    }
}

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

1.2.2 修饰静态方法

public class GlobalCounter {
    private static int globalCount = 0;
    // 锁定的是当前类的 Class 对象 (GlobalCounter.class)
    public static synchronized void incrementGlobal() {
        globalCount++;
    }
    public static synchronized int getGlobalCount() {
        return globalCount;
    }
}

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

1.2.3 修饰代码块 (Synchronized Block)

public class FineGrainedCounter {
    private int countA = 0;
    private int countB = 0;
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    // 只锁定操作 countA 的部分,提高并发度
    public void incrementA() {
        synchronized (lockA) { // 锁定指定的对象 lockA
            countA++;
        }
    }
    // 只锁定操作 countB 的部分
    public void incrementB() {
        synchronized (lockB) { // 锁定指定的对象 lockB
            countB++;
        }
    }
    // 锁定当前对象实例
    public void doSomething() {
        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 只能用来修饰变量。

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

2.3 实现原理

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

2.4. 优缺点

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

2.5. 适用场景

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

  • 状态标志位 如上面例子中的 shutdownRequested,用于通知其他线程停止工作。
  • 一次性安全发布 (Safe Publication) 在对象构造完成后,通过 volatile 引用发布,可以保证其他线程看到的是完全构造好的对象。
  • 双重检查锁定 (DCL) 的单例模式 在单例模式中,volatile 用于防止指令重排序导致其他线程拿到一个未完全初始化的对象。
public class Singleton {
    // volatile 防止 instance = new 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;
    }
}

3 总结

3.1 synchronized 与 volatile 的核心区别

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

3.2 适用场景

3.2.1 状态标志控制 使用volatile

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

class TaskRunner {
    private volatile boolean stopped = false; // 线程安全的状态标志
    public void run() {
        while (!stopped) { /* 执行任务 */ }
    }
    public void stop() { stopped = true; } // 修改立即可见
}

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

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

class Singleton {
    private static volatile Singleton instance; // 禁止指令重排序
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 禁止重排序:分配内存→初始化→赋值引用
                }
            }
        }
        return instance;
    }
}

3.2.3 临界区保护 使用synchronized

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

class BankAccount {
    private double balance;
    public synchronized void deposit(double amount) { // 整个方法同步
        balance += amount;
    }
    public void withdraw(double amount) {
        synchronized (this) { // 代码块同步
            balance -= amount;
        }
    }
}

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

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

class ProducerConsumer {
    private final Object lock = new Object();
    private boolean isProduced = false;
    public void produce() {
        synchronized (lock) {
            while (isProduced) { lock.wait(); } // 等待消费
            // 生产数据...
            isProduced = true;
            lock.notifyAll(); // 通知消费者
        }
    }
    public void consume() {
        synchronized (lock) {
            while (!isProduced) { lock.wait(); } // 等待生产
            // 消费数据...
            isProduced = false;
            lock.notifyAll(); // 通知生产者
        }
    }
}

到此这篇关于Java中 synchronized 和 volatile的核心区别解析的文章就介绍到这了,更多相关Java synchronized 和 volatile内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot 中的 @PutMapping 注解原理及使用小结

    Spring Boot 中的 @PutMapping 注解原理及使用小结

    在本文中,我们介绍了 Spring Boot 中的 @PutMapping 注解,它可以将 HTTP PUT 请求映射到指定的处理方法上,我们还介绍了 @PutMapping 注解的原理以及如何在 Spring Boot 中使用它,感兴趣的朋友跟随小编一起看看吧
    2023-12-12
  • Spring源码解密之自定义标签与解析

    Spring源码解密之自定义标签与解析

    这篇文章主要给大家介绍了关于Spring源码解密之自定义标签与解析的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参借鉴,下面随着小编来一起学习学习吧。
    2018-01-01
  • Java泛型与数据库应用实例详解

    Java泛型与数据库应用实例详解

    这篇文章主要介绍了Java泛型与数据库应用,结合实例形式详细分析了java继承泛型类实现增删改查操作相关实现技巧,需要的朋友可以参考下
    2019-08-08
  • Java ThreadLocal的使用场景总结

    Java ThreadLocal的使用场景总结

    ThreadLocal原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,从而导致它的理解难度大和使用成本高等问题。即便如此,ThreadLocal依旧有适合自己的使用场景,比如本文要介绍了这两种使用场景,除了ThreadLocal之外,还真没有合适的替代方案。
    2021-05-05
  • 如何基于FTP4J实现FTPS连接过程解析

    如何基于FTP4J实现FTPS连接过程解析

    这篇文章主要介绍了如何基于FTP4J实现FTPS连接过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • SpringBoot使用 Sleuth 进行分布式跟踪的过程分析

    SpringBoot使用 Sleuth 进行分布式跟踪的过程分析

    Spring Boot Sleuth是一个分布式跟踪解决方案,它可以帮助您在分布式系统中跟踪请求并分析性能问题,Spring Boot Sleuth是Spring Cloud的一部分,它提供了分布式跟踪的功能,本文将介绍如何在Spring Boot应用程序中使用Sleuth进行分布式跟踪,感兴趣的朋友一起看看吧
    2023-10-10
  • Spring Boot 项目集成 Redisson 实现延迟队列的详细过程

    Spring Boot 项目集成 Redisson 实现延迟队列的详细过程

    本文介绍延迟队列在订单超时等场景的应用及四种技术方案对比,推荐Redisson延迟队列,提供项目结构与测试源码,对Spring Boot  Redisson延迟队列相关知识感兴趣的朋友一起看看吧
    2025-06-06
  • Spring Boot 集成 Quartz 使用Cron 表达式实现定时任务

    Spring Boot 集成 Quartz 使用Cron 表达式实现定

    本文介绍了如何在SpringBoot项目中集成Quartz并使用Cron表达式进行任务调度,通过添加Quartz依赖、创建Quartz任务、配置任务调度以及启动项目,可以实现定时任务的执行,Cron表达式提供了灵活的任务调度方式,适用于各种复杂的定时任务需求,感兴趣的朋友一起看看吧
    2025-03-03
  • Java重写与重载之间的区别

    Java重写与重载之间的区别

    本文主要介绍了Java重写与重载之间的区别。具有一定的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • java JOptionPane类的介绍

    java JOptionPane类的介绍

    java JOptionPane类的介绍,需要的朋友可以参考一下
    2013-04-04

最新评论