8383525: DateTimeFormatterBuilder.getLocalizedDateTimePattern() concurrency issue

Reviewed-by: jlu
This commit is contained in:
Naoto Sato 2026-05-05 15:44:23 +00:00
parent 19e86634d7
commit 2bef1d34f0
2 changed files with 118 additions and 28 deletions

View File

@ -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)));
}
/**

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