You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring控制器序列化同步:避免返回对象后序列化与线程并发访问冲突的非深拷贝方案

解决Spring控制器中MyObject的并发序列化冲突问题

你的问题戳中了一个很常见的并发坑:get()方法里的synchronized只保护到返回MyObject引用的那一刻,而Spring后续的JSON序列化操作是在这个同步块之外执行的——这就导致序列化线程和执行f()的修改线程同时操作同一个MyObject实例,引发并发冲突。除了深拷贝,还有这几个实用方案可以解决:

  • 把JSON序列化过程纳入同步保护范围
    与其返回MyObject让Spring后续序列化,不如在synchronized块内部直接完成序列化,返回已经处理好的JSON字符串。这样整个读写操作都被锁保护,不会出现并发访问:

    @GetMapping
    synchronized String get() throws JsonProcessingException {
        // 实际项目里建议把ObjectMapper注入进来,不要每次新建
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(myObject);
    }
    

    这种方式最简单直接,Spring会直接把字符串返回给客户端,完全跳过后续的序列化步骤,从根源上避免了外部线程访问MyObject的问题。

  • 让MyObject本身具备线程安全性
    如果能修改MyObject的实现,让它的所有读写操作都是线程安全的,那即使序列化和修改操作并发进行,也不会出现不一致的状态。比如:

    • 用AtomicInteger、AtomicReference等原子类替代普通字段
    • 给modify()方法以及所有字段的getter方法加上synchronized修饰符
    • 如果MyObject里包含集合,改用ConcurrentHashMap、CopyOnWriteArrayList这类并发容器
      这种方案适合需要频繁读写MyObject的场景,从对象本身层面解决并发问题,不需要修改控制器的逻辑。
  • 用读写锁优化并发性能
    因为get()(包括后续的序列化)是读操作,f()里的modify()是写操作,用ReentrantReadWriteLock可以比synchronized更高效地处理这种读写分离的场景:多个读操作可以同时进行,而写操作会独占锁。不过要注意,必须把序列化过程也纳入读锁的保护范围,所以最好还是结合返回JSON字符串的方式:

    @RestController
    class MyController {
        MyObject myObject;
        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock readLock = lock.readLock();
        private final Lock writeLock = lock.writeLock();
    
        @GetMapping
        String get() throws JsonProcessingException {
            readLock.lock();
            try {
                ObjectMapper mapper = new ObjectMapper();
                return mapper.writeValueAsString(myObject);
            } finally {
                readLock.unlock();
            }
        }
    
        void f() {
            for (;;) {
                writeLock.lock();
                try {
                    myObject.modify();
                } finally {
                    writeLock.unlock();
                }
            }
        }
    }
    

    这种方案在高并发读的场景下,性能比单纯用synchronized要好很多。

  • 使用不可变DTO做中间转换
    定义一个和MyObject对应的DTO类,并且把DTO的字段都设为final,在synchronized块内将MyObject的字段值复制到DTO中,然后返回这个DTO。因为DTO是不可变的,后续Spring序列化时根本不会有修改操作,自然不会有并发冲突:

    // 不可变DTO
    class MyObjectDto {
        private final int field1;
        private final String field2;
    
        // 构造时直接赋值所有字段
        public MyObjectDto(MyObject myObject) {
            this.field1 = myObject.getField1();
            this.field2 = myObject.getField2();
        }
    
        // 只提供getter,不提供setter
        public int getField1() { return field1; }
        public String getField2() { return field2; }
    }
    
    @RestController
    class MyController {
        MyObject myObject;
    
        @GetMapping
        synchronized MyObjectDto get() {
            return new MyObjectDto(myObject);
        }
    
        void f() {
            for (;;) {
                synchronized (this) {
                    myObject.modify();
                }
            }
        }
    }
    

    这种方式比深拷贝更轻量,因为你只需要复制需要对外暴露的字段,不需要拷贝整个对象的所有层级结构。

这些方案各有优劣,你可以根据自己的业务场景选择:比如追求简单选序列化字符串;追求长期可维护性选线程安全的MyObject;高并发场景选读写锁;需要灵活控制返回字段选DTO转换。

内容的提问来源于stack exchange,提问作者user6529117

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 20:02:43