2010-03-11 8 views
9

Dies ist ein Problem, auf das ich häufig stoße, wenn ich mit komplexeren Systemen arbeite und das habe ich nie gut gelöst. Es beinhaltet normalerweise Variationen über das Thema eines gemeinsamen Objekts, dessen Konstruktion und Initialisierung notwendigerweise zwei verschiedene Schritte sind. Dies ist im Allgemeinen aufgrund von Architekturanforderungen ähnlich wie Applets, so dass Antworten, die vorschlagen, dass ich Konstruktion und Initialisierung konsolidiere, nicht nützlich sind. Die Systeme müssen spätestens auf Java 4 ausgerichtet sein, so dass Antworten, die eine Unterstützung vorschlagen, die nur in späteren JVMs verfügbar ist, ebenfalls nicht nützlich sind.Wie erstelle ich in Java einen threadsicheren Einmallesewert?

Als Beispiel, sagen wir, ich eine Klasse, die in einen Anwendungsrahmen passen, wie so strukturiert ist:

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 
    } 
} 

Ich kann nicht endgültig machen someobject, da es nicht bis zur Inbetriebnahme festgelegt werden kann() wird aufgerufen. Aber ich möchte wirklich, dass es seine Semantik zum Einmalschreiben widerspiegelt und in der Lage ist, direkt von mehreren Threads darauf zuzugreifen, vorzugsweise um eine Synchronisation zu vermeiden.

Die Idee ist, zum Ausdruck bringen und ein gewisses Maß an finalness zu erzwingen, ich vermute, dass ich einen generischen Container erstellen könnte, wie so (UPDATE - korrigiert Threading sematics dieser Klasse):

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; 
    } 

} 

und dann in MyClass, oben, tun:

private final WormRef<SomeObject> someObject; 

MyClass() { 
    someObject=new WormRef<SomeObject>(); 
    } 

public void startup() { 
    someObject.init(new SomeObject(...)); 
    } 

public void sometimeLater() { 
    someObject.get().doSomething(); 
    } 

, die einige Fragen für mich aufwirft:

  1. Gibt es einen besseren Weg, oder existierendes Java-Objekt (müsste in Java 4 verfügbar sein)?

In zweiter Linie, die Sicherheit in Bezug auf Gewinde:

  1. Ist das Thread-sicher vorgesehen, die kein anderer Thread greift someObject.get() erst nach seinem set() aufgerufen wurde. Die anderen Threads rufen nur Methoden auf MyClass zwischen startup() und shutdown() auf - das Framework garantiert dies.
  2. Angesichts der völlig unsynchronisierten WormReference Container ist es immer möglich, unter JMM einen Wert von object, die weder Null noch ein Verweis auf ein SomeObject? Mit anderen Worten, garantiert das JMM immer, dass kein Thread den Speicher eines Objekts so beobachten kann, dass es sich auf die Werte bezieht, die sich bei der Zuweisung des Objekts auf dem Heapspeicher befanden. Ich glaube, die Antwort lautet "Ja", weil die Zuweisung den zugewiesenen Speicher explizit auf Null setzt - aber kann CPU-Caching dazu führen, dass an einem bestimmten Speicherort etwas anderes beobachtet wird?
  3. Ist es ausreichend, WormRef.reference volatile zu machen, um richtige Multithread-Semantik zu gewährleisten?

Hinweis der primäre Stoßrichtung dieser Frage ist, wie man die finalness von someObject ohne in der Lage zu markieren es tatsächlich finalauszudrücken und durchzusetzen; Sekundär ist, was für die Fadensicherheit notwendig ist. Das heißt, nicht auf den Thread-Sicherheitsaspekt davon hängen.

+0

I „nicht die anderen Threads starten, bis er initialisiert ist“ nehme sagen, wird nicht akzeptabel? – MSN

+0

@MSN: Tatsächlich ist es in den meisten Fällen selbstverständlich, dass die anderen Threads erst nach der Initialisierung gestartet werden. –

+0

An alle löschte ich meine Antwort, weil das OP beleidigend war, dass ich seine Frage redigierte, um meine Antwort aufzuerlegen. Das macht keinen Sinn. – BalusC

Antwort

0

Dies ist meine letzte Antwort, Regis :

/** 
* 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) Verleihen die Spielshow "Also du ein Millionär sein wollen", veranstaltet von Regis Philburn.

1

Theoretisch wäre es ausreichend, startup() zu umschreiben wie folgt:

public synchronized void startup() { 
    if (someObject == null) someObject = new SomeObject(); 
} 

By the way, obwohl die WoRmObject endgültig, Fäden noch set() mehrmals aufrufen kann. Sie müssen wirklich eine Synchronisierung hinzufügen.

Update: Ich spielte ein wenig um sie herum und schuf eine SSCCE, Sie finden es nützlich kann es ein bisschen herum zu spielen :)

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; 
    } 

} 

Die CountDownLatch verursachen alle await() Anrufe bis zu blockieren Der Countdown erreicht null.

+1

Nicht, es sei denn, get() wird auch synchronisiert. –

+0

Ich könnte mich irren, aber ich denke, wenn es garantiert ist, dass Startup abgeschlossen ist, bevor alle Thread-Zugriffe get(), Synchronisation von Get möglicherweise ausreichend sein. –

+0

@Alexander: Messepunkt. Sie möchten dann das 'get()' blockieren, bis 'someObject' nicht mehr null ist. Der einzige unsynchronisierte (und unangenehme?) Weg hierfür wäre das Hinzufügen von 'while (someObject == null);' nach oben der 'get()' Methode. – BalusC

0
  1. Wie wäre es mit Synchronisation?
  2. Nein, es ist nicht threadsicher. Ohne Synchronisierung wird der neue Status Ihrer Variablen möglicherweise nicht an andere Threads weitergegeben.
  3. Ja, soweit ich weiß sind Referenzen atomar, so dass Sie entweder Null oder die Referenz sehen. Beachten Sie, dass der Zustand des referenzierten Objekts eine ganz andere Geschichte ist
1

ich durch die Erklärung Ihrer someObjectvolatile beginnen würde.

private volatile SomeObject someObject; 

Volatile Schlüsselwort erzeugt eine Speicherbarriere, die separate Threads bedeutet, wird immer dann aktualisiert, wenn Speicher siehe someObject referenzieren.

In Ihrer aktuellen Implementierung können einige Threads immer noch einigeObject als null sehen, selbst nachdem startup aufgerufen wurde.

Tatsächlich wird diese volatile Technik viel von den Sammlungen verwendet, die im java.util.concurrent Paket erklärt werden.

Und wie einige andere Poster hier vorschlagen, wenn alle anderen Fehler fallen auf volle Synchronisation zurück.

+1

Volitale verhindert nicht, dass 'get()' vor 'startup()' aufgerufen wird. Es verhindert nur, dass auf die Referenz gleichzeitig zugegriffen wird (d. H. Ihr Zugriff wird synchronisiert) und dies kann einen Leistungsabfall in "get()" verursachen, als ob es "synchronisiert" wäre. – BalusC

+0

@BalusC: Guter Punkt über den Aufruf von 'get()' vor 'init()'; Ich werde auch einen Test für null im Getter machen müssen. –

1

Ich würde die Setter-Methode in WoRmObject, entfernen und eine synchronisierte Methode init() zur Verfügung stellen, die eine Ausnahme if (object != null)

+0

Danke - ich wollte eigentlich überprüfen, dass es nicht schon eingestellt war. Und ich mag den 'init()' Vorschlag sehr (es ist eigentlich genau das, was ich normalerweise für die einmalige Initialisierung tue). Code aktualisiert –

0

Könnten Sie einen Thread verwenden wirft, das nur den Wert jedes Thread erlaubt einmal eingestellt werden?

+0

Das würde nicht funktionieren; Das Objekt wird unter mehreren Threads geteilt. –

1

Verwenden Sie AtomicReference als Delegat in diesem Objektcontainer, den Sie erstellen möchten. Zum Beispiel:

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: Das wird einmal eingestellt, mit einigen lazy-Initialisierung-Methode. Es ist nicht perfekt zum Blockieren mehrerer Anrufe zu einem (vermutlich teuer) init(), aber es könnte schlimmer sein. Sie könnten die Instanziierung von myBar in den Konstruktor einfügen und später einen Konstruktor hinzufügen, der auch die Zuweisung zulässt, sofern die richtigen Informationen angegeben werden.

Es gibt einige allgemeine Diskussion über Thread-Safe, Singleton Instanziierung (die Ihrem Problem ziemlich ähnlich ist) zum Beispiel this site.

+0

Ein guter Vorschlag - +1. Aber AtomicReference unter der Haube behält eine volatile Referenz bei, so dass es in diesem Fall so aussieht, als wäre es nur ein Doppel-Wrapping für keinen Gewinn gegenüber einer volatilen Referenz. Außerdem ist das 'init()' nicht (immer) teuer; es muss nur bis nach dem Bau zurückgestellt werden. –

+0

sicher, aber es behandelt einige der Boilerplate Logik mit verschiedenen Tests für wann init(), etc. – Carl

+0

Ich sehe, wo du damit gehst; aber das WORM-Objekt kann sich nicht selbst initiieren, es benötigt etwas Externes, um init() mit dem Objekt aufzurufen, das es umbrechen muss. –

0

Es gibt eine Menge falscher Möglichkeiten, faule Instanziierung zu tun, besonders in Java. Der naive Ansatz besteht darin, ein privates Objekt, eine öffentliche synchronisierte Init-Methode und eine öffentliche unsynchronisierte get-Methode zu erstellen, die eine Null-Überprüfung für Ihr Objekt durchführt und bei Bedarf init aufruft. Die Feinheiten des Problems kommen bei der Durchführung der Null-Überprüfung in einer Thread-sicheren Weise.

Dieser Artikel sollte von Nutzen sein: http://en.wikipedia.org/wiki/Double-checked_locking

dieses spezielle Thema, in Java, wird ausführlich in Doug Lea 'Concurrent Programming in Java' diskutiert, die etwas veraltet ist, und in ‚Java Concurrency in Practice Mitautorin von Lea und anderen. Insbesondere wurde CPJ vor der Veröffentlichung von Java 5 veröffentlicht, wodurch die Nebenläufigkeitskontrollen von Java erheblich verbessert wurden.

Ich kann mehr Details posten, wenn ich nach Hause komme und Zugang zu besagten Büchern habe.

1

Es ist höchstwahrscheinlich Thread Safe, von Ihrer Beschreibung des Frameworks. Es muss irgendwo zwischen myobj.startup() und myobj eine Speicherbarriere für andere Threads verfügbar gewesen sein. Dies garantiert, dass die Schreibvorgänge in startup() für andere Threads sichtbar sind. Daher müssen Sie sich nicht um die Thread-Sicherheit kümmern, da das Framework dies tut. Es gibt jedoch kein kostenloses Mittagessen; Immer wenn ein anderer Thread über das Framework Zugriff auf myobj erhält, muss er synchron oder flüchtig gelesen werden.

Wenn Sie in das Framework schauen und den Code im Pfad auflisten, sehen Sie sync/volatile an den richtigen Stellen, die Ihren Code threadsicher machen. Das heißt, wenn das Framework korrekt implementiert ist.

Lassen Sie uns ein typisches Swing-Beispiel untersuchen, in dem ein Worker-Thread Berechnungen durchführt, die Ergebnisse in einer globalen Variablen x speichert und dann ein Repaint-Ereignis sendet. Der GUI-Thread liest nach Erhalt des Repaint-Ereignisses die Ergebnisse aus der globalen Variablen x und zeichnet entsprechend neu.

Weder der Worker-Thread noch der Repaint-Code führt eine Synchronisation oder flüchtiges Lesen/Schreiben auf irgendetwas durch. Es muss Zehntausende solcher Implementierungen geben. Zum Glück sind sie alle Thread-sicher, obwohl die Programmierer keine besondere Aufmerksamkeit geschenkt haben. Warum? Weil die Ereigniswarteschlange synchronisiert ist; wir haben eine schöne happends-vor-Kette:

write x - insert event - read event - read x 

Deshalb schreiben x und lesen x richtig synchronisiert werden, implizit über Veranstaltungsrahmen.

+0

Guter Punkt. Und tatsächlich stimmt das in meinem speziellen Anwendungsszenario. Ganz allgemein bin ich immer noch geneigt, die Referenz im WORM-Wrapper flüchtig zu machen, also ist es immer sicher, obwohl in diesen Fällen, in denen ich Split-Initialisierung habe, sie immer im Kontext eines korrekt synchronisierten Frameworks stehen. –

0

Nur meine kleine Version basiert auf AtomicReference. Es ist wahrscheinlich nicht die beste, aber ich glaube, es ist sauber und einfach zu bedienen:

public static class ImmutableReference<V> { 
    private AtomicReference<V> ref = new AtomicReference<V>(null); 

    public boolean trySet(V v) 
    { 
     if(v == null) 
      throw new IllegalArgumentException("ImmutableReference cannot hold null values"); 

     return ref.compareAndSet(null, v); 
    } 

    public void set(V v) 
    { 
     if(!trySet(v)) throw new IllegalStateException("Trying to modify an immutable reference"); 
    } 

    public V get() 
    { 
     V v = ref.get(); 
     if(v == null) 
      throw new IllegalStateException("Not initialized immutable reference."); 

     return v; 
    } 

    public V tryGet() 
    { 
     return ref.get(); 
    } 
} 
+0

Nicht gut - es muss in Java 4 laufen (siehe Frage). –

0

Erste Frage: Warum können Sie nicht nur eine private Methode starten machen, im Konstruktor aufgerufen, dann kann es sein, Finale. Dies würde die Threadsicherheit nach dem Aufruf des Konstruktors sicherstellen, da er vorher unsichtbar ist und erst gelesen wird, nachdem der Konstruktor zurückgegeben wurde.Oder faktorisieren Sie die Klassenstruktur neu, damit die Startmethode das MyClass-Objekt als Teil des Konstruktors erstellen kann. In einigen Fällen scheint dieser spezielle Fall wie ein Fall von schlechter Struktur zu sein, wo Sie wirklich nur endgültig und unveränderlich machen wollen.

Der einfache Ansatz, wenn die Klasse unveränderlich ist und erst gelesen wird, nachdem sie erstellt wurde, dann wickle sie in eine unveränderbare Liste von guava. Sie können auch einen eigenen unveränderlichen Wrapper erstellen, der defensiv kopiert, wenn Sie aufgefordert werden, die Referenz zurückzugeben, sodass ein Client die Referenz nicht ändern kann. Wenn es intern unveränderlich ist, ist keine weitere Synchronisation erforderlich, und unsynchronisierte Lesevorgänge sind zulässig. Sie können Ihren Wrapper so einstellen, dass er auf Anfrage defensiv kopiert wird, so dass selbst Versuche, auf ihn zu schreiben, sauber fehlschlagen (er tut einfach nichts). Möglicherweise benötigen Sie eine Speicherbarriere, oder Sie können eine lazy-Initialisierung durchführen. Beachten Sie jedoch, dass die faule Initialisierung möglicherweise eine weitere Synchronisierung erfordert, da Sie während der Erstellung des Objekts möglicherweise mehrere unsynchronisierte Leseanforderungen erhalten.

Der etwas kompliziertere Ansatz würde die Verwendung einer Aufzählung beinhalten. Da Aufzählungen Singleton garantiert sind, ist sie, sobald die Aufzählung erstellt wird, für immer fixiert. Sie müssen weiterhin sicherstellen, dass das Objekt intern unveränderlich ist, aber es garantiert seinen Singleton-Status. Ohne viel Aufwand.

+0

Erste Frage: Nun, "Dies ist in der Regel wegen der architektonischen Anforderungen, ähnlich Applets, so dass Antworten, die vorschlagen, dass ich Konstruktion und Initialisierung konsolidieren, sind nicht sinnvoll." –

-1

Die folgende Klasse konnte Ihre Frage beantworten. Einige Thread-Sicherheit erreicht durch die Verwendung einer volatile Zwischenvariable in Verbindung mit final Wert Keeper in den bereitgestellten generischen. Sie können eine weitere Erhöhung des Werts durch Verwendung von synchronized Setter/Getter in Erwägung ziehen. Ich hoffe es hilft.

https://stackoverflow.com/a/38290652/6519864

Verwandte Themen