0

0

如何在Java中处理多线程下共享变量问题

P粉602998670

P粉602998670

发布时间:2025-10-06 11:35:02

|

478人浏览过

|

来源于php中文网

原创

处理Java多线程共享变量需解决可见性、原子性和有序性问题,常用方案包括synchronized保证互斥与可见性,volatile确保变量可见但不保证原子性,ReentrantLock提供更灵活的锁机制,Atomic类利用CAS实现高效原子操作,ThreadLocal则通过线程本地副本避免共享。选择策略应基于访问模式、竞争程度及性能需求权衡,无统一最优解。

如何在java中处理多线程下共享变量问题

在Java多线程环境中处理共享变量,核心在于确保数据在不同线程间的可见性、原子性和有序性,从而避免数据不一致和潜在的程序错误。这通常通过使用 synchronized 关键字、volatile 关键字、java.util.concurrent.locks 包下的锁机制以及 java.util.concurrent.atomic 包下的原子类来实现。选择哪种方式,很大程度上取决于共享变量的类型、访问模式以及对性能和复杂度的权衡。

解决方案

在我看来,处理Java多线程下共享变量问题,并没有一劳永逸的“银弹”,更多的是一个策略选择和权衡的过程。以下是一些我常用的,并且被广泛认可的解决方案:

  1. synchronized 关键字: 这是Java语言内置的同步机制,可以直接作用于方法或代码块。当一个线程进入 synchronized 方法或代码块时,它会获取对象的锁。这不仅保证了同一时刻只有一个线程可以执行被同步的代码(互斥性),也隐式地保证了内存可见性——当线程释放锁时,它所做的所有修改都会被刷新到主内存;当线程获取锁时,它会从主内存中读取最新的共享变量值。它的好处是使用简单,但缺点是粒度较粗,且无法中断或尝试获取锁。

    class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public synchronized int getCount() {
            return count;
        }
    }
  2. volatile 关键字: volatile 主要解决的是共享变量的可见性问题,它能确保对一个 volatile 变量的读操作总是能看到最新写入的值,并且禁止编译器和处理器volatile 变量相关的操作进行重排序。但需要注意的是,volatile 并不保证原子性。也就是说,对于 count++ 这种复合操作(读-修改-写),volatile 是无法保证其线程安全的。它适用于那些不需要原子性操作,只需要保证可见性的场景,比如一个状态标志位。

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

    class FlagHolder {
        private volatile boolean running = true;
    
        public void stop() {
            running = false; // 保证其他线程能立即看到这个修改
        }
    
        public void run() {
            while (running) {
                // do some work
            }
            System.out.println("Thread stopped.");
        }
    }
  3. java.util.concurrent.locks.Lock 接口及其实现: ReentrantLockLock 接口最常用的实现之一,它提供了比 synchronized 更细粒度的控制。比如,它可以实现公平锁、非公平锁,可以尝试获取锁(tryLock()),可以中断正在等待锁的线程(lockInterruptibly()),还可以结合 Condition 实现更复杂的线程间通信。在我看来,当 synchronized 无法满足需求时,ReentrantLock 往往是一个不错的选择,但它的使用也需要更小心,比如必须在 finally 块中释放锁,以避免死锁。

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    class AtomicCounter {
        private int count = 0;
        private final Lock lock = new ReentrantLock();
    
        public void increment() {
            lock.lock(); // 获取锁
            try {
                count++;
            } finally {
                lock.unlock(); // 确保锁被释放
            }
        }
    
        public int getCount() {
            lock.lock();
            try {
                return count;
            } finally {
                lock.unlock();
            }
        }
    }
  4. java.util.concurrent.atomic 包: 这个包提供了一系列原子类,如 AtomicIntegerAtomicLongAtomicReference 等。它们通过底层的 CAS (Compare-And-Swap) 操作来保证原子性,而无需使用锁,因此在某些场景下能提供更高的性能,尤其是在低竞争环境下。它们适用于对单个变量进行原子操作的场景,比如计数器、序列生成器等。

    import java.util.concurrent.atomic.AtomicInteger;
    
    class AtomicCounterCAS {
        private AtomicInteger count = new AtomicInteger(0);
    
        public void increment() {
            count.incrementAndGet(); // 原子性地递增
        }
    
        public int getCount() {
            return count.get();
        }
    }
  5. ThreadLocal 这是一种完全不同的思路。它不解决共享变量的同步问题,而是通过为每个线程提供一个独立的变量副本,从而避免了共享。当每个线程都需要维护自己的状态,且这些状态不希望被其他线程访问时,ThreadLocal 是一个非常优雅的解决方案。

    class UserContext {
        private static final ThreadLocal currentUser = new ThreadLocal<>();
    
        public static void setCurrentUser(String user) {
            currentUser.set(user);
        }
    
        public static String getCurrentUser() {
            return currentUser.get();
        }
    
        public static void clear() {
            currentUser.remove(); // 避免内存泄漏
        }
    }

为什么直接使用普通变量在多线程中会导致问题?

这其实是并发编程中最基础也是最容易踩坑的地方,我个人觉得理解这一点比记住各种解决方案更重要。当你直接在多个线程中操作一个普通(非volatile,非synchronized保护)的共享变量时,通常会遇到三大核心问题:可见性、原子性和有序性,它们共同构成了Java内存模型(JMM)需要解决的核心矛盾。

  1. 可见性问题 (Visibility): 想象一下,每个CPU都有自己的高速缓存,当一个线程修改了一个共享变量的值,这个修改可能仅仅发生在它自己的CPU缓存中,而没有立即刷新到主内存。其他线程可能还在使用它们各自CPU缓存中旧的变量值。这就导致了一个线程对变量的修改,对另一个线程来说是“不可见”的。比如,你有一个 boolean flag = false;,线程A把它改成了 true,但线程B可能永远看不到这个 true,因为它一直在读取自己缓存中的 false。这在实际开发中挺麻烦的,特别是当你调试一个看似简单的bug时,发现变量值怎么都不对,往往就是可见性在作祟。

  2. 原子性问题 (Atomicity): 原子性指的是一个操作是不可中断的,要么全部执行成功,要么全部不执行,不会出现执行了一半的情况。很多我们看起来是“一个”操作的语句,在底层实际上是由多个CPU指令组成的。最经典的例子就是 i++。这一个简单的表达式,在JVM层面通常会分解为三个步骤:

    • 读取 i 的当前值。
    • i 的值加 1。
    • 将新值写回 i。 如果在多线程环境下,两个线程同时执行 i++,它们可能同时读取到 i 的旧值,然后各自加 1,再写回。最终 i 的值可能只增加了 1,而不是期望的 2。这就破坏了操作的原子性,导致数据丢失或不一致。
  3. 有序性问题 (Ordering): 为了提高性能,编译器和处理器可能会对指令进行重排序。这意味着你代码中语句的执行顺序,不一定是你编写的顺序。只要重排序不会影响单线程程序的正确性,JVM就允许这种优化。但在多线程环境下,这种重排序可能会导致意想不到的问题。例如,一个线程可能在初始化某个对象之前,就先发布了对这个对象的引用,导致其他线程访问到一个未完全初始化的对象。volatile 关键字的一个重要作用就是禁止这种重排序,确保特定操作的有序性。

所以,当我们说要“处理多线程下共享变量问题”时,其实就是在想办法解决这三大难题,确保数据在并发访问时的正确性和一致性。

除了传统的 synchronized,还有哪些更高效或灵活的并发控制手段?

在现代Java并发编程中,synchronized 固然是基石,但它在灵活性和性能上并非总是最优解。有时候你会发现,面对更复杂的并发场景,或者对性能有更高要求时,java.util.concurrent 包(通常简称JUC包)提供了很多更强大、更灵活的工具

Getimg.ai
Getimg.ai

getimg.ai是一套神奇的ai工具。生成大规模的原始图像

下载
  1. ReentrantLock 我前面提过它,但值得再深入一点。它比 synchronized 灵活太多了。

    • 公平性选择: ReentrantLock 可以选择是公平锁还是非公平锁。公平锁会按照线程请求锁的顺序来授予锁,虽然避免了饥饿,但性能开销会大一些;非公平锁则允许“插队”,性能通常更好。
    • 可中断性: 线程在等待 ReentrantLock 时,可以响应中断。synchronized 就不行,一旦线程进入等待锁的状态,就只能一直等下去。
    • 尝试获取锁: tryLock() 方法允许线程尝试获取锁,如果获取不到,可以立即返回,而不是一直阻塞。这在一些需要避免死锁或者超时处理的场景中非常有用。
    • 条件变量 (Condition): ReentrantLock 可以配合 Condition 接口实现比 Object.wait()/notify() 更加细粒度的线程等待/通知机制。一个锁可以有多个 Condition,每个 Condition 都可以关联一个等待队列,这在复杂的生产者-消费者模型中非常实用。
  2. Atomic 类家族: 比如 AtomicInteger, AtomicLong, AtomicReference 等。它们的核心是利用了CPU的 CAS (Compare-And-Swap) 指令。CAS 是一种乐观锁的实现,它不需要加锁就能保证原子性。它的工作原理是:在更新变量时,首先比较内存中的当前值与你期望的旧值是否相等,如果相等,则说明没有其他线程修改过,就更新为新值;如果不相等,则说明有其他线程修改过了,本次操作失败,可以重试。这个过程是硬件层面保证的原子性。在我看来,对于单个变量的原子操作,Atomic 类通常比 synchronized 性能更好,因为它避免了线程阻塞和上下文切换的开销。

  3. StampedLock 这是Java 8引入的一个更高级的读写锁。传统的 ReentrantReadWriteLock 在读多写少的场景下性能很好,但当写锁被持有时,所有读锁和写锁都会被阻塞。StampedLock 则提供了三种模式:写锁、悲观读锁和乐观读。乐观读允许在没有锁的情况下读取数据,如果发现数据在读取过程中被修改,则可以重新尝试读取。这在读操作远多于写操作的场景下,能显著提高并发度。当然,它的API也相对复杂一些,需要更谨慎地使用。

  4. 并发集合: JUC 包中提供了大量线程安全的集合类,如 ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueue 等。这些集合在内部已经处理了并发访问的同步问题,我们直接使用它们通常比自己手动同步 ArrayListHashMap 更高效、更安全。比如,ConcurrentHashMap 采用分段锁或CAS等技术,实现了比 HashtableCollections.synchronizedMap 更高的并发性能。

  5. 并发工具类: JUC 包还提供了一些用于协调线程协作的工具,比如:

    • CountDownLatch 允许一个或多个线程等待其他线程完成操作。
    • CyclicBarrier 允许一组线程互相等待,直到所有线程都到达一个公共屏障点。
    • Semaphore 控制同时访问某个资源的线程数量。
    • Exchanger 允许两个线程在某个点交换数据。 这些工具在构建复杂并发系统时,能大大简化线程间的协作逻辑。

选择哪种方式,真的是要根据具体的业务场景和性能要求来定。有时候 synchronized 简单粗暴却有效,有时候 ReentrantLock 的灵活性不可或缺,而 Atomic 类则在特定场景下能提供惊人的性能提升。

如何选择合适的并发策略来优化多线程应用性能?

选择合适的并发策略,说实话,这有点像在不同口味的咖啡豆里挑一款最适合你味蕾的,需要经验、对业务的理解以及对各种并发工具特性的深入认知。没有放之四海而皆准的“最佳”方案,更多的是一种权衡。

  1. 分析共享资源的特性和访问模式: 这是我首先会考虑的。

    • 读多写少? 如果你的共享变量大部分时间是用来读取,只有少量修改,那么读写锁(如 ReentrantReadWriteLockStampedLock)会是很好的选择。它们允许多个读线程同时访问,而写线程独占。StampedLock 的乐观读甚至可以进一步提升读的并发性。
    • 写多读少或读写均衡? 如果写操作频繁,那么传统的 synchronizedReentrantLock 可能会更合适,或者考虑使用 Atomic 类(如果适用)。
    • 竞争激烈程度? 如果共享变量的竞争非常激烈,线程频繁地争抢锁,那么上下文切换的开销会很大。这时候可以考虑无锁Atomic 操作,或者通过细化锁的粒度来减少竞争。
  2. 考虑操作的原子性需求:

    • 单个变量的原子操作? 如果只是对一个 intlong 或对象引用进行简单的原子更新(如 i++、设置引用),AtomicIntegerAtomicLongAtomicReference 通常是最高效的选择,因为它避免了锁的开销。
    • 复合操作? 如果是涉及多个变量或复杂逻辑的复合操作,那么 synchronizedReentrantLock 提供的互斥性是必需的。volatile 在这种情况下是不够的。
  3. 性能与复杂度的权衡:

    • synchronized 最简单易用,由JVM管理,不易出错。但灵活性差,无法中断,无法尝试获取锁,性能在某些高竞争场景下可能不如 LockAtomic。如果并发需求不复杂,且性能瓶颈不在锁上,synchronized 是一个稳妥且推荐的选择。
    • ReentrantLock 提供更高的灵活性(公平性、可中断、条件变量),性能在某些场景下可能优于 synchronized。但需要手动管理锁的获取和释放(通常在 finally 块中),增加了代码的复杂性和出错的风险。
    • Atomic 类: 性能通常最高,因为它基于无锁的 CAS 操作。但它只适用于单个变量的原子操作,不适用于保护复杂的代码块。
    • ThreadLocal 如果每个线程只需要一份自己的数据,完全避免了共享,也就彻底避免了同步问题,性能极佳。但它不是用来解决共享变量问题的,而是避免共享。
  4. 避免死锁和活锁:

    • 死锁: 多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。这需要仔细设计锁的获取顺序,或者使用 tryLock 并设置超时时间。
    • 活锁: 线程不断尝试获取资源,但总是失败,导致无法向前推进。通常发生在线程不断回滚操作并重试时。
  5. 利用JUC包中的高级工具和并发集合:

    • 并发集合: 优先使用 ConcurrentHashMapCopyOnWriteArrayList 等JUC提供的线程安全集合,而不是自己去同步 HashMapArrayList。这些集合经过精心设计和优化,通常比手写同步代码更高效、更健壮。
    • 线程池: 合理使用 ExecutorService 和线程池来管理线程的生命周期和任务的执行,可以减少线程创建和销毁的开销,提高资源利用率。
    • 并发工具: CountDownLatchCyclicBarrierSemaphore 等工具在协调线程协作方面非常强大,能简化复杂逻辑。

最终,选择合适的并发策略往往是一个迭代的过程。你可能从一个简单的 synchronized 开始,如果发现性能瓶颈,再逐步优化,考虑使用 ReentrantLockAtomic 类,甚至重构代码以使用并发集合或 ThreadLocal。关键在于理解每种工具的优缺点,并结合实际场景做出最适合的决策。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java中boolean的用法
java中boolean的用法

在Java中,boolean是一种基本数据类型,它只有两个可能的值:true和false。boolean类型经常用于条件测试,比如进行比较或者检查某个条件是否满足。想了解更多java中boolean的相关内容,可以阅读本专题下面的文章。

350

2023.11.13

java boolean类型
java boolean类型

本专题整合了java中boolean类型相关教程,阅读专题下面的文章了解更多详细内容。

29

2025.11.30

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

198

2023.11.20

counta和count的区别
counta和count的区别

Count函数用于计算指定范围内数字的个数,而CountA函数用于计算指定范围内非空单元格的个数。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

198

2023.11.20

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

544

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

93

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

197

2025.08.29

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

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

精品课程

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

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.8万人学习

Java 教程
Java 教程

共578课时 | 52.5万人学习

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

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