0

0

Java单例模式下的并发数据一致性保障:避免竞态条件的实践指南

DDD

DDD

发布时间:2025-11-11 17:22:01

|

965人浏览过

|

来源于php中文网

原创

Java单例模式下的并发数据一致性保障:避免竞态条件的实践指南

本文深入探讨了java单例模式在多线程环境下共享配置数据时面临的并发问题。当多个线程同时尝试更新和读取单例管理的共享状态时,可能导致数据不一致。文章通过分析一个具体的竞态条件案例,逐步展示了如何通过引入同步机制,从简单的忙等待(并指出其局限性)到更健壮的`synchronized`关键字,确保在并发操作中数据始终保持最新和一致,从而有效避免因并发访问引起的错误。

单例模式与并发挑战

单例模式作为一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。这在管理全局配置、日志记录器或线程池等场景中非常有用。然而,当这个唯一的单例实例包含可变状态,并且在多线程环境中被并发访问时,就可能出现数据一致性问题,即所谓的竞态条件(Race Condition)。

考虑一个ConfigManagerWithThreadSafeBlock单例,它负责管理应用程序的配置信息(例如,存储在Map中的键值对)。如果一个线程正在更新配置(如密码),而另一个线程同时尝试读取该配置,那么读取线程可能会获取到旧的、未更新的数据,从而导致应用程序行为异常。

以下是初始的ConfigManagerWithThreadSafeBlock实现及其并发访问场景:

// ConfigManagerWithThreadSafeBlock.java (初始版本)
package com.designpattern.singleton;

import java.util.HashMap;
import java.util.Map;

public class ConfigManagerWithThreadSafeBlock {

    private static ConfigManagerWithThreadSafeBlock threadsafeblock;
    private Map configMap = new HashMap<>() {{
            put("password", "oldpassword"); // 初始密码
    }};

    private ConfigManagerWithThreadSafeBlock() {
        // 私有构造器,确保单例
    }

    public void update(String key, String value) {
        configMap.put(key, value); // 更新配置
    }

    public void display() {
        for (Map.Entry entry : configMap.entrySet()) {
            System.out.println(entry.getKey()+" : "+entry.getValue()); // 显示配置
        }
    }

    public static ConfigManagerWithThreadSafeBlock getInstance() {
        // 双重检查锁定,确保线程安全的单例初始化
        ConfigManagerWithThreadSafeBlock result = threadsafeblock;
        if (result != null) {
            return result;
        }
        synchronized(ConfigManagerWithThreadSafeBlock.class) {
            if (threadsafeblock == null) {
                threadsafeblock = new ConfigManagerWithThreadSafeBlock();
            }
            return threadsafeblock;
        }
    }
}

// Singleton.java (并发测试)
package com.designpattern.singleton;

public class Singleton {
    public static void main(String args[]) {
        Thread threadblock1 = new Thread(new ThreadSafeBlock1());
        Thread threadblock2 = new Thread(new ThreadSafeBlock2());

        threadblock1.start(); // 线程1更新密码
        threadblock2.start(); // 线程2读取密码
    }

    static class ThreadSafeBlock1 implements Runnable {
        @Override
        public void run() {
            ConfigManagerWithThreadSafeBlock safeblockinit1 = ConfigManagerWithThreadSafeBlock.getInstance();
            System.out.println("Threadsafe Block1");
            safeblockinit1.update("password", "newpassword"); // 更新为"newpassword"
        }
    }

    static class ThreadSafeBlock2 implements Runnable {
        @Override
        public void run() {
            ConfigManagerWithThreadSafeBlock safeblockinit2 = ConfigManagerWithThreadSafeBlock.getInstance();
            System.out.println("Threadsafe Block2");
            safeblockinit2.display(); // 读取并显示密码
        }
    }
}

在上述代码中,threadblock1尝试将"password"更新为"newpassword",而threadblock2则尝试读取"password"的值。由于这两个操作没有同步,程序运行时,threadblock2很可能在threadblock1完成更新之前或更新对threadblock2可见之前读取,从而输出"password : oldpassword",而不是期望的"password : newpassword"。

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

解决方案探讨

为了解决上述数据不一致问题,我们需要在update和display方法之间建立同步机制,确保在任何给定时刻,只有一个线程可以修改或读取共享的configMap,或者至少确保读取操作能获取到最新的写入。

方案一:基于标志位的忙等待(不推荐)

一种尝试性的解决方案是引入一个boolean类型的标志位(例如islocked),在更新操作开始时将其设置为true,在更新结束时设置为false。读取操作则在一个循环中检查此标志位,如果为true则短暂休眠,直到标志位变为false再进行读取。

// ConfigManagerWithThreadSafeBlock.java (方案一:基于标志位的忙等待)
package com.designpattern.singleton;

import java.util.HashMap;
import java.util.Map;

public class ConfigManagerWithThreadSafeBlock {

    private static ConfigManagerWithThreadSafeBlock threadsafeblock;
    private volatile boolean islocked = false; // 使用volatile确保可见性

    private Map configMap = new HashMap<>() {{
            put("password", "oldpassword");
    }};

    private ConfigManagerWithThreadSafeBlock() {

    }

    public void update(String key, String value) {
        islocked = true; // 标记正在更新
        configMap.put(key, value);
        islocked = false; // 更新完成
    }

    public void display() {
        while (islocked) { // 忙等待
            try {
                Thread.sleep(10); // 短暂休眠,避免CPU空转
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
                e.printStackTrace();
            }
        }
        // 当islocked为false时,进行读取
        for (Map.Entry entry : configMap.entrySet()) {
            System.out.println(entry.getKey()+" : "+entry.getValue());
        }
    }

    public static ConfigManagerWithThreadSafeBlock getInstance() {
        // 单例初始化部分保持不变
        ConfigManagerWithThreadSafeBlock result = threadsafeblock;
        if (result != null) {
            return result;
        }
        synchronized(ConfigManagerWithThreadSafeBlock.class) {
            if (threadsafeblock == null) {
                threadsafeblock = new ConfigManagerWithThreadSafeBlock();
            }
            return threadsafeblock;
        }
    }
}

分析与局限性:

  • 可见性问题: islocked变量必须声明为volatile。否则,一个线程对islocked的修改可能不会立即对另一个线程可见,导致读取线程一直看不到更新完成的信号。
  • 忙等待(Busy-Waiting): while (islocked) { Thread.sleep(10); } 是一种忙等待。虽然加入了Thread.sleep(),但它仍然会周期性地唤醒线程并检查条件,浪费CPU资源。在并发量高或等待时间长的情况下,这种方式效率低下。
  • 竞态条件隐患: 尽管引入了islocked,但在某些复杂的时序下,仍然可能存在竞态条件。例如,如果update方法在设置islocked = false之后,display方法在检查islocked为false之后,但在此期间另一个update操作又开始了,那么display仍然可能读取到中间状态。
  • 不适用于复杂场景: 这种简单的标志位机制难以处理更复杂的同步需求,例如多个写入者或更精细的锁粒度。

尽管在特定简单场景下,这种方案可能"看起来"有效,但它并非一个健壮和推荐的并发控制方式。

方案二:使用 synchronized 关键字(推荐)

Java提供了内置的同步机制——synchronized关键字,它可以用于方法或代码块,确保在任何给定时间只有一个线程可以执行被同步的代码。这是解决此类并发问题的最常用且健壮的方法。

VISBOOM
VISBOOM

AI虚拟试衣间,时尚照相馆。

下载

为了确保update和display操作的数据一致性,我们可以将这两个方法都声明为synchronized。当一个线程进入synchronized方法时,它会获取到该对象实例的锁;其他线程如果尝试进入同一个对象的任何synchronized方法,则必须等待锁释放。

// ConfigManagerWithThreadSafeBlock.java (方案二:使用synchronized关键字)
package com.designpattern.singleton;

import java.util.HashMap;
import java.util.Map;

public class ConfigManagerWithThreadSafeBlock {

    private static ConfigManagerWithThreadSafeBlock threadsafeblock;
    private Map configMap = new HashMap<>() {{
            put("password", "oldpassword");
    }};

    private ConfigManagerWithThreadSafeBlock() {

    }

    // 使用synchronized修饰方法,确保线程安全
    public synchronized void update(String key, String value) {
        configMap.put(key, value);
    }

    // 使用synchronized修饰方法,确保线程安全
    public synchronized void display() {
        for (Map.Entry entry : configMap.entrySet()) {
            System.out.println(entry.getKey()+" : "+entry.getValue());
        }
    }

    public static ConfigManagerWithThreadSafeBlock getInstance() {
        // 单例初始化部分保持不变
        ConfigManagerWithThreadSafeBlock result = threadsafeblock;
        if (result != null) {
            return result;
        }
        synchronized(ConfigManagerWithThreadSafeBlock.class) {
            if (threadsafeblock == null) {
                threadsafeblock = new ConfigManagerWithThreadSafeBlock();
            }
            return threadsafeblock;
        }
    }
}

使用此修改后的ConfigManagerWithThreadSafeBlock类运行Singleton.java,输出将变为:

Threadsafe Block1
Threadsafe Block2
password : newpassword

这正是我们期望的结果。

分析与优点:

  • 简单易用: synchronized关键字是Java语言内置的,使用起来非常直观。
  • 数据一致性保障: synchronized确保了同一时刻只有一个线程可以执行update或display方法,从而避免了竞态条件,保证了对configMap的原子性访问。
  • 内存可见性: synchronized不仅提供互斥访问,还保证了进入同步块的线程可以看到之前线程在同步块中修改的最新数据(happens-before原则),解决了volatile所需解决的可见性问题。
  • 避免忙等待: 线程在等待锁时会被阻塞,不会进行忙等待,CPU资源得到更有效利用。

方案三:使用 java.util.concurrent 包下的高级并发工具

对于更复杂的并发场景,例如读操作远多于写操作,synchronized方法可能会导致性能瓶颈,因为它每次都完全锁定对象,即使是只读操作也无法并发进行。在这种情况下,可以使用java.util.concurrent.locks.ReadWriteLock来提高并发性。

ReadWriteLock允许:

  • 多个读线程同时访问共享资源(读锁)。
  • 一个写线程独占访问共享资源(写锁),此时不允许任何读线程或写线程访问。
// ConfigManagerWithThreadSafeBlock.java (方案三:使用ReadWriteLock)
package com.designpattern.singleton;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.Lock;

public class ConfigManagerWithThreadSafeBlock {

    private static ConfigManagerWithThreadSafeBlock threadsafeblock;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private Map configMap = new HashMap<>() {{
            put("password", "oldpassword");
    }};

    private ConfigManagerWithThreadSafeBlock() {

    }

    public void update(String key, String value) {
        writeLock.lock(); // 获取写锁
        try {
            configMap.put(key, value);
        } finally {
            writeLock.unlock(); // 释放写锁
        }
    }

    public void display() {
        readLock.lock(); // 获取读锁
        try {
            for (Map.Entry entry : configMap.entrySet()) {
                System.out.println(entry.getKey()+" : "+entry.getValue());
            }
        } finally {
            readLock.unlock(); // 释放读锁
        }
    }

    public static ConfigManagerWithThreadSafeBlock getInstance() {
        // 单例初始化部分保持不变
        ConfigManagerWithThreadSafeBlock result = threadsafeblock;
        if (result != null) {
            return result;
        }
        synchronized(ConfigManagerWithThreadSafeBlock.class) {
            if (threadsafeblock == null) {
                threadsafeblock = new ConfigManagerWithThreadSafeBlock();
            }
            return threadsafeblock;
        }
    }
}

分析与优点:

  • 高并发性: 允许多个读线程同时访问,提高了读取密集型应用的性能。
  • 精细控制: Lock接口提供了比synchronized更灵活的锁定机制,例如尝试获取锁、定时获取锁等。
  • 适用场景: 适用于读多写少的场景。

注意事项与总结

  1. 选择合适的同步机制:
    • 对于简单场景或读写操作同样频繁的场景,synchronized关键字通常是最佳选择,因为它简单、安全且由JVM优化。
    • 对于读操作远多于写操作的场景,ReadWriteLock可以提供更好的并发性能。
    • 对于单个变量的原子操作,可以考虑使用java.util.concurrent.atomic包下的原子类(如AtomicReference、AtomicInteger等)。
    • 对于集合类,可以考虑使用java.util.concurrent包下的并发集合(如ConcurrentHashMap、CopyOnWriteArrayList等),它们在内部实现了线程安全。
  2. 避免死锁: 在使用多个锁或复杂同步逻辑时,务必小心避免死锁的发生。死锁通常发生在多个线程互相持有对方所需的锁,并无限期等待对方释放锁的情况。
  3. 性能考量: 任何同步机制都会引入一定的性能开销。在确保线程安全的前提下,应尽量选择开销最小、并发性最高的方案。
  4. volatile关键字: volatile关键字确保了变量的内存可见性,即一个线程对volatile变量的修改会立即对其他线程可见。它不提供原子性,但对于某些标志位或状态变量的同步是必不可少的。在上述islocked的方案中,volatile是必须的,但其本身无法替代完整的同步机制。

总之,在Java多线程环境中处理单例模式下的共享可变状态时,必须采取适当的同步措施来确保数据的一致性。从简单的synchronized方法到更高级的ReadWriteLock,选择正确的并发工具是构建健壮、高效并发应用程序的关键。理解每种机制的优缺点和适用场景,能够帮助开发者有效避免竞态条件,保障程序的正确运行。

热门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

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

94

2023.09.25

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

94

2023.09.25

c++中volatile关键字的作用
c++中volatile关键字的作用

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

69

2025.10.23

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1078

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

169

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1358

2025.12.29

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

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

精品课程

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

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.7万人学习

Java 教程
Java 教程

共578课时 | 52万人学习

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

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