NumberFormat / DecimalFormat 的线程安全动态模式

Thread-safe dynamic pattern for NumberFormat / DecimalFormat

未能在网络上找到合适的解决方案,因此我想问问我使用 java 格式的方式是否正确。

1) 在 NumberFormat.java 文档中说

Number formats are generally not synchronized. It is recommended to create separate format instances for each thread.

我们一直在多线程环境中使用格式对象(静态初始化),到目前为止没有任何问题。可能是因为一旦定义了格式,我们就不会改变它们的状态(即之后不会调用任何设置器)

2) 我现在需要定义一种新格式,它应该根据一些额外的逻辑在逗号后输出一个或两个有效数字。我这样做的方法是定义一个新的格式包装器,并根据覆盖的 #format(double, StringBuffer, FieldPosition) 方法中的情况委托给两个不同的 DecimalFormat。这是相关代码:

private final NumberFormat FORMAT = new DecimalFormat() {
    private final NumberFormat DECIMAL_FORMAT = new DecimalFormat("0.##");
    private final NumberFormat DECIMAL_FORMAT_DIGIT = new DecimalFormat(
                    "0.0#");
    public StringBuffer format(double number, StringBuffer result, java.text.FieldPosition fieldPosition) {
        if ((number >= 10 && Math.ceil(number) == number)) {
            return DECIMAL_FORMAT.format(number, result, fieldPosition);
        } else {
            return DECIMAL_FORMAT_DIGIT.format(number, result, fieldPosition);
        }
    }
};

这是最佳做法吗?我担心实际上没有使用包装器 class(它仅用于遵守 NumberFormat 接口并委托内部格式的所有工作)。我不想调用 DecimalFormat#applyPattern(),因为我认为这会影响不稳定的并发性。

谢谢

我没有发表评论的名誉,但关于第 2 点,我看到的最大缺点是您没有覆盖 DecimalFormat class 的所有行为,因此您得到的实例将表现不一致。例如:

final StringBuffer buffer = new StringBuffer();
FORMAT.format(0.111d, buffer, new FieldPosition(0));
System.out.println(buffer.toString());

final StringBuffer buffer2 = new StringBuffer();
FORMAT.format(new BigDecimal(0.111d), buffer2, new FieldPosition(0));
System.out.println(buffer2.toString());

产量

0.11
0.111

如果您要走那条路线,最好覆盖所有必要的方法,以便获得一致的行为。或者,您可以在 ThreadLocal 中存储一个 Function,它封装了您想要的逻辑:

private final ThreadLocal<Function<Double, String>> DECIMAL_FORMATTER = new ThreadLocal<Function<Double, String>>() {
  @Override
  protected Function<Double, String> initialValue() {
    final DecimalFormat decimalFormat = new DecimalFormat("0.##");
    final DecimalFormat decimalFormatDigit = new DecimalFormat("0.0#");
    return (number) -> {
      if ((number >= 10 && Math.ceil(number) == number)) {
        return decimalFormat.format(number);
      } else {
        return decimalFormatDigit.format(number);
      }
    };
  }
};

System.out.println(DECIMAL_FORMATTER.get().apply(0.111d));
  1. We have been using format objects (statically initialized) in a multi-threaded environment with no issues so far. Is it maybe because once the formats are defined, we their state is not changed (ie, no setters are called afterwards)

不可能确切地说出您没有发现任何问题的原因,因为我们不知道您是如何使用它们的。在我的脑海中,有几个原因可能是:

  • 您没有使用 DecimalFormat 中使用可变实例变量的任何代码路径;
  • 您“巧合地”应用了互斥,因此您永远不会一次在多个线程中使用该实例;
  • 您使用的实现实际上确实正确同步(请注意 Javadoc 说“通常不同步”,而不是“从不同步”);
  • 您实际上 有问题,但您没有充分监控它们;
  • 等等

关于同步问题,正如我昨天看到其他人评论的那样,如果您不同步,则不能保证您会看到问题;只是不保证你也看不到。

要点是,如果您不应用同步,那么您可能完全没有意识到任何数量的细微变化,您会随心所欲地摆布。今天有效,明天无效;你将有一份全能的工作来找出原因。

  1. Is it the best practice?

这里我能想到几个问题:

  1. 通过扩展 class,您可能会违反 fragile base class problem

    简而言之,除非您实际上明确地在 DecimalFormat 实例上调用 public StringBuffer format(double, StringBuffer, java.text.FieldPosition ) 方法,否则您无法可靠地知道您的重写方法是否实际上是调用的方法:更改为基础 class (DecimalFormat) 的实现可能会改变您调用该方法所依赖的逻辑。

  2. 您有三个可变实例 - FORMATDECIMAL_FORMATDECIMAL_FORMAT_DIGIT - 它们有各种方式的 setter 来改变它们的行为。

    应该将所有这些设置器传播到所有实例,以便它们的行为一致,例如如果你在 FORMAT 上调用 setPositivePrefix,你也应该在 DECIMAL_FORMATDECIMAL_FORMAT_DIGIT.

    上调用相同的方法

除非您实际上需要将 FORMAT 作为参数传递给方法,否则如果您只定义一个调用您的逻辑的普通旧方法,它会更加健壮:基本上,移动您正在覆盖的方法出于匿名子class:

private static StringBuffer formatWithSpecialLogic(double number, StringBuffer result, java.text.FieldPosition fieldPosition) {

然后,如果您想使用该特殊逻辑,您必须显式调用该方法。

既然你说你需要一个 NumberFormat 的实例,我建议你扩展 class 并实现所需的方法,而不是扩展 DecimalFormat用于继承:

private final NumberFormat FORMAT = new NumberFormat() {
    private final NumberFormat DECIMAL_FORMAT = new DecimalFormat("0.##");
    private final NumberFormat DECIMAL_FORMAT_DIGIT = new DecimalFormat("0.0#");

    @Override
    public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
        if ((number >= 10 && Math.ceil(number) == number)) {    // or number % 1 == 0
            return DECIMAL_FORMAT.format(number, result, fieldPosition);
        } else {
            return DECIMAL_FORMAT_DIGIT.format(number, result, fieldPosition);
        }
    }

    @Override
    public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
        return format((double)number, result, fieldPosition);
    }

    @Override
    public Number parse(String source, ParsePosition parsePosition) {
        return DECIMAL_FORMAT.parse(source, parsePosition);
    }
};

这减少了脆弱的基础 class 问题,因为我们使用的基础 class 应该被继承。但是仍然存在所有死配置方法的问题。理想情况下,您将覆盖它们以传播它们的更改或抛出一些异常。