Synchronizacja na obiektach String w Javie

Mam webapp, że jestem w trakcie wykonywania testów obciążenia / wydajności, szczególnie na funkcji, w której oczekujemy kilkuset użytkowników, aby uzyskać dostęp do tej samej strony i uderzając odśwież co około 10 sekund na tej stronie. Jednym z obszarów poprawy, które odkryliśmy, że możemy dokonać za pomocą tej funkcji było buforowanie odpowiedzi z usługi internetowej przez pewien okres czasu, ponieważ DANE się nie zmieniają.

Po zaimplementowaniu tego podstawowego buforowania, w niektórych dalszych testach I okazało się, że nie zastanawiałem się, w jaki sposób współbieżne wątki mogą uzyskać dostęp do pamięci podręcznej w tym samym czasie. Odkryłem, że w ciągu ~100ms około 50 wątków próbowało pobrać obiekt z pamięci podręcznej, stwierdzając, że wygasł, uderzając w usługę internetową, aby pobrać dane, a następnie umieszczając obiekt z powrotem w pamięci podręcznej.

Oryginalny kod wyglądał mniej więcej tak:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  final String key = "Data-" + email;
  SomeData[] data = (SomeData[]) StaticCache.get(key);

  if (data == null) {
      data = service.getSomeDataForEmail(email);

      StaticCache.set(key, data, CACHE_TIME);
  }
  else {
      logger.debug("getSomeDataForEmail: using cached object");
  }

  return data;
}

Więc, aby upewnić się, że tylko jeden wątek wywołał usługę sieciową, gdy obiekt w key wygasły, myślałem, że muszę zsynchronizować operację Cache get / set,i wydawało się, że użycie klucza cache będzie dobrym kandydatem do obiektu do synchronizacji (w ten sposób, wywołania tej metody dla poczty e-mail [email protected] nie będą blokowane przez wywołania metody do [email protected]).

Zaktualizowałem metodę, aby wyglądała tak:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

Dodałem również linie logowania dla rzeczy takich jak "przed blok synchronizacji", "wewnątrz bloku synchronizacji"," o opuszczeniu bloku synchronizacji " i " po blok synchronizacji", dzięki czemu mogłem określić, czy skutecznie synchronizowałem operację get / set.

Jednak wygląda na to, że to nie zadziałało. Moje logi testowe mają wyjście takie jak:

(wyjście logu to 'threadname' 'nazwa loggera ''wiadomość')
http-80-Processor253 jsp.w związku z tym, że nie jest to możliwe, nie jest to możliwe.]} http-80-Processor253 jsp.zobacz-stronę-getSomeDataForEmail: wewnątrz bloku synchronizacji
http-80-Procesor253 cache.StaticCache-get: object at key [[email protected]] wygasło
HTTP-80-Processor253 cache.StaticCache-get: key [[email protected]] zwracanie wartości [null]
http-80-Processor263 jsp.w związku z tym, że nie jest to możliwe, nie jest to możliwe.]} http-80-Processor263 jsp.zobacz-stronę-getSomeDataForEmail: wewnątrz bloku synchronizacji
HTTP-80-Processor263 cache.StaticCache-get: object at key [[email protected]] wygasło
HTTP-80-Processor263 cache.StaticCache-get: key [[email protected]] zwracanie wartości [null]
http-80-Processor131 jsp.w związku z tym, że nie jest to możliwe, nie jest to możliwe.]} http-80-Processor131 jsp.zobacz-stronę-getSomeDataForEmail: wewnątrz bloku synchronizacji
HTTP-80-Processor131 cache.StaticCache-get: object at key [[email protected]] wygasło
HTTP-80-Processor131 cache.StaticCache-get: key [[email protected]] powrót wartość [null]
http-80-Processor104 jsp.zobacz-stronę-getSomeDataForEmail: wewnątrz bloku synchronizacji
HTTP-80-Processor104 cache.StaticCache-get: object at key [[email protected]] wygasło
HTTP-80-Processor104 cache.StaticCache-get: key [[email protected]] zwracanie wartości [null]
http-80-Processor252 jsp.w związku z tym, że nie jest to możliwe, nie jest to możliwe.]} http-80-Processor283 jsp.view-page-getSomeDataForEmail: about to wprowadź blok synchronizacji
http-80-Processor2 jsp.w związku z tym, że nie jest to możliwe, nie jest to możliwe.]} http-80-Processor2 jsp.view-page-getSomeDataForEmail: Inside synchronization block

Chciałem zobaczyć tylko jeden wątek na raz wchodzący/wychodzący z bloku synchronizacji wokół operacji get / set.

Czy jest problem z synchronizacją na obiektach String? Myślałem, że klucz pamięci podręcznej będzie dobrym wyborem, ponieważ jest unikalny dla operacja, i mimo że final String key jest zadeklarowana w metodzie, myślałem, że każdy wątek otrzyma odniesienie do tego samego obiektu i dlatego synchronizuje się na tym pojedynczym obiekcie.

Co ja tu robię źle?

Update: po bliższym przyjrzeniu się logom, wydaje się, że metody z tą samą logiką synchronizacji, w których klucz jest zawsze taki sam, takie jak

final String key = "blah";
...
synchronized(key) { ...

Nie wykazują tego samego problemu współbieżności - tylko jeden wątek na raz wchodzi do bloku.

Aktualizacja 2 : dziękujemy wszystkim za pomoc! Zaakceptowałem pierwszą odpowiedź na temat intern()Ing Strings, która rozwiązała mój początkowy problem - gdzie wiele wątków wchodziło do zsynchronizowanych bloków, gdzie myślałem, że nie powinny, ponieważ key ' s miały tę samą wartość.

Jak zauważyli inni, używanie intern() do tego celu i synchronizacja na tych ciągach rzeczywiście okazuje się złym pomysłem - podczas testów JMeter przeciwko webapp symulować oczekiwane obciążenie, widziałem używany rozmiar sterty rosnąć do prawie 1GB w prawie 20 minut.

Obecnie używam prostego rozwiązania synchronizacji całej metody-ale ja naprawdę podobnie jak próbki kodu dostarczone przez martinprobst i MBCook, ale ponieważ mam około 7 podobnych metod getData() obecnie w tej klasie (ponieważ potrzebuje około 7 różnych kawałków danych z serwisu internetowego), nie chciałem dodawać prawie zduplikowanej logiki o uzyskiwaniu danych z sieci. i uwalnianie zamków do każdej metody. Ale to jest zdecydowanie bardzo, bardzo cenne informacje do wykorzystania w przyszłości. Myślę, że to są ostatecznie poprawne odpowiedzi na temat tego, jak najlepiej zrobić operację taką jak ten wątek-bezpieczne, i dałbym więcej głosów na te odpowiedzi, gdybym mógł!

Author: Vadzim, 2008-09-25

16 answers

Bez wkładania mojego mózgu w pełni do biegu, z szybkiego skanowania tego, co mówisz, wygląda to tak, jakbyś musiał intern () twoje struny:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

Dwa ciągi znaków o tej samej wartości niekoniecznie są tym samym obiektem.

Zauważ, że może to wprowadzić nowy punkt sporny, ponieważ głęboko w maszynie wirtualnej intern() może wymagać uzyskania blokady. Nie mam pojęcia, jak wyglądają nowoczesne maszyny wirtualne w tej dziedzinie, ale mam nadzieję, że są diabelsko zoptymalizowane.

Zakładam, że wiesz, że StaticCache nadal musi być bezpieczny dla wątków. Ale twierdzenie, że nie powinno być małe w porównaniu z tym, co byś miał, gdybyś blokował pamięć podręczną, a nie tylko klucz podczas wywoływania getSomeDataForEmail.

Odpowiedź na pytanie update :

Myślę, że to dlatego, że literał Łańcuchowy zawsze daje ten sam obiekt. Dave Costa zaznacza w komentarzu, że jest jeszcze lepiej: literalność zawsze daje kanoniczną reprezentację. Więc wszystkie literały ciągów z tym samym wartość gdziekolwiek w programie daje ten sam obiekt.

Edit

Inni wskazywali, że synchronizacja na łańcuchach intern jest naprawdę złym pomysłem - częściowo dlatego, że tworzenie łańcuchów intern jest dozwolone, aby spowodować ich istnienie w nieskończoność, a częściowo dlatego, że jeśli więcej niż jeden bit kodu w twoim programie synchronizuje się na łańcuchach intern, masz zależności między tymi bitami kodu, a zapobieganie blokadom lub innym błędom może być niemożliwe. niemożliwe.

Strategie unikania tego poprzez przechowywanie obiektu lock na łańcuch kluczowy są opracowywane w innych odpowiedziach podczas pisania.

Oto alternatywa - nadal używa pojedynczej blokady, ale wiemy, że będziemy potrzebować jednego z nich do pamięci podręcznej i tak, a mówiłeś o 50 wątkach, a nie 5000, więc może to nie być śmiertelne. Zakładam również, że wąskie gardło wydajności jest powolne blokowanie I / o w DoSlowThing (), które w związku z tym skorzysta na tym, że nie jest serializowana. Jeśli to nie jest wąskie gardło, to:

  • Jeśli procesor jest zajęty, to takie podejście może nie być wystarczające i potrzebujesz innego podejścia.
  • Jeśli procesor nie jest zajęty, a dostęp do serwera nie jest wąskim gardłem, to takie podejście jest przesadą i równie dobrze możesz zapomnieć zarówno o tym, jak i o blokowaniu na klucz, umieścić duży zsynchronizowany (StaticCache) wokół całej operacji i zrobić to w łatwy sposób.

Oczywiście to podejście wymaga przetestowania pod kątem skalowalności przed użyciem-niczego nie gwarantuję.

Ten kod nie wymaga, aby StaticCache był zsynchronizowany lub w inny sposób bezpieczny dla wątku. Należy to ponownie sprawdzić, jeśli jakikolwiek inny kod (na przykład zaplanowane oczyszczanie starych danych) kiedykolwiek dotknie pamięci podręcznej.

IN_PROGRESS jest fałszywą wartością-nie do końca czystą, ale kod jest prosty i oszczędza posiadanie dwóch hashtabli. Nie radzi sobie z przerywanym wydarzeniem, ponieważ Nie wiem, co Twoja aplikacja chce zrobić w tym przypadku. Również, jeśli DoSlowThing() konsekwentnie zawodzi dla danego klucza ten kod w jego obecnej postaci nie jest do końca elegancki, ponieważ każdy wątek będzie próbował go ponownie. Ponieważ Nie wiem, jakie są kryteria awarii i czy mogą być tymczasowe czy stałe, ja też się tym nie zajmuję, po prostu upewniam się, że wątki nie blokują się na zawsze. W praktyce możesz umieścić wartość danych w pamięci podręcznej, która wskazuje "niedostępne", być może z powodu i limitu czasu, kiedy ponowić próbę.

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

Every time anything is dodane do pamięci podręcznej, wszystkie wątki budzą się i sprawdzają pamięć podręczną (bez względu na to, jakiego klucza chcą), więc możliwe jest uzyskanie lepszej wydajności przy mniej kontrowersyjnych algorytmach. Jednak wiele z tych prac będzie miało miejsce podczas obfitego bezczynnego blokowania czasu procesora na I / O, więc może to nie być problem.

Jeśli zdefiniujesz odpowiednie abstrakcje dla pamięci podręcznej i powiązanej z nią blokady, zwracanych danych, atrapy in_progress i slow operacja do wykonania. Przetaczanie całości do metody w pamięci podręcznej może nie być złym pomysłem.
 36
Author: Steve Jessop,
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
2008-09-26 12:37:48

Synchronizacja na Intern 'D String może nie być dobrym pomysłem - poprzez internowanie go, ciąg zamienia się w obiekt globalny, a jeśli zsynchronizujesz się na tych samych intern' D string w różnych częściach aplikacji, możesz uzyskać naprawdę dziwne i zasadniczo nierozerwalne problemy z synchronizacją, takie jak blokady. To może wydawać się mało prawdopodobne, ale kiedy to się dzieje, masz naprawdę przerąbane. Z reguły Synchronizuj się tylko na obiekcie lokalnym, w którym masz całkowitą pewność, że żaden kod nie jest poza Twój moduł może go zablokować.

W Twoim przypadku możesz użyć zsynchronizowanej hashtable do przechowywania obiektów blokujących klucze.

Np.:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

Ten kod ma warunek race, w którym dwa wątki mogą umieścić obiekt w tabeli lock po sobie. To jednak nie powinno być problemem, ponieważ wtedy masz tylko jeden wątek wywołujący webservice i aktualizujący pamięć podręczną, co nie powinno być problemem.

Jeśli po jakimś czasie unieważniasz pamięć podręczną, należy sprawdzić, czy dane są null ponownie po pobraniu go z pamięci podręcznej, w lock != null case.

Alternatywnie i znacznie łatwiej, możesz zsynchronizować całą metodę wyszukiwania pamięci podręcznej ("getSomeDataByEmail"). Oznacza to, że wszystkie wątki muszą się zsynchronizować, gdy uzyskają dostęp do pamięci podręcznej, co może być problemem z wydajnością. Ale jak zawsze, najpierw wypróbuj to proste rozwiązanie i sprawdź, czy to naprawdę problem! W wielu przypadkach nie powinno tak być, ponieważ prawdopodobnie spędzasz znacznie więcej czasu przetwarzanie wyniku niż synchronizacja.

 24
Author: Martin Probst,
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
2008-09-25 16:17:17

Ciągi są nie dobrymi kandydatami do synchronizacji. Jeśli musisz zsynchronizować Łańcuch ID, można to zrobić za pomocą łańcucha do utworzenia mutex (Zobacz "synchronizing on an ID"). To, czy koszt tego algorytmu jest wart, zależy od tego, czy wywołanie usługi wiąże się z jakimkolwiek znaczącym I / o.

Także:

  • mam nadzieję, że StaticCache.metody get () i set () są threadsafe.
  • String.intern () jest kosztem (taka, która różni się w zależności od implementacji VM) i powinna być używana ostrożnie.
 9
Author: McDowell,
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
2008-12-03 13:34:13

Inni zasugerowali internowanie łańcuchów, i to zadziała.

Problem polega na tym, że Java musi trzymać internowane ciągi. Powiedziano mi, że robi to nawet jeśli nie posiadasz referencji, ponieważ wartość musi być taka sama następnym razem, gdy ktoś użyje tego ciągu. Oznacza to, że internowanie wszystkich ciągów może zacząć pochłaniać pamięć, co przy obciążeniu, które opisujesz, może być dużym problemem.

Widziałem na to dwa rozwiązania:

Możesz zsynchronizować na innym obiekcie

Zamiast wiadomości e-mail Utwórz obiekt, który przechowuje wiadomość e-mail (powiedzmy obiekt User), który przechowuje wartość wiadomości e-mail jako zmienną. Jeśli masz już inny obiekt, który reprezentuje daną osobę (powiedzmy, że wyciągnąłeś coś z bazy danych na podstawie jej adresu e-mail), możesz tego użyć. Implementując metodę equals i metodę hashcode, możesz upewnić się, że Java traktuje obiekty tak samo podczas wykonywania statycznej pamięci podręcznej.contains (), aby dowiedzieć się, czy dane są już w pamięć podręczna (będziesz musiał zsynchronizować na pamięci podręcznej).

Właściwie, możesz zachować drugą mapę dla obiektów do namierzenia. Coś takiego:

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

To uniemożliwi 15 pobrań na ten sam adres e-mail na jednym. Będziesz potrzebował czegoś, aby zapobiec pojawieniu się zbyt wielu wpisów na mapie emailLocks. Użycie LRUMapS z Apache Commons zrobiłoby to.

Będzie to wymagało pewnych poprawek, ale może to rozwiązać twój problem.

Użyj innego klucz

Jeśli chcesz znosić ewentualne błędy (Nie wiem, jak ważne jest to), możesz użyć hashcode łańcucha znaków jako klucza. ints nie muszą być internowani.

Podsumowanie

Mam nadzieję, że to pomoże. Gwintowanie jest zabawne, prawda? Możesz również użyć sesji, aby ustawić wartość oznaczającą "już pracuję nad znalezieniem tego" i sprawdzić, czy drugi (trzeci, n-ty) wątek musi spróbować utworzyć lub po prostu poczekać, aż wynik się pojawi w skrytce. Chyba miałem trzy propozycje.
 5
Author: MBCook,
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
2008-09-25 16:04:40

Możesz użyć narzędzi współbieżnych 1.5, aby zapewnić pamięć podręczną zaprojektowaną tak, aby umożliwić dostęp do wielu współbieżnych i pojedynczy punkt dodawania (tj. tylko jeden wątek kiedykolwiek wykonujący kosztowny obiekt "creation"):

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

Oczywiście, to nie obsługuje WYJĄTKÓW, jak chcesz, a pamięć podręczna nie ma wbudowanego eksmisji. Może jednak mógłbyś użyć go jako podstawy do zmiany klasy StaticCache.

 5
Author: oxbow_lakes,
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
2008-09-27 17:26:01

Twoim głównym problemem nie jest to, że może być wiele instancji ciągu o tej samej wartości. Głównym problemem jest to, że musisz mieć tylko jeden monitor, na którym można zsynchronizować dostęp do obiektu StaticCache. W przeciwnym razie wiele wątków może skończyć się jednoczesną modyfikacją StaticCache (chociaż pod różnymi kluczami), co najprawdopodobniej nie obsługuje jednoczesnej modyfikacji.

 2
Author: Alexander,
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
2008-09-25 15:39:28

Wywołanie:

   final String key = "Data-" + email;

Tworzy nowy obiekt przy każdym wywołaniu metody. Ponieważ ten obiekt jest tym, czego używasz do blokowania, a każde wywołanie tej metody tworzy nowy obiekt, to tak naprawdę nie synchronizujesz dostępu do mapy na podstawie klucza.

To wyjaśnia Twoją edycję. Gdy masz statyczny ciąg, to będzie działać.

Użycie intern() rozwiązuje problem, ponieważ zwraca łańcuch z wewnętrznej puli przechowywanej przez klasę String, która zapewnia, że jeśli dwa ciągi są równe, zostanie użyty ten w Puli. Zobacz

Http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern()

 2
Author: Mario Ortegón,
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
2008-09-25 15:46:21

Użyj porządnego frameworka buforowania, takiego jak ehcache .

Implementacja dobrej pamięci podręcznej nie jest tak łatwa, jak niektórzy uważają.

Odnośnie tego komentarza.intern() jest źródłem wycieków pamięci, co w rzeczywistości nie jest prawdą. Internowane ciągi zbierane śmieci,może to potrwać dłużej, ponieważ na niektórych JVM (SUN) są przechowywane w przestrzeni Perm, która jest dotykana tylko przez pełne GC.

 2
Author: kohlerm,
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
2008-10-08 08:48:10

Jest to dość późno, ale jest tu sporo błędnego kodu.

W tym przykładzie:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

Synchronizacja jest nieprawidłowo ustawiona. W przypadku statycznej pamięci podręcznej, która obsługuje API get/put, powinna być przynajmniej synchronizacja wokół operacji get i getIfAbsentPut, dla bezpiecznego dostępu do pamięci podręcznej. Zakres synchronizacji będzie sam cache.

Jeśli trzeba dokonać aktualizacji samych elementów danych, dodaje to dodatkowe warstwa synchronizacji, która powinna znajdować się na poszczególnych elementach danych.

SynchronizedMap może być używany zamiast jawnej synchronizacji, ale należy zachować ostrożność. Jeśli używane są złe interfejsy API (get I put zamiast putIfAbsent), operacje nie będą miały niezbędnej synchronizacji, pomimo użycia zsynchronizowanej mapy. Zauważ komplikacje wprowadzone przez użycie putIfAbsent: albo wartość put musi być obliczona nawet w przypadkach, gdy nie jest potrzebna (ponieważ put nie może wiedzieć, czy wartość put jest potrzebna, dopóki zawartość pamięci podręcznej nie zostanie zbadana), lub wymaga starannego użycia delegacji (np. użycie Future, które działa, ale jest nieco niedopasowane; patrz poniżej), gdzie wartość put jest uzyskiwana na żądanie w razie potrzeby.

Wykorzystanie kontraktów Futures jest możliwe, ale wydaje się raczej niezręczne, i być może trochę przesadzone. Przyszłe API jest podstawą dla operacji asynchronicznych, w szczególności dla operacji, które mogą nie zostać zakończone natychmiast. Angażowanie przyszłości bardzo pewnie dodaje warstwę tworzenia wątków -- dodatkowe prawdopodobnie niepotrzebne komplikacje.

Głównym problemem użycia Future do tego typu operacji jest to, że Future z natury wiąże się z wielowątkowością. Użycie Future, gdy nowy wątek nie jest konieczny, oznacza ignorowanie wielu maszyn przyszłości, co czyni go zbyt ciężkim API do tego zastosowania.

 1
Author: Thomas Bitonti,
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
2013-08-22 19:34:17

Oto bezpieczne, krótkie rozwiązanie Java 8, które wykorzystuje mapę dedykowanych obiektów blokady do synchronizacji:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Ma wadę, że klucze i Obiekty blokujące zachowują się na mapie na zawsze.

Można to zrobić tak:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key);
        }
    }
    return data;
}

Ale wtedy popularne klawisze będą stale ponownie umieszczane na mapie, a obiekty blokujące będą ponownie przydzielane.

Update: a to pozostawia możliwość race condition, gdy dwa wątki jednocześnie wejdą do zsynchronizowanej sekcji na ten sam klucz, ale z różnymi zamkami.

Więc może być bardziej bezpieczne i wydajne w użyciu wygasający Guava Cache :

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

Należy zauważyć, że zakłada się tutaj, że StaticCache jest bezpieczny dla wątku i nie będzie cierpiał na równoczesne odczyty i zapisy dla różnych kluczy.

 1
Author: Vadzim,
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-11-12 19:37:30

Dlaczego po prostu nie renderować statycznej strony html, która jest serwowana użytkownikowi i regenerowana co X minut?

 0
Author: MattW.,
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
2008-09-25 15:59:34

Sugerowałbym również całkowite pozbycie się konkatenacji strun, jeśli jej nie potrzebujesz.

final String key = "Data-" + email;

Czy są inne rzeczy/typy obiektów w pamięci podręcznej, które używają adresu e-mail, że potrzebujesz dodatkowych "danych -" na początku klucza?

Jeśli nie, to bym to zrobił

final String key = email;

I unikasz też tworzenia dodatkowych ciągów.

 0
Author: John Gardner,
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
2008-09-25 19:06:59

Inny sposób synchronizacji na obiekcie string:

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if(obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if(obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}
 0
Author: celen,
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
2016-10-12 02:33:27

W przypadku, gdy inni mają podobny problem, następujący kod działa, o ile mogę powiedzieć:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

W przypadku OP, będzie on używany w następujący sposób:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

Jeśli nic nie zostanie zwrócone z kodu zsynchronizowanego, metoda synchronize może być napisana w następujący sposób:

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}
 0
Author: ragnaroh,
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-11-26 20:25:46

To pytanie wydaje mi się nieco zbyt szerokie, a więc podsuwało równie szeroki zestaw odpowiedzi. Postaram się więc odpowiedzieć na pytanie z którego zostałem przekierowany, niestety to zostało zamknięte jako DUPLIKAT.

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

Po operacji (zewnętrznej) lock zostanie uzyskana (wewnętrzna) blokada, aby uzyskać wyłączny dostęp do mapy na krótki czas, a jeśli dany obiekt jest już na mapie, bieżący wątek będzie czekał, w przeciwnym razie wprowadzi nowy Condition do mapę, zwolnić (wewnętrzną) blokadę i kontynuować, a zamek (zewnętrzny) jest uważany za uzyskany. Operacja (zewnętrzna) unlock, najpierw pozyskanie (wewnętrznej) blokady, zasygnalizuje Condition, a następnie usunie obiekt z mapy.

Klasa nie używa współbieżnej wersji Map, ponieważ każdy dostęp do niej jest strzeżony przez pojedynczy (wewnętrzny) zamek.

Proszę zauważyć, że semantyczna metody lock() tej klasy jest inna niż ReentrantLock.lock(), powtarzane lock() wywołania bez sparowania unlock() zawiesi bieżący wątek w nieskończoność.

Przykład użycia, który może mieć zastosowanie w danej sytuacji, opisany op

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }
 0
Author: igor.zh,
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-05-11 18:31:56

Dodałem małą klasę lock, która może blokować / synchronizować na dowolnym kluczu, w tym na łańcuchach.

Patrz implementacja dla Java 8, Java 6 i mały test.

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

Public class DynamicKeyLock implementuje blokadę { private final Static ConcurrentHashMap locksMap = new ConcurrentHashMap(); private final t key;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Test:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}
 0
Author: AlikElzin-kilaka,
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-05-27 13:33:44