Это - проблема, с которой я часто встречаюсь в работе с более сложными системами и которую я никогда не выяснял хороший способ решить. Это обычно включает вариации на тему общего объекта, конструкция которого и инициализация являются обязательно двумя отличными шагами. Это обычно из-за архитектурных требований, подобный апплетам, таким образом, ответы, которые предлагают, чтобы я консолидировал конструкцию и инициализацию, не полезны. Системы должны предназначаться для Java 4 самое позднее, так ответы, которые предлагают, поддержка, доступная только в позже JVMs, не полезны также.
Как пример, скажем, у меня есть класс, который структурирован для вписывания в среду разработки приложения как так:
public class MyClass
{
private /*ideally-final*/ SomeObject someObject;
MyClass() {
someObject=null;
}
public void startup() {
someObject=new SomeObject(...arguments from environment which are not available until startup is called...);
}
public void shutdown() {
someObject=null; // this is not necessary, I am just expressing the intended scope of someObject explicitly
}
}
Я не могу сделать someObject финал, так как он не может быть установлен, пока запуск () не вызывается. Но я действительно хотел бы, чтобы это отразило свою неперезаписываемую семантику и смогло непосредственно получить доступ к нему от нескольких потоков, предпочтительно избежав синхронизации.
Так как идея состоит в том, чтобы выразить и осуществлять степень окончательности, я предугадываю, что мог создать универсальный контейнер, как так (ОБНОВЛЕНИЕ - исправленная поточная обработка sematics этого класса):
public class WormRef<T>
{
private volatile T reference; // wrapped reference
public WormRef() {
reference=null;
}
public WormRef<T> init(T val) {
if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
reference=val;
return this;
}
public T get() {
if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
return reference;
}
}
и затем в MyClass
, выше, сделайте:
private final WormRef<SomeObject> someObject;
MyClass() {
someObject=new WormRef<SomeObject>();
}
public void startup() {
someObject.init(new SomeObject(...));
}
public void sometimeLater() {
someObject.get().doSomething();
}
Который поднимает некоторые вопросы для меня:
Во вторую очередь, с точки зрения потокобезопасности:
someObject.get()
до окончания set()
был назван. Другие потоки только вызовут методы на MyClass между запуском () и завершением работы () - платформа гарантирует это.WormReference
контейнер, под любым JMM когда-либо возможно видеть значение object
который не является ни пустым указателем, ни ссылкой на SomeObject? Другими словами, делает JMM, всегда гарантируют, что никакой поток не может наблюдать память объекта быть независимо от того, что значения, оказалось, были на "куче", когда объект был выделен. Я полагаю, что ответ - "Да", потому что выделение явно обнуляет выделенную память - но кэширование ЦП может привести к чему-то еще наблюдаемому в данной ячейке памяти?Обратите внимание, что основная тяга этого вопроса состоит в том, как выразить и осуществить окончательность someObject
не имея возможности на самом деле отметить его final
; вторичный то, что необходимо для потокобезопасности. Таким образом, не становитесь слишком одержимыми аспектом потокобезопасности этого.
Это мой последний ответ, Реджис1 :
/**
* Provides a simple write-one, read-many wrapper for an object reference for those situations
* where you have an instance variable which you would like to declare as final but can't because
* the instance initialization extends beyond construction.
* <p>
* An example would be <code>java.awt.Applet</code> with its constructor, <code>init()</code> and
* <code>start()</code> methods.
* <p>
* Threading Design : [ ] Single Threaded [x] Threadsafe [ ] Immutable [ ] Isolated
*
* @since Build 2010.0311.1923
*/
public class WormRef<T>
extends Object
{
private volatile T reference; // wrapped reference
public WormRef() {
super();
reference=null;
}
public WormRef<T> init(T val) {
// Use synchronization to prevent a race-condition whereby the following interation could happen between three threads
//
// Thread 1 Thread 2 Thread 3
// --------------- --------------- ---------------
// init-read null
// init-read null
// init-write A
// get A
// init-write B
// get B
//
// whereby Thread 3 sees A on the first get and B on subsequent gets.
synchronized(this) {
if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); }
reference=val;
}
return this;
}
public T get() {
if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); }
return reference;
}
} // END PUBLIC CLASS
(1) Отсылка к игровому шоу "Так вы хотите стать миллионером", которое вел Реджис Филберн.
Теоретически было бы достаточно переписать startup()
следующим образом:
public synchronized void startup() {
if (someObject == null) someObject = new SomeObject();
}
Кстати, хотя WoRmObject
является конечным, потоки все еще могут вызывать set()
несколько раз. Вам действительно нужно добавить синхронизацию.
update: Я немного поигрался с этим и создал SSCCE, возможно, вы найдете полезным немного поиграться с этим :)
package com.stackoverflow.q2428725;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String... args) throws Exception {
Bean bean = new Bean();
ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS);
Future<String> result1 = executor.submit(new GetTask(bean));
Future<String> result2 = executor.submit(new GetTask(bean));
System.out.println("Result1: " + result1.get());
System.out.println("Result2: " + result2.get());
executor.shutdown();
}
}
class Bean {
private String property;
private CountDownLatch latch = new CountDownLatch(1);
public synchronized void startup() {
if (property == null) {
System.out.println("Setting property.");
property = "foo";
latch.countDown();
} else {
System.out.println("Property already set!");
}
}
public String get() {
try {
latch.await();
} catch (InterruptedException e) {
// handle.
}
return property;
}
}
class StartupTask implements Runnable {
private Bean bean;
public StartupTask(Bean bean) {
this.bean = bean;
}
public void run() {
System.out.println("Starting up bean...");
bean.startup();
System.out.println("Bean started!");
}
}
class GetTask implements Callable<String> {
private Bean bean;
public GetTask(Bean bean) {
this.bean = bean;
}
public String call() {
System.out.println("Getting bean property...");
String property = bean.get();
System.out.println("Bean property got!");
return property;
}
}
Защелка CountDownLatch
заставит все вызовы await()
блокироваться, пока обратный отсчет не достигнет нуля.
Я бы начал с объявления вашего someObject
volatile
.
private volatile SomeObject someObject;
Ключевое слово Volatile создает барьер памяти, что означает, что отдельные потоки всегда будут видеть обновленную память при обращении к someObject
.
В вашей текущей реализации некоторые потоки могут по-прежнему видеть someObject как null
даже после вызова startup
.
На самом деле этот метод volatile
часто используется коллекциями, объявленными в пакете java.util.concurrent
.
И, как предлагают здесь некоторые другие плакаты, если все остальное не помогает, вернитесь к полной синхронизации.
Я бы удалил метод setter в WoRmObject и предоставил синхронизированный метод init(), который выдает исключение if (object != null)
Судя по вашему описанию фреймворка, это, скорее всего, потокобезопасность. Должен быть барьер памяти где-то между вызовом myobj.startup ()
и предоставлением myobj
доступным для других потоков. Это гарантирует, что записи в startup () будут видны другим потокам. Поэтому вам не нужно беспокоиться о безопасности потоков, потому что это делает фреймворк. Хотя бесплатного обеда нет; Каждый раз, когда другой поток получает доступ к myobj
через фреймворк, он должен включать синхронизацию или энергозависимое чтение.
Если вы заглянете в структуру и перечислите код в пути, вы увидите sync / volatile в нужных местах, которые сделают ваш код потокобезопасным. То есть, если фреймворк реализован правильно.
Давайте рассмотрим типичный пример свинга, когда рабочий поток выполняет некоторые вычисления, сохраняет результаты в глобальной переменной x
, а затем отправляет событие перерисовки. Поток графического интерфейса пользователя после получения события перерисовки считывает результаты из глобальной переменной x
и соответственно перерисовывает.
Ни рабочий поток, ни код перерисовки не выполняют синхронизацию или изменчивое чтение / запись чего-либо. Таких реализаций должны быть десятки тысяч. К счастью, все они потокобезопасны, хотя программисты не обращали на них особого внимания. Почему? Потому что очередь событий синхронизирована; у нас есть прекрасная цепочка «случай до того»:
write x - insert event - read event - read x
Следовательно, записи x
и чтение x
правильно синхронизируются, неявно через структуру событий.
Можете ли вы использовать ThreadLocal, который позволяет устанавливать значение каждого потока только один раз?
Есть много неправильных способов сделать ленивое инстанцирование, особенно в Java.
Короче говоря, наивный подход заключается в создании приватного объекта, публичного синхронизированного метода init и публичного несинхронизированного метода get, который выполняет проверку на нуль вашего объекта и вызывает init, если необходимо. Тонкости проблемы заключаются в выполнении проверки нуля потокобезопасным способом.
Эта статья может быть полезна: http://en.wikipedia.org/wiki/Double-checked_locking
Эта конкретная тема в Java подробно обсуждается в книге Дуга Ли "Concurrent Programming in Java", которая несколько устарела, и в "Java Concurrency in Practice", написанной в соавторстве с Ли и другими. В частности, CPJ был опубликован до выхода Java 5, которая значительно улучшила средства управления параллелизмом в Java.
Я могу опубликовать более конкретные сведения, когда вернусь домой и получу доступ к указанным книгам.
Рассмотрите возможность использования AtomicReference в качестве делегата в объекте-контейнере, который вы пытаетесь создать. Например:
public class Foo<Bar> {
private final AtomicReference<Bar> myBar = new AtomicReference<Bar>();
public Bar get() {
if (myBar.get()==null) myBar.compareAndSet(null,init());
return myBar.get();
}
Bar init() { /* ... */ }
//...
}
EDITED: Это будет установлено один раз, с каким-то ленивым методом инициализации. Это не идеально для блокирования нескольких вызовов (предположительно дорогих) init()
, но могло быть и хуже. Вы можете засунуть инстанцирование myBar
в конструктор, а затем позже добавить конструктор, позволяющий присваивать также, если предоставить правильную информацию.
Есть некоторое общее обсуждение потокобезопасного инстанцирования синглтонов (что довольно похоже на вашу проблему), например, на этом сайте.