Java Happens-Before и Thread Safety
Предположим, что у меня есть класс, который обертывает HashMap следующим образом:
public final class MyClass{
private final Map<String, String> map;
//Called by Thread1
public MyClass( int size ){
this.map = new HashMap<String, String>( size );
}
//Only ever called by Thread2
public final String put( String key, String val ){
return map.put( key, value );
}
//Only ever called by Thread2
public final String get( String key ){
return map.get( key );
}
//Only ever called by Thread2
public final void printMap( ){
//Format and print the contents of the map
}
}
Этот класс инициализируется через "Thread1".
Тем не менее, put, get, printMap и другие операции только когда-либо, вызываемые "Thread2".
Я правильно понимаю, что этот класс является потокобезопасным как:
-
Поскольку ссылка на карту объявлена окончательной, все остальные потоки будут видеть начальное состояние карты (происходит-до этого установлено).
-
Так как put/get/printMap/etc вызывается только Thread2, нет необходимости в взаимном исключении.
Спасибо
Ответы
Ответ 1
Итак, то, что вы просите, является правильным предположением. Вам не нужно делать это потокобезопасным, если вы можете гарантировать, что он используется только таким образом. Вы не можете передать наполовину построенный объект в java, поэтому "Конструктор может не завершиться" невозможен.
Итак, если вы делаете
new Thread(new Runnable() {
public void run() {
final MyClass instance = new MyClass(10);
new Thread(new Runnable() {
public void run() {
instance.put("one", "one");
....
}
}).start();
}
}).start();
Вы в порядке:) Это то, что вы описали, созданное Thread1, но используемое только Thread2. Невозможно совпадение потока с самим собой.
Thread-Safe - это другое определение, в котором скомпонованный объект может безопасно взаимодействовать несколькими потоками. В описанном вами случае этого сценария не происходит, поскольку у вас по существу есть поток, который строит, и другой, который манипулирует.
Ответ 2
Это немного сложно ответить, потому что JLS не содержит понятия класса, который является потокобезопасным — все это указывает на взаимосвязь между действиями (например, запись в поле, чтение из поля и т.д.).
Тем не менее, по большинству определений этот класс не является потокобезопасным - он содержит расы данных, вызванные несинхронизированным доступом к не-потокобезопасной карте. Тем не менее, ваше использование этого потока является потокобезопасным, потому что вы благополучно публикуете this.map
в Thread 2 после построения, и в этот момент this.map
доступен только один поток одним потоком, и в этом случае безопасность потоков не является проблемой.
Другими словами, это лишь немного больше, чем вопрос о том, является ли HashMap
потокобезопасным при создании и доступе только в пределах одного потока. Ответ в этом случае заключается в том, что HashMap
не является потокобезопасным, но он не должен быть.
Аналогично, ваш класс не является потокобезопасным, но похоже, что это может и не быть.
Ответ 3
Если вы согласитесь на свое определение того, что произойдет, тогда он действительно будет потокобезопасным. То есть, только нить-2 будет помещаться и получать с карты.
Так как Map объявлен окончательным, вы устанавливаете связь между событиями перед записью потока-1 и чтением потока-2.
a) Поскольку ссылка на карту объявлена окончательной, все остальные потоки будет видеть начальное состояние карты (происходит-раньше установлено).
Да. Указывается пустой HashMap с size
.
b) Так как put/get/printMap/etc вызывается только Thread2, там нет необходимости в взаимном исключении.
Да, хотя этот тип логики обычно пугает меня на практике:)
Ответ 4
Во-первых: Инициализация final и volatile поля в openJDK всегда появляются перед возвратом ссылки на объект конструктором. Переупорядочение не произошло. Тогда построение объекта является потокобезопасным.
Во-вторых: если методы put, get, printMap вызывается в Thread2, то исключение mutuall не нужно.
В ваш случай класса использования безопасен.
Ответ 5
Вопрос здесь не в самой реализации класса, а в том, что манипуляция описываемого вами класса поточно-безопасна или нет, поэтому реализация класса не имеет большого значения.
Я могу представить два возможных способа доступа Thread2 к экземпляру MyClass
.
- Оба Thread1 и Thread2 запускаются независимо. В этом случае он не является потокобезопасным, потому что Thread2 может вызвать put/get/printMap/etc до того, как завершится конструктор, работающий в Thread1, в результате чего появится NPE. Экземпляр
MyClass
, к которому обратился Thread2, может быть нулевым в зависимости от того, как Thread2 обращается к нему. Если это общий экземпляр, когда Thread2 обращается к нему, он может быть нулевым, если конструктор MyClass
не завершил выполнение в Thread1.
- Thread1 создает экземпляр и передает этот экземпляр Thread2. В этом случае он является потокобезопасным, но это фактически не имеет значения, потому что нет многопоточного доступа к этому экземпляру, потому что Thread2 должен ждать передачи экземпляра.
Итак, в описанной ситуации ответ на самом деле зависит не от реализации класса, а от того, как его общий экземпляр. И в худшем случае (путь 1) он не является потокобезопасным.