多线程场景下Callable调用工具类通用方法的最优实现
并行任务下日期比较的线程安全与性能优化方案
问题描述
我启动最多200个并行运行的Callable类,这些类会调用工具类的通用方法areEqual。发现不给这个方法加synchronized修饰就会出现错误,即使把方法移到Callable类中问题依然存在。添加synchronized后能正常运行,但担心性能下降——并行任务会互相阻塞,无法达到最大运行速度。
具体错误场景:不加synchronized时,只要不涉及日期比较就不会出错;但混合使用java.util.Date和Timestamp做日期比较(转成Date再转为字符串)时就会报错。加锁后恢复正常,说明方法逻辑本身是正确的。
简化代码片段
CompareTableTF类
public class CompareTableTF { ... public void compareTable(...) { ... while (...) { try { myTask = new CompareTableTask(...); completion.submit(myTask); } catch (InterruptedException | ExecutionException e) {...} ... } }
CompareTableTask类
public class CompareTableTask implements Callable<Integer> { private List<Object> row1; public CompareTableTask(List<Object> row1) { this.row1=row1; } @Override public Integer call() { int result=1; List<Object> row2=getRowToCompare(...); if (isEqual(row2)) { result=0; } return result; } private boolean isEqual(List<Object> row2) { ... for (int i=0; i<n; i++) { if (! Tools.areEqual(row1.get(i), row2.get(i))) return false; } return true; } }
Tools类
public class Tools { private final static SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S"); ... public synchronized static boolean areEqual(Object obj1, Object obj2) { String sObj1, sObj2; Double dObj1, dObj2; Date dtObj1, dtObj2; boolean resultat = true; // Check if one only is empty ... // Compare as objects if (!obj1.equals(obj2)) { // if differents, compare as strings sObj1=obj1.toString(); sObj2=obj2.toString(); if (!sObj1.equals(sObj2)) { // if differents, verify if are instanceof java.util.Date, convert them to strings, compare as strings if (obj1 instanceof Date && obj2 instanceof Date) { if (obj1 instanceof Timestamp) {dtObj1 = new Date(((Timestamp) obj1).getTime());} else {dtObj1 = (Date) obj1;} if (obj2 instanceof Timestamp) {dtObj2 = new Date(((Timestamp) obj2).getTime());} else {dtObj2 = (Date) obj2;} sObj1 = dateTimeFormat.format(dtObj1); sObj2 = dateTimeFormat.format(dtObj2); result = sObj1.equals(sObj2); } else { // if differents (and not date), last compare as Double resultat=false; try { dObj1 = new Double(sObj1); dObj2 = new Double(sObj2); if (dObj1.equals(dObj2)) { resultat=true; } } catch (NumberFormatException e) { } } } return result; } }
最优实现方案
问题根源在于**SimpleDateFormat是线程不安全的**——多个线程共享同一个实例时,会导致日期格式化逻辑混乱,进而出现比较错误。给areEqual加synchronized只是通过串行化执行规避了这个问题,但牺牲了并行性能。以下是两种更优的解决思路:
1. 为每个线程分配独立的SimpleDateFormat实例
使用ThreadLocal缓存每个线程的格式化实例,避免多线程共享,同时减少频繁创建对象的开销:
public class Tools { // 用ThreadLocal存储每个线程独有的SimpleDateFormat实例 private static final ThreadLocal<SimpleDateFormat> dateTimeFormat = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S") ); // 移除synchronized修饰符 public static boolean areEqual(Object obj1, Object obj2) { String sObj1, sObj2; Double dObj1, dObj2; Date dtObj1, dtObj2; boolean resultat = true; // 原有逻辑不变,仅修改格式化实例的获取方式 ... if (obj1 instanceof Date && obj2 instanceof Date) { ... // 获取当前线程的格式化实例 sObj1 = dateTimeFormat.get().format(dtObj1); sObj2 = dateTimeFormat.get().format(dtObj2); resultat = sObj1.equals(sObj2); } ... return resultat; } }
2. 使用Java 8+线程安全的日期API(推荐)
Java 8引入的DateTimeFormatter是线程安全的,配合新的日期时间类(如LocalDateTime)可以更规范地处理日期转换:
public class Tools { private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.S"); public static boolean areEqual(Object obj1, Object obj2) { String sObj1, sObj2; Double dObj1, dObj2; boolean resultat = true; ... if (obj1 instanceof Date && obj2 instanceof Date) { // 将Date/Timestamp转换为LocalDateTime LocalDateTime dt1 = ((Date) obj1).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); LocalDateTime dt2 = ((Date) obj2).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); // 使用线程安全的DateTimeFormatter格式化 sObj1 = dateTimeFormatter.format(dt1); sObj2 = dateTimeFormatter.format(dt2); resultat = sObj1.equals(sObj2); } ... return resultat; } }
效果说明
两种方案都无需给areEqual方法加synchronized,每个并行任务可以独立执行,不会互相阻塞,既能保证线程安全,又能充分利用并行性能。其中Java 8+的日期API方案更推荐,它不仅线程安全,还提供了更清晰、类型安全的日期处理逻辑,避免旧API的潜在问题。
内容的提问来源于stack exchange,提问作者vinsse2001
相关产品推荐
相关产品推荐

