将一个类改为不可变,以用于共享保证线程安全

方案1中的静态内部类默认是“可变的”,而不是“不可变的”。这是一个非常重要的区分,直接关系到线程安全的最终效果。

🔍 为什么说它是“可变”的?

在我之前给出的方案1示例中,因为类提供了setter方法,并且字段不是final的,所以它的实例在创建后,状态可以被改变:

public static class StatusAndSeq {
    private String status; // 非final字段
    private Integer seq;   // 非final字段
    
    // 有setter方法,意味着状态可变
    public void setStatus(String status) { this.status = status; }
    public void setSeq(Integer seq) { this.seq = seq; }
}

这种“可变性”正是并发风险的来源。ConcurrentHashMap只能保证你安全地存取这个对象的引用,但如果两个线程同时拿到同一个引用,并各自调用它的setter方法,就会发生数据竞争和不一致。

✅ 如何将其改造为真正的“不可变类”?

要让一个类不可变,需要遵循以下原则,我们把它应用到StatusAndSeq上:

public static class ImmutableStatusAndSeq {
    // 1. 所有字段用 private final 声明
    private final String status;
    private final Integer seq;
    
    // 2. 不提供setter方法
    // 3. 通过构造器一次性初始化所有字段
    public ImmutableStatusAndSeq(String status, Integer seq) {
        this.status = status;
        this.seq = seq;
    }
    
    // 4. 只提供getter方法
    public String getStatus() { return status; }
    public Integer getSeq() { return seq; }
    
    // 5. (如果字段是引用类型,确保其不可变或做防御性拷贝,这里String和Integer本身就是不可变的)
}

关键区别对比表:

特性 可变类 (方案1原版) 不可变类 (改造后)
字段修饰 private String status private final String status
setter方法 (setStatus)
状态可变性 创建后可修改 创建后不可修改
并发安全性 低,需额外同步 高,可被多线程安全共享
使用方式 修改对象属性 替换整个对象

🛡️ 如何在并发场景中使用?

对于不可变对象,你的操作模式是“替换”而非“修改”,这天然适合ConcurrentHashMap

@Service
public class MyDataService {
    // 存储不可变对象
    private final ConcurrentHashMap<String, ImmutableStatusAndSeq> dataMap = new ConcurrentHashMap<>();
    
    // 更新数据:直接放入一个新的不可变对象
    public void updateData(String key, String newStatus, Integer newSeq) {
        dataMap.put(key, new ImmutableStatusAndSeq(newStatus, newSeq));
    }
    
    // 线程安全地“修改”seq:实际上是用新对象替换旧对象
    public void incrementSeqAtomically(String key) {
        dataMap.compute(key, (k, current) -> {
            if (current == null) {
                return new ImmutableStatusAndSeq("INIT", 1);
            }
            // 基于旧值创建全新的不可变对象
            return new ImmutableStatusAndSeq(current.getStatus(), current.getSeq() + 1);
        });
    }
}

这种模式是最高效、最安全的。多个线程可以同时读取同一个ImmutableStatusAndSeq实例而无需任何锁。当需要“更新”时,就通过putcompute方法原子地替换为一个新的实例

📝 与方案2(Java Record)的对比

如果你使用 Java 14+ 的 Record,那它就是天生的、语法级的不可变类

// 这短短一行就等价于上面整个不可变类的定义
public record StatusAndSeqRecord(String status, Integer seq) {}

Record的所有字段都是private final的,没有setter,只有访问器(如status())。它清晰地向所有阅读代码的人声明:“我是不可变的数据载体”。

💡 最终建议

  1. 如果你的业务逻辑允许(即数据更新不极端频繁,且对象很小),强烈建议使用不可变对象。这是实现高并发读写的“银弹”。
  2. 选择哪种不可变实现?
    • Java < 14:使用上面手动编写的不可变静态内部类
    • Java ≥ 14:毫不犹豫地使用 Record,代码最简洁,意图最明确。
  3. 如果业务上必须频繁修改大对象,使用不可变对象可能带来GC压力。这时,你需要退而求其次,使用可变对象 + ConcurrentHashMap的原子方法(如compute,将所有针对一个键的“读-改-写”逻辑封装在一个原子操作内。

简单来说,方案1的静态内部类默认可变,但你可以通过将其字段设为final并移除setter,轻松将其转变为线程安全的不可变类。 这在并发设计中是一个至关重要的实践。

posted @ 2025-12-11 09:46  dirgo  阅读(3)  评论(0)    收藏  举报