0

0

什么是线程安全?如何理解Java中的“原子性”、“可见性”、“有序性”?

夢幻星辰

夢幻星辰

发布时间:2025-09-03 21:21:01

|

995人浏览过

|

来源于php中文网

原创

线程安全指多线程并发访问共享资源时程序行为正确,核心在于原子性、可见性、有序性。原子性保障操作不可分割,通过synchronized、Lock或Atomic类实现;可见性确保线程间修改及时可见,由volatile、synchronized或final保证;有序性防止指令重排序,依赖volatile和synchronized维持执行顺序。三者共同构建并发安全的基石。

什么是线程安全?如何理解java中的“原子性”、“可见性”、“有序性”?

线程安全,简单来说,就是指在多线程环境下,当多个线程并发访问同一段代码或同一份共享数据时,程序的行为仍然是可预测的、正确的,不会因为并发导致数据损坏或逻辑错误。而Java中理解线程安全,核心就在于掌握并发编程的三大特性:原子性、可见性和有序性,它们分别保障了操作的不可分割、内存修改的及时同步以及指令执行的顺序一致性。

当我们谈论线程安全,其实是在聊一个很实际的问题:当程序里的多个“工人”(线程)同时去操作同一个“工具箱”(共享变量)时,怎么才能保证这个工具箱不会被搞乱,每个工人都能拿到正确的东西,并且最终的结果是大家预期的?这在单线程世界里根本不是个事儿,但一旦引入并发,各种意想不到的问题就冒出来了,比如数据脏读、丢失更新,甚至死锁。

我个人觉得,线程安全不仅仅是写出没有bug的代码,更是一种思维模式的转变。它要求我们从一开始就预设:任何共享资源都可能被多个线程同时访问。这种预设会引导我们去思考如何保护这些资源,无论是通过加锁、使用并发容器,还是通过无锁算法。它强迫我们去理解底层内存模型,去思考数据在不同CPU缓存之间是如何同步的。很多时候,我们以为加个

synchronized
就万事大吉了,但实际上,如果对它的作用范围、锁粒度没有深刻理解,反而可能引入新的性能瓶颈,甚至更隐蔽的并发问题。所以,这不仅仅是语法层面的问题,更是对系统设计和运行时行为的深刻洞察。

为什么线程不安全会导致程序行为异常?

线程不安全之所以会导致程序行为异常,根源在于CPU的执行方式和内存模型的复杂性。举个最简单的例子,一个

i++
操作,我们看起来就一行代码,但实际上它在JVM层面可能被拆分成了好几步:读取
i
的值、对
i
加1、将新值写回
i
。设想一下,两个线程同时执行
i++
,如果线程A读到
i
是0,正准备加1,还没来得及写回,线程B也读到了
i
是0,然后B加1写回
i
变成1。接着A也加1写回
i
变成1。最终,
i
只增加了1次,而不是预期的2次。这就是典型的丢失更新。

立即学习Java免费学习笔记(深入)”;

更深层次看,现代CPU为了性能,会有多级缓存。每个CPU核心都有自己的L1、L2缓存,L3缓存可能由多个核心共享。当一个线程修改了某个变量,这个修改可能只先写到了它所在CPU核心的缓存里,并没有立即刷新到主内存。如果另一个线程在另一个CPU核心上读取这个变量,它读到的可能是旧值。这就是“可见性”问题。

还有指令重排序。编译器和处理器为了优化性能,可能会对指令进行重新排序,只要最终结果在单线程环境下保持一致。但在多线程环境下,这种重排序可能会打乱我们预设的执行顺序,导致一些依赖于特定顺序的并发操作出现问题。比如,一个对象的初始化和对其引用赋值,如果重排序了,其他线程可能在对象还没完全初始化完成时就拿到了它的引用,导致空指针或其他异常。这些都是我们肉眼难以察觉,却实实在在会影响程序正确性的因素。

Java并发编程中“原子性”具体指什么?如何保障?

原子性,顾名思义,就是指一个或多个操作,要么全部执行成功,要么全部不执行,中间不能被任何因素打断。它是一个不可分割的整体。在Java里,最直观的原子操作就是基本数据类型的读写(除了

long
double
在32位JVM上可能不是原子操作,但现代JVM通常会保证)。但我们实际遇到的问题往往是复合操作,比如
i++
,它就不是原子的。

要保障复合操作的原子性,最常见也最直接的手段就是使用锁机制。比如

synchronized
关键字,它能保证被其修饰的代码块或方法在同一时间只有一个线程可以执行。当一个线程进入
synchronized
代码块时,它会获取锁;当它退出时,会释放锁。其他试图进入的线程会被阻塞,直到锁被释放。

public class AtomicExample {
    private int count = 0;

    public synchronized void increment() {
        count++; // 这里的count++操作在synchronized块内,因此是原子的
    }

    public int getCount() {
        return count;
    }
}

除了

synchronized
java.util.concurrent.locks.Lock
接口也提供了更灵活的锁机制,比如
ReentrantLock
。它提供
lock()
unlock()
方法来手动控制锁的获取和释放。

Type Studio
Type Studio

一个视频编辑器,提供自动转录、自动生成字幕、视频翻译等功能

下载

此外,

java.util.concurrent.atomic
提供了一系列原子类,比如
AtomicInteger
AtomicLong
AtomicReference
等。这些类内部使用了CAS(Compare-And-Swap)操作,这是一种无锁的乐观并发策略。CAS操作是硬件层面支持的原子指令,它会比较内存中的值与预期值是否相等,如果相等则更新为新值,否则不进行任何操作。这种方式避免了锁的开销,在某些场景下能提供更好的性能。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // AtomicInteger的incrementAndGet方法是原子的
    }

    public int getCount() {
        return count.get();
    }
}

我个人觉得,选择哪种方式,很多时候取决于具体场景和性能要求。

synchronized
用起来简单,但可能粒度粗;
Lock
更灵活,但需要手动管理;原子类则是在特定场景下提供高性能的无锁解决方案。

Java中的“可见性”和“有序性”如何影响并发编程?

可见性和有序性,这两兄弟在并发编程中常常是隐形的杀手,它们不像原子性那样直接导致数据计算错误,而是通过更微妙的方式让程序行为变得不可预测。

可见性(Visibility): 简单来说,可见性是指当一个线程修改了共享变量的值,其他线程能够立即(或者说,及时)看到这个修改。正如前面提到的CPU缓存问题,一个线程对共享变量的修改,可能仅仅停留在自己的工作内存(CPU缓存)中,而没有及时刷新到主内存。其他线程如果从主内存或者自己的旧缓存中读取,就会读到过期的值。

Java中保障可见性的主要手段有:

  1. volatile
    关键字
    volatile
    修饰的变量,能保证对其读写操作的可见性。当一个变量被
    volatile
    修饰后,它的每次修改都会立即刷新到主内存,并且每次读取都会从主内存中重新加载。这解决了CPU缓存不一致的问题。但要注意,
    volatile
    只保证可见性,不保证原子性。例如,
    volatile int i = 0; i++;
    i++
    依然不是原子操作。
  2. synchronized
    关键字
    synchronized
    不仅能保证原子性,也能保证可见性。当一个线程释放
    synchronized
    锁时,它会把所有共享变量的修改刷新到主内存;当一个线程获取
    synchronized
    锁时,它会使本地内存中的共享变量副本失效,从而从主内存中重新加载最新值。
  3. final
    关键字
    :被
    final
    修饰的字段在构造器中一旦初始化完成,并且构造器没有逸出(即在构造器完成之前,
    this
    引用没有被其他线程可见),那么在其他线程中就能看到
    final
    字段的正确初始化值。
  4. java.util.concurrent
    包中的各种工具类
    :例如
    Atomic
    类、
    CountDownLatch
    CyclicBarrier
    ThreadPoolExecutor
    等,它们内部都妥善处理了可见性问题。

有序性(Ordering): 有序性是指程序执行的顺序,在单线程环境下,我们通常认为代码是按照书写顺序执行的。但在多线程环境下,编译器和处理器为了优化性能,可能会对指令进行重排序,只要不改变单线程程序的执行结果(as-if-serial语义)。这种重排序在并发场景下就可能出问题。

例如经典的双重检查锁定(DCL)单例模式,如果不对实例变量使用

volatile
修饰,就可能因为指令重排序导致其他线程获取到一个未完全初始化的对象。

// 假设这是DCL单例模式的错误示范(缺少volatile)
public class Singleton {
    private static Singleton instance; // 缺少volatile

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 1. 分配内存 2. 初始化对象 3. 设置instance指向内存
                }
            }
        }
        return instance;
    }
}

instance = new Singleton();
这一行,JVM实际上可能执行了三个步骤:

  1. 分配对象的内存空间。
  2. 初始化对象(调用构造器)。
  3. instance
    变量指向分配的内存地址。

如果2和3发生了重排序,变成了1 -> 3 -> 2,那么在步骤3完成后,

instance
已经不为
null
了,但对象可能还没完全初始化。此时,如果另一个线程进来,看到
instance
不为
null
,直接返回了未完全初始化的对象,就会导致问题。加上
volatile
关键字,就能禁止这种重排序,确保对象初始化完成后才将引用赋值给
instance

保障有序性的主要手段:

  1. volatile
    关键字
    volatile
    除了保证可见性,还有一个重要的作用就是禁止指令重排序。它通过插入内存屏障来做到这一点。
  2. synchronized
    关键字
    synchronized
    同样也能保证有序性,它通过对临界区代码的互斥访问,确保了临界区内的代码是按序执行的,并且在释放锁前会进行内存屏障操作,保证了在此之前的写操作对后续的读操作可见。

总的来说,理解这三特性,就像是理解并发世界的“物理定律”。原子性是操作的完整性,可见性是状态的同步性,有序性是指令的逻辑序列。它们共同构成了我们构建健壮并发程序的基石,缺一不可。很多时候,我们遇到的并发问题,追根溯源,往往是这三者中的一个或多个没有被妥善处理。这需要我们深入思考,而不是简单地堆砌同步机制

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

834

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

738

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

734

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

9

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.5万人学习

JavaScript
JavaScript

共185课时 | 18.4万人学习

HTML教程
HTML教程

共500课时 | 4.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号