mirror of
https://github.com/openjdk/jdk.git
synced 2026-05-09 21:19:38 +00:00
8383525: DateTimeFormatterBuilder.getLocalizedDateTimePattern() concurrency issue
Reviewed-by: jlu
This commit is contained in:
parent
19e86634d7
commit
2bef1d34f0
@ -131,11 +131,12 @@ public class LocaleResources {
|
||||
|
||||
// Input Skeleton map for "preferred" and "allowed"
|
||||
// Map<"preferred"/"allowed", Map<"region", "skeleton">>
|
||||
private static Map<String, Map<String, String>> inputSkeletons;
|
||||
private static final LazyConstant<Map<String, Map<String, String>>> INPUT_SKELETONS =
|
||||
LazyConstant.of(LocaleResources::initSkeletons);
|
||||
|
||||
// Skeletons for "j" and "C" input skeleton symbols for this locale
|
||||
private String jPattern;
|
||||
private String CPattern;
|
||||
private final LazyConstant<String> jPattern = LazyConstant.of(() -> resolveInputSkeleton("preferred"));
|
||||
private final LazyConstant<String> CPattern = LazyConstant.of(this::initCPattern);
|
||||
|
||||
LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) {
|
||||
this.locale = locale;
|
||||
@ -607,8 +608,6 @@ public class LocaleResources {
|
||||
}
|
||||
|
||||
private String getLocalizedPatternImpl(String requestedTemplate, String calType) {
|
||||
initSkeletonIfNeeded();
|
||||
|
||||
// input skeleton substitution
|
||||
var skeleton = substituteInputSkeletons(requestedTemplate);
|
||||
|
||||
@ -657,12 +656,14 @@ public class LocaleResources {
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void initSkeletonIfNeeded() {
|
||||
private static Map<String, Map<String, String>> initSkeletons() {
|
||||
// "preferred"/"allowed" input skeleton maps
|
||||
if (inputSkeletons == null) {
|
||||
inputSkeletons = new HashMap<>();
|
||||
Pattern p = Pattern.compile("([^:]+):([^;]+);");
|
||||
ResourceBundle r = localeData.getDateFormatData(Locale.ROOT);
|
||||
var inputSkeletons = new HashMap<String, Map<String, String>>();
|
||||
Pattern p = Pattern.compile("([^:]+):([^;]+);");
|
||||
|
||||
// CLDR is guaranteed to implement ResourceBundleBasedAdapter
|
||||
if (LocaleProviderAdapter.forType(LocaleProviderAdapter.Type.CLDR) instanceof ResourceBundleBasedAdapter rbba) {
|
||||
var r = rbba.getLocaleData().getDateFormatData(Locale.ROOT);
|
||||
Stream.of("preferred", "allowed").forEach(type -> {
|
||||
var inputRegionsKey = SKELETON_INPUT_REGIONS_KEY + "." + type;
|
||||
Map<String, String> typeMap = new HashMap<>();
|
||||
@ -676,19 +677,17 @@ public class LocaleResources {
|
||||
inputSkeletons.put(type, typeMap);
|
||||
});
|
||||
}
|
||||
return inputSkeletons;
|
||||
}
|
||||
|
||||
// j/C patterns for this locale
|
||||
if (jPattern == null) {
|
||||
jPattern = resolveInputSkeleton("preferred");
|
||||
CPattern = resolveInputSkeleton("allowed");
|
||||
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
|
||||
if (CPattern.length() == 2) {
|
||||
var ba = new byte[2];
|
||||
ba[0] = (byte)CPattern.charAt(1);
|
||||
ba[1] = (byte)CPattern.charAt(0);
|
||||
CPattern = new String(ba);
|
||||
}
|
||||
private String initCPattern() {
|
||||
// C patterns for this locale
|
||||
var cp = resolveInputSkeleton("allowed");
|
||||
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
|
||||
if (cp.length() == 2) {
|
||||
cp = "" + cp.charAt(1) + cp.charAt(0);
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -698,11 +697,22 @@ public class LocaleResources {
|
||||
* @return resolved skeletons for this locale, defaults to "h" if none found.
|
||||
*/
|
||||
private String resolveInputSkeleton(String type) {
|
||||
var regionToSkeletonMap = inputSkeletons.get(type);
|
||||
return regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-" + locale.getCountry(),
|
||||
regionToSkeletonMap.getOrDefault(locale.getCountry(),
|
||||
regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-001",
|
||||
regionToSkeletonMap.getOrDefault("001", "h"))));
|
||||
var regionToSkeletonMap = INPUT_SKELETONS.get().get(type);
|
||||
|
||||
if (regionToSkeletonMap != null) {
|
||||
for (var region: new String[] {
|
||||
locale.getLanguage() + "-" + locale.getCountry(),
|
||||
locale.getCountry(),
|
||||
locale.getLanguage() + "-001",
|
||||
"001"}) {
|
||||
var hour = regionToSkeletonMap.get(region);
|
||||
if (hour != null) {
|
||||
return hour;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "h";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -714,8 +724,8 @@ public class LocaleResources {
|
||||
*/
|
||||
private String substituteInputSkeletons(String requestedTemplate) {
|
||||
var cCount = requestedTemplate.chars().filter(c -> c == 'C').count();
|
||||
return requestedTemplate.replaceAll("j", jPattern)
|
||||
.replaceFirst("C+", CPattern.replaceAll("([hkHK])", "$1".repeat((int)cCount)));
|
||||
return requestedTemplate.replaceAll("j", jPattern.get())
|
||||
.replaceFirst("C+", CPattern.get().replaceAll("([hkHK])", "$1".repeat((int)cCount)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
80
test/jdk/sun/util/locale/provider/SkeletonRaceTest.java
Normal file
80
test/jdk/sun/util/locale/provider/SkeletonRaceTest.java
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8383525
|
||||
* @modules jdk.localedata
|
||||
* @summary Make sure that input skeleton init code is thread safe
|
||||
* @run junit SkeletonRaceTest
|
||||
*/
|
||||
|
||||
import java.time.chrono.IsoChronology;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
public class SkeletonRaceTest {
|
||||
@Test
|
||||
void testSkeletonRace() {
|
||||
// Without the fix, LocaleResources throws an NPE
|
||||
for (int run = 0; run < 10; run++) {
|
||||
assertDoesNotThrow(this::doRaceTest);
|
||||
}
|
||||
}
|
||||
|
||||
private void doRaceTest() throws InterruptedException, ExecutionException {
|
||||
Locale[] locales = Locale.getAvailableLocales();
|
||||
int threads = 50;
|
||||
|
||||
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
|
||||
CountDownLatch ready = new CountDownLatch(threads);
|
||||
CountDownLatch go = new CountDownLatch(1);
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < threads; i++) {
|
||||
Locale locale = locales[i % locales.length];
|
||||
futures.add(pool.submit(() -> {
|
||||
ready.countDown();
|
||||
go.await();
|
||||
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
|
||||
"yMd", IsoChronology.INSTANCE, locale);
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
|
||||
ready.await();
|
||||
go.countDown();
|
||||
for (Future<?> f : futures) f.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user