2013-08-12 4 views
8

Widziałem ten typ kodu dużo w projektach, w których aplikacja chce globalnego posiadacza danych, więc używają statycznego singletonu, do którego ma dostęp każdy wątek.Czym jest widoczność pamięci dla zmiennych, do których dostęp jest uzyskiwany w statycznych singletonach w języku Java?

public class GlobalData { 

    // Data-related code. This could be anything; I've used a simple String. 
    // 
    private String someData; 
    public String getData() { return someData; } 
    public void setData(String data) { someData = data; } 

    // Singleton code 
    // 
    private static GlobalData INSTANCE; 
    private GlobalData() {} 
    public synchronized GlobalData getInstance() { 
     if (INSTANCE == null) INSTANCE = new GlobalData(); 
     return INSTANCE; 
    } 
} 

Mam nadzieję, że łatwo będzie zobaczyć, co się dzieje. W dowolnym momencie można wywołać GlobalData.getInstance().getData() w dowolnym wątku. Jeśli dwa wątki wywołują setData() z różnymi wartościami, nawet jeśli nie możesz zagwarantować, który z nich "wygrywa", nie martwię się tym.

Ale bezpieczeństwo wątków nie jest tutaj moim problemem. Martwię się tylko o widoczność pamięci. Ilekroć w Javie jest bariera pamięci, pamięć podręczna jest zsynchronizowana pomiędzy odpowiednimi wątkami. Bariera pamięci zdarza się podczas przechodzenia przez synchronizacji, dostęp do zmiennych lotnych itp

Wyobraźmy sobie następującą scenariusz dzieje się w porządku chronologicznym:

// Thread 1 
GlobalData d = GlobalData.getInstance(); 
d.setData("one"); 

// Thread 2 
GlobalData d = GlobalData.getInstance(); 
d.setData("two"); 

// Thread 1 
String value = d.getData(); 

nie jest to możliwe, że ostatnia wartość value w wątku 1 puszka nadal być "one"? Powodem jest, że wątek 2 nigdy nie wywoływał żadnych zsynchronizowanych metod po wywołaniu d.setData("two"), więc nigdy nie było bariery pamięci? Zauważ, że bariera pamięci w tym przypadku dzieje się za każdym razem, gdy wywoływana jest getInstance(), ponieważ jest zsynchronizowana.

+1

Mówisz: "Można wywołać GlobalData.getInstance(). GetData() w dowolnym momencie na dowolnym wątku. Jest bezpieczny dla wątków, więc nie martwię się o to ", ale w rzeczywistości nie jest bezpieczny dla wątków dokładnie ze względu na opisywany scenariusz. Bezpieczeństwo wątków to nie tylko obserwowanie obiektu w niespójnym stanie. Chodzi także o oglądanie nieaktualnych danych. – yair

+0

@yair Tak, ale naprawdę próbowałem zbadać aspekt widoczności pamięci w przeciwieństwie do niespójnych stanów na podstawie wykonania synchronicznego. –

Odpowiedz

1

Czy nie jest możliwe, że ostatnia wartość wartości w wątku 1 może nadal być "jedna"?

Tak jest. Model pamięci java oparty jest na wcześniejszych relacjach (hb). W twoim przypadku masz tylko getInstance wyjście - przed kolejnym getInstance wpisem, ze względu na zsynchronizowane słowo kluczowe.

Więc jeśli weźmiemy swój przykład (zakładając, przeplatanie nici jest w tej kolejności):

// Thread 1 
GlobalData d = GlobalData.getInstance(); //S1 
d.setData("one"); 

// Thread 2 
GlobalData d = GlobalData.getInstance(); //S2 
d.setData("two"); 

// Thread 1 
String value = d.getData(); 

Masz hb S1 S2. Jeśli wywołałeś d.getData() z Thread2 po S2, zobaczyłbyś "jeden". Ale ostatni odczyt d nie gwarantuje "dwóch".

2

Masz całkowitą rację.

Nie ma żadnej gwarancji, że zapis w jednym Thread będzie widoczny, to inny.

się do udzielenia gwarancji będzie trzeba użyć słowa kluczowego volatile:

private volatile String someData; 

Nawiasem mówiąc można wykorzystać classloader Java wątku dostawcy bezpieczny leniwy init swojej Singleton jako udokumentowane here. W ten sposób unika się słowa kluczowego synchronized, a zatem oszczędza pewne blokowanie.

Warto zauważyć, że obecnie przyjętą najlepszą praktyką jest używanie enum do przechowywania pojedynczych danych w Javie.

2

Prawidłowe, możliwe jest, że wątek 1 nadal widzi wartość jako "jeden", ponieważ nie wystąpiło zdarzenie synchronizacji pamięci i nie ma zdarzenia przed związkiem między wątkiem 1 a wątkiem 2 (patrz sekcja 17.4.5 of the JLS).

Jeśli someData był volatile, wątek 1 zobaczyłby wartość jako "dwie" (przy założeniu, że wątek 2 został ukończony, zanim wątek 1 uzyska wartość).

Wreszcie i poza tematem, implementacja singletona jest nieco mniejsza niż idealna, ponieważ synchronizuje się przy każdym dostępie. Ogólnie lepiej jest użyć wyliczenia, aby zaimplementować singleton lub przynajmniej przypisać instancję w statycznym inicjalizatorze, więc żadne wywołanie konstruktora nie jest wymagane w metodzie getInstance.

Powiązane problemy