Потокобезопасный класс в Java с помощью синхронизированных блоков
Скажем, у нас очень простой Java-класс MyClass
.
public class MyClass {
private int number;
public MyClass(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
Существует три способа создания потокобезопасного Java-класса, который имеет некоторое состояние:
-
Сделайте это по-настоящему неизменным
public class MyClass {
private final int number;
public MyClass(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
-
Сделать поле number
volatile
.
public class MyClass {
private volatile int number;
public MyClass(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
-
Используйте блок synchronized
. Классическая версия этого подхода, описанная в главе 4.3.5 Java Concurrency на практике. И самое смешное в том, что в примере есть ошибка, о которой упоминается в описании этой книги.
public class MyClass {
private int number;
public MyClass(int number) {
setNumber(number);
}
public synchronized int getNumber() {
return number;
}
public synchronized void setNumber(int number) {
this.number = number;
}
}
Есть еще один факт, который следует добавить в контекст обсуждения. В Multhithreaded среде JVM может свободно изменять порядок команд за пределами блока synchronized
, сохраняя логическую последовательность, и происходит - до отношений, определенных JVM. Это может привести к тому, что объект публикации не будет правильно сконструирован еще в другом потоке.
У меня есть несколько вопросов относительно третьего случая.
-
Будет ли это эквивалентно следующему фрагменту кода:
public class MyClass {
private int number;
public MyClass(int number) {
synchronized (this){
this.number = number;
}
}
public synchronized int getNumber() {
return number;
}
public synchronized void setNumber(int number) {
this.number = number;
}
}
-
Будет ли предотвращено переупорядочение в третьем случае или возможно, чтобы JVM изменила порядок надстроек и, следовательно, опубликовала объект со значением по умолчанию в поле number
?
-
Если ответ на второй вопрос да, то у меня есть еще один вопрос.
public class MyClass {
private int number;
public MyClass(int number) {
synchronized (new Object()){
this.number = number;
}
}
public synchronized int getNumber() {
return number;
}
public synchronized void setNumber(int number) {
this.number = number;
}
}
Этот странно выглядящий synchronized (new Object())
должен предотвращать эффект переупорядочения. Будет ли это работать?
Чтобы быть ясными, все эти примеры не имеют практических приложений. Мне просто интересно узнать о нюансах многопоточности.
Ответы
Ответ 1
synchronized(new Object())
ничего не сделает, поскольку синхронизация выполняется только на объекте, который вы синхронизируете. Поэтому, если поток A синхронизируется на oneObject
, а поток B синхронизируется на anotherObject
, между ними не происходит. Поскольку мы можем знать, что ни один другой поток никогда не будет синхронизироваться с созданным там new Object()
, это не установит происшествие между предыдущим потоком.
Что касается вашего synchronzied
в конструкторе, если ваш объект безопасно опубликован в другой поток, он вам не нужен; и если это не так, вы, вероятно, в беспорядке, как есть. Я немного задал этот вопрос в списке concurrency -interest и интересный поток, приведенный. См., В частности, это электронное письмо, в котором указывается, что даже при синхронизации вашего конструктора при отсутствии безопасной публикации другой поток мог видеть значения по умолчанию в ваших полях, и это электронное письмо, которое (imho) связывает все это вместе.
Ответ 2
В вопросе №3 synchronized(new Object())
не работает и ничего не будет препятствовать. Компилятор может определить, что никакие другие потоки не могут синхронизировать этот объект (так как ничто другое не может получить доступ к объекту.) Это явный пример в статье Брайана Гетца " Java теория и практика: оптимизация синхронизации в Mustang".
Даже если вам нужно было синхронизировать в конструкторе, и даже если ваш блок synchronized(new Object())
был полезен, т.е. вы синхронизировались на другом долгоживущем объекте, так как ваши другие методы синхронизируются на this
, вы есть проблемы с видимостью, если вы не синхронизируете одну и ту же переменную. То есть вы действительно хотите, чтобы ваш конструктор также использовал synchronized(this)
.
В стороне:
Синхронизация на this
считается плохой. Вместо этого синхронизируйте в каком-то частном конечном поле. Вызывающие могут синхронизировать ваш объект, что может привести к тупиковой ситуации. Рассмотрим следующее:
public class Foo
{
private int value;
public synchronized int getValue() { return value; }
public synchronized void setValue(int value) { this.value = value; }
}
public class Bar
{
public static void deadlock()
{
final Foo foo = new Foo();
synchronized(foo)
{
Thread t = new Thread() { public void run() { foo.setValue(1); } };
t.start();
t.join();
}
}
}
Это не очевидно для вызывающих участников класса Foo
, что это затормозит. Лучше всего держать свою семантику блокировки внутренней и частной для вашего класса.