Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4699885
ICU-23321 DateTimePatternGenerator: cloneAsThawed() should not mutate…
jabagawee Mar 21, 2026
b2a6cc9
ICU-23321 RelativeDateTimeFormatter: add missing synchronization in f…
jabagawee Mar 21, 2026
726a3c9
ICU-23321 StandardPluralRanges: replace broken DCL with holder pattern
jabagawee Mar 21, 2026
4ea33ab
ICU-23321 CollationTailoring: ensure maxExpansions visibility across …
jabagawee Mar 21, 2026
11f0839
ICU-23321 Transliterators: ensure safe publication of shared fields
jabagawee Mar 21, 2026
ba83971
ICU-23321 SimpleCache: make type and capacity fields final
jabagawee Mar 21, 2026
6f57fa8
ICU-23321 Add SimpleCache concurrency tests
jabagawee Mar 21, 2026
f63e531
ICU-23321 MeasureUnit: eliminate class-level monitor contention
jabagawee Mar 21, 2026
282c356
ICU-23321 Currency: eliminate class-level monitor contention
jabagawee Mar 21, 2026
b0223b3
ICU-23321 ULocale: eliminate class-level monitor on getDefault(Category)
jabagawee Mar 21, 2026
bd60753
ICU-23321 ULocale: replace synchronized loadAliasData() with holder p…
jabagawee Mar 21, 2026
b803993
ICU-23321 TimeZone: ensure TZ_IMPL visibility and add private lock ob…
jabagawee Mar 21, 2026
b62618f
ICU-23321 Replace synchronized caches with ConcurrentHashMap (7 files)
jabagawee Mar 21, 2026
c211c37
ICU-23321 Volatile DCL and holder pattern for one-time init (7 files)
jabagawee Mar 21, 2026
6ee2590
ICU-23321 AtomicReferenceArray for indexed lazy initialization (2 files)
jabagawee Mar 21, 2026
0f6437b
ICU-23321 Fix thread-safety issues found by missing-synchronization a…
jabagawee Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,34 @@ class CharsetISCII extends CharsetICU {
private static final int NO_CHAR_MARKER = 0xfffe;

/* Used for proper conversion to and from Gurmukhi */
private static UnicodeSet PNJ_BINDI_TIPPI_SET;
private static UnicodeSet PNJ_CONSONANT_SET;
private static final class PNJSetsHolder {
static final UnicodeSet PNJ_BINDI_TIPPI_SET;
static final UnicodeSet PNJ_CONSONANT_SET;

static {
UnicodeSet consonant = new UnicodeSet();
consonant.add(0x0a15, 0x0a28);
consonant.add(0x0a2a, 0x0a30);
consonant.add(0x0a35, 0x0a36);
consonant.add(0x0a38, 0x0a39);

UnicodeSet bindiTippi = new UnicodeSet();
bindiTippi.addAll(consonant);
bindiTippi.add(0x0a05);
bindiTippi.add(0x0a07);
bindiTippi.add(0x0a41, 0x0a42);
bindiTippi.add(0x0a3f);

consonant.compact();
bindiTippi.compact();
consonant.freeze();
bindiTippi.freeze();

PNJ_CONSONANT_SET = consonant;
PNJ_BINDI_TIPPI_SET = bindiTippi;
}
}

private static final short PNJ_BINDI = 0x0a02;
private static final short PNJ_TIPPI = 0x0a70;
private static final short PNJ_SIGN_VIRAMA = 0x0a4d;
Expand Down Expand Up @@ -1285,32 +1311,6 @@ public CharsetISCII(String icuCanonicalName, String javaCanonicalName, String[]
int option = Integer.parseInt(icuCanonicalName.substring(14));

extraInfo = new UConverterDataISCII(option);

initializePNJSets();
}

/* Initialize the two UnicodeSets use for proper Gurmukhi conversion if they have not already been created. */
private void initializePNJSets() {
if (PNJ_BINDI_TIPPI_SET != null && PNJ_CONSONANT_SET != null) {
return;
}
PNJ_BINDI_TIPPI_SET = new UnicodeSet();
PNJ_CONSONANT_SET = new UnicodeSet();

PNJ_CONSONANT_SET.add(0x0a15, 0x0a28);
PNJ_CONSONANT_SET.add(0x0a2a, 0x0a30);
PNJ_CONSONANT_SET.add(0x0a35, 0x0a36);
PNJ_CONSONANT_SET.add(0x0a38, 0x0a39);

PNJ_BINDI_TIPPI_SET.addAll(PNJ_CONSONANT_SET);
PNJ_BINDI_TIPPI_SET.add(0x0a05);
PNJ_BINDI_TIPPI_SET.add(0x0a07);

PNJ_BINDI_TIPPI_SET.add(0x0a41, 0x0a42);
PNJ_BINDI_TIPPI_SET.add(0x0a3f);

PNJ_CONSONANT_SET.compact();
PNJ_BINDI_TIPPI_SET.compact();
}

/*
Expand Down Expand Up @@ -1691,7 +1691,8 @@ protected CoderResult decodeLoop(
/* Check to make sure that consonant clusters are handled correctly for Gurmukhi script. */
if (data.currentDeltaToUnicode == PNJ_DELTA
&& data.prevToUnicodeStatus != 0
&& PNJ_CONSONANT_SET.contains(data.prevToUnicodeStatus)
&& PNJSetsHolder.PNJ_CONSONANT_SET.contains(
data.prevToUnicodeStatus)
&& (this.toUnicodeStatus + PNJ_DELTA) == PNJ_SIGN_VIRAMA
&& (targetUniChar + PNJ_DELTA) == data.prevToUnicodeStatus) {
if (offsets != null) {
Expand Down Expand Up @@ -1730,7 +1731,7 @@ protected CoderResult decodeLoop(
*/
if (data.currentDeltaToUnicode == PNJ_DELTA
&& (targetUniChar + PNJ_DELTA) == PNJ_BINDI
&& PNJ_BINDI_TIPPI_SET.contains(
&& PNJSetsHolder.PNJ_BINDI_TIPPI_SET.contains(
this.toUnicodeStatus + PNJ_DELTA)) {
targetUniChar = PNJ_TIPPI - PNJ_DELTA;
cr =
Expand All @@ -1743,7 +1744,7 @@ protected CoderResult decodeLoop(
PNJ_DELTA);
} else if (data.currentDeltaToUnicode == PNJ_DELTA
&& (targetUniChar + PNJ_DELTA) == PNJ_SIGN_VIRAMA
&& PNJ_CONSONANT_SET.contains(
&& PNJSetsHolder.PNJ_CONSONANT_SET.contains(
this.toUnicodeStatus + PNJ_DELTA)) {
/* Store the current toUnicodeStatus code point for later handling of consonant cluster in Gurmukhi. */
data.prevToUnicodeStatus = this.toUnicodeStatus + PNJ_DELTA;
Expand Down Expand Up @@ -2041,7 +2042,7 @@ protected CoderResult encodeLoop(
} // end of switch
if (converterData.currentDeltaFromUnicode == PNJ_DELTA
&& tempContextFromUnicode == PNJ_ADHAK
&& PNJ_CONSONANT_SET.contains(sourceChar + PNJ_DELTA)) {
&& PNJSetsHolder.PNJ_CONSONANT_SET.contains(sourceChar + PNJ_DELTA)) {
/* If the previous codepoint is Adhak and the current codepoint is a consonant, the targetByteUnit should be C + Halant + C. */
/* reset context char */
converterData.contextCharFromUnicode = 0x0000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.charset.spi.CharsetProvider;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -25,11 +24,31 @@
*/
public final class CharsetProviderICU extends CharsetProvider {
/**
* List of available ICU Charsets, empty during static initialization. Not a Set or Map, so that
* we can add different Charset objects with the same name(), which means that they are
* .equals(). See ICU ticket #11493.
* Holds the list of available ICU Charsets, loaded once. Uses the holder class idiom (JLS
* 12.4.2) for thread-safe lazy initialization. Not a Set or Map, so that we can add different
* Charset objects with the same name(), which means that they are .equals(). See ICU ticket
* #11493.
*/
private static List<Charset> icuCharsets = Collections.<Charset>emptyList();
private static final class AvailableCharsetsHolder {
static final List<Charset> CHARSETS;

static {
List<Charset> icucs = new LinkedList<Charset>();
int num = UConverterAlias.countAvailable();
for (int i = 0; i < num; ++i) {
String name = UConverterAlias.getAvailableName(i);
try {
Charset cs = getCharset(name, "");
icucs.add(cs);
} catch (UnsupportedCharsetException ex) {
} catch (IOException e) {
}
// add only charsets that can be created!
}
// Unmodifiable so that charsets().next().remove() cannot change it.
CHARSETS = List.copyOf(icucs);
}
}

/**
* Default constructor
Expand Down Expand Up @@ -264,28 +283,8 @@ private static final String[] getAliases(String encName) throws IOException {
return (ret);
}

/**
* Lazy-init the icuCharsets list. Could be done during static initialization if constructing
* all of the Charsets were cheap enough. See ICU ticket #11481.
*/
private static final synchronized void loadAvailableICUCharsets() {
if (!icuCharsets.isEmpty()) {
return;
}
List<Charset> icucs = new LinkedList<Charset>();
int num = UConverterAlias.countAvailable();
for (int i = 0; i < num; ++i) {
String name = UConverterAlias.getAvailableName(i);
try {
Charset cs = getCharset(name, "");
icucs.add(cs);
} catch (UnsupportedCharsetException ex) {
} catch (IOException e) {
}
// add only charsets that can be created!
}
// Unmodifiable so that charsets().next().remove() cannot change it.
icuCharsets = Collections.unmodifiableList(icucs);
private static List<Charset> getAvailableICUCharsets() {
return AvailableCharsetsHolder.CHARSETS;
}

/**
Expand All @@ -297,8 +296,7 @@ private static final synchronized void loadAvailableICUCharsets() {
*/
@Override
public final Iterator<Charset> charsets() {
loadAvailableICUCharsets();
return icuCharsets.iterator();
return getAvailableICUCharsets().iterator();
}

/**
Expand All @@ -310,10 +308,10 @@ public final Iterator<Charset> charsets() {
*/
@Deprecated
public static final String[] getAvailableNames() {
loadAvailableICUCharsets();
String[] names = new String[icuCharsets.size()];
List<Charset> charsets = getAvailableICUCharsets();
String[] names = new String[charsets.size()];
int i = 0;
for (Charset cs : icuCharsets) {
for (Charset cs : charsets) {
names[i++] = cs.name();
}
return names;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private static final int strlen(byte[] sArray, int sBegin) {
* offsets[]
*/

static ByteBuffer gAliasData = null;
static volatile ByteBuffer gAliasData = null;

private static final boolean isAlias(String alias) {
if (alias == null) {
Expand All @@ -111,14 +111,16 @@ private static final boolean isAlias(String alias) {

private static final String CNVALIAS_DATA_FILE_NAME = "cnvalias.icu";

private static final synchronized boolean haveAliasData() throws IOException {
boolean needInit;

needInit = gAliasData == null;
private static boolean haveAliasData() throws IOException {
if (gAliasData != null) {
return true;
}
synchronized (UConverterAlias.class) {
if (gAliasData != null) {
return true;
}

/* load converter alias data from file if necessary */
if (needInit) {
ByteBuffer data = null;
/* load converter alias data from file if necessary */
int[] tableArray = null;
int tableStart;

Expand All @@ -142,17 +144,12 @@ private static final synchronized boolean haveAliasData() throws IOException {
gNormalizedStringTable = new byte[tableArray[normalizedStringTableIndex] * 2];
b.get(gNormalizedStringTable);

data = ByteBuffer.allocate(0); // dummy UDataMemory object in absence
// of memory mapping

if (gOptionTable[0] != STD_NORMALIZED) {
throw new IOException("Unsupported alias normalization");
}

if (gAliasData == null) {
gAliasData = data;
data = null;
}
// Write gAliasData last: it's the volatile flag read by the fast path
gAliasData = ByteBuffer.allocate(0);
}

return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ int getUCAVersion() {
CollationData ownedData;
Trie2_32 trie;
UnicodeSet unsafeBackwardSet;
public Map<Integer, Integer> maxExpansions;
public volatile Map<Integer, Integer> maxExpansions;

/*
* Not Cloneable: A CollationTailoring cannot be copied.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
import com.ibm.icu.text.RbnfLenientScannerProvider;
import com.ibm.icu.text.RuleBasedCollator;
import com.ibm.icu.util.ULocale;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Returns RbnfLenientScanners that use the old RuleBasedNumberFormat implementation behind
Expand All @@ -29,15 +28,15 @@
@Deprecated
public class RbnfScannerProviderImpl implements RbnfLenientScannerProvider {
private static final boolean DEBUG = ICUDebug.enabled("rbnf");
private Map<String, RbnfLenientScanner> cache;
private final ConcurrentHashMap<String, RbnfLenientScanner> cache;

/**
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
public RbnfScannerProviderImpl() {
cache = new HashMap<String, RbnfLenientScanner>();
cache = new ConcurrentHashMap<>();
}

/**
Expand Down Expand Up @@ -73,17 +72,16 @@ public RbnfScannerProviderImpl() {
public RbnfLenientScanner get(ULocale locale, String extras) {
RbnfLenientScanner result = null;
String key = locale.toString() + "/" + extras;
synchronized (cache) {
result = cache.get(key);
if (result != null) {
return result;
}
// Avoid computeIfAbsent: createScanner() constructs a RuleBasedCollator (expensive)
// and would hold the ConcurrentHashMap bin lock for the entire duration. Using
// get-then-putIfAbsent allows parallel construction with first-write-wins.
result = cache.get(key);
if (result != null) {
return result;
}
result = createScanner(locale, extras);
synchronized (cache) {
cache.put(key, result);
}
return result;
RbnfLenientScanner existing = cache.putIfAbsent(key, result);
return existing != null ? existing : result;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ public RuleBasedCollator clone() throws CloneNotSupportedException {
}

private final void initMaxExpansions() {
if (tailoring.maxExpansions != null) {
return;
}
synchronized (tailoring) {
if (tailoring.maxExpansions == null) {
tailoring.maxExpansions =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// © 2026 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.util;

import com.ibm.icu.text.CollationElementIterator;
import com.ibm.icu.text.Collator;
import com.ibm.icu.text.RuleBasedCollator;
import com.ibm.icu.util.ULocale;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Concurrency regression tests for CollationTailoring. */
@RunWith(JUnit4.class)
public class CollationConcurrencyTest extends ConcurrencyTest {

/** Concurrent getMaxExpansion() must see the volatile maxExpansions field. */
@Test
public void testCollationMaxExpansionsConcurrent() throws Exception {
String[] localeIDs = {"en", "de", "ja", "zh", "ko", "fr"};
runConcurrent(
"CollationMaxExpansions",
tid -> {
for (int i = 0; i < ITERATIONS; i++) {
String localeID = localeIDs[(tid + i) % localeIDs.length];
RuleBasedCollator col =
(RuleBasedCollator) Collator.getInstance(new ULocale(localeID));
assertNotNull("Collator should not be null for " + localeID, col);
// getCollationElementIterator triggers initMaxExpansions()
// which reads/writes the volatile maxExpansions field
CollationElementIterator cei =
col.getCollationElementIterator("test string");
int ce = cei.next();
assertTrue(
"should produce at least one collation element",
ce != CollationElementIterator.NULLORDER);
int maxExp = cei.getMaxExpansion(ce);
assertTrue("max expansion should be positive", maxExp > 0);
}
});
}
}
Loading
Loading