Spring控制器序列化同步:避免返回对象后序列化与线程并发访问冲突的非深拷贝方案
你的问题戳中了一个很常见的并发坑: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

