Gson Type Adapter是否线程安全?多线程下BigDecimal序列化异常问题
问题解决:多线程下ThreadLocal导致BigDecimal序列化异常
核心原因
线程池复用线程时,ThreadLocal中残留的scale值未被清理,导致原本不需要格式化的请求意外获取到之前请求设置的scale,触发了setScale(scale, RoundingMode.FLOOR)逻辑,将金额错误序列化为带固定小数位的形式(比如0.0)。
解决方案
在所有设置过ThreadLocal scale值的代码路径中,使用try-finally块强制清理ThreadLocal,确保线程复用不会携带旧值:
1. 完善ThreadLocalContextUtil工具类
确保工具类提供明确的清理方法:
public class ThreadLocalContextUtil { private static final ThreadLocal<Integer> FRACTIONAL_DIGITS = new ThreadLocal<>(); public static Integer getFractionalDigits() { return FRACTIONAL_DIGITS.get(); } public static void setFractionalDigits(Integer scale) { FRACTIONAL_DIGITS.set(scale); } // 新增ThreadLocal清理方法 public static void removeFractionalDigits() { FRACTIONAL_DIGITS.remove(); } }
2. 在业务代码中强制清理
所有需要设置scale的请求处理逻辑,必须在finally块中清理ThreadLocal,无论请求成功或失败:
// 示例:需要格式化金额的API处理逻辑 public ResponseEntity<?> processAmountRequest() { try { // 设置当前请求需要的小数位数 ThreadLocalContextUtil.setFractionalDigits(2); // 执行业务逻辑,触发BigDecimal序列化 BigDecimal amount = new BigDecimal("100"); return ResponseEntity.ok(amount); } finally { // 强制清理ThreadLocal,避免线程复用残留旧值 ThreadLocalContextUtil.removeFractionalDigits(); } }
3. (可选)适配器中增加防御性校验
如果担心极端场景下清理不及时,可以在适配器中增加校验,仅当scale为合法有效值时才执行格式化逻辑,作为兜底方案:
@Override public void write(final JsonWriter out, final BigDecimal value) throws IOException { if (value == null) { out.nullValue(); return; } final var scale = ThreadLocalContextUtil.getFractionalDigits(); // 仅当scale为非负有效值时才格式化,避免意外的0或负数导致的错误序列化 if (scale != null && scale >= 0) { final var newVal = value.setScale(scale, RoundingMode.FLOOR); out.value(newVal); } else { out.jsonValue(value.toPlainString()); } }
关键说明
ThreadLocal实现线程隔离,但线程池中的线程会被重复使用,如果不主动清理,线程持有的ThreadLocal值会一直存在,直到线程被销毁或被新值覆盖。这是多线程环境下使用ThreadLocal的常见陷阱,必须通过try-finally确保资源清理。
内容的提问来源于stack exchange,提问作者Chếnhếng Seang
相关产品推荐
相关产品推荐

