2012-01-23 24 views
10

To nie jest praca domowa dla mnie, to zadanie dla studentów z jakiejś uczelni. Jestem zainteresowany rozwiązaniem z osobistego interesu.Czy ten wątek klasy Java jest bezpieczny?

Zadanie polega na utworzeniu klasy (Calc), która zawiera liczbę całkowitą. Te dwie metody add i mul powinny dodać lub pomnożyć tę liczbę całkowitą.

Dwa wątki są konfigurowane jednocześnie. Jeden wątek powinien wywoływać c.add (3) dziesięć razy, drugi powinien wywoływać c.mul (3) dziesięć razy (na tym samym obiekcie Calc oczywiście).

Klasa Calc powinna upewnić się, że operacje są wykonywane naprzemiennie (dodaj, mul, dodaj, mul, dodaj, mul, ..).

Nie pracowałem zbyt często z problemami związanymi z konkurencją - jeszcze mniej z Javą. Opracowałem następującą implementację dla Calc:

class Calc{ 

    private int sum = 0; 
    //Is volatile actually needed? Or is bool atomic by default? Or it's read operation, at least. 
    private volatile bool b = true; 

    public void add(int i){ 
     while(!b){} 
     synchronized(this){ 
       sum += i; 
      b = true; 
     } 
    } 

    public void mul(int i){ 
     while(b){} 
     synchronized(this){ 
      sum *= i; 
      b = false; 
     } 
    } 

} 

Chciałbym wiedzieć, czy jestem na dobrej drodze. I na pewno jest bardziej elegancki sposób na chwilę (b) część. Chciałbym usłyszeć myśli twoich ludzi.

PS: Podpis metody nie może zostać zmieniony. Poza tym nie jestem ograniczony.

+2

można wykorzystać [AtomicBoolean] (http://docs.oracle.com/javase/6/docs/api/java/util/ concurrent/atomic/AtomicBoolean.html) i [AtomicInteger] (http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/AtomicInteger.html). – CoolBeans

+0

@AviramSegal - do opracowania: Jest to 1 wątek wywołujący mul() dziesięć razy z rzędu. Nie dziesięć wątków, które wywołują mul(). – s3rius

+0

Wątek bezpieczny, czy nie, każdy, kto używa w ten sposób wartości boolean jako zadania dla uczniów, powinien zostać skasowany. Nazwa pola "b". Świetnie, po prostu świetnie. –

Odpowiedz

3

lotny potrzebna jest inaczej optymalizator może zoptymalizować pętli if(b)while(true){}

ale można to zrobić z wait i notify

public void add(int i){ 

    synchronized(this){ 
     while(!b){try{wait();}catch(InterruptedException e){}}//swallowing is not recommended log or reset the flag 
      sum += i; 
     b = true; 
     notify(); 
    } 
} 

public void mul(int i){ 
    synchronized(this){ 
     while(b){try{wait();}catch(InterruptedException e){}} 
     sum *= i; 
     b = false; 
     notify(); 
    } 
} 

jednak w tym przypadku (b sprawdzane wewnątrz synchronizacji bloku) lotny nie potrzebne

+1

Chciałbym użyć notifyAll, na wypadek gdyby wymaganie się zmieniło i mógłbyś mieć kilka dodawania i/lub pomnażania wątków (w takim przypadku miałbyś zakleszczenie). –

+0

Czy masz jakieś referencje na poparcie stwierdzenia o optymalizacji rzeczy, które nie są niestabilne? Rozumiałem, że chodzi bardziej o widoczność ... czy jest to część tego samego problemu? wszelkie linki, które pomogą lepiej zrozumieć, będą mile widziane :) – Toby

+0

@Toby sprawdź [ten wpis na blogu] (http://mailinator.blogspot.com/2009/06/on-java-visibility.html) –

3

Tak, volatile jest potrzebna, a nie dlatego, cesja z boolean do drugiego nie jest atomowa, ale aby zapobiec buforowania zmiennej tak, że jego zaktualizowana wartość nie jest widoczne dla innych wątków, które czytasz. Również sum powinno być volatile, jeśli zależy Ci na ostatecznym wyniku.

Powiedziawszy to, byłoby prawdopodobnie bardziej elegancko zastosować wait i notify, aby utworzyć ten efekt przeplatania.

class Calc{ 

    private int sum = 0; 
    private Object event1 = new Object(); 
    private Object event2 = new Object(); 

    public void initiate() { 
     synchronized(event1){ 
      event1.notify(); 
     } 
    } 

    public void add(int i){ 
     synchronized(event1) { 
      event1.wait(); 
     } 
     sum += i; 
     synchronized(event2){ 
      event2.notify(); 
     } 
    } 

    public void mul(int i){ 
     synchronized(event2) { 
      event2.wait(); 
     } 
     sum *= i; 
     synchronized(event1){ 
      event1.notify(); 
     } 
    } 
} 

Następnie po uruchomieniu oba wątki, zadzwoń initiate aby zwolnić pierwszy wątek.

+0

Oczywiście, nie ma żadnych gwarancji postępu w specyfikacjach Java./Wierzę, że "zsynchronizowane" jest tutaj niepotrzebne./Ale zajęte czekać? –

+0

+1 dla problemu chaching.Czytałem o tym wcześniej, ale zupełnie zapomniałem. – s3rius

+0

@ s3rius: Dokonałem edycji za pomocą kodu za pomocą wait i notify. Dla mnie wydaje się to o wiele bardziej jasne. – Tudor

11

Spróbuj użyć interfejsu Lock:

class Calc { 

    private int sum = 0; 
    final Lock lock = new ReentrantLock(); 
    final Condition addition = lock.newCondition(); 
    final Condition multiplication = lock.newCondition(); 

    public void add(int i){ 

     lock.lock(); 
     try { 
      if(sum != 0) { 
       multiplication.await(); 
      } 
      sum += i; 
      addition.signal(); 

     } 
     finally { 
      lock.unlock(); 
     } 
    } 

    public void mul(int i){ 
     lock.lock(); 
     try { 
      addition.await(); 
      sum *= i; 
      multiplication.signal(); 

     } finally { 
      lock.unlock(); 
     } 
    } 
} 

Blokada działa jak zsynchronizowanego bloków. Ale metody będą czekać na .await(), jeśli inny wątek zawiera lock do czasu wywołania .signal().

+0

i kto pierwszy? –

+0

Jeśli zmodyfikujesz przykład, aby uwzględnić dwa warunki (aby obsłużyć zmianę operacji), wznowię. – Perception

+0

Hmm, nie jestem pewien, czy używanie tej samej zmiennej sygnału jest bezpieczne dla wątków. Może istnieć scenariusz, w którym wątek wywołujący add będzie sygnalizował allowAccess, a następnie ponownie odzyskać blokadę i znaleźć sygnał. – Tudor

10

To, co zrobiłeś, to zapętlona pętla: uruchamiasz pętlę, która zatrzymuje się tylko po zmianie zmiennej. Jest to zła technika, ponieważ powoduje, że procesor jest bardzo obciążony, zamiast prostego powodowania, że ​​wątek czeka, aż flaga zostanie zmieniona.

Chciałbym użyć dwóch semaphores: jednego dla multiply i jednego dla add. Przed dodaniem, add musi nabyć addSemaphore i wydać zezwolenie na multiplySemaphore, kiedy to zostanie wykonane i na odwrót.

private Semaphore addSemaphore = new Semaphore(1); 
private Semaphore multiplySemaphore = new Semaphore(0); 

public void add(int i) { 
    try { 
     addSemaphore.acquire(); 
     sum += i; 
     multiplySemaphore.release(); 
    } 
    catch (InterrupedException e) { 
     Thread.currentThread().interrupt(); 
    } 
} 

public void mul(int i) { 
    try { 
     multiplySemaphore.acquire(); 
     sum *= i; 
     addSemaphore.release(); 
    } 
    catch (InterrupedException e) { 
     Thread.currentThread().interrupt(); 
    } 
} 
+0

Myślę, że wersja ReentrantLock jest nieco bardziej zrozumiała. To powiedziawszy, wydaje się być bardziej jasne, jeśli chodzi o stan początkowy i może być łatwiejsze do dostosowania, więc daj +1. –

0

Program jest w pełni bezpieczne dla wątków:

  1. Wartość logiczna flaga jest ustawiona na lotny, więc JVM nie wie buforować wartości i utrzymać zapisem dostęp do jednego wątku na raz.

  2. Dwie kluczowe sekcje blokują bieżący obiekt, co oznacza, że ​​tylko jeden wątek będzie miał dostęp w tym samym czasie. Zauważ, że jeśli wątek znajduje się wewnątrz zsynchronizowanego bloku, żaden wątek nie może znajdować się w żadnej innej sekcji krytycznej.

Powyższe dotyczy każdej instancji klasy. Na przykład, jeśli tworzone są dwie instancje, wątki będą mogły wchodzić w wiele krytycznych sekcji naraz, ale będą ograniczone do jednego wątku na instancję, na sekcję krytyczną. Czy to ma sens?

+0

Ale można go poprawić. Jakie było pytanie. – Jivings

3

Hmmm. Istnieje wiele problemów z rozwiązaniem. Po pierwsze, lotność nie jest wymagana dla atomowości, ale dla widoczności. Nie będę tu wchodził, ale możesz przeczytać więcej na temat Java memory model. (I tak, boolean jest atomowy, ale nie ma tu znaczenia). Poza tym, jeśli uzyskujesz dostęp do zmiennych tylko w zsynchronizowanych blokach, to nie muszą one być niestabilne.

Teraz zakładam, że to przez przypadek, ale twoja zmienna b nie jest dostępna tylko w zsynchronizowanych blokach, i to jest niestabilne, więc faktycznie twoje rozwiązanie będzie działać, ale nie jest ono ani idiomatyczne, ani zalecane, ponieważ jesteś czekanie, aż b zmieni się w zajętej pętli. Palisz cykle procesora za darmo (tak nazywamy spin-lock i czasami może się przydać).

idiomatycznym rozwiązaniem będzie wyglądać następująco:

class Code { 
    private int sum = 0; 
    private boolean nextAdd = true; 

    public synchronized void add(int i) throws InterruptedException { 
     while(!nextAdd) 
      wait(); 
     sum += i; 
     nextAdd = false; 
     notify(); 
    } 

    public synchronized void mul(int i) throws InterruptedException { 
     while(nextAdd) 
      wait(); 
     sum *= i; 
     nextAdd = true; 
     notify(); 
    } 
} 
+0

Pytanie - czy tam być różnica między while (! b) wait(); i jeśli (! b) wait(); ? – s3rius

+1

@ s3rius Tak. Musisz zawsze przetestować stan monitora (taki, na który czekasz()) w pętli while. Są dwa powody tego. Po pierwsze, nie wiesz, dlaczego wątek został obudzony (za pomocą notify() lub notifyAll()). Może dlatego, że zmienił się stan, który testujesz, może z innego powodu. Po drugie, w niektórych architekturach sprzętowych/systemowych mogą występować zbędne pobudki wątków, tj. Wybudzenia bez żadnego powodu. To, jak sądzę, nie zdarza się w większości architektur, ale nadal pozostaje idiom: zawsze musisz przetestować warunek, na który czekasz w pętli. – pron

+0

To wyjaśnia, dzięki. – s3rius

5

Jak inni wspomniano, volatile w roztworze jest wymagane. Ponadto twoje rozwiązanie wiruje, co może zmarnować sporo cykli procesora. Powiedział, że nie widzę żadnych problemów, jeśli chodzi o poprawność.

Osobiście zaimplementować to z parą semaforów:

private final Semaphore semAdd = new Semaphore(1); 
private final Semaphore semMul = new Semaphore(0); 
private int sum = 0; 

public void add(int i) throws InterruptedException { 
    semAdd.acquire(); 
    sum += i; 
    semMul.release(); 
} 

public void mul(int i) throws InterruptedException { 
    semMul.acquire(); 
    sum *= i; 
    semAdd.release(); 
} 
+0

Mieliśmy dokładnie ten sam pomysł! –

+0

@JBNizet: Tak, ale szybciej dotarłeś do tego rozwiązania (sporo czasu poświęciłem na majsterkowanie z innymi prymitywami, zanim zdecydowałem, że semafory są najlepszym rozwiązaniem.) – NPE

+0

A więc? rozwój to nie konkurs prędkości. Straciłabym dużo czasu, gdyby tak było. Niestety, na więcej sposobów niż inne, stackoverflow jest. –

Powiązane problemy