【避坑指南】初始化与更新共享数据赋值的一致性问题

文章目录

在并发系统中,共享数据的初始化与更新若缺乏一致性控制,极易因线程竞争或可见性缺失导致脏读、状态不一致、引发线程安全漏洞等问题。本文通过一个配置热更新的代码案例,剖析下问题的原因和优化方案,并展示如何通过final关键字固化初始引用,结合CopyOnWriteArrayList的写时复制机制,采用线程安全容器与不可变对象重构前后的核心代码对比,解决资源竞争和可见性问题,并提升性能。

一、原始代码分析

原始代码中,在 DynamicTablesConfiguration 类中,dynamicTablesConfigList 是一个共享的数据结构,用于存储动态表的配置信息。

然而,dynamicTablesConfigList初始化和更新过程中存在一些潜在的问题,可能导致数据不一致或线程安全问题。

java 复制代码
@Configuration
@NacosCustomNotificationConfigurationEnabled
@AutoConfigureBefore(value = { NacosCustomNotificationConfig.class })
@Data
@Slf4j
public class DynamicTablesConfiguration {

    @Bean
    public DynamicTablesConfigurationNotificationListener newDynamicTablesConfigurationListener() {

        return new DynamicTablesConfigurationNotificationListener(this);
    }

    public void changeDynamicTablesConfig(List<DynamicTableDTO> dynamicTablesConfigs) {

        if (TypeChecker.isEmpty(dynamicTablesConfigs)) {
            return;
        }

        dynamicTablesConfigs.forEach(x -> {
            String schema = x.getSchema();
            if (TypeChecker.isEmpty(myDynamicSchemas) || myDynamicSchemas.contains(schema)) {
                if (TypeChecker.isEmpty(x.getDyTableNameList())) {
                    return;
                }
                x.getDyTableNameList().forEach(t -> dynamicTablesConfigList.add(schema + StringPool.DOT + t));
            }
        });

        log.info(">>>>>>>DynamicTablesConfiguration changed config: {}", dynamicTablesConfigList);

    }

    @Value("${point.dynamic.schemas:}")
    private List<String> myDynamicSchemas;

    private List<String> dynamicTablesConfigList = Collections.synchronizedList(Lists.newArrayList());



}

具体问题:

  • 初始化问题:
    dynamicTablesConfigList 使用了 Collections.synchronizedList 包装,确保线程安全。
    但是,dynamicTablesConfigList 的初始化是在类实例化时进行的,如果在多线程环境下,可能会导致多个实例同时初始化该列表,从而引发潜在的问题。
  • 更新问题:
    changeDynamicTablesConfig 方法中,对 dynamicTablesConfigList 进行了添加操作,但由于 dynamicTablesConfigList 是一个共享的可变对象,如果多个线程同时调用该方法,可能会导致数据不一致或丢失更新。

优化方案

为了确保初始化和更新共享数据的一致性和线程安全性,可以采取以下优化措施:

  • 使用不可变对象:
    将 dynamicTablesConfigList 设计为不可变对象,每次更新时生成一个新的列表实例,而不是直接修改现有列表。这可以避免并发修改带来的问题。
  • 使用原子引用:
    使用 AtomicReference 包装 dynamicTablesConfigList,确保在更新时的原子性操作。
  • 双重检查锁定:
    在初始化时使用双重检查锁定(Double-Checked Locking)模式,确保在多线程环境下只初始化一次。
  • 使用读写锁:
    使用 ReadWriteLock 来管理对 dynamicTablesConfigList 的读写操作,提高并发性能。

二、优化后代码

以下是一个优化后的示例,使用 CopyOnWriteArrayList 和不可变列表来确保数据的一致性和线程安全性:

bash 复制代码
@Configuration
@NacosCustomNotificationConfigurationEnabled
@AutoConfigureBefore(value = {NacosCustomNotificationConfig.class})
@Data
@Slf4j
public class DynamicTablesConfiguration {

    @Bean
    public DynamicTablesConfigurationNotificationListener newDynamicTablesConfigurationListener() {
        return new DynamicTablesConfigurationNotificationListener(this);
    }

    public void changeDynamicTablesConfig(List<DynamicTableDTO> dynamicTablesConfigs) {
        if (TypeChecker.isEmpty(dynamicTablesConfigs)) {
            return;
        }

        // 使用临时列表来收集新的配置
        List<String> newConfigs = new CopyOnWriteArrayList<>();

        dynamicTablesConfigs.forEach(x -> {
            String schema = x.getSchema();
            if (TypeChecker.isEmpty(myDynamicSchemas) || myDynamicSchemas.contains(schema)) {
                if (TypeChecker.isEmpty(x.getDyTableNameList())) {
                    return;
                }
                x.getDyTableNameList().forEach(t -> newConfigs.add(schema + StringPool.DOT + t));
            }
        });

        // 替换旧的配置列表
        dynamicTablesConfigList.clear();
        dynamicTablesConfigList.addAll(newConfigs);

        log.info(">>>>>>>DynamicTablesConfiguration changed config: {}", dynamicTablesConfigList);
    }

    @Value("${point.dynamic.schemas:}")
    private List<String> myDynamicSchemas;

    private final CopyOnWriteArrayList<String> dynamicTablesConfigList = new CopyOnWriteArrayList<>();

    public List<String> getDynamicTablesConfigList() {
        return new CopyOnWriteArrayList<>(dynamicTablesConfigList);
    }
}

优化点说明:

  • 使用 final 关键字:确保 dynamicTablesConfigList 在初始化后不能被重新赋值。

  • 使用 CopyOnWriteArrayList:确保对列表的读写操作是线程安全的。

  • 方法职责清晰,changeDynamicTablesConfig 负责更新配置,getDynamicTablesConfigList 提供只读访问。

  • 获取共享数据的方法

    getDynamicTablesConfigList() 返回副本以保护原始数据

三、优秀开源框架方案使用参考

以下是一些优秀开源框架中使用 final 关键字和 CopyOnWriteArrayList 来处理初始化与更新共享数据赋值一致性的方案。这些框架通过线程安全的集合以及不可变引用确保了多线程环境下的数据一致性。

比如:Spring Framework - 使用 CopyOnWriteArrayList 管理监听器

Spring 框架在事件监听器注册机制中广泛使用了 CopyOnWriteArrayList 来存储动态监听器列表,同时通过 final 修饰符确保引用不可变。

示例代码:

java 复制代码
import org.springframework.context.event.EventListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class SpringEventListenerManager {

    private final CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();

    public void registerListener(EventListener listener) {
        listeners.add(listener);
    }

    public void unregisterListener(EventListener listener) {
        listeners.remove(listener);
    }

    public void notifyListeners(Event event) {
        for (EventListener listener : listeners) {
            listener.onEvent(event);
        }
    }

    public interface EventListener {
        void onEvent(Event event);
    }

    public static class Event {
        private String message;

        public Event(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }
}

特点

使用 final 修饰 CopyOnWriteArrayList,确保其引用不可变。

利用 CopyOnWriteArrayList 的特性,在多线程环境下安全地管理监听器列表。

以上开源框架的源码中,分别使用了该技术点来处理初始化与更新共享数据赋值的一致性问题。