8309622: Re-examine the cache mechanism in BaseLocale

Reviewed-by: dfuchs, rriggs
This commit is contained in:
Naoto Sato 2024-03-04 18:40:50 +00:00
parent 6f8d351e86
commit f615ac4bdf
4 changed files with 175 additions and 399 deletions
src/java.base/share/classes

@ -1,5 +1,5 @@
/*
* Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1996, 2024, 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
@ -51,6 +51,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.spi.LocaleNameProvider;
import java.util.stream.Stream;
import jdk.internal.util.ReferencedKeyMap;
import jdk.internal.util.StaticProperty;
import jdk.internal.vm.annotation.Stable;
@ -60,7 +61,6 @@ import sun.util.locale.InternalLocaleBuilder;
import sun.util.locale.LanguageTag;
import sun.util.locale.LocaleExtensions;
import sun.util.locale.LocaleMatcher;
import sun.util.locale.LocaleObjectCache;
import sun.util.locale.LocaleSyntaxException;
import sun.util.locale.LocaleUtils;
import sun.util.locale.ParseStatus;
@ -987,29 +987,20 @@ public final class Locale implements Cloneable, Serializable {
if (locale != null) {
return locale;
}
return Cache.LOCALECACHE.get(baseloc);
return LOCALE_CACHE.computeIfAbsent(baseloc, Locale::createLocale);
} else {
LocaleKey key = new LocaleKey(baseloc, extensions);
return Cache.LOCALECACHE.get(key);
return LOCALE_CACHE.computeIfAbsent(key, Locale::createLocale);
}
}
private static class Cache extends LocaleObjectCache<Object, Locale> {
private static final Cache LOCALECACHE = new Cache();
private Cache() {
}
@Override
protected Locale createObject(Object key) {
if (key instanceof BaseLocale) {
return new Locale((BaseLocale)key, null);
} else {
LocaleKey lk = (LocaleKey)key;
return new Locale(lk.base, lk.exts);
}
}
private static final ReferencedKeyMap<Object, Locale> LOCALE_CACHE = ReferencedKeyMap.create(true, ConcurrentHashMap::new);
private static Locale createLocale(Object key) {
return switch (key) {
case BaseLocale base -> new Locale(base, null);
case LocaleKey lk -> new Locale(lk.base, lk.exts);
default -> throw new InternalError("should not happen");
};
}
private static final class LocaleKey {

@ -69,9 +69,9 @@ import jdk.internal.access.JavaUtilResourceBundleAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.reflect.CallerSensitive;
import jdk.internal.reflect.Reflection;
import jdk.internal.util.ReferencedKeyMap;
import sun.security.action.GetPropertyAction;
import sun.util.locale.BaseLocale;
import sun.util.locale.LocaleObjectCache;
import sun.util.resources.Bundles;
import static sun.security.util.SecurityConstants.GET_CLASSLOADER_PERMISSION;
@ -2867,123 +2867,122 @@ public abstract class ResourceBundle {
if (baseName == null) {
throw new NullPointerException();
}
return new ArrayList<>(CANDIDATES_CACHE.get(locale.getBaseLocale()));
return new ArrayList<>(CANDIDATES_CACHE.computeIfAbsent(locale.getBaseLocale(),
Control::createCandidateList));
}
private static final CandidateListCache CANDIDATES_CACHE = new CandidateListCache();
private static final ReferencedKeyMap<BaseLocale, List<Locale>> CANDIDATES_CACHE = ReferencedKeyMap.create(true, ConcurrentHashMap::new);
private static class CandidateListCache extends LocaleObjectCache<BaseLocale, List<Locale>> {
protected List<Locale> createObject(BaseLocale base) {
String language = base.getLanguage();
String script = base.getScript();
String region = base.getRegion();
String variant = base.getVariant();
private static List<Locale> createCandidateList(BaseLocale base) {
String language = base.getLanguage();
String script = base.getScript();
String region = base.getRegion();
String variant = base.getVariant();
// Special handling for Norwegian
boolean isNorwegianBokmal = false;
boolean isNorwegianNynorsk = false;
if (language.equals("no")) {
if (region.equals("NO") && variant.equals("NY")) {
variant = "";
isNorwegianNynorsk = true;
} else {
isNorwegianBokmal = true;
// Special handling for Norwegian
boolean isNorwegianBokmal = false;
boolean isNorwegianNynorsk = false;
if (language.equals("no")) {
if (region.equals("NO") && variant.equals("NY")) {
variant = "";
isNorwegianNynorsk = true;
} else {
isNorwegianBokmal = true;
}
}
if (language.equals("nb") || isNorwegianBokmal) {
List<Locale> tmpList = getDefaultList("nb", script, region, variant);
// Insert a locale replacing "nb" with "no" for every list entry with precedence
List<Locale> bokmalList = new ArrayList<>();
for (Locale l_nb : tmpList) {
var isRoot = l_nb.getLanguage().isEmpty();
var l_no = Locale.getInstance(isRoot ? "" : "no",
l_nb.getScript(), l_nb.getCountry(), l_nb.getVariant(), null);
bokmalList.add(isNorwegianBokmal ? l_no : l_nb);
if (isRoot) {
break;
}
bokmalList.add(isNorwegianBokmal ? l_nb : l_no);
}
return bokmalList;
} else if (language.equals("nn") || isNorwegianNynorsk) {
// Insert no_NO_NY, no_NO, no after nn
List<Locale> nynorskList = getDefaultList("nn", script, region, variant);
int idx = nynorskList.size() - 1;
nynorskList.add(idx++, Locale.getInstance("no", "NO", "NY"));
nynorskList.add(idx++, Locale.getInstance("no", "NO", ""));
nynorskList.add(idx++, Locale.getInstance("no", "", ""));
return nynorskList;
}
// Special handling for Chinese
else if (language.equals("zh")) {
if (script.isEmpty() && !region.isEmpty()) {
// Supply script for users who want to use zh_Hans/zh_Hant
// as bundle names (recommended for Java7+)
switch (region) {
case "TW", "HK", "MO" -> script = "Hant";
case "CN", "SG" -> script = "Hans";
}
}
if (language.equals("nb") || isNorwegianBokmal) {
List<Locale> tmpList = getDefaultList("nb", script, region, variant);
// Insert a locale replacing "nb" with "no" for every list entry with precedence
List<Locale> bokmalList = new ArrayList<>();
for (Locale l_nb : tmpList) {
var isRoot = l_nb.getLanguage().isEmpty();
var l_no = Locale.getInstance(isRoot ? "" : "no",
l_nb.getScript(), l_nb.getCountry(), l_nb.getVariant(), null);
bokmalList.add(isNorwegianBokmal ? l_no : l_nb);
if (isRoot) {
break;
}
bokmalList.add(isNorwegianBokmal ? l_nb : l_no);
}
return bokmalList;
} else if (language.equals("nn") || isNorwegianNynorsk) {
// Insert no_NO_NY, no_NO, no after nn
List<Locale> nynorskList = getDefaultList("nn", script, region, variant);
int idx = nynorskList.size() - 1;
nynorskList.add(idx++, Locale.getInstance("no", "NO", "NY"));
nynorskList.add(idx++, Locale.getInstance("no", "NO", ""));
nynorskList.add(idx++, Locale.getInstance("no", "", ""));
return nynorskList;
}
// Special handling for Chinese
else if (language.equals("zh")) {
if (script.isEmpty() && !region.isEmpty()) {
// Supply script for users who want to use zh_Hans/zh_Hant
// as bundle names (recommended for Java7+)
switch (region) {
case "TW", "HK", "MO" -> script = "Hant";
case "CN", "SG" -> script = "Hans";
}
}
}
return getDefaultList(language, script, region, variant);
}
private static List<Locale> getDefaultList(String language, String script, String region, String variant) {
List<String> variants = null;
return getDefaultList(language, script, region, variant);
}
if (!variant.isEmpty()) {
variants = new ArrayList<>();
int idx = variant.length();
while (idx != -1) {
variants.add(variant.substring(0, idx));
idx = variant.lastIndexOf('_', --idx);
private static List<Locale> getDefaultList(String language, String script, String region, String variant) {
List<String> variants = null;
if (!variant.isEmpty()) {
variants = new ArrayList<>();
int idx = variant.length();
while (idx != -1) {
variants.add(variant.substring(0, idx));
idx = variant.lastIndexOf('_', --idx);
}
}
List<Locale> list = new ArrayList<>();
if (variants != null) {
for (String v : variants) {
list.add(Locale.getInstance(language, script, region, v, null));
}
}
if (!region.isEmpty()) {
list.add(Locale.getInstance(language, script, region, "", null));
}
if (!script.isEmpty()) {
list.add(Locale.getInstance(language, script, "", "", null));
// Special handling for Chinese
if (language.equals("zh")) {
if (region.isEmpty()) {
// Supply region(country) for users who still package Chinese
// bundles using old convention.
switch (script) {
case "Hans" -> region = "CN";
case "Hant" -> region = "TW";
}
}
}
List<Locale> list = new ArrayList<>();
// With script, after truncating variant, region and script,
// start over without script.
if (variants != null) {
for (String v : variants) {
list.add(Locale.getInstance(language, script, region, v, null));
list.add(Locale.getInstance(language, "", region, v, null));
}
}
if (!region.isEmpty()) {
list.add(Locale.getInstance(language, script, region, "", null));
list.add(Locale.getInstance(language, "", region, "", null));
}
if (!script.isEmpty()) {
list.add(Locale.getInstance(language, script, "", "", null));
// Special handling for Chinese
if (language.equals("zh")) {
if (region.isEmpty()) {
// Supply region(country) for users who still package Chinese
// bundles using old convention.
switch (script) {
case "Hans" -> region = "CN";
case "Hant" -> region = "TW";
}
}
}
// With script, after truncating variant, region and script,
// start over without script.
if (variants != null) {
for (String v : variants) {
list.add(Locale.getInstance(language, "", region, v, null));
}
}
if (!region.isEmpty()) {
list.add(Locale.getInstance(language, "", region, "", null));
}
}
if (!language.isEmpty()) {
list.add(Locale.getInstance(language, "", "", "", null));
}
// Add root locale at the end
list.add(Locale.ROOT);
return list;
}
if (!language.isEmpty()) {
list.add(Locale.getInstance(language, "", "", "", null));
}
// Add root locale at the end
list.add(Locale.ROOT);
return list;
}
/**

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2024, 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
@ -33,34 +33,35 @@
package sun.util.locale;
import jdk.internal.misc.CDS;
import jdk.internal.util.ReferencedKeySet;
import jdk.internal.util.StaticProperty;
import jdk.internal.vm.annotation.Stable;
import java.lang.ref.SoftReference;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
public final class BaseLocale {
public static @Stable BaseLocale[] constantBaseLocales;
public static final byte ENGLISH = 0,
FRENCH = 1,
GERMAN = 2,
ITALIAN = 3,
JAPANESE = 4,
KOREAN = 5,
CHINESE = 6,
SIMPLIFIED_CHINESE = 7,
TRADITIONAL_CHINESE = 8,
FRANCE = 9,
GERMANY = 10,
ITALY = 11,
JAPAN = 12,
KOREA = 13,
UK = 14,
US = 15,
CANADA = 16,
CANADA_FRENCH = 17,
ROOT = 18,
public static final byte ROOT = 0,
ENGLISH = 1,
US = 2,
FRENCH = 3,
GERMAN = 4,
ITALIAN = 5,
JAPANESE = 6,
KOREAN = 7,
CHINESE = 8,
SIMPLIFIED_CHINESE = 9,
TRADITIONAL_CHINESE = 10,
FRANCE = 11,
GERMANY = 12,
ITALY = 13,
JAPAN = 14,
KOREA = 15,
UK = 16,
CANADA = 17,
CANADA_FRENCH = 18,
NUM_CONSTANTS = 19;
static {
CDS.initializeFromArchive(BaseLocale.class);
@ -90,6 +91,10 @@ public final class BaseLocale {
}
}
// Interned BaseLocale cache
private static final ReferencedKeySet<BaseLocale> CACHE =
ReferencedKeySet.create(true, ConcurrentHashMap::new);
public static final String SEP = "_";
private final String language;
@ -107,27 +112,17 @@ public final class BaseLocale {
private static final boolean OLD_ISO_CODES = StaticProperty.javaLocaleUseOldISOCodes()
.equalsIgnoreCase("true");
// This method must be called with normalize = false only when creating the
// Locale.* constants and non-normalized BaseLocale$Keys used for lookup.
private BaseLocale(String language, String script, String region, String variant,
boolean normalize) {
if (normalize) {
this.language = LocaleUtils.toLowerString(language).intern();
this.script = LocaleUtils.toTitleString(script).intern();
this.region = LocaleUtils.toUpperString(region).intern();
this.variant = variant.intern();
} else {
this.language = language;
this.script = script;
this.region = region;
this.variant = variant;
}
private BaseLocale(String language, String script, String region, String variant) {
this.language = language;
this.script = script;
this.region = region;
this.variant = variant;
}
// Called for creating the Locale.* constants. No argument
// validation is performed.
private static BaseLocale createInstance(String language, String region) {
return new BaseLocale(language, "", region, "", false);
return new BaseLocale(language, "", region, "");
}
public static BaseLocale getInstance(String language, String script,
@ -153,8 +148,8 @@ public final class BaseLocale {
// Check for constant base locales first
if (script.isEmpty() && variant.isEmpty()) {
for (BaseLocale baseLocale : constantBaseLocales) {
if (baseLocale.getLanguage().equals(language)
&& baseLocale.getRegion().equals(region)) {
if (baseLocale.language.equals(language)
&& baseLocale.region.equals(region)) {
return baseLocale;
}
}
@ -165,8 +160,15 @@ public final class BaseLocale {
language = convertOldISOCodes(language);
}
Key key = new Key(language, script, region, variant, false);
return Cache.CACHE.get(key);
// Obtain the "interned" BaseLocale from the cache. The returned
// "interned" instance can subsequently be used by the Locale
// instance which guarantees the locale components are properly cased/interned.
return CACHE.intern(new BaseLocale(language, script, region, variant),
(b) -> new BaseLocale(
LocaleUtils.toLowerString(b.language).intern(),
LocaleUtils.toTitleString(b.script).intern(),
LocaleUtils.toUpperString(b.region).intern(),
b.variant.intern()));
}
public static String convertOldISOCodes(String language) {
@ -199,14 +201,14 @@ public final class BaseLocale {
if (this == obj) {
return true;
}
if (!(obj instanceof BaseLocale)) {
return false;
if (obj instanceof BaseLocale other) {
return LocaleUtils.caseIgnoreMatch(other.language, language)
&& LocaleUtils.caseIgnoreMatch(other.region, region)
&& LocaleUtils.caseIgnoreMatch(other.script, script)
// variant is case sensitive in JDK!
&& other.variant.equals(variant);
}
BaseLocale other = (BaseLocale)obj;
return language == other.language
&& script == other.script
&& region == other.region
&& variant == other.variant;
return false;
}
@Override
@ -231,128 +233,26 @@ public final class BaseLocale {
public int hashCode() {
int h = hash;
if (h == 0) {
// Generating a hash value from language, script, region and variant
h = language.hashCode();
h = 31 * h + script.hashCode();
h = 31 * h + region.hashCode();
h = 31 * h + variant.hashCode();
int len = language.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(language.charAt(i));
}
len = script.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(script.charAt(i));
}
len = region.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(region.charAt(i));
}
len = variant.length();
for (int i = 0; i < len; i++) {
h = 31*h + variant.charAt(i);
}
if (h != 0) {
hash = h;
}
}
return h;
}
private static final class Key {
/**
* Keep a SoftReference to the Key data if normalized (actually used
* as a cache key) and not initialized via the constant creation path.
*
* This allows us to avoid creating SoftReferences on lookup Keys
* (which are short-lived) and for Locales created via
* Locale#createConstant.
*/
private final SoftReference<BaseLocale> holderRef;
private final BaseLocale holder;
private final boolean normalized;
private final int hash;
private Key(String language, String script, String region,
String variant, boolean normalize) {
BaseLocale locale = new BaseLocale(language, script, region, variant, normalize);
this.normalized = normalize;
if (normalized) {
this.holderRef = new SoftReference<>(locale);
this.holder = null;
} else {
this.holderRef = null;
this.holder = locale;
}
this.hash = hashCode(locale);
}
public int hashCode() {
return hash;
}
private int hashCode(BaseLocale locale) {
int h = 0;
String lang = locale.getLanguage();
int len = lang.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(lang.charAt(i));
}
String scrt = locale.getScript();
len = scrt.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(scrt.charAt(i));
}
String regn = locale.getRegion();
len = regn.length();
for (int i = 0; i < len; i++) {
h = 31*h + LocaleUtils.toLower(regn.charAt(i));
}
String vart = locale.getVariant();
len = vart.length();
for (int i = 0; i < len; i++) {
h = 31*h + vart.charAt(i);
}
return h;
}
private BaseLocale getBaseLocale() {
return (holder == null) ? holderRef.get() : holder;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Key && this.hash == ((Key)obj).hash) {
BaseLocale other = ((Key) obj).getBaseLocale();
BaseLocale locale = this.getBaseLocale();
if (other != null && locale != null
&& LocaleUtils.caseIgnoreMatch(other.getLanguage(), locale.getLanguage())
&& LocaleUtils.caseIgnoreMatch(other.getScript(), locale.getScript())
&& LocaleUtils.caseIgnoreMatch(other.getRegion(), locale.getRegion())
// variant is case sensitive in JDK!
&& other.getVariant().equals(locale.getVariant())) {
return true;
}
}
return false;
}
public static Key normalize(Key key) {
if (key.normalized) {
return key;
}
// Only normalized keys may be softly referencing the data holder
assert (key.holder != null && key.holderRef == null);
BaseLocale locale = key.holder;
return new Key(locale.getLanguage(), locale.getScript(),
locale.getRegion(), locale.getVariant(), true);
}
}
private static class Cache extends LocaleObjectCache<Key, BaseLocale> {
private static final Cache CACHE = new Cache();
public Cache() {
}
@Override
protected Key normalizeKey(Key key) {
return Key.normalize(key);
}
@Override
protected BaseLocale createObject(Key key) {
return Key.normalize(key).getBaseLocale();
}
}
}

@ -1,114 +0,0 @@
/*
* Copyright (c) 2010, 2023, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
/*
*******************************************************************************
* Copyright (C) 2009-2010, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*/
package sun.util.locale;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public abstract class LocaleObjectCache<K, V> {
private final ConcurrentMap<K, CacheEntry<K, V>> map;
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
public LocaleObjectCache() {
this(16, 0.75f, 16);
}
public LocaleObjectCache(int initialCapacity, float loadFactor, int concurrencyLevel) {
map = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel);
}
public V get(K key) {
V value = null;
cleanStaleEntries();
CacheEntry<K, V> entry = map.get(key);
if (entry != null) {
value = entry.get();
}
if (value == null) {
key = normalizeKey(key);
V newVal = createObject(key);
if (key == null || newVal == null) {
// subclass must return non-null key/value object
return null;
}
CacheEntry<K, V> newEntry = new CacheEntry<>(key, newVal, queue);
entry = map.putIfAbsent(key, newEntry);
if (entry == null) {
value = newVal;
} else {
value = entry.get();
if (value == null) {
map.put(key, newEntry);
value = newVal;
}
}
}
return value;
}
protected V put(K key, V value) {
CacheEntry<K, V> entry = new CacheEntry<>(key, value, queue);
CacheEntry<K, V> oldEntry = map.put(key, entry);
return (oldEntry == null) ? null : oldEntry.get();
}
@SuppressWarnings("unchecked")
private void cleanStaleEntries() {
CacheEntry<K, V> entry;
while ((entry = (CacheEntry<K, V>)queue.poll()) != null) {
map.remove(entry.getKey(), entry);
}
}
protected abstract V createObject(K key);
protected K normalizeKey(K key) {
return key;
}
private static class CacheEntry<K, V> extends SoftReference<V> {
private final K key;
CacheEntry(K key, V value, ReferenceQueue<V> queue) {
super(value, queue);
this.key = key;
}
K getKey() {
return key;
}
}
}