2010-10-03 8 views
6

Próbowałem odciąć rywalizację o wątki w moim kodzie, zastępując niektóre bloki synchronized za pomocą AtomicBoolean.AtomicBoolean vs zsynchronizowany blok

Oto przykład z synchronized:

public void toggleCondition() { 
    synchronized (this.mutex) { 
     if (this.toggled) { 
      return; 
     } 

     this.toggled = true; 
     // do other stuff 
    } 
} 

i alternatywa z AtomicBoolean:

public void toggleCondition() { 
    if (!this.condition.getAndSet(true)) { 
     // do other stuff 
    } 
} 

Wykorzystując własności CAS AtomicBoolean „s powinien być sposób szybciej niż opierając się na synchronizacji więc wpadłem little micro-benchmark.

Dla 10 równoczesnych wątków i 1000000 iteracji, AtomicBoolean pojawia się tylko nieznacznie szybciej niż blok synchronized.

Średni czas (na gwint) spędził na toggleCondition() z AtomicBoolean: 0,0338

Średni czas (na gwint) spędził na toggleCondition() z zsynchronizowanych: 0,0357

wiem mikro odniesienia są warte co są warte, ale czy różnica nie powinna być większa?

+1

Być może na początku nie było wiele sprzeczności w oryginalnym kodzie. Co skłoniło cię do myślenia, że ​​trzeba zoptymalizować? –

+0

Uruchamianie testów wydajności w bibliotece, którą napisałem. Miałem domyślną implementację przy użyciu zsynchronizowanych bloków i ręcznej synchronizacji wait()/notify(). Następnie wypróbowałem inną wersję, używając tylko java.util.concurrent i otrzymałem najgorsze wyniki. Oto klasy: http://github.com/brunodecarvalho/hotpotato/blob/master/src/main/java/org/factor45/hotpotato/request/ConcurrentHttpRequestFuture.java i http://github.com/brunodecarvalho/hotpotato /blob/master/src/main/java/org/factor45/hotpotato/request/DefaultHttpRequestFuture.java – biasedbit

+0

Zdałem sobie sprawę po kolejnym mikro-benchmarku (tak, wiem, że mikro benchmarki są nieco wadliwe ...), że problem polegał faktycznie na użyciu CountDownLatch przez wait()/notify(). Poza tym, że podejście AtomicBoolean nie było całkowicie bezpieczne, więc rzuciłem to. – biasedbit

Odpowiedz

6

Wiem, że mikro-benchmarki są warte tego, co warte są, ale czy różnica nie powinna być wyższa?

Myślę, że problem tkwi w twoim benchmarku. Wygląda na to, że każda nić ma zamiar przełączyć stan tylko raz. Benchmark poświęci większość czasu na tworzenie i niszczenie wątków. Prawdopodobieństwo, że dowolny wątek będzie przełączał warunek w tym samym czasie, co dowolny inny wątek, spowoduje przełączenie go w stan bliski zeru.

AtomicBoolean ma przewagę wydajności nad blokowaniem prymitywnym, gdy istnieje poważna rywalizacja o kondycję. Dla niezakłóconego stanu spodziewałbym się niewielkiej różnicy.

Zmień swój wzorzec, tak aby każdy wątek przełączał warunek kilka milionów razy. To zagwarantuje wiele rywalizacji o blokady i spodziewam się, że zobaczysz różnicę w wydajności.

EDIT

Jeżeli scenariusz zamierzałeś Test obejmował tylko jeden przełącznik na wątek (i 10 wątków), to jest mało prawdopodobne, że aplikacja będzie doświadczyć rywalizacji, i dlatego jest mało prawdopodobne, że za pomocą AtomicBoolean będzie zrobić jakąkolwiek różnicę.

W tym miejscu powinienem zapytać, dlaczego skupiasz swoją uwagę na tym konkretnym aspekcie. Czy profilowałeś swoją aplikację i ustaliłeś, że masz problem z blokadą kłódki? A może po prostu zgadujesz? Czy otrzymałeś już standardowy wykład na temat zła przedwczesnej optymalizacji?

+0

Ja tylko taktowanie czasu zajmuje wywołanie toggleCondition(). – biasedbit

+1

Wiem o tym. Problem polega na tym, że sposób, w jaki jest zapisywany test porównawczy, oznacza, że ​​nie ma prawie żadnego sporu. Zmień kod tak, aby pojawił się znaczący spór i spodziewam się, że zobaczysz różnicę w skuteczności. –

+0

Sprawdź moje odpowiedzi na komentarz od matt b na pytanie. W przypadku rzeczywistego użycia czytałem gorsze wartości dla wersji przy użyciu AtomicBoolean; ale wygląda na to, że w rzeczywistości CountDownLatch spowodował spowolnienie. Mimo to, jak sugerujesz, że zwiększam tam rywalizację? – biasedbit

3

Patrząc na rzeczywistą implementację, mam na myśli, patrząc na kod jest o wiele lepszy niż jakikolwiek microbenchmark (które są mniej niż bezużyteczne w Javie lub innym środowisku wykonawczym GC), nie jestem zaskoczony, że nie jest "znacznie szybszy". Zasadniczo robi niejawną zsynchronizowaną sekcję.

/** 
* Atomically sets to the given value and returns the previous value. 
* 
* @param newValue the new value 
* @return the previous value 
*/ 
public final boolean getAndSet(boolean newValue) { 
    for (;;) { 
     boolean current = get(); 
     if (compareAndSet(current, newValue)) 
      return current; 
    } 
} 

/** 
* Atomically sets the value to the given updated value 
* if the current value {@code ==} the expected value. 
* 
* @param expect the expected value 
* @param update the new value 
* @return true if successful. False return indicates that 
* the actual value was not equal to the expected value. 
*/ 
public final boolean compareAndSet(boolean expect, boolean update) { 
    int e = expect ? 1 : 0; 
    int u = update ? 1 : 0; 
    return unsafe.compareAndSwapInt(this, valueOffset, e, u); 
} 

A potem to z com.sun.Unsafe.java

/** 
* Atomically update Java variable to <tt>x</tt> if it is currently 
* holding <tt>expected</tt>. 
* @return <tt>true</tt> if successful 
*/ 
public final native boolean compareAndSwapInt(Object o, long offset, 
               int expected, 
               int x); 

nie ma magicznego w tym, twierdzenie zasób jest suką i bardzo skomplikowane. Dlatego używanie zmiennych final i praca z niezmiennymi danymi jest tak powszechne w rzeczywistych językach współbieżnych, takich jak Erlang. Cała ta złożoność, która zjada czas procesora, jest przekazywana, a przynajmniej przesunięta w mniej skomplikowane miejsce.