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));
- 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 说“通常不同步”,而不是“从不同步”);
- 您实际上 有问题,但您没有充分监控它们;
- 等等
关于同步问题,正如我昨天看到其他人评论的那样,如果您不同步,则不能保证您会看到问题;只是不保证你也看不到。
要点是,如果您不应用同步,那么您可能完全没有意识到任何数量的细微变化,您会随心所欲地摆布。今天有效,明天无效;你将有一份全能的工作来找出原因。
- Is it the best practice?
这里我能想到几个问题:
通过扩展 class,您可能会违反 fragile base class problem。
简而言之,除非您实际上明确地在 DecimalFormat
实例上调用 public StringBuffer format(double, StringBuffer, java.text.FieldPosition )
方法,否则您无法可靠地知道您的重写方法是否实际上是调用的方法:更改为基础 class (DecimalFormat
) 的实现可能会改变您调用该方法所依赖的逻辑。
您有三个可变实例 - FORMAT
、DECIMAL_FORMAT
和 DECIMAL_FORMAT_DIGIT
- 它们有各种方式的 setter 来改变它们的行为。
您应该将所有这些设置器传播到所有实例,以便它们的行为一致,例如如果你在 FORMAT
上调用 setPositivePrefix
,你也应该在 DECIMAL_FORMAT
和 DECIMAL_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 应该被继承。但是仍然存在所有死配置方法的问题。理想情况下,您将覆盖它们以传播它们的更改或抛出一些异常。
未能在网络上找到合适的解决方案,因此我想问问我使用 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));
- 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 说“通常不同步”,而不是“从不同步”);
- 您实际上 有问题,但您没有充分监控它们;
- 等等
关于同步问题,正如我昨天看到其他人评论的那样,如果您不同步,则不能保证您会看到问题;只是不保证你也看不到。
要点是,如果您不应用同步,那么您可能完全没有意识到任何数量的细微变化,您会随心所欲地摆布。今天有效,明天无效;你将有一份全能的工作来找出原因。
- Is it the best practice?
这里我能想到几个问题:
通过扩展 class,您可能会违反 fragile base class problem。
简而言之,除非您实际上明确地在
DecimalFormat
实例上调用public StringBuffer format(double, StringBuffer, java.text.FieldPosition )
方法,否则您无法可靠地知道您的重写方法是否实际上是调用的方法:更改为基础 class (DecimalFormat
) 的实现可能会改变您调用该方法所依赖的逻辑。您有三个可变实例 -
FORMAT
、DECIMAL_FORMAT
和DECIMAL_FORMAT_DIGIT
- 它们有各种方式的 setter 来改变它们的行为。您应该将所有这些设置器传播到所有实例,以便它们的行为一致,例如如果你在
上调用相同的方法FORMAT
上调用setPositivePrefix
,你也应该在DECIMAL_FORMAT
和DECIMAL_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 应该被继承。但是仍然存在所有死配置方法的问题。理想情况下,您将覆盖它们以传播它们的更改或抛出一些异常。