Java - Предпочитаемый дизайн для использования изменяемой ссылки на объект в другом потоке?
public class ObjectA {
private void foo() {
MutableObject mo = new MutableObject();
Runnable objectB = new ObjectB(mo);
new Thread(objectB).start();
}
}
public class ObjectB implements Runnable {
private MutableObject mo;
public ObjectB(MutableObject mo) {
this.mo = mo;
}
public void run() {
//read some field from mo
}
}
Как видно из примера кода выше, я передаю изменяемый объект классу, который реализует Runnable и будет использовать изменяемый объект в другом потоке. Это опасно, потому что ObjectA.foo() все еще может изменять состояние изменяемого объекта после запуска нового потока. Каков предпочтительный способ обеспечения безопасности потока здесь? Должен ли я сделать копию MutableObject при передаче его в ObjectB? Должен ли изменяемый объект обеспечивать надлежащую внутреннюю синхронизацию? Я сталкивался с этим много раз прежде, особенно когда пытался использовать SwingWorker в ряде приложений с графическим интерфейсом. Я обычно стараюсь убедиться, что ТОЛЬКО неизменные ссылки на объекты передаются в класс, который будет использовать их в другом потоке, но иногда это может быть сложно.
4 ответа
Это сложный вопрос, и ответ, к сожалению, "это зависит". У вас есть три варианта, когда речь заходит о поточной безопасности вашего класса:
- Сделайте это неизменным, тогда вам не о чем беспокоиться. Но это не то, что вы спрашиваете.
- Сделайте это потокобезопасным. То есть обеспечьте достаточное внутреннее управление параллелизмом для класса, чтобы клиентский код не беспокоился о параллельных потоках, модифицирующих объект.
- Сделайте его не потокобезопасным, и принудительно заставьте код клиента иметь некоторую внешнюю синхронизацию
По сути, вы спрашиваете, следует ли вам использовать #2 или #3. Вас беспокоит случай, когда другой разработчик использует класс и не знает, что он требует внешней синхронизации. Мне нравится использовать аннотации JCIP @ThreadSafe @Immutable @NotThreadSafe
как способ документировать намерения параллелизма. Это не пуленепробиваемое, так как разработчики все еще должны читать документацию, но если все в команде понимают эти аннотации и последовательно применяют их, это делает вещи более понятными.
Для вашего примера, если вы хотите сделать класс не поточно-ориентированным, вы можете использовать AtomicReference
чтобы было понятно и обеспечить синхронизацию.
public class ObjectA {
private void foo() {
MutableObject mo = new MutableObject();
Runnable objectB = new ObjectB(new AtomicReference<>( mo ) );
new Thread(objectB).start();
}
}
public class ObjectB implements Runnable {
private AtomicReference<MutableObject> mo;
public ObjectB(AtomicReference<MutableObject> mo) {
this.mo = mo;
}
public void run() {
//read some field from mo
mo.get().readSomeField();
}
}
Скопируйте объект. У вас не будет никаких странных проблем с видимостью, потому что вы передадите копию в новый поток. Thread.start всегда происходит до того, как новый поток входит в свой метод run. Если вы измените этот код для передачи объекта в существующий поток, вам потребуется правильная синхронизация. Я рекомендую очередь блокировки из Java.util.concurrent.
Я думаю, вы слишком усложняете это. Если это как пример (локальная переменная, ссылка на которую не сохраняется), вы должны верить, что никто не будет пытаться писать в нее. Если сложнее (A.foo()
имеет больше LOC), если возможно, создайте его только для передачи в поток.
new Thread(new MutableObject()).start();
Если нет (из-за инициализации), объявите его в блоке, чтобы он немедленно вышел из области видимости, даже, возможно, в отдельном частном методе.
{
MutableObject mo = new MutableObject();
Runnable objectB = new ObjectB(mo);
new Thread(objectB).start();
}
....
Не зная вашей точной ситуации, на этот вопрос будет сложно точно ответить. Ответ полностью зависит от того, что MutableObject
представляет, сколько других потоков может модифицировать его одновременно, и заботятся ли потоки, которые читают объект, о том, изменяется ли его состояние, когда они читают его.
Что касается безопасности потоков, внутренняя синхронизация всех операций чтения и записи в MutableObject
это, пожалуй, самая безопасная вещь, но она достигается за счет производительности. Если конкуренция за чтение и запись действительно высока, ваша программа может испытывать проблемы с производительностью. Вы можете получить более высокую производительность, пожертвовав некоторыми гарантиями взаимного исключения - стоит ли эти жертвы повышения производительности, полностью зависит от конкретной проблемы, которую вы пытаетесь решить.
Вы также можете поиграть в некоторые игры с "внутренней синхронизацией" своего MutableObject
, если это то, что вы в конечном итоге делаете. Если вы еще этого не сделали, я бы порекомендовал почитать разницу между volatile
а также synchronized
и понять, как каждый из них может быть использован для обеспечения безопасности потоков в различных ситуациях.