【问题标题】:Thread-safe dynamic pattern for NumberFormat / DecimalFormatNumberFormat / DecimalFormat 的线程安全动态模式
【发布时间】:2016-05-30 09:07:24
【问题描述】:

一直没能在网络上找到合适的解决方案,所以想问问我使用java格式的方式是否正确。

1) 在 NumberFormat.java 文档中它说

数字格式通常不同步。建议为每个线程创建单独的格式实例。

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

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);
        }
    }
};

这是最佳实践吗?我担心实际上不使用包装类(它仅用于遵守 NumberFormat 接口并将所有工作委托给内部格式)。我不想调用 DecimalFormat#applyPattern() ,因为我认为这会损害 volatile 并发。

谢谢

【问题讨论】:

  • 在多个线程中使用/共享 NumberFormat 实例不是一个好习惯。即使您没有遇到任何问题,但到目前为止,一旦您的应用程序达到真正的并发处理,它们就会发生。我们对 DateFormat 有类似的用例,其中使用了静态创建的日期格式,但是我们将应用程序扩展为大型数据处理,其中 n 个并发进程正在大量生成数据,我们发现该格式会给您带来非常奇怪和意想不到的结果。他们可能不会失败,但会给出一些意想不到的价值
  • @SangramJadhav,所以您可能会保留一个可重用的格式线程池,对吧?
  • 如果要分享格式,需要我们自己提供同步。或者您可以使用 ThreadLocal 来声明格式实例,这样每个线程都将拥有自己的格式化程序副本,而您不必提供同步。
  • 是的,如果,那么我将使用 ThreadLocal。谢谢。您对第 2 点有什么建议吗?

标签: java multithreading formatting format decimalformat


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

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

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

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

关键是,如果您不应用同步,那么您可能会随心所欲地受到许多您可能完全不知道的细微变化的影响。今天有效,明天无效;您将有一项全能的工作来找出原因。

  1. 这是最佳做法吗?

我可以在这里想到几个问题:

  1. 通过扩展类,您可能会与fragile base class problem 发生冲突。

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

  2. 您有三个可变实例 - FORMATDECIMAL_FORMATDECIMAL_FORMAT_DIGIT - 它们具有各种设置器来改变它们的行为。

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

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

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

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

【讨论】:

  • 1.我认为你错了。 DecimalFormat 扩展的 NumberFormat#format(double) 方法被标记为 final 以禁止它被覆盖。这对我来说意味着,创建者希望消费者覆盖其他方法格式(double,StringBuffer,FieldPosition) 2。我不调用任何结果格式的设置器。它也不是线程安全的(嗯,甚至比现在更安全)。 3.我无法将formatWithSpeicalLogic拉出类,因为javax.swing.text.NumberFormatter类隐式调用了format(double)
  • 1. “我认为你错了”没有错,只是可能过于悲观; FBCP 是extends 的必然结果。你只是认为它不会发生。而且,可能不会;我只是不会把房子赌在上面。 2.“我不叫任何二传手”你能保证你永远不会吗? 3) 那是你忽略的一个细节。
【解决方案2】:

我没有评论的声誉,但关于第 2 点,我看到的最大缺点是您没有覆盖 DecimalFormat 类的所有行为,因此您生成的实例的行为将不一致。例如:

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 中存储一个函数:

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));

【讨论】:

  • 问题是,我只需要一个执行所有逻辑的格式对象。原因是 javax.swing.text.NumberFormatter 在其构造函数中需要一个 java.text.NumberFormat 对象。因此,我选择了继承,而不是组合。我没有覆盖其余的方法,因为我们只对双输入感兴趣。但是,我有点担心由此产生的尴尬继承——第三个中有 2 个 NumberFormats ;(
【解决方案3】:

既然你说你需要一个NumberFormat 的实例,我建议你扩展那个类并实现所需的方法,而不是扩展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);
    }
};

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

【讨论】:

    猜你喜欢
    • 2011-12-07
    • 2012-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-04
    • 2012-09-22
    相关资源
    最近更新 更多