Если несколько потоков собираются совместно использовать экземпляр, вы должны использовать некоторые виды синхронизации.
Синхронизация необходима в основном по двум причинам:
- Это может гарантировать, что некоторые операции атомарные , поэтому система будет поддерживать согласованность
- Это гарантирует, что каждый поток видит одинаковые значения в памяти
Прежде всего, поскольку вы сделали reloadConfig()
общедоступным, ваш объект на самом деле не выглядит неизменным.Если объект действительно неизменный, то есть если после инициализации его значений они не могут измениться (что является желаемым свойством , которое должно быть в объектах, которые являются общими).
По вышеуказанной причине, вы должны синхронизировать весь доступ к карте: предположим, что поток пытается прочитать с него, в то время как другой поток вызывает reloadConfig()
.Произойдут плохие вещи.
Если это действительно так (изменяемые настройки), вы должны синхронизироваться как при чтении, так и при записи (по очевидным причинам). Потоки должны синхронизироваться на одном объекте (в противном случае синхронизация отсутствует).Единственный способ гарантировать, что все потоки будут синхронизироваться на одном и том же объекте, - это синхронизация на самом объекте или в правильно опубликованной, совместно используемой блокировке, например:
// synchronizes on the in instance itself:
class MyDBConfig1 {
// ...
public synchronized String getProperty(...) { ... }
public synchronized reloadConfig() { ... }
}
// synchronizes on a properly published, shared lock:
class MyDBConfig2 {
private final Object lock = new Object();
public String getProperty(...) { synchronized(lock) { ... } }
public reloadConfig() { synchronized(lock) { ... } }
}
Правильная публикация здесь гарантировано ключевым словом final .Это незаметно: оно гарантирует, что значение этого поля будет видно каждому потоку после инициализации (без него поток может увидеть, что lock == null
, и произойдут плохие вещи).
Вы могли бы улучшить кодвыше, используя (правильно опубликовано) ReadWriteReentrantLock
.Это может немного улучшить параллелизм, если вас это беспокоит.
Предположим, вы намеревались сделать MyDBConfig
неизменяемым, вам не нужно сериализовать доступ к хэш-карте (то есть вы не обязательнонужно добавить синхронизированное ключевое слово).Вы можете улучшить параллелизм.
Прежде всего, сделайте reloadConfig()
закрытым (это будет означать, что для потребителей этого объекта он действительно неизменен: единственный метод, который они видят, это getProperty(...)
, который, поего имя не должно изменять экземпляр).
Тогда вам нужно только гарантировать, что каждый поток увидит правильные значения в хэш-карте.Для этого вы можете использовать те же методы, что и выше, или поле volatile
, например:
class MyDBConfig {
private volatile boolean initialized = false;
public String getProperty(...) { if (initialized) { ... } else { throw ... } }
private void reloadConfig() { ...; initialized = true; }
public MyDBConfig(...) { ...; reloadConfig(); }
}
Ключевое слово volatile
очень тонкое.Изменчивые записи и изменчивые чтения имеют отношение , которое происходит до .Считается, что энергозависимая запись произойдет до последующего энергозависимого чтения того же (энергозависимого) поля.Это означает, что все области памяти, которые были изменены до (в программном порядке) энергозависимой записи, видны всем другим потокам после того, как они выполнили последовательное энергозависимое чтение того же (энергозависимого) поля.
В приведенном выше коде вы пишете true
в поле volatile после все значения были установлены.Затем метод чтения значений (getProperty(...)
) начинается с выполнения энергозависимого чтения того же поля.Тогда этот метод гарантированно увидит правильные значения.
В приведенном выше примере, если вы не публикуете экземпляр до завершения конструктора, гарантируется, что исключение не будет вызвано в методе getProperty(...)
(потому что до того, как конструктор завершит работу, вы написали true
в initialized).