Jak rozwiązać deklarację "Double-Checked Locking is Broken" w Javie?

Chcę zaimplementować leniwą inicjalizację dla wielowątkowości w Javie.
Mam taki kod:

class Foo {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            Helper h;
            synchronized(this) {
                h = helper;
                if (h == null) 
                    synchronized (this) {
                        h = new Helper();
                    } // release inner synchronization lock
                helper = h;
            } 
        }    
        return helper;
    }
    // other functions and members...
}

I otrzymuję deklarację "Podwójnie sprawdzona blokada jest zepsuta".
Jak mogę to rozwiązać?

Author: Hosam Aly, 2010-08-26

9 answers

Oto idiom zalecany w pozycji 71: rozsądnie używaj leniwej inicjalizacji z Efektywna Java:

Jeśli musisz użyć leniwej inicjalizacji dla wydajności na pole instancji, użyj double-check idiom. Ten idiom pozwala uniknąć kosztów blokowania przy dostępie do pola po jego zainicjowaniu (pozycja 67). Ideą idiomu jest sprawdź wartość pola dwukrotnie (stąd nazwa double-check): raz bez blokady, a następnie, jeśli pole wydaje się niezinicjalizowane, a drugi raz z blokadą. Tylko wtedy, gdy druga kontrola wskazuje, że pole jest niewtajemniczony robi połączenie zainicjuj pole. Ponieważ jest brak blokady, jeśli pole jest już inicjalizacji, Jest krytyczne , że pole należy zadeklarować volatile (Pozycja 66). Oto idiom:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
    FieldType result = field;
    if (result == null) { // First check (no locking)
        synchronized(this) {
            result = field;
            if (result == null) // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

Ten kod może wydawać się nieco zawiły. W szczególności potrzeba lokalnych zmienny wynik może być niejasne. Co? ta zmienna ma zapewnić, że pole jest odczytywane tylko raz we wspólnym przypadek, w którym jest już zainicjowany. Chociaż nie jest to bezwzględnie konieczne, może to popraw wydajność i jest więcej elegancki według standardów stosowanych do niskopoziomowe programowanie współbieżne. On moja maszyna, metoda powyżej jest o 25% szybciej niż oczywiste wersja bez zmiennej lokalnej.

Przed wydaniem 1.5, podwójne sprawdzenie idiom nie działał niezawodnie, ponieważ na semantyka modyfikatora lotnego nie były wystarczająco silne, aby go wspierać [Pugh01] Wprowadzono model pamięci w wersji 1.5 Naprawiono ten problem [JLS, 17, Goetz06 16]. Dzisiaj, idiom double-check jest techniką wybór leniwego inicjowania an pole instancji. Podczas gdy można aplikować the double-check idiom to static pola, jak również, nie ma powodu, aby zrób to: Leniwy Posiadacz inicjalizacji class idiom is a better wybór.

Odniesienie

  • Effective Java, Second Edition
    • pozycja 71: rozsądnie używaj leniwej inicjalizacji
 62
Author: Pascal Thivent,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-27 01:08:38

Oto wzór prawidłowego podwójnie sprawdzonego zamka.

class Foo {

  private volatile HeavyWeight lazy;

  HeavyWeight getLazy() {
    HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */
    if (tmp == null) {
      synchronized (this) {
        tmp = lazy;
        if (tmp == null) 
          lazy = tmp = createHeavyWeightObject();
      }
    }
    return tmp;
  }

}

Dla Singletona istnieje znacznie bardziej czytelny idiom dla leniwej inicjalizacji.

class Singleton {
  private static class Ref {
    static final Singleton instance = new Singleton();
  }
  public static Singleton get() {
    return Ref.instance;
  }
}
 10
Author: erickson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-26 20:58:45

Jedynym sposobem poprawnego blokowania w Javie jest użycie deklaracji "volatile" na danej zmiennej. Chociaż to rozwiązanie jest poprawne, należy pamiętać, że "lotne" oznacza, że linie pamięci podręcznej są spłukiwane przy każdym dostępie. Ponieważ "zsynchronizowany" spłukuje je na końcu bloku, może nie być w rzeczywistości bardziej wydajny (lub nawet mniej wydajny). Polecam po prostu nie używać podwójnie sprawdzonego blokowania, chyba że masz profilowany kod i stwierdziłeś, że jest problem z wydajnością w tym miejsce.

 3
Author: samkass,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-26 19:27:12

DCL using ThreadLocal By Brian Goetz @ JavaWorld

Co jest nie tak z DCL?

DCL opiera się na niezsynchronizowanym użyciu pola resource. To wydaje się być nieszkodliwe, ale tak nie jest. Aby zobaczyć dlaczego, wyobraź sobie, że wątek A znajduje się wewnątrz zsynchronizowanego bloku, wykonując polecenie resource = new Resource(); podczas gdy wątek B właśnie wprowadza getResource(). Rozważmy wpływ tej inicjalizacji na pamięć. Pamięć dla nowego obiektu Resource zostanie przydzielony; konstruktor zasobu zostanie wywołany, inicjalizując pola członkowskie nowego obiektu; a zasób pola SomeClass zostanie przypisany odniesienie do nowo utworzonego obiektu.
class SomeClass {
  private Resource resource = null;
  public Resource getResource() {
    if (resource == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
      }
    }
    return resource;
  }
}

Jednakże, ponieważ wątek B nie jest wykonywany wewnątrz zsynchronizowanego bloku, może widzieć te operacje pamięci w innej kolejności niż ten, który wykonuje wątek A. Może być tak, że B widzi te zdarzenia w następującej kolejności (a kompilator może również dowolnie zmieniać kolejność instrukcje takie jak ta): przydzielanie pamięci, przypisywanie referencji do zasobu, wywoływanie konstruktora. Przypuśćmy, że wątek B pojawia się po alokacji pamięci i ustawieniu pola zasobów, ale przed wywołaniem konstruktora. Widzi, że zasób nie jest null, pomija zsynchronizowany blok i zwraca odniesienie do częściowo zbudowanego zasobu! Nie trzeba dodawać, że wynik nie jest oczekiwany ani pożądany.

Czy ThreadLocal może pomóc naprawić DCL?

Możemy użyć ThreadLocal aby osiągnąć jawny cel idiomu DCL - leniwa inicjalizacja bez synchronizacji na wspólnej ścieżce kodu. Rozważmy tę (bezpieczną dla wątków) wersję DCL:

notowanie 2. DCL using ThreadLocal

class ThreadLocalDCL {
  private static ThreadLocal initHolder = new ThreadLocal();
  private static Resource resource = null;
  public Resource getResource() {
    if (initHolder.get() == null) {
      synchronized {
        if (resource == null) 
          resource = new Resource();
        initHolder.set(Boolean.TRUE);
      }
    }
    return resource;
  }
}

Myślę, że tutaj każdy wątek raz wejdzie do bloku synchronizacji, aby zaktualizować wartość threadLocal; wtedy nie będzie. Tak więc THREADLOCAL DCL zapewni, że wątek wejdzie tylko raz wewnątrz bloku synchronizacji.

Co tak naprawdę oznacza synchronized?

Java traktuje każdy wątek tak, jakby działał na własnym procesorze z własną pamięcią lokalną, z którą każdy rozmawia i synchronizuje się ze wspólną pamięcią główną. Nawet w systemie jednoprocesorowym model ten ma sens ze względu na działanie pamięci podręcznej i wykorzystanie rejestrów procesora do przechowywania zmiennych. Gdy wątek modyfikuje lokalizację w swojej pamięci lokalnej, ta modyfikacja powinna ostatecznie pojawić się również w pamięci głównej, a JMM określa reguły, kiedy JVM musi przesyłać dane między pamięć lokalna i główna. Architekci Javy zdali sobie sprawę, że zbyt restrykcyjny model pamięci poważnie osłabi wydajność programu. Starali się stworzyć model pamięci, który pozwoliłby programom dobrze działać na nowoczesnym sprzęcie komputerowym, jednocześnie zapewniając gwarancje, które umożliwiłyby interakcję wątków w przewidywalny sposób.

Podstawowym narzędziem Javy do renderowania interakcji między wątkami jest zsynchronizowane słowo kluczowe. Wielu programistów myśli o zsynchronizowanych ściśle w zakresie egzekwowania semafora wzajemnego wykluczenia (mutex), aby zapobiec wykonywaniu sekcji krytycznych przez więcej niż jeden wątek na raz. Niestety, intuicja ta nie do końca opisuje, co oznacza zsynchronizowany.

Semantyka synchronizacji rzeczywiście obejmuje wzajemne wykluczenie wykonania na podstawie statusu semafora, ale zawierają również zasady dotyczące interakcji wątku synchronizującego z pamięcią główną. W szczególności nabycie lub zwolnienie zamka wyzwala barierę pamięci-wymuszoną synchronizację między pamięcią lokalną wątku i pamięcią główną. (Niektóre procesory - jak Alpha-mają wyraźne instrukcje maszynowe do wykonywania barier pamięci.) Gdy wątek opuszcza zsynchronizowany blok, wykonuje barierę zapisu - musi wypłukać wszystkie zmienne zmodyfikowane w tym bloku do pamięci głównej przed zwolnieniem blokady. Podobnie, wchodząc do zsynchronizowanego bloku, wykonuje barierę odczytu - to tak, jakby pamięć lokalna została unieważniony i musi pobrać z pamięci głównej wszystkie zmienne, które będą odwoływane w bloku.

 3
Author: Kanagavelu Sugumar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-11 08:39:11

Zdefiniuj zmienną, która powinna być dwukrotnie sprawdzana za pomocą volatile midifier

Nie potrzebujesz zmiennej h. Oto przykład z tutaj

class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}
 2
Author: jutky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-26 19:15:38

Jak to, od kogo otrzymujesz deklarację?

Blokada Podwójnie sprawdzona jest stała. sprawdź Wikipedię:

public class FinalWrapper<T>
{
    public final T value;
    public FinalWrapper(T value) { this.value = value; }
}

public class Foo
{
   private FinalWrapper<Helper> helperWrapper = null;
   public Helper getHelper()
   {
      FinalWrapper<Helper> wrapper = helperWrapper;
      if (wrapper == null)
      {
          synchronized(this)
          {
              if (helperWrapper ==null)
                  helperWrapper = new FinalWrapper<Helper>( new Helper() );
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
 2
Author: irreputable,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-26 19:51:58

Jak kilku zauważyło, zdecydowanie potrzebujesz słowa kluczowego volatile, aby to działało poprawnie, chyba że wszystkie elementy obiektu są zadeklarowane final, w przeciwnym razie nie ma happens-before PR safe-publication i możesz zobaczyć wartości domyślne.

Mieliśmy dość ciągłych problemów z ludźmi, którzy się mylą, więc zakodowaliśmyLazyReference narzędzie, które ma ostateczną semantykę i zostało profilowane i dostrojone tak szybko, jak to możliwe.

 2
Author: Jed Wesley-Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-27 00:50:50

Kopiowanie poniżej z innego miejsca, co wyjaśnia, dlaczego użycie zmiennej lokalnej jako kopii zmiennej zmiennej przyspieszy działanie.

Stwierdzenie wymagające wyjaśnienia:

Ten kod może wydawać się nieco zawiły. W szczególności potrzeba wynik zmiennej lokalnej może być niejasny.

Wyjaśnienie:

Pole zostanie odczytane po raz pierwszy w instrukcji first if I drugi raz w oświadczeniu zwrotnym. Pole jest zadeklarowane lotne, co oznacza, że za każdym razem, gdy jest dostęp (z grubsza rzecz biorąc, nawet więcej przetwarzania może być wymagane do dostęp do zmiennych lotnych) i nie mogą być przechowywane w rejestrze przez kompilator. Po skopiowaniu do zmiennej lokalnej, a następnie użyciu w obu poleceń (if I return), optymalizację rejestru można wykonać poprzez JVM.

 2
Author: Animesh Chhabra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-17 05:04:56

Jeśli się nie mylę, jest też inne rozwiązanie, jeśli nie chcemy używać słowa kluczowego volatile

Na przykład biorąc poprzedni przykład

    class Foo {
        private Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        Helper newHelper = new Helper();
                        helper = newHelper;
                }
            }
            return helper;
        }
     }

Test jest zawsze na zmiennej pomocniczej, ale budowa obiektu jest wykonywana tuż przed newHelper, unika się częściowo skonstruowanego obiektu

 -1
Author: Branqueira,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-07 10:54:59