Одноэлементный шаблон очень распространен как в реальном мире, так и в мире кода программистов, а также является распространенным вопросом для разогрева на собеседованиях не только потому, что одноэлементный шаблон более важен в разработке кода бизнес-логики, но и из можно вывести одноэлементный шаблон, параллелизм, механизм блокировки и ряд других вопросов.Сегодня мы обсудим одноэлементный шаблон в шаблоне проектирования.
Шаблоны проектирования
Шаблоны проектирования впервые появились в строительной отрасли и начали появляться в индустрии разработки программного обеспечения в 1990-х годах. Можно сказать, что шаблоны проектирования — это набор эмпирических правил для разработки программного обеспечения, сформулированных старшими, которые десятилетиями сражались на передовой, и обобщение опыта проектирования кода. Его цель — улучшить возможность повторного использования кода, повысить его качество и надежность. (В некоторых материалах также говорится об улучшении читаемости кода. Бесспорно, что шаблон проектирования действительно может улучшить читаемость кода в некоторых сценариях, но автор считает, что в большинстве сценариев для минимизации связи между классами , Улучшение возможности повторного использования кода в определенной степени снижает читабельность кода.)
Мы знаем, что существуют некоторые важные принципы объектно-ориентированного проектирования в разработке и проектировании программного обеспечения, в основном следующие:
- Принцип единой ответственности: у класса должна быть одна и только одна причина для его изменения. С точки зрения непрофессионала, класс должен нести ответственность только за одно действие. Это также относится к методам. Каждый метод должен быть максимально понятным. Слишком много функций. реализованы в методе.
- Принцип «открыто-закрыто»: открыт для расширения, закрыт для модификации. Это может улучшить ремонтопригодность и возможность повторного использования системы. При изменении требований приложения исходные модули расширяются для соответствия новым требованиям.
- В принципе замены: принцип состоит в том, чтобы показать необходимость обеспечения того, чтобы унаследованные свойства, принадлежащие суперклассу, по-прежнему устанавливались в подклассе, проще говоря, родитель должен предоставить некоторый подкласс, родительский класс не является подклассом, который может быть расширен, подкласса вы можете расширить функциональность родительского класса, но не изменить функцию родительского класса.
- Принцип инверсии зависимостей: верхний уровень не должен зависеть от нижнего уровня, абстрактный не должен зависеть от конкретного, а конкретный должен зависеть от абстрактного, то есть программирование должно быть ориентировано на интерфейс, а не на конкретное программирование, которое может уменьшить связь между классами.
- По принципу изоляции интерфейса интерфейс должен быть маленьким и точным, а не большим и полным, и клиенту не нужно реализовывать все методы в интерфейсе, чтобы использовать некоторые функции в интерфейсе.
- Закон Деметры: принцип наименьшего знания, который означает, что для двух программных сущностей в мире программного обеспечения, если нет необходимости в общении, то нет необходимости напрямую звонить друг другу. С точки зрения зависимого объекта он должен зависеть только от необходимых зависимых объектов, а с точки зрения зависимого объекта раскрываются только те методы, которые должны быть раскрыты.
- Принцип композиции и повторного использования: композиция важнее наследования, а композиция предпочтительнее наследования для расширения функций классов. Выбирайте наследование, только если вам действительно нужно наследовать структуру.
Вышеуказанные принципы представляют собой методологии, обобщенные в области разработки программного обеспечения за последние несколько десятилетий, а шаблоны проектирования — это лучшие практики, обобщенные под руководством этих методологий.
В 1995 году четыре GoF в соавторстве написали 23 шаблона проектирования в знаменитой книге «Шаблоны проектирования: основы многократно используемого объектно-ориентированного программного обеспечения».Эти 23 шаблона проектирования можно условно разделить на три категории:
- Creational Design Patterns: этот класс в основном используется для описания того, как создаются объекты, и для разделения создания и использования объектов.
- Структурный шаблон проектирования: эта категория в основном предназначена для формирования более крупной структуры классов или объектов в соответствии с определенной структурой макета.
- Поведенческие шаблоны проектирования: эта категория в основном описывает взаимодействие между классами или объектами.
И что мы обсудим в этой статьеодноэлементный шаблон проектированияЭто один из творческих шаблонов проектирования.
одноэлементный шаблон проектирования
Шаблон проектирования singleton относится к шаблону, в котором класс singleton имеет только один экземпляр во всем процессе JVM с момента его создания до момента его уничтожения, и класс может создать этот экземпляр сам.
Предпосылки для реализации синглтона
Для реализации одноэлементного шаблона проектирования сначала необходимо обратить внимание на следующие три момента:
- Конструктор частный. Это самое основное, иначе внешний может создавать объекты по желанию через конструктор, тогда его нельзя назвать синглтоном.
- Внутренне содержит частный экземпляр статического одноэлементного шаблона.
- Предоставляет общедоступный статический метод для получения одноэлементного объекта.
Несколько реализаций одноэлементного шаблона
1. Голодный синглтон
выполнить
class Singleton1
{
private static Singleton1 instance=new Singleton1();
private Singleton1() {};
public static Singleton1 getInstance()
{
return instance;
}
}
Преимущества и недостатки
Преимущества этого метода реализации заключаются в простоте реализации, видно, что можно реализовать всего несколько строк кода, этот метод может хорошо работать в условиях многопоточности и обеспечивать потокобезопасность.
Недостатком является то, что класс singleton создается сразу после загрузки объекта, и отсутствует ленивая инициализация, которая может привести к трате ресурсов и медленному запуску при создании более ресурсоемкого экземпляра.
думать
Как голодный одноэлементный шаблон проектирования обеспечивает потокобезопасность?
Эту проблему нужно отнести к механизму загрузки классов java, который объясняется в конце статьи.Как создать статическую переменную для обеспечения потокобезопасности
2. Голодный китайский вариант
выполнить
class Singleton2
{
private static Singleton2 instance=null;
static
{
instance=new Singleton2();
}
private Singleton2(){};
public static Singleton2 getInstance()
{
return instance;
}
}
Этот метод почти ничем не отличается от приведенного выше голодного одноэлементного шаблона проектирования, и он также может обеспечить потокобезопасность. Что касается того, почему это может быть гарантировано, смотрите здесь.Как создать статическую переменную для обеспечения потокобезопасности
3. Ленивый одноэлементный дизайн
class Singleton3
{
private static Singleton3 instance=null;
private Singleton3(){};
public Singleton3 getInstance()
{
//1
if(instance==null)
{
//2
instance=new Singleton3();
}
return instance;
}
}
Преимущества и недостатки
Этот метод относительно прост в реализации, и объем кода относительно велик, и этот метод использует отложенную инициализацию, чтобы избежать пустой траты ресурсов.
Но этот метод хорошо работает только в одном потоке и не гарантирует одноэлементность при одновременном выполнении нескольких потоков.
думать
Почему этот метод не может гарантировать одноэлементность при одновременном выполнении нескольких потоков?
Взгляните на картинку ниже:
Когда поток A выполняет метод getInstance, он определяет, что экземпляр имеет значение null, входит в if и просто готовится создать объект. Это связано с тем, что процессор был ограблен потоком B. Это связано с тем, что поток B выполняет метод getInstance и оценивает что экземпляр также является нулевым. Затем поток A снова захватывает ЦП.Поскольку он был оценен ранее, он создаст одноэлементный объект, а также, когда поток B получит ЦП, он также создаст объект потока. Это создает два объекта.
4. Ленивый синглтон (потокобезопасность 1)
выполнить
class Singleton4
{
private static Singleton4 instance=null;
private Singleton4(){};
public synchronized Singleton4 getInstance()
{
if(instance==null)
{
instance=new Singleton4();
}
return instance;
}
}
Преимущества и недостатки
Этот метод решает проблему безопасности потоков, упомянутую выше, и не вносит слишком много сложности в кодирование, а также использует synchronized для обеспечения синхронизации методов.
Недостаток заключается в том, что производительность этого метода невелика при относительно высоком уровне параллелизма.Даже если был создан последующий одноэлементный объект, каждый раз при получении одноэлементного объекта он должен блокироваться и разблокироваться.
Пополнить
При постоянной оптимизации синхронизации в java, эскалации блокировок и т.п. потеря производительности синхронизации не такая уж и серьезная, но этот способ все же лаконичен, но не красив.
5. Ленивый синглтон (блокировка с двойной проверкой)
выполнить
class Singleton5
{
private static volatile Singleton5 instance=null;
private Singleton5(){};
public Singleton5 getInstance()
{
if(instance==null)
{
synchronized (Singleton5.class)
{
if(instance==null)
{
instance=new Singleton5();
}
}
}
return instance;
}
}
Преимущества и недостатки
Этот метод улучшен для четвертого метода записи, так как при создании объекта singleton его не нужно блокировать и синхронизировать, а необходимо лишь обеспечить потокобезопасность процесса создания объекта singleton. метод заключается в уточнении детализации блокировки и блокировке только того места, где блокировка заблокирована.
Недостатком является то, что увеличивается объем кода, требуются синхронизированные и изменчивые гарантии, и его трудно понять.
думать
1. Зачем нужны два if-оценки?После того, как оценка будет нулевой, вы можете войти в блок кода синхронизации и напрямую создать объект.Можно ли сделать это один раз? Вот еще одна картинка:
Поскольку проверка if уже была выполнена, когда несколько потоков одновременно выполняют синхронизированный блок, она не будет проверяться снова, когда поток получает блокировку, даже если другие потоки создали объект. Поэтому, когда поток получает блокировку, ему необходимо снова проверить, завершили ли другие потоки инициализацию объекта-одиночки в течение периода блокировки.
2. Зачем нужен volatile для модификации объекта singleton и можно ли его использовать без него?
Вот анализ этого кода
//
if(instance==null)
{
instance=new Singleton5();
}
Создание объектов JVM можно условно разделить на следующие три шага:
- Сначала найдите соответствие класса на основе полного имени класса и определите, был ли класс загружен, проверен, подготовлен и проанализирован.Если нет, выполните описанные выше шаги, а затем создайте для него соответствующий объект класса. .
- Убедившись, что класс загружен, в памяти кучи выделяется блок памяти и выполняется инициализация класса.
- Создайте ссылку в стеке, чтобы указать на этот фрагмент памяти, выделенный в памяти кучи.
Это имеет место при нормальных обстоятельствах, и тогда мы говорим, что пока есть нормальные условия, будут ненормальные условия.Ненормальные условия заключаются в том, что JVM переупорядочивает инструкции в соответствии с производительностью, потоком инструкций и т. д. для фактического выполнения . После переупорядочивания 3 может стоять перед 2, то есть может быть ситуация 1->3->2, поэтому в случае многопоточности в стеке потоков уже есть ссылка на этот экземпляр класса, т.е. считается, что объект был создан, тогда, когда приведенное выше суждение будет выполнено, он вернет false, чтобы напрямую вернуть экземпляр объекта, но экземпляр на самом деле не полностью инициализирован, что выставит экземпляр одноэлементного объекта, который не был был полностью инициализирован заранее для внешнего мира.Эта ситуация более опасна.
Так что использование volatile будет работать? Да, одной из функций volatile является предотвращение переупорядочения инструкций.Volatile предотвращает переупорядочение последующих инструкций вперед, вставляя барьер памяти, таким образом гарантируя, что jvm действительно выполняется в порядке 1-> 2-> 3., все будет хорошо.
6. Перечисление
выполнить
enum Singleton6
{
INSTANCE;
public void doSomething()
{
System.out.println("doSomething....");
}
//该方法可不需要,直接通过Singleton6.INSTANCE也可。
public static Singleton6 getInstance()
{
return INSTANCE;
}
}
Преимущества и недостатки
Способ перечисления для реализации singleton — это способ реализации, рекомендованный в «Эффективной Java», потому что он очень прост в реализации и может обеспечить безопасность потоков при многопоточности. В то же время, для некоторых средств, которые могут разрушить сериализацию, о которых мы поговорим позже, этот метод также может быть предотвращен.
Недостатки: Приходится искать недостатки.Хотя этот способ реализации выглядит идеально, практического применения не так много, то есть обсуждения более восторженные, но применение не получило широкого распространения.
думать
1. Как перечисление обеспечивает потокобезопасность?
Давайте декомпилируем приведенный выше код, чтобы увидеть, какие секреты стоят за перечислением.
Отсюда видно, что наш INSTANCE был изменен с помощью static final, так что при загрузке класса JVM гарантирует его потокобезопасность. Подробнее см. здесьКак создать статическую переменную для обеспечения потокобезопасности
2. Как защищена нумерация от уничтожения?
Первый спойлер, есть два способа уничтожить шаблон singleton: сериализация и отражение. Итак, давайте сначала посмотрим, как перечисление предотвращает разрушение одноэлементного шаблона сериализацией.
Давайте посмотрим, какие операции необходимы для обычной сериализации?
public static void main(String[] args) throws IOException, ClassNotFoundException
{
FileOutputStream fout = new FileOutputStream("Singleton.obj");
ObjectOutputStream out=new ObjectOutputStream(fout);
Singleton1 instance = Singleton1.getInstance();
out.writeObject(instance);
FileInputStream fin = new FileInputStream("Singleton.obj");
ObjectInputStream in = new ObjectInputStream(fin);
Singleton1 singleton1 = (Singleton1) in.readObject();
System.out.println(instance==singleton1);
}
Результат:
Поскольку это так, давайте проследим за подсказками и посмотрим, что есть у метода readObject() ObjectInputStream и метода writeObject() ObjectOutputStream?
public final void writeObject(Object obj) throws IOException
{
if (enableOverride)
{
writeObjectOverride(obj);
return;
}
try
{
writeObject0(obj, false);//这里是关键
}
catch (IOException ex)
{
if (depth == 0)
{
writeFatalException(ex);
}
throw ex;
}
}
writeObject0(obj, false) вызывается в writeObject, затем щелкнем и увидим этот метод
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try
{
//... ...
//省略了其他的代码。。
// remaining cases
if (obj instanceof String)
{
writeString((String) obj, unshared);
}
else if (cl.isArray())
{
writeArray(obj, desc, unshared);
}
else if (obj instanceof Enum)
{
writeEnum((Enum<?>) obj, desc, unshared);//这里是关键
}
else if (obj instanceof Serializable)
{
writeOrdinaryObject(obj, desc, unshared);
}
else
{
if (extendedDebugInfo)
{
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
}
else
{
throw new NotSerializableException(cl.getName());
}
}
}
finally
{
depth--;
bout.setBlockDataMode(oldMode);
}
}
Здесь мы опускаем много другого кода, оставляя только ключевые части, самая важная находится в строке 21, вы можете видеть, что механизм сериализации java реализует метод writeEnum((Enum> ) obj, desc, unshared);
private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
bout.writeByte(TC_ENUM);//这个是一个标志
ObjectStreamClass sdesc = desc.getSuperDesc();
writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
handles.assign(unshared ? null : en);
writeString(en.name(), false);
}
Отсюда видно, что сериализация не сериализует никакие поля при сериализации перечисления, она просто сериализует флаг (126) класса перечисления и его описание и имя класса перечисления. То же самое соответствует методу readEnum.
private Enum<?> readEnum(boolean unshared) throws IOException
{
if (bin.readByte() != TC_ENUM)
{
throw new InternalError();
}
//读出类描述信息+++++++++++++++++++++++++++++++++++++++++++
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum())
{
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null)
{
handles.markException(enumHandle, resolveEx);
}
//读出枚举名称+++++++++++++++++++++++++++++++++++++++++++
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null)
{
try
{
//根据名称找出枚举类+++++++++++++++++++++++++++++++++++++++++++
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex)
{
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared)
{
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
Здесь мы в основном смотрим на код, отмеченный знаком +.Вы можете видеть, что перечисление выполняется по его сериализованному имени при десериализации. Итак, это механизм java для обеспечения безопасности сериализации перечисления.
3. Перечисление является антирефлексивным?
Прежде всего, тип перечисления не предоставляет конструктора без параметров, в его унаследованном родительском классе Enum есть конструктор следующего вида:
protected Enum(String name, int ordinal)
{
this.name = name;
this.ordinal = ordinal;
}
Тогда некоторые студенты могут сказать, что создать экземпляр, вызвав конструктор через отражение, — это не то же самое? это. . . Мы также точно знаем, что перечисления не позволяют отражению создавать объекты. Затем давайте подумаем о методе newInstance(), который в конечном итоге будет вызываться, если объект создается путем отражения, так что давайте посмотрим, что делает этот метод?
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
//省略若干代码。。。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
//省略若然代码。。。
return inst;
}
Вы можете видеть, что перечисление проверяется отдельно в методе newInstance.Если вы настаиваете на рефлексивном создании, вы можете получить только подарочный пакет IllegalArgumentException. Мы обсудили связанные вопросы перечисления здесь.
7. Статический внутренний класс
выполнить
class Singleton7
{
private static class SingletonHolder
{
private static final Singleton7 instance=new Singleton7();
}
private Singleton7(){};
public static Singleton7 getInstance()
{
return SingletonHolder.instance;
}
}
Преимущества и недостатки
Этот метод является элегантным способом реализации одноэлементного шаблона.Он разумно использует функцию безопасности потоков загрузчика классов для обеспечения безопасности потоков при инициализации объекта-одиночки.Как создать статическую переменную для обеспечения потокобезопасности, а статические внутренние классы загружаются только при первом использовании, поэтому возможна ленивая инициализация. И кодирование относительно простое.
Недостатки, я не знаю недостатков этого.Если вы знаете, вы можете сказать мне в области комментариев, но если вам нужно сказать это, это то, что необходимы дополнительные усилия, чтобы сериализация и отражение не разрушали одноэлементный режим .
8. Синглтон без блокировки CAS
выполнить
class Singleton8
{
private static final AtomicReference<Singleton8> cas=new AtomicReference<>();
private Singleton8(){};
public static Singleton8 getInstance()
{
for(;;)
{
Singleton8 instance = cas.get();
if(instance==null)
{
boolean b = cas.compareAndSet(null, new Singleton8());//++++++++++++++++++++++++++++
if(b)
{
break;
}
}
else
{
break;
}
}
return cas.get();
}
}
Преимущества и недостатки
Таким образом, AtomicReference в пакете Atomic использует cas для создания объектов, что позволяет избежать использования блокирующих блокировок и повысить производительность и параллелизм в некоторых сценариях.
Этот метод имеет много недостатков.Во-первых, если несколько потоков выполняют строку кода + одновременно, только один поток может выполнить cas, но несколько потоков могут создать новые объекты Singleton8(), но замена не будет успешной. , поэтому может привести к потере памяти. В то же время, если CAS не сработает, ресурсы ЦП будут потрачены впустую, а пропускная способность системы снизится.
9. Контейнер-синглтон
выполнить
class Singleton9
{
private static ConcurrentHashMap<String,Singleton9> map=new ConcurrentHashMap<>();
private Singleton9(){};
public Singleton9 getInstance()
{
if(!map.contains("singleton"))
{
map.putIfAbsent("singleton",new Singleton9());
}
return map.get("singleton");
}
}
Преимущества и недостатки
Таким образом, класс контейнера используется для хранения одноэлементных объектов, а ConcurrentHashMap может обеспечить безопасность потоков.Если вам не нужна безопасность потоков, вы можете использовать HashMap.
Но этот недостаток также очевиден: для создания объекта-синглета необходимо поддерживать дополнительный объект-контейнер. Это удобно, когда имеется большое количество одноэлементных объектов, которыми необходимо управлять единообразно.
существующие проблемы
Как упоминалось ранее, помимо перечисления, вышеописанные одноэлементные шаблоны проектирования имеют две общие проблемы, то есть одноэлементный шаблон легко разрушается. Есть два способа сломать шаблон singleton:
- Сериализация нарушает одноэлементный шаблон
- Отражение разрушает одноэлементный шаблон.
Предотвращение отражения и сериализации
Сначала давайте посмотрим, как другие типы должны препятствовать созданию объектов отражением, за исключением одноэлементного шаблона, реализованного перечислением. Причина, по которой отражение может нарушить сериализацию, заключается в том, что отражение обращается к частному конструктору, который вызывает метод newInstance() частного конструктора для создания объекта. В это время мы можем добавить флаг флага переменной-члена внутри его класса. чтобы конструктор не вызывался во второй раз.
private Singleton1()
{
if(flag)
{
throw new RuntimeException("不可以通过反射调用哦!");
}
flag=true;
}
Для механизма предотвращения сериализации «Эффективная Java» говорит нам, что нам нужно только реализовать метод readResolve, например этот
public Singleton1 readResolve()
{
return instance;
}
Почему это? Мы по-прежнему смотрим на readObject->readObject0->checkResolve(readOrdinaryObject(unshared))->readOrdinaryObject из readObject ObjectInputStream
Причина находится в этом методе:
Если объект реализует метод readResolve, вызывается метод readResolve объекта.
Синглтон весной
Шаблон одноэлементного проектирования, который мы реализовали выше, рассматривается с точки зрения JVM: от создания до уничтожения процесса JVM наш одноэлементный класс поддерживает только один объект-экземпляр. Синглтон Spring рассматривается с точки зрения контейнера.От создания до уничтожения контейнера Spring гарантирует, что в контейнере поддерживается только один экземпляр объекта класса синглтона. Нижний слой контейнера Spring реализует режим singleton через вышеупомянутый девятый, то есть контейнер singleton, Это очень понятно, ведь Spring нужно управлять большим количеством объектов singleton, а управлять этими объектами нужно централизованно и унифицированный способ, поэтому реализация контейнера - лучший способ. В то же время Spring использует HashMap в качестве базового контейнера для хранения одноэлементных объектов и получает объекты в соответствии с идентификатором объекта (называемым bean-компонентом в Spring), что означает, что синглтон Spring не является потокобезопасным, и до тех пор, пока Идентификатор bean-компонента несовместим, Spring считается разными объектами.
Суммировать
Сказав так много одноэлементных шаблонов проектирования, какой из них мы обычно используем? Автор считает, что жадный одноэлементный шаблон проектирования и его варианты, статический шаблон внутреннего класса и метод перечисления — все это хороший выбор. Ввиду того, что метод перебора в настоящее время обсуждается более бурно, но широко не используется, в общем, какой из них выбрать, следует все же выбрать наиболее подходящий метод реализации согласно конкретному сценарию.
Как создать статическую переменную для обеспечения потокобезопасности
Потокобезопасность, создаваемая статическими переменными, гарантируется для нас jvm, который должен начинаться с процесса загрузки классов jvm.Мы знаем, что классы в java создаются загрузчиком классов из определенного места (жесткий диск, сеть и т. д.). ) Загружается в память jvm. Процесс от загрузки класса в память до создания объекта обычно делится на загрузку, проверку, точность, синтаксический анализ и инициализацию. На этапе подготовки вызывается метод конструктора класса для выполнения операций инициализации статических переменных и статических полей. Попутно мы смотрим на ClassLoader, что и делает загрузчик классов.
Загрузчик класса загружает файл байт-кода класса в указанное место в память с помощью своего метода loadClass и создает соответствующий объект класса в качестве записи исходных данных класса в области методов. Итак, давайте посмотрим, что делает метод loadClass? ,
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name))
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
//...省略了一系列类加载的代码。
return c;
}
}
Видно, что loadClass использует synchronized для обеспечения потокобезопасности процесса загрузки, а инициализация статических переменных происходит в процессе загрузки классов загрузчиком классов, поэтому JVM может гарантировать, что инициализация статических переменных и статических полей является потоковой. -безопасно.