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 ответа

Решение

Это сложный вопрос, и ответ, к сожалению, "это зависит". У вас есть три варианта, когда речь заходит о поточной безопасности вашего класса:

  1. Сделайте это неизменным, тогда вам не о чем беспокоиться. Но это не то, что вы спрашиваете.
  2. Сделайте это потокобезопасным. То есть обеспечьте достаточное внутреннее управление параллелизмом для класса, чтобы клиентский код не беспокоился о параллельных потоках, модифицирующих объект.
  3. Сделайте его не потокобезопасным, и принудительно заставьте код клиента иметь некоторую внешнюю синхронизацию

По сути, вы спрашиваете, следует ли вам использовать #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 и понять, как каждый из них может быть использован для обеспечения безопасности потоков в различных ситуациях.

Другие вопросы по тегам