Skip to content

Commit 2bef1d3

Browse files
committed
8383525: DateTimeFormatterBuilder.getLocalizedDateTimePattern() concurrency issue
Reviewed-by: jlu
1 parent 19e8663 commit 2bef1d3

2 files changed

Lines changed: 118 additions & 28 deletions

File tree

src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,12 @@ public class LocaleResources {
131131

132132
// Input Skeleton map for "preferred" and "allowed"
133133
// Map<"preferred"/"allowed", Map<"region", "skeleton">>
134-
private static Map<String, Map<String, String>> inputSkeletons;
134+
private static final LazyConstant<Map<String, Map<String, String>>> INPUT_SKELETONS =
135+
LazyConstant.of(LocaleResources::initSkeletons);
135136

136137
// Skeletons for "j" and "C" input skeleton symbols for this locale
137-
private String jPattern;
138-
private String CPattern;
138+
private final LazyConstant<String> jPattern = LazyConstant.of(() -> resolveInputSkeleton("preferred"));
139+
private final LazyConstant<String> CPattern = LazyConstant.of(this::initCPattern);
139140

140141
LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) {
141142
this.locale = locale;
@@ -607,8 +608,6 @@ public String getLocalizedPattern(String requestedTemplate, String calType) {
607608
}
608609

609610
private String getLocalizedPatternImpl(String requestedTemplate, String calType) {
610-
initSkeletonIfNeeded();
611-
612611
// input skeleton substitution
613612
var skeleton = substituteInputSkeletons(requestedTemplate);
614613

@@ -657,12 +656,14 @@ private String matchSkeleton(String skeleton, String calType) {
657656
.orElse(null);
658657
}
659658

660-
private void initSkeletonIfNeeded() {
659+
private static Map<String, Map<String, String>> initSkeletons() {
661660
// "preferred"/"allowed" input skeleton maps
662-
if (inputSkeletons == null) {
663-
inputSkeletons = new HashMap<>();
664-
Pattern p = Pattern.compile("([^:]+):([^;]+);");
665-
ResourceBundle r = localeData.getDateFormatData(Locale.ROOT);
661+
var inputSkeletons = new HashMap<String, Map<String, String>>();
662+
Pattern p = Pattern.compile("([^:]+):([^;]+);");
663+
664+
// CLDR is guaranteed to implement ResourceBundleBasedAdapter
665+
if (LocaleProviderAdapter.forType(LocaleProviderAdapter.Type.CLDR) instanceof ResourceBundleBasedAdapter rbba) {
666+
var r = rbba.getLocaleData().getDateFormatData(Locale.ROOT);
666667
Stream.of("preferred", "allowed").forEach(type -> {
667668
var inputRegionsKey = SKELETON_INPUT_REGIONS_KEY + "." + type;
668669
Map<String, String> typeMap = new HashMap<>();
@@ -676,19 +677,17 @@ private void initSkeletonIfNeeded() {
676677
inputSkeletons.put(type, typeMap);
677678
});
678679
}
680+
return inputSkeletons;
681+
}
679682

680-
// j/C patterns for this locale
681-
if (jPattern == null) {
682-
jPattern = resolveInputSkeleton("preferred");
683-
CPattern = resolveInputSkeleton("allowed");
684-
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
685-
if (CPattern.length() == 2) {
686-
var ba = new byte[2];
687-
ba[0] = (byte)CPattern.charAt(1);
688-
ba[1] = (byte)CPattern.charAt(0);
689-
CPattern = new String(ba);
690-
}
683+
private String initCPattern() {
684+
// C patterns for this locale
685+
var cp = resolveInputSkeleton("allowed");
686+
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
687+
if (cp.length() == 2) {
688+
cp = "" + cp.charAt(1) + cp.charAt(0);
691689
}
690+
return cp;
692691
}
693692

694693
/**
@@ -698,11 +697,22 @@ private void initSkeletonIfNeeded() {
698697
* @return resolved skeletons for this locale, defaults to "h" if none found.
699698
*/
700699
private String resolveInputSkeleton(String type) {
701-
var regionToSkeletonMap = inputSkeletons.get(type);
702-
return regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-" + locale.getCountry(),
703-
regionToSkeletonMap.getOrDefault(locale.getCountry(),
704-
regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-001",
705-
regionToSkeletonMap.getOrDefault("001", "h"))));
700+
var regionToSkeletonMap = INPUT_SKELETONS.get().get(type);
701+
702+
if (regionToSkeletonMap != null) {
703+
for (var region: new String[] {
704+
locale.getLanguage() + "-" + locale.getCountry(),
705+
locale.getCountry(),
706+
locale.getLanguage() + "-001",
707+
"001"}) {
708+
var hour = regionToSkeletonMap.get(region);
709+
if (hour != null) {
710+
return hour;
711+
}
712+
}
713+
}
714+
715+
return "h";
706716
}
707717

708718
/**
@@ -714,8 +724,8 @@ private String resolveInputSkeleton(String type) {
714724
*/
715725
private String substituteInputSkeletons(String requestedTemplate) {
716726
var cCount = requestedTemplate.chars().filter(c -> c == 'C').count();
717-
return requestedTemplate.replaceAll("j", jPattern)
718-
.replaceFirst("C+", CPattern.replaceAll("([hkHK])", "$1".repeat((int)cCount)));
727+
return requestedTemplate.replaceAll("j", jPattern.get())
728+
.replaceFirst("C+", CPattern.get().replaceAll("([hkHK])", "$1".repeat((int)cCount)));
719729
}
720730

721731
/**
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8383525
27+
* @modules jdk.localedata
28+
* @summary Make sure that input skeleton init code is thread safe
29+
* @run junit SkeletonRaceTest
30+
*/
31+
32+
import java.time.chrono.IsoChronology;
33+
import java.time.format.DateTimeFormatterBuilder;
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.Locale;
37+
import java.util.concurrent.CountDownLatch;
38+
import java.util.concurrent.ExecutionException;
39+
import java.util.concurrent.ExecutorService;
40+
import java.util.concurrent.Executors;
41+
import java.util.concurrent.Future;
42+
43+
import org.junit.jupiter.api.Test;
44+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
45+
46+
public class SkeletonRaceTest {
47+
@Test
48+
void testSkeletonRace() {
49+
// Without the fix, LocaleResources throws an NPE
50+
for (int run = 0; run < 10; run++) {
51+
assertDoesNotThrow(this::doRaceTest);
52+
}
53+
}
54+
55+
private void doRaceTest() throws InterruptedException, ExecutionException {
56+
Locale[] locales = Locale.getAvailableLocales();
57+
int threads = 50;
58+
59+
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
60+
CountDownLatch ready = new CountDownLatch(threads);
61+
CountDownLatch go = new CountDownLatch(1);
62+
List<Future<?>> futures = new ArrayList<>();
63+
64+
for (int i = 0; i < threads; i++) {
65+
Locale locale = locales[i % locales.length];
66+
futures.add(pool.submit(() -> {
67+
ready.countDown();
68+
go.await();
69+
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
70+
"yMd", IsoChronology.INSTANCE, locale);
71+
return null;
72+
}));
73+
}
74+
75+
ready.await();
76+
go.countDown();
77+
for (Future<?> f : futures) f.get();
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)