Jak ustalić, czy obiekt jest zablokowany (zsynchronizowany), aby nie blokować w Javie?

81

Mam proces A, który zawiera tabelę w pamięci z zestawem rekordów (rekord A, rekord B itp.)

Teraz ten proces może uruchomić wiele wątków, które mają wpływ na rekordy, a czasami możemy mieć 2 wątki próbujące uzyskać dostęp do tego samego rekordu - należy odrzucić tę sytuację. W szczególności, jeśli rekord jest ZABLOKOWANY przez jeden wątek, chcę, aby inny wątek został przerwany (nie chcę BLOKOWAĆ ani CZEKAĆ).

Obecnie robię coś takiego:

synchronized(record)
{
performOperation(record);
}

Ale to sprawia mi problemy ... ponieważ podczas gdy Process1 wykonuje operację, jeśli Process2 wchodzi, blokuje / czeka na zsynchronizowaną instrukcję, a po zakończeniu Process1 wykonuje operację. Zamiast tego chcę coś takiego:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

Jakieś wskazówki, jak można to osiągnąć? Każda pomoc byłaby bardzo mile widziana. Dzięki,

Shaitan00
źródło

Odpowiedzi:

87

Należy zauważyć, że w chwili otrzymania takich informacji są one nieaktualne. Innymi słowy, możesz usłyszeć, że nikt nie ma zamka, ale kiedy próbujesz go zdobyć, blokujesz, ponieważ inny wątek usunął blokadę między czekiem a tobą próbującym go zdobyć.

Brian ma rację Lock, ale myślę, że to, czego naprawdę chcesz, to jego tryLockmetoda:

Lock lock = new ReentrantLock();
......
if (lock.tryLock())
{
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
}
else
{
    // Someone else had the lock, abort
}

Możesz także zadzwonić tryLockz pewnym czasem oczekiwania - więc możesz spróbować zdobyć go na jedną dziesiątą sekundy, a następnie przerwać, jeśli nie możesz go uzyskać (na przykład).

(Myślę, że szkoda, że ​​Java API nie - o ile mi wiadomo - nie zapewnia takiej samej funkcjonalności dla „wbudowanego” blokowania, jak Monitorklasa w .NET. Z drugiej strony jest mnóstwo inne rzeczy, których nie lubię na obu platformach, jeśli chodzi o wątkowanie - na przykład każdy obiekt potencjalnie mający monitor!)

Jon Skeet
źródło
Tak. Trafne spostrzeżenie. Wziąłem przykładowy kod dosłownie, podczas gdy powyższa implementacja jest zdecydowanie bardziej niezawodna
Brian Agnew
5
Ale jak używać blokady dla każdego rekordu? Obecnie rekordy są przechowywane w HashTable of records ... więc potrzebuję pasującej Hashtable of Locks? Staram się zapewnić możliwie największą współbieżność, więc jeśli proces chce uzyskać dostęp do recordC, który powinien być w porządku (jeśli tylko recordB jest zablokowany) - używam globalnej LOCK, to jest to zasadniczo to samo, co blokowanie całej tablicy hashy. ... to ma sens?
Shaitan00
@ Shaitan00: Najłatwiejszym sposobem byłoby zablokowanie rekordu. Zasadniczo chcesz mieć jedną blokadę skojarzoną z każdym rekordem - więc umieść ją w obiekcie.
Jon Skeet
Oczywiście :) Zakładam, że nie muszę zarządzać odblokowywaniem? Oznacza to, że kiedy opuszcza {} z .tryLock (), zostanie automatycznie i natychmiast odblokowany, prawda?
Shaitan00
1
Musisz zarządzać odblokowywaniem. Zobacz samouczek, do którego odwołałem się w mojej odpowiedzi, i metodę unlock () w bloku
final
24

Spójrz na obiekty Lock wprowadzone w pakietach współbieżności Java 5.

na przykład

Lock lock = new ReentrantLock()
if (lock.tryLock()) {
   try {
      // do stuff using the lock...
   }
   finally {
      lock.unlock();
   }
}
   ...

Obiekt ReentrantLock zasadniczo robi to samo, co tradycyjny synchronizedmechanizm, ale ma większą funkcjonalność.

EDYCJA: Jak zauważył Jon, isLocked()metoda informuje Cię w tym momencie , a następnie te informacje są nieaktualne. Metoda tryLock () zapewni bardziej niezawodne działanie (pamiętaj, że możesz jej używać również z limitem czasu)

EDYCJA # 2: Przykład zawiera teraz tryLock()/unlock()dla przejrzystości.

Brian Agnew
źródło
11

Znalazłem to, możemy użyć Thread.holdsLock(Object obj)do sprawdzenia, czy obiekt jest zablokowany:

Zwraca truewtedy i tylko wtedy, gdy bieżący wątek przechowuje blokadę monitora na określonym obiekcie.

Zwróć uwagę, że Thread.holdsLock()zwraca, falsejeśli blokada jest utrzymywana przez coś, a wątek wywołujący nie jest wątkiem, który utrzymuje blokadę.

Rahul Kulshreshtha
źródło
8

Chociaż powyższe podejście przy użyciu obiektu Lock jest najlepszym sposobem na zrobienie tego, jeśli musisz mieć możliwość sprawdzenia blokowania za pomocą monitora, można to zrobić. Jednak zawiera ostrzeżenie dotyczące zdrowia, ponieważ technika ta nie jest przenośna na maszyny wirtualne inne niż Oracle Java i może się zepsuć w przyszłych wersjach maszyn wirtualnych, ponieważ nie jest obsługiwanym publicznym interfejsem API.

Oto jak to zrobić:

private static sun.misc.Unsafe getUnsafe() {
    try {
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void doSomething() {
  Object record = new Object();
  sun.misc.Unsafe unsafe = getUnsafe(); 
  if (unsafe.tryMonitorEnter(record)) {
    try {
      // record is locked - perform operations on it
    } finally {
      unsafe.monitorExit(record);
    }
  } else {
      // could not lock record
  }
}

Moją radą byłoby użycie tego podejścia tylko wtedy, gdy nie możesz refaktoryzować kodu w celu wykorzystania do tego obiektów java.util.concurrent Lock i jeśli używasz maszyny wirtualnej Oracle.

Chris Wraith
źródło
12
@alestanis, nie zgadzam się. Tutaj czytam dzisiaj te odpowiedzi i cieszę się z wszystkich odpowiedzi / komentarzy, bez względu na to, kiedy są udzielane.
Tom
@Tom Te odpowiedzi są oflagowane przez system SO, dlatego zostawiłem wiadomość. Zobaczysz, kiedy zaczniesz recenzować :)
alestanis
2
@alestanis, myślę, że to niefortunne - często widzę stare pytania, które mają zaakceptowane odpowiedzi, ale mają również nowsze, lepsze odpowiedzi, albo z powodu zmian technicznych, albo dlatego, że zaakceptowana odpowiedź nie była w rzeczywistości całkowicie poprawna.
Tom
1
Oprócz tego, że ten wątek jest stary i ma dobrą odpowiedź, ta odpowiedź tutaj nie jest dobra. Jeśli naprawdę chcesz zablokować za pomocą monitora, możesz użyć Thread.holdsLock (obiekt).
Tobias,
3

Chociaż odpowiedzi Lock są bardzo dobre, pomyślałem, że opublikuję alternatywę przy użyciu innej struktury danych. Zasadniczo różne wątki chcą wiedzieć, które rekordy są zablokowane, a które nie. Jednym ze sposobów jest śledzenie zablokowanych rekordów i upewnianie się, że struktura danych ma odpowiednie niepodzielne operacje do dodawania rekordów do zablokowanego zestawu.

Jako przykładu użyję CopyOnWriteArrayList, ponieważ jest to mniej „magiczne” dla ilustracji. CopyOnWriteArraySet jest bardziej odpowiednią strukturą. Jeśli masz średnio wiele rekordów zablokowanych w tym samym czasie, implementacje te mogą mieć wpływ na wydajność. Prawidłowo zsynchronizowany zestaw HashSet również zadziała, a blokady są krótkie.

Zasadniczo kod użycia wyglądałby tak:

CopyOnWriteArrayList<Record> lockedRecords = ....
...
if (!lockedRecords.addIfAbsent(record))
    return; // didn't get the lock, record is already locked

try {
    // Do the record stuff
}        
finally {
    lockedRecords.remove(record);
}

Dzięki temu nie musisz zarządzać blokadą dla każdego rekordu i zapewnia jedno miejsce, w którym z jakiegoś powodu konieczne jest wyczyszczenie wszystkich blokad. Z drugiej strony, jeśli kiedykolwiek będziesz mieć więcej niż kilka rekordów, prawdziwy zestaw HashSet z synchronizacją może działać lepiej, ponieważ odnośniki dodawania / usuwania będą miały wartość O (1) zamiast liniowej.

Po prostu inny sposób patrzenia na rzeczy. Zależy to tylko od rzeczywistych wymagań dotyczących gwintowania. Osobiście użyłbym Collection.synchronizedSet (new HashSet ()), ponieważ będzie to naprawdę szybkie ... jedyną konsekwencją jest to, że wątki mogą dawać wyniki, gdy inaczej by tego nie robiły.

PSpeed
źródło
Jeśli chcesz mieć pełną kontrolę, możesz chyba umieścić to w swojej własnej Lockklasie.
bvdb
1

Innym rozwiązaniem jest użycie limitów czasu (jeśli nie miałeś szans na udzielenie odpowiedzi). tj. poniżej jeden zwróci wartość null po 1 sekundzie zawieszenia:

ExecutorService executor = Executors.newSingleThreadExecutor();
        //create a callable for the thread
        Future<String> futureTask = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return myObject.getSomething();
            }
        });

        try {
            return futureTask.get(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            //object is already locked check exception type
            return null;
        }
HRgiger
źródło
-2

Dzięki za to pomogło mi rozwiązać problem wyścigu. Zmieniłem go trochę, żeby nosić zarówno pasek, jak i szelki.

Oto moja propozycja POPRAWY przyjętej odpowiedzi:

Możesz zapewnić sobie bezpieczny dostęp do tryLock()metody, wykonując coś takiego:

  Lock localLock = new ReentrantLock();

  private void threadSafeCall() {
    boolean isUnlocked = false;

    synchronized(localLock) {
      isUnlocked = localLock.tryLock();
    }

    if (isUnlocked) {
      try {
        rawCall();
      }
      finally {
        localLock.unlock();
      }
    } else {
      LOGGER.log(Level.INFO, "THANKS! - SAVED FROM DOUBLE CALL!");
    }
  }

Pozwoliłoby to uniknąć sytuacji, w której możesz otrzymać dwa połączenia tryLock()prawie w tym samym czasie, co spowodowałoby, że zwrot byłby potencjalnie pełen wątpliwości. Chciałbym teraz, jeśli się mylę, to może skończyło mi się tutaj ostrzeżenie. Ale hej! Mój występ jest teraz stabilny :-) ..

Przeczytaj więcej o moich problemach z programowaniem na moim blogu .

Schwartz
źródło
1
używanie synchronizacji na obiekcie blokady jest bardzo, bardzo złe. Celem blokady jest uniknięcie zsynchronizowania, abyś mógł przekroczyć limit czasu lub szybko wrócić, gdy nie możesz uzyskać blokady.
Ajax