Java, współbieżność dla praktyków

Twórz bezpieczne i wydajne aplikacje wielowątkowe Chcesz podnieść wydajność swoich aplikacji? Planujesz stworzenie syste

845 65 20MB

Polish Pages [183] Year 2007

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Java, współbieżność dla praktyków

Table of contents :
Przedmowa (9)
Rozdział 1. Wprowadzenie (13)
1.1. (Bardzo) krótka historia współbieżności (13)
1.2. Zalety wątków (15)
1.3. Ryzyka związane z wątkami (18)
1.4. Wątki są wszędzie (21)
Część I Podstawy (25)
Rozdział 2. Wątki i bezpieczeństwo (27)
2.1. Czym jest bezpieczeństwo wątkowe? (29)
2.2. Niepodzielność (31)
2.3. Blokady (35)
2.4. Ochrona stanu za pomocą blokad (39)
2.5. Żywotność i wydajność (41)
Rozdział 3. Współdzielenie obiektów (45)
3.1. Widoczność (45)
3.2. Publikacja i ucieczka (51)
3.3. Odosobnienie w wątku (54)
3.4. Niezmienność (58)
3.5. Bezpieczna publikacja (61)
Rozdział 4. Kompozycja obiektów (67)
4.1. Projektowanie klasy bezpiecznej wątkowo (67)
4.2. Odosobnienie egzemplarza (71)
4.3. Delegacja bezpieczeństwa wątkowego (76)
4.4. Dodawanie funkcjonalności do istniejących klas bezpiecznych wątkowo (82)
4.5. Dokumentowanie strategii synchronizacji (86)
Rozdział 5. Bloki budowania aplikacji (89)
5.1. Kolekcje synchronizowane (89)
5.2. Kolekcje współbieżne (94)
5.3. Kolejki blokujące oraz wzorzec producenta i konsumenta (97)
5.4. Metody blokujące i przerywane (102)
5.5. Synchronizatory (104)
5.6. Tworzenie wydajnego, skalowalnego bufora wyników (112)
Podsumowanie części I (117)
Część II Struktura aplikacji współbieżnej (119)
Rozdział 6. Wykonywanie zadań (121)
6.1. Wykonywanie zadań w wątkach (121)
6.2. Szkielet Executor (125)
6.3. Znajdowanie sensownego zrównoleglenia (132)
Podsumowanie (141)
Rozdział 7. Anulowanie i wyłączanie zadań (143)
7.1. Anulowanie zadań (144)
7.2. Zatrzymanie usługi wykorzystującej wątki (158)
7.3. Obsługa nietypowego zakończenia wątku (167)
7.4. Wyłączanie maszyny wirtualnej (170)
Podsumowanie (173)
Rozdział 8. Zastosowania pul wątków (175)
8.1. Niejawnie splecione zadania i strategie wykonania (175)
8.2. Określanie rozmiaru puli wątków (178)
8.3. Konfiguracja klasy ThreadPoolExecutor (179)
8.4. Rozszerzanie klasy ThreadPoolExecutor (187)
8.5. Zrównoleglenie algorytmów rekurencyjnych (188)
Podsumowanie (195)
Rozdział 9. Aplikacje z graficznym interfejsem użytkownika (197)
9.1. Dlaczego graficzne interfejsy użytkownika są jednowątkowe? (197)
9.2. Krótkie zadanie interfejsu graficznego (201)
9.3. Długie czasowo zadania interfejsu graficznego (203)
9.4. Współdzielone modele danych (208)
9.5. Inne postacie podsystemów jednowątkowych (209)
Podsumowanie (210)
Część III Żywotność, wydajność i testowanie (211)
Rozdział 10. Unikanie hazardu żywotności (213)
10.1. Blokada wzajemna (213)
10.2. Unikanie i diagnostyka blokad wzajemnych (223)
Podsumowanie (228)
Rozdział 11. Wydajność i skalowalność (229)
11.1. Myślenie na temat wydajności (229)
11.2. Prawo Amdahla (233)
11.3. Koszta wprowadzane przez wątki (237)
11.4. Zmniejszanie rywalizacji o blokadę (240)
11.5. Przykład - porównanie wydajności obiektów Map (250)
11.6. Redukcja narzutu przełączania kontekstu (251)
Podsumowanie (253)
Rozdział 12. Testowanie programów współbieżnych (255)
12.1. Testy sprawdzające poprawność (256)
12.2. Testowanie wydajności (268)
12.3. Unikanie pomyłek w testach wydajności (273)
12.4. Testy uzupełniające (278)
Podsumowanie (281)
Część IV Techniki zaawansowane (283)
Rozdział 13. Blokady jawne (285)
13.1. Interfejs Lock i klasa ReentrantLock (285)
13.2. Rozważania na temat wydajności (290)
13.3. Uczciwość (291)
13.4. Wybór między synchronized i ReentrantLock (293)
13.5. Blokady odczyt-zapis (294)
Podsumowanie (297)
Rozdział 14. Tworzenie własnych synchronizatorów (299)
14.1. Zarządzanie zależnością od stanu (299)
14.2. Wykorzystanie kolejek warunków (306)
14.3. Jawne obiekty warunków (314)
14.4. Anatomia synchronizatora (316)
14.5. Klasa AbstractQueuedSynchronizer (318)
14.6. AQS w klasach synchronizatorów pakietu java.util.concurrent (321)
Podsumowanie (324)
Rozdział 15. Zmienne niepodzielne i synchronizacja nieblokująca (325)
15.1. Wady blokowania (326)
15.2. Sprzętowa obsługa współbieżności (327)
15.3. Klasy zmiennych niepodzielnych (331)
15.4. Algorytmy nieblokujące (335)
Podsumowanie (342)
Rozdział 16. Model pamięci Javy (343)
16.1. Czym jest model pamięci i dlaczego ma mnie interesować? (343)
16.2. Publikacja (350)
16.3. Bezpieczeństwo inicjalizacji (355)
Podsumowanie (356)
Dodatki (357)
Dodatek A Adnotacje związane ze współbieżnością (359)
A.1. Adnotacje dla klas (359)
A.2. Adnotacje pól i metod (360)
Dodatek B Bibliografia (361)
Skorowidz (365)

Citation preview

W sp ó łb ie żn o ść d la p r a k t y k ó w

:£W.tt»Pobrimn
do b u fo n K fl .■ jł ■

jE IG H T -9 0 W !D T H **1 20 > ^ Śte H !l> « B p W IO T H » 1 2 a > < / T D >

< S E L E C T N A M E = "c n c h c d " ó n C lia n gc -= ,*io n d C a ch cd (ih ls.fo fm )">

Poznaj zasad y tworzenia aplikacji wielowątkowych ia Jak projektow ać aplikacje wielowątkowe? M W jaki sposób unikać blokow ania wątków? ES Jak testować aplikacje wielowątkowe?

ADDISON-WESLEY

Helion

'§^1 ~ m id

Brian Goetz, Tim Peierls, Joshua Bloch Joseph Bowbeer, David Holmes, Doug Lea

Podziękow ania Książka powstała w wyniku procesu projektowego związanego z pakietem j a v a . u t i l . «•concurrent tworzonego na podstawie Java Community Process JSR 166 w celu dołą­ czenia do w ydania Java 5.0. W iele osób w spółtw orzyło JSR. 166. W szczególności chcem y podziękow ać M artinow i Buchholzowi za w ykonanie całej pracy związanej z przeniesieniem kodu do JDK. D ziękujemy też wszystkim czytelnikom listy maiiingowej c o n c u r re n c y - in te re s t, którzy podsuw ali w łasne pom ysły i uw agi na tem at szkiców interfejsu programistycznego. K siążka bezsprzecznie została w zb o g a co n a su g estia m i i p o m o c ą niew ielkiej armii recenzentów, doradców, osób dopingujących i domorosłych krytyków. Chcielibyśmy podziękować następującym osobom: Dion Aimaer, Tracy Bialik, Cindy Bloch, M artin Buchholz, Paul Christmann, C liff Click, Stuart Halloway, David I-Iovemeyer, Jason H unter, M ichale H unter, Jerem y H ylton, H einz K abutz, R obert K uhar, R am nivas Laddad, Jared Levy, N icole Lewis, Victor Luchangco, Jeremy M anson, Paul Martin, Berna M assingill, M ichael M aurer, Ted N ew ard, K irk Pepperdine, Bill Pugh, Sam Pul lara, Russ Rufer, Bill Scherer, Jeffrey Siegal, Bruce Tate, Gil Tone, Paul Tyma. D ziękujem y też członkom grupy Silicon V alley Patterns. Group, z którymi przepro­ w adziliśm y wiele interesujących konwersacji technicznych. Otrzymaliśmy też od nich wsparcie, dzięki którem u książka m ogła stać się lepsza. Jesteśmy szczególnie wdzięczni Cliflbvvi B iffle’owi, Barry’emu Hayesowi, Dawidowi Kurzyniecowi, Angelice Langer, Doronowi Rajwanowi i Billowi Venncrsowi za recenzje całego rękopisu z pełną szczegółowością, znajdowanie błędów w przykładach i za suge­ stie dotyczące poprawienia książki. D ziękujem y ICatrinic A very za doskonalą pracę redakcyjną i Rosemary Simpson za w ykonanie skorow idzu w bardzo krótkim term inie. A m iem u Dcwarowi dziękujem y za wykonanie ilustracji. Dziękujemy całemu zespołowi z Addison-Wesley, który pomógł przekuć wizję na rze­ czyw istą książkę.. A nn Sellers uruchom iła cały projekt, natomiast Greg Doench prze­ prow adził go płynnie aż do sam ego końca. E lizabeth Ryan kierow ała procesem jej powstawania. D odatkow o dziękujem y tysiącom program istów, którzy pośrednio przyczynili się do powstania oprogramowania umożliwiającego napisanie tej książki, włączając w to TEX, LA TEX , A dobe Acrobat, pic, grap, A dobe Illustrator, Perl, Apache Ant, IntclliJ Idea, GNU emaes, Subversion, TortoiseSVN i oczywiście platformy Java i jej bibliotek.

Spis treści Przedmowa Rozdział 1 .

Część I

;9

W prowadzenie .............................................................................................................. 1 3 1.1. (Bardzo) krótka historia współbieżności .................................................................. ¡3 ! .2. Zalety wątków ............................................................................................................ 15 1.3. Ryzyka związane z wątkami...................................................................................... 18 1.4. Wątki są wszędzie ....................................................................................................... 21

Podstawy ....................................................................................... 25

27 Rozdział 2 . Wątki i b ezp ieczeń stw o ........................................................ 2.1. Czym jest bezpieczeństwo wątkowe?......................................................................... 29 2.2. Niepodzielność ...........................................................................................................31 2.3. Blokady.........................................................................................................................35 2.4. Ochrona stanu za pomocą blokad................................................................................39 2.5. Żywotność i wydajność ...............................................................................................41 Rozdział 3 . W spółdzielenie ob iek tów .............................. 45 3.1. Widoczność ................................................................................................................. 45 3.2. Publikacja i ucieczka ................................................................................................... 51 3.3. Odosobnienie w wątku ........................................... ...................................................54; 3.4. Niezmienność .................................................................................................... 58. 3.5. Bezpieczna publikacja ................................................................................................61 1 Rozdział 4 . Kompozycja ob iek tów ................................................................... 67 4.1. Projektowanie klasy bezpiecznej wąlkowo ................................................................ 67, 4.2. Odosobnienie egzemplarza.......................................................................................... 71 4.3. Delegacja bezpieczeństwa wątkowego....................................................................... 76 4.4. Dodawanie funkcjonalności do istniejących kJas bezpiecznych wątkowo ...............82 4.5. Dokumentowanie strategii synchronizacji.................................................................. 86 Rozdział 5. Bloki budowania aplikacji ................................................................................... 8 9 5.1. Kolekcje synchronizowane.......................................................................................... S9 5.2. Kolekcje współbieżne .................................................................................................. 94 5.3. Kolejki blokujące oraz wzorzec producenta i konsumenta ....................................... 97 5.4. Metody blokujące i przerywane ................................................................................102 5.5. Synchronizatory........................................................................................................ 104 5.6. Tworzenie wydajnego, skalowalnego bufora wyników ......................................... 112 Podsumowanie części 1 ....................... 117

6

Java. W spółbieżność dla praktyków

Spis treści

7

Część 11

Struktura aplikacji współbieżnej ........................................ 119

Część IV Techniki zaawansowane .............................................

Rozdział 6 .

W ykonywanie zadań .............................................................................................. 1 2 1 6.1. Wykonywanie zadań w wątkach ............................................................................. 12 i 6.2. Szkielet Executor ...................................................................................................... 125 6.3. Znajdowanie sensownego zrównoleglenia ..... 132 Podsumowanie............. 141

Rozdział 7.

Anulowanie i w yłączanie zadań ........................................................................ 1 4 3 7.1. Anulowanie zadań .................................................................................................... 144 7.2. Zatrzymanie usługi wykorzystującej wątki ............... 158 7.3. Obsługa nietypowego zakończenia w ątku.................................................................167 7.4. Wyłączanie maszyny wirtualnej .............................................................................. 170 Podsumowanie.................................................................................................................. 173

Rozdział 1 3 . Blokady jaw ne ................................................................................ 13.1. Interfejs Lock i klasa RecntrantLock............................................... '.......................285 13.2. Rozważania na temat wydajności ............................................................................ 290 13.3. Uczciwość................................................................ 13.4. Wybór między synchronized i RecntrantLock ....................................................... 293 13.5. Blokady odczyt-zapis ...............................................................................................294 Podsumowanie................................................................................................................... 297

Rozdział 8.

Rozdział 9 .

Część III

Z astosow an ia pul w ątk ów .................................................................................. 1 7 5 8.1. Niejawnie splecione zadania i strategie wykonania ................................................ 175 8.2. Określanie rozmiaru puli wątków............................................................................ 178 8.3. Konfiguracja klasy ThreadPoolExecutor................................................................. 179 8.4. Rozszerzanie klasy ThreadPoolExecutor ................................................................ 187 8.5. Zrównoleglenie algorytmów rekurencyjnych.......................................................... 188 Podsumowanie................................................................................................................. 195 Aplikacje z graficznym interfejsem u ży tk o w n ik a...................... 197 9.1. Dlaczego graficzne interfejsy użytkownika sąjednowątkowe? ............................. 197 9.2. Krótkie zadanie interfejsu graficznego ...................... 201 9.3. Długie czasowo zadania interfejsu graficznego .......................................................203 9.4. Współdzielone modcletdanych ................................ 208 9.5. Inne postacie podsystemów jednowątkowych........................................................209 Podsumowanie ........................................................................................................ 210

283

Rozdział 1 4 . Tworzenie w łasnych synchronizatorów ...........................................................2 9 9 14.1. Zarządzanie zależnością od stanu ............................................................................ 299 14.2. Wykorzystanie kolejek warunków ..........................................................................306 14.3. jawne obiekty warunków ........................................................................................ 314 14.4. Anatomia synchronizatora ........................................................................................316, 14.5. Klasa AbstractQueuedSynehronizer........................................................................... 318 14.6. AQS w klasach synchronizatorów pakietu java.util.concurrent ............. 321 Podsumowanie................................................................................................................... 324 i Rozdział 1 5 . Zm ienne niepodzielne i synchronizacja n ie b lo k u ją c a ......... ....... . 3 2 5 15.1. Wady blokowania .....................................................................................................326 15.2. Sprzętowa obsługa wspólbieżności .........................................................................327 15.3. Klasy zmiennych niepodzielnych .....................................................:..................... 331 15.4. Algorytmy nieblokującc .......................................................................................... 335 Podsumowanie................................................................... ........... ...................................342 Rozdział 1 6 . Model pam ięci J a v y ..................................... 343 16.1. Czym jest model pamięci i dlaczego mamnie interesować? 343 ' 16.2. Publikacja ................................................................................................................. 350 16.3. Bezpieczeństwo inicjalizacji.................................................................................... 355 Podsumowanie..................................................................................................... 356

Żywotność, wydajność i te s to w a n ie ................................... 2 1 1

Rozdział 1 0 . Unikanie hazardu żyw otn ości ....................... 213 10.1. Blokada wzajemna................................................................................................... 213 10.2. Unikanie i diagnostyka blokad wzajemnych .......................................................... 223 Podsumowanie...................................................................................................................228 Rozdział 1 1 . W ydajność i sk a lo w a ln o ść ................................................................................... 2 2 9 11.1. Myślenie na temat wydajności ................. 229 11.2. Prawo Amdahla ........................................................................................................233 11.3. Koszta wprowadzane przez wątki .......................................................................... 237 11.4. Zmniejszanie rywalizacji o blokadę .................................................................. 240 11.5. Przykład — porównanie wydajności obiektów Map ........ :.................................250 11.6. Redukcja narzutu przełączania kontekstu................................ 251 Podsumowanie...................................................................................................................253 Rozdział 1 2 . T estow an ie program ów w spółbieżnych ................................................... 2 5 5 12.1. Testy sprawdzające poprawność ............................................................................ 256 12.2. Testowanie wydajności......................................................................................... . 268 . 12.3. Unikanie pomyłek w testach wydajności ............................................................... 273 12.4. Testy uzupełniające ..............................................................................................278 Podsumowanie...................................................................................................................281

Dodatki ........... D odatek A

D odatek B

357

A dnotacje zw iązane z e w sp ó łb ieżn o ścią ........................................................ 3 5 9 A J . Adnotacje dla k las......................................................................................................359 A.2. Adnotacje pól i m etod....................................................................................... 360 Bibliografia ......................................................... Skorowidz

361; 365

Java. W spółbieżność dla praktyków

Przedmowa W czasie pisania tego tekstu procesory w ielordzeniowe stają się na tyle niedrogie, że trafiają do domowych systemów komputerowych średniej klasy. Nieprzypadkowo wiele zespołów projektow ych otrzym uje coraz to więcej inform acji o błędach związanych z wątkami w swoicli projektach. W jednym z komentarzy na forum programistów NetBeans napisanym przez głównego programistę aplikacji wskazano, że pojedyncza klasa była ju ż 14 razy poprawiana pod kątem problemów dotyczącycli wątkowości. Dion Almaer, wcześniej redaktor TheServerSide, w ostatnim czasie umieścił w blogu infor­ mację (po bardzo długiej i trudnej „w ypraw ie” w poszukiwaniu błędu), że większość programów Javy jest tak naszpikowana błędami współbieżności, że działa tylko „przez przypadek”. N iestety, tworzenie, testow anie i debugow anie program ów wielowątkowych p o trafi; być wyjątkowo trudne, poniew aż błędy w spółbieżności nie pojaw iają się w sposób i przewidywalny. Z drugiej strony, gdy się jed n ak pojawią, to najczęściej w najmniej ; odpowiednim momencie — w systemie produkcyjnym przy dużym obciążeniu. Jednym z wyzwań dotyczących tworzenia programów działających współbieżnie w Javie je st pogodzenie różnic w funkcjach oferowanych przez platformę i sposobie myślenia o działaniu współbieżnym twórców aplikacji. Język zapewnia m echanizm y niskopo-, ziomowe, na przykład synchronizację i oczekiwanie warunkowe, ale należy je konse­ kwentnie stosować w implementacji protokołów i strategii na poziomie aplikacji. Bez tych strategii zbyt łatwo napisać aplikację, która kom piluje się i w ydaje się działać poprawnie, choć tak naprawdę jest uszkodzona. W iele istniejących książek na temat współbieżności za bardzo skupia się na mechanizmach niskopoziomowych i interfej­ sach API, pomijając wzorce i strategie na poziomic projektu. Java 5.0 stanowi ogromny postęp w projektowaniu aplikacji wielowątkowych, bo do­ starcza zarówno komponenty wysokiego poziomu, ja k i dodatkowe mechanizmy niskopoziomowe, które początkującym i profesjonalistom ułatw iają tworzenie aplikacji współbieżnych. Autorzy są członkami JCP Expert Group, która tworzyła tc elementy. Poza opisem zachow ania i podstaw ow ych funkcji przedstaw iam y rów nież w zorce projektowe i scenariusze użycia decydujące o wprowadzeniu tych mechanizmów do bibliotek platformy.

Java. W spółbieżność dla praktyków

10

Naszym celem jest przedstawienie czytelnikom reguł projektowania i modeli mentalnych, które uproszczą — i zw iększą przyjemność — tworzenie poprawnych, wysoce wy­ dajnych klas i aplikacji w spółbieżnych w Javie.

Przedmowa

11

Rozdział 4. opisuje techniki łączenia klas bezpiecznych wątlcowo w większe klasy również zapewniające bezpieczeństwo pod kątem wątków. Rozdział 5. opisuje bloki budujące współbieżność -— kolekcje bezpieczne w ątkow o i synchronizatory — znaj-t dujące się w bibliotekach platformy.

Mam nadzieję, że z tą książką nie będziesz się nudził. Brian Goetz WiUiston, VT m arzec 2006

Jak korzystać z książki? Aby rozwiązać problem niedopasowania poziomów między mechanizmami niskopoziomowymi a wymaganymi strategiami poziomu projektowego, przedstawiamy uprosz­ czony zbiór reguł pisania program ów współbieżnych. Eksperci m ogą przeczytać te reguły i powiedzieć: „Hmm, to nic do końca praw da — klasa C je st bezpieczna pod kątem w ątków , naw et jeśli lamie regułę R” . Choć m ożna pisać popraw ne aplikacje łamiące przedstawione reguły, wymaga to bardzo dobrej znajomości szczegółów dzia­ łania m odelu pam ięciow ego Javy, a chcem y, by programiści mogli pisać poprawne programy bez zaznajam iania się z tą szczegółow ą wiedzą. Stałe przestrzeganie wska­ zanych uproszczonych reguł zapewnia poprawne i łatwe w konserwacji programy.

S tru k tu ra aplikacji współbieżnej. Część Ił (rozdziały od 6. do 9.) opisuje, w jaki sposób wykorzystać wątki do poprawy przepływności i szybkości odpowiedzi aplikacji współ­ bieżnych. Rozdział 6. skupia się na identyfikacji zadań równoległych i wykonywaniem ich w szkielecie w ykonaw czyni zadań. Rozdział 7. om aw ia rozw iązania dotyczące przekonywania wątków i zacłań, by zakończyły się wcześniej niż w typowej sytuacji.: To, w jak i sposób program radzi sobie z anulow aniem zadań, często stanow i jeden z głównych czynników w skazujących, czy aplikacja w spółbieżna je st bardzo dobra czy ledwie zipie. Rozdział 8. opisuje niektóre bardziej zaawansowane szkielety wyko­ nywania zadań. Rozdział 9. skupia się na technikach poprawy szybkości odpowiedzi podsystemów jednowątkowych. Żyw otność, w yd ajn o ść i testow anie. C zęść III (rozdziały od 10. do 12.) opisuje spo-; soby upewniania się, że programy współbieżne rzeczywiście wykonują to, czego się od nich spodziewamy, i czy robią to odpowiednio szybko. Rozdział 10. omawia, w jaki; sposób uniknąć błędów żywotności, które m ogą uniem ożliw ić aplikacji czynienie p o -f stępów. Rozdział 11. opisuje techniki poprawy wydajności i skalowalności wspólbież-j nego kodu. Rozdział 12. om awia techniki testow ania w spółbieżnego kodu zarówno; pod względem poprawności, ja k i wydajności.

\

Zakładamy, że Czytelnik zna pew ne podstawowe mechanizmy zapewniania wspótbieżności w Javie. N iniejsza książka nie stanow i w prow adzenia do w spółbieżności — w tym celu wystarczy przeczytać rozdział związany z wątkami w dowolnej, ogólnej książce na temat Javy, na przykład w The Ja va P ro g ra m m in g L a n g u a g e (Arnold i in., 2005). N ie zawiera też pełnej encyklopedycznej wiedzy na temat całej współbieżności — w tym celu lepiej sięgnąć po książkę C oncurrent P rogram m ing in Java (Lea, 2000). O feruje ona raczej praktyczne reguły projektow ania, które pom agają program iście w trudnym procesie tworzenia bezpiecznych i wydajnych klas współbieżnych. W razie potrzeby wskazujemy odpowiednie podrozdziały w książkach The Ja v a P ro g ra m m in g Language , C oncurrent P rogram m ing in Java, The Java Language Specification (Gosling i in., 2005) i Effective Java (Bloch, 2001), używając odnośników [JPL], [CPJ], [JLS] i [E.IJ. Po rozdziale w prow adzającym książka została podzielona na cztery części. P odstaw y. Część I (rozdziały od 2. do 5.) skupiają się na podstaw owych aspektach współbieżności i bezpieczeństwa wątków oraz sposobach tworzenia klas bezpiecznych pod kątem wątków na podstawie podstawowych bloków dostępnych w bibliotece klas. Podsum ow anie najw ażniejszych reguł przedstaw ionych w pierwszej części książki znajduje się na stronie 110. Rozdziały 2. i 3. stanow ią podstaw ę całej książki. Znajdują się tam niemal wszystkie zasady dotyczące unikania hazardu we współbieżności, konstrukcji klas bezpiecznych pod kątem wątków i weryfikacji tego bezpieczeństwa. Czytelnicy preferujący „praktykę” niż „teorię” m ogą od razu przejść do części 11, ale powinni wrócić do tych dwóch roz­ działów przed rozpoczęciem pisania współbieżnego kodu!

Techniki zaaw ansow ane. Część IV (rozdziały od 13. do 16.) opisuje tematy interesujące ' jedynie bardziej doświadczonych programistów: jaw ne blokady, zmienne niepodzielne,; algorytmy niebiokujące i tworzenie własnych synchronizatorów.

Przykładowy kod Choć wiele przedstawianych koncepcji ogólnych m ożna zastosow ać w wersjach Javy : wcześniejszych niż 5.0 łub nawet w innych językach programowania, większość przyT kładów (i wszystkie informacje o modelu pamięci Javy) zakładają stosowanie wersji 5.0 lub.nowszej. Niektóre przykłady m ogą w ykorzystyw ać elementy bibliotek dodane dopiero w Javie 6. Przykładowy kod został maksymalnie odchudzony, by zawierał tylko najistotniejsze, kwestie. Pełna w ersja przykładow ego kodu i dodatkow e m ateriały dostępne są pod adresem.ftp ://ftp.helion.pl/przyklacly/javw sp.zip. Przykładowy kod należy do jednej z trzech kategorii: „dobrego przykładu”, „niezbyt dobrego przykładu” i „złego przykładu”. Dobre przykłady przedstaw iają techniki, które warto naśladować. Złe ilustrują podejścia, których zdecydowanie nie należy powielać. Zaw ierają dodatkowo ikonę pana Y uka1, by wskazać, że kod je st „toksyczny” (patrz Pan Yuk jest zastrzeżonym znakiem towarowym Szpitala Dziecięcego w Pittsburghu. Został użyty za pozwoleniem.

Java. W spółbieżność dla praktyków

12

listing 1.). N iezbyt dobre przykłady zaw ierają techniki, które niekoniecznie są z le , ale byw ają delikatne, ryzykowne lub nie działają wydajnie. Zostały oznaczone panem M ożna Lepiej (listing 2.). Listing 1 . Zly sposób sortow ania listy. N ie rób tak _____________________________ p u b lic l i s t ) / / Nigdy nie zwraca zlej odpowiedzi (dobrej też nie). S yste m .e xit(0 );

{

} Listing 2 . N ieoptym alny sposób sortow ania listy _______________________________ p ub lic l i s t ) } fo r ( in t NO ; i +getNext()

j ; Diagramy takie jak*, n a - r y s u n k u .i.l -przedstawiają jeden; z. możliwych? scenariuszy; i a ■ synchronizacji, program je s t zepsuty. .Istnieją trzy sposoby je go naprawy: • «.ą

i }

1 )

♦ uniknąć współtlzielenia zmiennej przez wiele, wątków, :

-

j

♦ uczynić zmienną niemożliwą do modyfikacji,

,

użyć synchronizacji przy wszystkich dostępach.do zmiennej.r-u>Av-;r:-:-.--.:;-

}.

-

,

j

jeśli nie rozw ażałeś w spółbieżnego dostępu w trakcie projektowania klasy, niektóre j z podejść m ogą wymagać dużego nakładu środków, więc naprawa błędu często nie je st j; tak tryw ialna, ja k m ogłoby się wydawać. Z n a c zn ie ła tw iej od p o czą tk u p la n o w a ć { klasę bezpieczną w ąlk ow o niż później dostosow yw ać ją do takiego bezpieczeństw a.

!,

W dużym programie w ykrycie w szystkich m iejsc, w których kilka wątków korzysta z tej samej zm iennej, nie je st proste. Na szczęście podstaw ow e techniki obiektow e pomagające pisać dobrze zorganizow ane i łatwe w utrzymaniu klasy — hermetyzacja i ukrywanie danych — pom agają rów nież w bezpieczeństw ie wątkowym. Im mniej kodu ma dostęp do konkretnej zm iennej, tym łatwiej zapew nić, iż w szystkie użycia będą poprawnie synchronizowane, i prościej wskazać wszystkie momenty korzystania ze zm iennej. Język Java nie w ym usza herm etyzacji stanu, więc nic nie stoi na przeszkodzie, by stan przechowywać w polach publicznych (nawet w tych statycznych) lub też udostępniać referencję do obiektu wewnętrznego. Z drugiej strony pamiętaj, że im. bardziej hermetyczny jest stan programu, tym łatwiej zapewnić i w przyszłości utrzym ać bezpieczeństwo wątkowe.

i i j

j. l\

i]

- ;;.-:W trakcie: projektowania klas bezpiecznych .pod; kątem wątków; dobrymi >przyjacióimi ui ; •• okazują się podstawowe techniki obiektowe: ,hermetyzacjap blokowanie; zmiarni jasne J ; określenie niezmienników. . ... J '

"i

B yw ają sytuacje, w których dobre techniki obiektowe leżą w sprzeczności z rzeczywistymi wymaganiami, bo osiągnięcie odpowiedniej wydajności lub zgodności wstecz. w ym aga pośw ięcenia niektórych zasad dobrego projektowania. Czasem abstrakcji? i herm etyzacja leżą w sprzeczności z w yd ajn o ścią— choć nie tak często, ja k wydaje się wielu programisto!!!. Zawsze jednak lepiej napisać poprawny kod, a dopiero później go przyspieszać. Nawet wtedy optymalizuj system tylko w sytuacji, gdy testy wydajności i narzucone wymagania to wymuszają, a te same testy potwierdzają wzrost wydajności po wprowadzeniu popraw ek1. ’ We współbieżnym kodzie szczególnie mocno warto zastanawiać się nad potrzebą każdej optymalizacji. Ponieważ błędy wspóibieżno.śei trudno powtórzyć w środowisku testowym, korzyść z niewielkiej optymalizacji rzadko używanego kodu na niewiele się zda, jeśli spowoduje jednocześnie w pewnych sytuacjach blącl działania programu.

; ‘ i ■ . :

29

Jeśli naprawdę musisz w pewnym miejscu uniknąć hermetyzacji, nie wszystko stracone. N aw et w tedy program da się odpow iednio zabezpieczyć, choć będzie to znacznie trudniejsze. Co w ięcej, bezpieczeństw o w ątkow e takiego program u będzie bardziej icruche, zw iększając tym sam ym nie tylko koszt i ryzyko czasu tworzenia programu, ale również koszt i ryzyko jego konserwacji. Do tej pory term iny „klasa bezpieczna w ąlkow o” i „program bezpieczny wątlcowo” pojawiały się niem al zamiennie. C zy bezpieczny pod kątem w ątków program składa się w yłącznie z odpow iednio zabezpieczonych ldas? N iekoniecznie — program za­ wierający tylko ldasy bezpieczne wątlcowo może nie być bezpieczny pod kątem wątków, natomiast program składający się po części z klas niezabezpieczonych m oże’być bez­ pieczny wątlcowo. Tę kw estię d o ty czącą kom pozycji klas bezpiecznych wątlcowo omawia rozdział 4. W każdym przypadku pojęcie klasy bezpiecznej w ątkow o m a sens tylko wtedy, gdy klasa ta hermetyzuje własny stan. Choć sam term in stosuje się kodu, tak naprawdę dotyczy stan u . M a zastosowanie jedynie dla całego ciała kodu hermety­ zującego stan, niezależnie, czy jest to pojedyncza klasa czy cały program.

2.1. Czym je s t b ezp ieczeń stw o w ątkow e? Zdefiniowanie bezpieczeństwa wątkowego okazuje się trudne. Bardziej formalne próby jego wyrażenia są tak złożone, że nie oferują praktycznych wskazówek ani intuicyjnego pojęcia tematu. W iele nieformalnych opisów stanowi błędne koło. Szybkie wyszukanie definicji w wyszukiwarce Google zw róciło kilka „definicji”: .. .może zostać wywołany przez wiele wątków programu bez niechcianych interakcji między tymi wątkami. .. .może zostać wywołany przez więcej niż jeden w ątek w danym momencie bez przeprowadzania jakichkolw iek dodatkowych działań po stronie kodu wywołującego. Przy tego rodzaju definicjach nietrudno się dziwić, że bezpieczeństwo wątkowe stwarza tyle problemów! Ich opis brzmi bardzo tajem niczo: „klasa je st bezpieczna pod kątem wątków, jeśli może być bezpiecznie stosowana przez wiele w ątków” . Z technicznego punktu widzenia definicja ta jest poprawna, ale z praktycznego punktu widzenia niewiele wnosi. Jak odróżnić klasę bezpieczną od niebezpiecznej? Co rozum iem y przez „bez­ pieczna”? Sercem każdej rozsądnej definicji bezpieczeństwa wątkowego je st pojęcie p o p raw n o ­ ści. Jeśli definicja okazuje się rozmyta, zapewne brakuje jej jasnej definicji poprawności. : Poprawność oznacza, że klasa jest zgodna zc sw oją specyfikacją. Dobra specyfikacja : definiuje n ie z m ie n n ik i ograniczające stan obiektu i w a ru n k i końcow e opisujące : efekty w ykonyw anych operacji. Poniew aż bardzo rzadko piszem y tak szczegółowe specyfikacja dla w łasnych klas, skąd m ożem y m ieć pew ność, że są popraw ne? Nie

C zęść I ♦ Podstawy

30

możemy, ale nie powstrzymuje nas to od ich stosowania, jeśli tylko się przekonamy, że „kod działa” . Ta „w iara w kod” to często najbliższy stan popraw ności. Załóżmy, że popraw ność jcdnow ątkow a to coś, „o czym w iem y, gdy to zobaczym y” . Po tej optymistycznej definicji „poprawności” jako czegoś, co potrafimy rozwiązać, zdefb. niujemy bezpieczeństwo wątkowe w nieco mniej kołowy sposób — klasa jest bezpieczna pod kątem wątków, jeśli działa poprawnie nawet wtedy, gdy jest jednocześnie stosowana przez wiele w ątków ”. j Klasa je s t bezpieczna pod kątem w ątków, je ś li działa poprawnie nawet wtedy, ' ■: gdy je s t: jednocześnie ;stosowana: przez 'wiele .wątków niezależnle. od sposobu .za-; i .hamnonogramowania lub ułożenia tych wątków w .systemie; wykonawczym i bez; J '. . . wymuszania dodatkowej synchronizacji lub warunków na kodzie:jej używającym.: : , x ;

Ponieważ dowolny program jednowątkow y stanowi poddziedzinę programów wielo­ wątkowych, nie może być bezpieczny pod kątem wątków, jeśli nie jest poprawny w śro­ dowisku jednow ątkow ym 2. Jeśli obiekt został poprawnie zaimplementowany, żadna sekwencja operacji — wywołania metod publicznych i odczyt lub zapis pól publicznych — nie powinna złamać warunków końcowych i niezmienników. Żaden zbiór operacji przeprow adzonych sekw encyjnie lub w spółbieżnie na egzem plarzu klasy b ezpiecz­ niej w ątk ów « nie m oże sp o w o d o w a ć przejścia do n iep op raw n ego stanu. Klasy bezpieczne wątkowo hermetyzują całą wymaganą synchronizację, by; klient ; ; nie m usiał stosować własnej. 1 i

2 .1 .1 . Przykład — serwlet bezstanowy W rozdziale 1. wymieniliśmy kilka szkieletów aplikacyjnych, które tw orzą wątki i z icli poziom u w yw ołują kom ponenty użytkownika, czym w ym uszają zabezpieczenie się program isty pod kątem w ątków . Bardzo często w ym óg bezpieczeństwa w ątkowegd nic wynika bezpośrednio ze stosowania wątków w aplikacji, ale z chęci użycia systemu takiego ja k serw lely. W ykonam y prosty przykład — bazującą na serw ietach usługę rozkładającą iiczbę na czynniki. Będziem y j ą rozbudowywać przy jednoczesnym za­ pewnieniu bezpieczeństw a typów. L isting 2.1 przedstaw ia prosty serw iet rozkładający liczbę na czynniki. O dnajduje liczbę zaw artą w żądaniu, wylicza wyniki i umieszcza wynik w odpowiedzi. Listing 2 .1 . Serw iet bezstanow y

___________________________ ■

PThreadSafe p u b lic class S ta te le s s F a c to riz e r implements S e rvle t { p u b lic void service(Servlet.Request req. ServletResporise resp) { B ig ln te g e r i » extractFrom Request(req):

“ Jeśli martwi kogoś pojawienie się w tym miejscu „poprawności”, niech klasę bezpiecznąpotl kątem wątków traktuje jako klasę, która nie jest bardziej uszkodzona w środowisku wielowątkowym niż w środowisku jednowątkowym.

Rozdział 2. ♦ Wątki i b e z p ie c z e ń s tw o

31

B ig ln te g e rf] fa c to rs - f a c t o r ( i) : encodelntoResponsetresp, fa c to r s ) ;

Klasa S ta te le s s F a c to riz e r, podobnie ja k większość innych klas serwletowych, jest bezstanowa — nie m a własnych pół i nie korzysta z pól innych klas. Ulotny stan wy­ magany do przeprowadzenia obliczeń zaw ierają zmienne lokalne przechowywane na stosie wątku, który przetwarza dane żądanie. Jeden wątek stosujący S ta te le ssF a cto rize r nie może wpłynąć w żaden sposób na wynik innego wątku stosującego ldasę S ta te le s s ­ F a c to riz e r, bo oba wątki nie w spółdzielą stanu (nie m ają części wspólnej). Ponieważ akcje wątku korzystającego z obiektu bezstanowego nie wpływają na poprawność operacji innych wątków, obiekty bezstanowe są bezpieczne pod kątem wątków. 1 ‘ Obiekty bezstanowe zawsze są bezpieczne pod kątem wątków.

.



j

Większość serw letów m ożna zaim plem entow ać bez stosowania stanu, co znacząco redukuje nakład środków potrzebny na bezpieczeństwo wątkowe. Bezpieczeństwo staje się problemem dopiero wtedy, gdy wątki chcą zapam iętać pew ne informacje między żądaniami.

2.2. N iepodzielność Co się stanie, jeśli dodamy jeden element stanu do wcześnie jszego obiektu bezstanowe­ go? Przypuśćm y, że chcem y m ierzyć liczbę przetw orzonych żądań i wprowadzamy w tym celu licznik. Oczywistym podejściem byłoby dodanie pola typu long do serwlelu i inkrementację go w każdym żądaniu, co przedstawia klasa UnsafeCountingFactorizer z listingu 2.2. Listing 2 .2 . Serwlet, który zlicza żądania bez wym aganej synchronizacji. Nie rób tak PNotThreadSafe ' p u b lic class U nsafeCountingFactorizer implements S e rv le t { p riv a te long count = 0: p u b lic long getC ountt) { re tu rn count; } p u b lic void servicetSer-vletRequest req. ServletResporise resp) { B ig ln te g e r i » extractFrom Request(req); B ig ln te g e rf] fa c to rs = f a c t o r ( i) : ++count; encodelntoResponsetresp, f a c t o r s ) :

) Niestety, klasa UnsafeCountingFactorizer nie jest bezpieczna pod kątem wątków, choć działa całkow icie popraw nie w środow isku jednow ątkow ym . Podobnie ja k klasa UnsafeSequence z rozdziału 1. podatna jest na u tra tę aktualizacji. Operacji inkrementacji,

C zęść I ♦ Podstawy

32

++count, wydaje się pojedynczą akcją z powodu jej krótkiego zapisu, ale nie je st niepo­

dzielna, co oznacza, że jest wykonywana w jednej niepodzielnej akcji. Tak naprawdę stanowi jedynie skrót dla trzech operacji dyskretnych: pobrania wartości, zwiększenia jej o jeden i zapisu nowej wartości. To przykład operacji odczyt, m odyfikacja, zapis, w której nowy stan powstaje na podstawie poprzedniego. Rysunek 1.1 z rozdziału 1. przedstawia, co może się stać, jeśli dwa wątki będą chciały w tym samym czasie zwiększyć licznik bez stosowania synchronizacji. W niesprzyjają­ cych okolicznościach oba wątki m ogą odczytać z licznika wartość 9, uaktualnić j ą dó 10 i zapisać now ą wartość. Nie na takim działaniu nam zależy, bo gubimy jedną inkrementację, pom ijając w zliczaniu jedno wykonanie serwłetu. Pewnie sądzisz, że usłudze internetowej tego rodzaju niewielka pomyłka zmniejszająca dokładność je st w pełni akceptow alna. Czasem rzeczyw iście tak jest. Ale co, jeśli licznika używamy do generowania unikatowych wartości i wtedy zwrócimy tę samą wartość w dwóch różnych wywołaniach, co w konsekwencji doprowadzi do poważnych problemów z integralnością danych3? Możliwość zaistnienia niepoprawnych wyników w elekcie nieszczęśliwego układu czasu wykonania wątków jest tak istotna we w spół-1 bieżności, że zyskała w łasną nazwę — wyścig.

2 .2 .1 . Wyścig Klasa U nsafeC ountingFactorizer zawiera kilka wyścigów, które czynią uzyskiwane wyniki niepewnymi. W yścig występuje wtedy, gdy poprawność obliczeń zależy od wzajemnego ułożenia czasu wykonania kilku wątków w trakcie pracy systemu. Innymi słowy, w yścig w ystępuje wówczas, gdy poprawny w ynik uzyskamy tylko wtedy, gdy dopisze szczęście'1. Najczęstszym przypadkiem wyścigu jest sytuacja sp raw d ź i wykonaj, w której potencjalnie nieaktualna informacja służy do podjęcia decyzji co do wykonania kolejnych kroków. W rzeczywistym świecie wyścig zdarza się niejednokrotnie. Przypuśćmy, że planujemy spotkać się z przyjacielem w południe w kawiarni uniwersyteckiej, ale gdy tam dociera­ my, zdajemy sobie sprawę, że istnieją dwie kawiarnie uniwersyteckie w różnych bu­ dynkach. N ie mamy pewności, w której z nich się umówiliśmy. Poniew aż o 12:10 nie m a jeszcze przyjaciela, postanawiamy przejść się do drugiej kawiarni, ale po dotarciu do drugiej kawiarni okazuje się, że tam też go nie ma. Możliwości jest wiele: przyjaciel się spóźnia i nie pojawił się jeszcze w żadnej kawiarni, przyjaciel przyszedł do kawiarni 3 Podejście przedstawione w klasach UnsafeSeąuence i UnsafeCountingFactorizer podatne jest równie/ na wykorzystanie nieaktualnych danych (patrz punkt 3.i.i). Ą Termin wyścig często mylony jest z terminem wyścig danych, który występuje przy braku synchronizacji koordynującej dostęp do współdzielonej niestałej wartości. Wyścig danych ryzykuje się dla niesynchronizowancj zmiennej, gdy wątek zapisuje daną, która może zostać w tym samym momencie odczytana przez inny wątek, łub gdy odczytuje zmienną zapisaną przed chwilą przez inny' wątek. Kod z wyścigiem danych nie ma jasno sprecyzowanej semantyki w modelu pamięci Javy. Nie wszystkie wyścigi to wyścigi danych i na odwrót, ale obie sytuacje mogą prowadzić do niepoprawnego działania systemu. Klasa UnsafeCountingFactorizer ma zarówno wyścig, jak i wyścig danych. Więcej informacji o wyścigach danych zawiera rozdział 16.

R o z d z ia ł

2. ♦ Wątki i bezpieczeństw o

33

A po naszym jej opuszczeniu, p rzyjaciel byl w kaw iarni B, ale poszedł nas szukać w kawiarni A, gdy my z niej wyszliśmy. Przypuśćm y najgorsze, czyli ostatnią możli­ wość. Jest 12:15, byliśmy w obu kawiarniach i zastanaw iam y się, czy' nie wystawiono nas do wiatru. Co teraz zrobić? W rócić do kaw iarni A? Ile razy m ożna wracać tam i z powrotem? Jeżeli wcześniej nie ustaliliśmy wyjścia z takiej sytuacji, możemy spędzić . cale popołudnie, krążąc między kawiarniami. Problem z „pobiegnę sprawdzić drugą kawiarnię, czy może go tam nie ma” polega na tym, że w trakcie biegu przyjaciel również może się poruszać. W kawiarni A mówimy „tu go nie m a" i idziem y go szukać w drugiej. To sam o pow iem y w kawiarni B, ale nie w tym sam ym czasie. Przejście m iędzy kawiarniami zajmuje kilka minut. W tym czasie zm ianie m oże ulec stan system u. Przykład z kaw iarniam i ilustruje w yścig, poniew aż uzyskanie pożądanego wyniku i (spotkanie przyjaciela) zależy od względnego czasu zajścia różnych zdarzeń (dotarcia? do kawiarni, czasu oczekiwania w niej itp.). Obserwacja, że nie ma go w kawiarni A, i może okazać się błędna kilka chwil po wyjściu z niej, bo przyjaciel mógł wejść tylnymi t drzwiami. To właśnie unieważnienie obserwacji charakteryzuje większość wyścigów, i czyli zastosowanie nieaktualnych danych do wykonania pewnych działań w przyszłości, i Ten rodzaj wyścigu nosi nazwę sp raw d ź i w ykonaj. Sprawdzamy, czy coś jest p raw d ą} (plik X nie istnieje) i podejm ujem y na podstaw ie tej obserw acji o k reśloną reakcję 1 (utw orzenie X). W czasie m iedzy spraw dzeniem a reak cją stan m ógł ulec zm ianie (ktoś inny utworzył plik X), co spowoduje błąd (niespodziewany wyjątek, nadpisanie : danych, uszkodzenie pliku).

2.2.2. Przykład — wyścig w leniwej inicjalizacji Bardzo często zasadę spraw dź i w ykonaj w ykorzystuje in ic ja liz a c ja leniw a. Celem takiej inicjalizacji jest opóźnienie powstania-obiektu aż do momentu, w którym rzeczy­ wiście będzie potrzebny. Dodatkowo gwarantujem y jeg o inicjalizację tylko raz. Klasa , LazylnitSafe z listingu 2.3 ilustruje inicjalizację leniwą. M etoda get Instance () sprawdza, i najpierw, czy obiekt Expensi veObject został utworzony. Jeśli tak, po prostu go zwraca. W przeciwnym razie tworzy obiekt i zapam iętuje go przed zw róceniem , by uniknąć w przyszłości jego bardzo kosztownego tworzenia. Listing 2 .3 . Wyścig iv leniwej inicjalizacji. N ie rób tak _____ ONotThreadSafe p u b lic class LazylnitR ace { p riv a te ExpensiveObject instance = n u ll; p u b lic Expensi veObject g e tln sta n ce O { i f (in sta n ce =■= n u ll) instance = new ExpensiveO bjectO ; re tu rn instance:

P P P IÄ I C zęść I ♦ Podstawy

34

Klasa LazylnitR ace zaw iera wyścig podminowujący jej poprawność. Powiedzmy, że wątki A i B w ykonują metodę getlnsianceO w tym samym czasie. A widzi, że instance w ynosi n u li, w ięc tw orzy now y obiekt ExpensiveObject. B także widzi, że instance wynosi nul 1, więc również tworzy nowy obiekt. To, jak duże jest prawdopodobieństwo ,. zauw ażenia in sta n ce równego n u li przez inne obiekty, zaicży od tego, ja k diugo A zajm ie utworzenie obiektu ExpensiveObject i od harmonogram u.wykonania wątków. G dy B zauw aży w artość n u li, utw orzy w łasn ą w ersję obiektu. D wa w yw ołania getlnstanceC ) zw rócą różne obiekty, choć zawsze powinien to być ten sam obiekt. O peracja zliczania z klasy U nsafeC ountingFactorizer zaw iera jeszcze inny rodzaj wyścigu. O peracje o sekwencji odczyt, modyfikacja, zapis (takie ja k inkrementocja licznika), definiują zmianę stanu obiekt na podstawie stanu poprzedniego. Aby zwiększyć licznik, należy poznać jego wcześniejszy stan i zapewnić, by nikt inny w czasie mody­ fikacji go nie inkrementował. Podobnie ja k w iększość błędów wspólbieżności także wyścig nie zaw sze prowadzi do niepopraw nego działania — w ym agany je st specyficzny układ wykonania wątków. N iem niej w yścig potrafi spraw ić duże problem y. Jeśli klasa LazylnitR ace sluży_ do w ypełnienia rejestru o zasięgu całej aplikacji, naw et nieduże praw dopodobieństw o zwrócenia różnych egzemplarzy w kolejnych wywołaniach prowadzić może do niespój­ nego widoku na rejestr. Gdyby zastosować UnsafeSequence do generowania identyfikato­ rów, dwa obiekty mogłyby otrzymać ten sam identyfikator, łamiąc więzy integralności referencyjnej.

2 .2 .3 . Akcje złożone Zarówno Laz.ylnitRace, jak i Unsa feCounti ngFactori zer zawierają ciąg operacji, który pow inien być niep o d zieln y względem innych operacji na tym sam ym stanie. Aby uniknąć w yścigu, musi istnieć sposób zapobieżenia użyciu zmiennej przez inne wątki, gdy ta znajduje się w środku operacji modyfikacji. Inne wątki m ogą obserwować stan lub modyfikować zmienną tylko przed lub po zakończeniu operacji, ale nie w jej trakcie. > : Operacje A i B są’ niepodzielne względem siebie, je ś li z punktu widzenia wątku wy- i ' ; tonującego A, gdy inny w ątek wykonuje B, operacja-B zostaje: wykonana w całości . . albo wcale.; Operacja niepodzielna je s t nierozeiwalna względem wszystkich operaty j. j .(także siebie),- które dotyczą tego sam ego stanu. A u4

G dyby operacja inlcrem entacji była niepodzielna, w yścig w klasie UnsafeSeauence. z rysunku 1.1 nie m ógłby w ystąpić. K ażda inkrem entacja zaw sze pow odow ałaby zwiększenie wartości o 1. Aby zapewnić bezpieczeństwo pod kątem wątków, operacje sprawdź i wykonaj (leniwa inicjał izacja) oraz odczytaj, zmodyfikuj i zapisz (inkrementa­ cja) m uszą być niepodzielne. W ym ienione wcześniej operacje nazyw am y ak cjam i złożonym i — sekwencjami działań, które m uszą zostać wykonanie niepodzielne, by były bezpieczne w ątkow o. W tym punkcie rozważymy blokady, czyli w budowany w Javę mechanizm zapew niania niepodzielności. N a razie naprawimy problem w inny sposób, używając istniejącej klasy bezpiecznej wątkowo. Listing 2.4 przedstawia nową w ersję kodu — klasę Count i ngFactori zer.

Rozdział 2. ♦ Wątki i bezpieczeństw o

35

Listing 2 .4. Serwlet zliczający żądania za pom ocą obiektu A tom icLong _________ PThreadSafe p u b lic class C ountingF actorizer extends G enericS ervlet implements S e rv le t { p riv a te fin a l AtomicLong count = new AtoniicLongCO); p u b lic long getCountO { re tu rn c o u n t.g e t t ) : } p u b lic void serviceCServletRequest req. ServletResponse resp) { BjgJnteger i = extractFrom R equest(req): B ig ln te g e rQ fa c to rs = f a c t o r ( i ) ; count. i ncrementAndGet( ) ; encodelntoResponseCresp, f a c t o r s ) ;

s } Pakiet ja v a .u ti 1 .concurrente.atomie zawiera klasy zm iennych niepodzielnych, które zapewniają niepodzielne przejścia stanów dla liczb i referencji do obiektów. Zastępując licznik typu long klasą AtomicLong,'mamy pewność, że wszystkie akcje dotyczące stanu licznika są niepodzielne5. Poniew aż stan serwlctu to stan licznika, a licznik jest bezpieczny wątkowo, serwlet ponownie działa poprawnie dla wielu wątków. Mogliśmy dodać licznik do serwletu i zachować bezpieczeństwo pod kątem wątków, używając istniejącej klasy bezpiecznej w ątkowo do zarządzania stanem licznika. Gdy do klasy bezstanowej dodamy pojedynczy element, wynikowa klasa będzie bezpieczna pod kątem w ątków , je śli tylko stan zaw iera obiekt bezpieczny w ątkow o. W krótce okaże się jednak, że przejście z jednej zmiennej stanowej do wielu nie jest tak proste ja k przejście z zera do jednej zmiennej. I: .Gdy ma tó. sensp waitoistosować istniejące klasy.bezpieczne wątkowo,,na przykład : i i.;; Atom:icLong,:,by zarządzać stanem : klasy: Łatwiej określić możliwe stany.:! przejścia [r ; dla istniejącego .obiektu: niż :dla dowolnej zmiennej. Ułatwia to! nie' tylko tworzenie,: i i ale i konserwację głównej klasy. ■ ' 1 -■ , .

2.3. Blokady Mogliśmy dodać je d n ą zm ienną stanow ą do serwletu, zachowując przy tym jego bez­ pieczeństwo wątkowe, jeśli tylko ta zmienna była klasą bezpieczną pod kątem wątków. Jeśli jednak chcemy dodać do serwletu kilka innych zmiennych stanowych, czy m ogą to po prostu być kolejne zabezpieczone Idasy? Wyobraźmy sobie, że chcielibyśmy zwiększyć wydajność serwletu, buforując ostatnio wyliczony wynik, bo mogłoby się okazać, iż kolejny klient też chce rozkładu na czynniki tej samej wartości (oczywiście nie jest to najlepsza strategia buforowania, lepsza pojawi Klasa CountingFactorizer używa metody irtcrementAndGet(), która zwiększa licznik i zwraca nową wartość. W przykładzie ignorujemy zwróconą wartość.

C zęść I ♦ Podstawy

36

się w podrozdziale 5.6). Aby zaimplementować tę strategię, musimy zapam iętać dwa elementy: liczbę i jej rozldad.

m ogą zauw ażyć złam anie stałości niezm iennika. Podobnie obu w artości nie m ożna : pobrać jednocześnie — między pobraniami poszczególnych wartości przez w ątek A, wątek B może je zmienić. W takiej sytuacji A zauważy błąd niezmiennika. . •< : ;f j ii i* u

N ie w szystkie dane m uszą chronić blokady — jedynie zmienne dane udostępniane ;T wielu wątkom. W rozdziale l. wskazaliśmy, w jaki sposób dodanie obiektu TimerTask T (zadania asynchronicznego) m oże w prow adzić w ym óg bezpieczeństw a wątkowego Ę rozlewającego się po całym programie, szczególnie jeśli program je st skibo hermety- ii zowany. Rozważmy program jednowąikowy przetwarzający dużą ilość danych. Program i jednowątkow y nie wymaga synchronizacji, ponieważ wiele wątków nie współdzieli,j;. żadnych danych. Wyobraźmy sobie teraz, że chcemy dodać funkcję wymagającą tworze-;! nia migawek postępu przetwarzania, by program w przypadku a w arii nie musiał wy- J konyw ać w szystkich obliczeń od początku. W tym celu warto wykorzystać obiekt Ti - Jf inerTask urucham iany co 10-ty raz i zapisujący aktualny stan do plilcu. Poniew aż TimerTask zostanie w yw ołany z innego w ątku (zarządzanego przez klasę! Tirner), dane zapam iętyw ane w m igaw ce s ą w tym m om encie używ ane p rzez dwaj? wątki: główny wątek programu i wątek Timer. Oznacza to, żc nie tylko kod z TimerTask j musi stosować synchronizację — konieczna jest ona również w całym innym kodzie | aplikacji, który korzysta z tych danych. Program, który wcześniej nie w ym agał s y n -| chronizacji, teraz potrzebuje jej w niemal każdym miejscu. | Gdy zm ienną chroni blokada — czyli że każdy dostęp do zmiennej wymaga uzyskania#, blokady — m am y pew ność, iż tylko jeden wątek w danym momencie ma dostęp clo| zmiennej. Gdy klasa zawiera niezmienniki obejmujące więcej niż jedną zmienną stanową,:) 9 Okazuje się, że patrząc wstecz, decyzja ta prawdopodobnie nie była najrozsądniejsza — nie tylko bywa myląca, ale również wymaga od programistów implementujących maszynę wirtualną dokonywania wyboru między rozmiarem obiektu i wydajnością blokad. u,Narzędzia do audytu kodu, na przykład !’•'indBugs, potrafią znajdować zmienne, które są często, ale nie zawsze dostępne tylko po uzyskaniu blokady. Niejednokrotnie jest to oznaką błędu.

Rozdział 2 . ♦ Wątki i bezpieczeństw o

41

pojawia się dodatkowy w ym óg — wszystkie zm ienne dotyczące niezm iennika m uszą być chronione tą sam ą blokadą. W ten sposób ich modyfikacja lub odczyt są operacjami niepodzielnym i zapew niającym i stałość niezm iennika. Zasadę te przedstawia klasa SynchronizedFactorizer — zarówno buforowana wartość, jak i buforowany rozkład; chroniony je st przez tę sam ą blokadę wewnętrzną. ;; •j, ‘ Dla każdego 'niezmiennika obejmującego, '.więcej-niż jedną:zmienną.wszystkie zmienne’;;?1i|; T : clotycz£ice tego nlezmiennika:muszą być/chronione przez, tę samą blokadę. , . , ;|

Jeśli synchronizacja jest lekarstwem na wyścig, dlaczego wszystkich metod nie pop rzed za: się modyfikatorem synchronized? Okazuje się, że w zależności od aplikacji, taki sposób w prow adzenia bloków synchronized zapew ni za m a łą lub za d u żą synchronizację. . Sama synchronizacja wszystkich metod, jak czyni to klasa Vector, nie zawsze wystarcza do niepodzielności operacji złożonych: i f ( ¡v e c to r.c o n ta in s (e le m e n t)) v e c to r .adtK elem ent):

T a próba wstawienia elementu, jeśli nie istnieje, zawiera wyścig, choć obie wykorzy­ stywane metody są niepodzielne. Metody synchronizowane zapewniają niepodzielność poszczególnych operacji, a ich połączenie wym aga dodatkowej blokady, by zapewnić niepodzielność operacji złożonej. Podrozdział 4.4 opisuje sposoby bezpiecznego do­ daw ania złożonych operacji niepodzielnych do obiektów bezpiecznych pod kątem wątków. W tym samym czasie synchronizacja każdej metody prowadzi do problemów z żywotnością i w ydajnością, co rów nież dobitnie przedstawia klasa Synchronized­ Factorizer.

2.5. Żyw otność i wydajność W klasie UnsafeCachingFactorizer w prow adziliśm y do serwletu proste buforowanie, mając nadzieję na poprawę wydajności. Buforowanie wymaga współdzielonego stanu, który to znowu wymaga synchronizacji w celu zapewnienia spójności. Synchronizacja wprowadzona w klasie S ynchronizedFactorizer działa, ale oferuje m arną wydajność. Strategia synchronizacji SynchronizedFactorizer polega na ochronie zm iennych sta­ nowych blokadą wewnętrzną serwletu. Ochronę uzyskaliśmy, synchronizując całą metodę service(). To proste, ogólne rozwiązanie zapewnia poprawność wyników, ale za bardzo wysoką cenę. Ponieważ m etoda je st synchronizow ana w całości, m oże j ą w ykonyw ać tylko jeden wątek. N ie odpow iada to zalecanem u użyciu szkieletu serw letow ego, w którym to serwlety powinny jednocześnie obsługiwać w iele żądań od klientów. W ykonywanie jednotorowe przy dużym obciążeniu z pew nością zdenerwuje klientów. Jeśli serwlet jest zajęty liczeniem rozkładu dużej liczby, pozostałe klienty m uszą czekać, aż aktualny wątek zakończy działanie. Dopiero wtedy serw let rozpocznie liczenie nowej wartości. Jeśli system zawiera kilka procesorów, nie zostaną one wykorzystane nawet w przypad­ ku dużego obciążenia. Nawet żądanie dotyczące bardzo szybkiego wyliczenia wartości; (pobraniem wyniku z bufora) będzie w ykonyw ane bardzo długo, bo musi czekać na; zakończenie innego długiego żądania.

Ul»!!»1*!!-

T C zęść I ♦ Podstawy

42

Rysunek 2.1 przedstawia, co się dzieje, gdy nadchodzi wiele żądań dotyczących jednego serwletu rozkładu na czynniki: są kolejnowane i wykonywane sekwencyjnie. Przed­ staw ioną aplikację internetow ą cechuje słaba współbieżność — liczbę jednocześnie w ykonyw anych o g raniczają nie dostępne zasoby, ale sam a struktura aplikacji. N a szczęście łatwo poprawić współbieżność serwletu przy zachowaniu obecnego poziomu bezpieczeństw a w ątkow ego — w ystarczy ograniczyć zakres obow iązyw ania bloku synchronized. Z drugiej strony należy uw ażać, by bloku nie uczynić zbyt małym. Operacji niepodzielnej nie można podzielić na dwa bloki synchronized. W arto jednak usunąć z takiego bloku długo działające operacje, które nie wpływają na współdzielony stan, bo w tym czasie nie ma przeciwwskazań, by inne wątki mogły odczytywać ten stan. Rysunek 2 .1 . Słaba w spółbieżność klasy SynchronizedFactorizer

A

—»

■:

Rozdział 2. ♦ Wątki i bezpieczeństw o

43

++cacheHits: fa c to rs " la s tF a c to rs .c lo n e O ;

i f (fa c to rs “ = n u ll) { fa c to rs = f a c t o r ( i) ; synchronized ( t h is ) { lastMumber - i ; la s tF a c to rs = fa c to rs .c lo n e O ;

} }

encodelntoResponseCresp. f a c t o r s ) :

rozkład n

L

rozkład m

łl

rozkład m

U

K lasa CachedFactorizer z listingu 2.8 zm ienia strukturę serwletu w taki sposób, by stosował dwa osobne bloki .synchronized ograniczone do krótkich fragmentów kodu. Jeden blok chroni sekwencję sprawdź i wykonaj, która sprawdza, czy można po prostu zw rócić zbuforow any wynik. Drugi blok aktualizuje wartość i czynniki uzyskane po rozkładzie. Dodatkowo klasa ponownie zawiera licznik wykonań z odpowiednią inkrem entacjąw bloku synchronized. Poniew aż liczniki to także współdzielone elementy stanu, m uszą być synchronizowane w każdym miejscu. Fragmenty kodu poza blokami, synchronizującymi korzystają w yłącznie ze zmiennych lokalnych (umieszczanych na stosie), które nie są współdzielone i tym samym nie wym agają synchronizacji. Listing 2 .8 . Serwlel. który buforuje ostatni w ynik i zlicza wywalania @ThreadSafe p u b lic class CachedFactorizer implements S e rvle t { @ GuardedBy("this") p riv a te B ig lrite g e r lastNumber; @GuardedBy("this") p riv a te B ig ln te g e rO la s tF a c to rs ; @GuardedBy(“ t h is '') p riv a te long h its : @GuardedBy(” t h is " ) p riv a te long cacheHits; p u b lic synchronized long g e tl-lits O { re tu rn h it s : } p u b lic synchronized double getCacheHitR atioO { re tu rn (double) cacheHits / (double) h it s :

} p u b lic vo id servicetS ervletR equest req, ServletResponse resp) { B ig ln te g e r i = extractFrom R equest(req): B ig ln te g e rC ] fa c to rs n u ll; synchronized ( t h is ) { + + h its : i f ( i .equals(last.Number)) {

Klasa C achedFactorizer nie stosuje klasy AtomicLong. K orzysta raczej ze zwykłej zmiennej typu long. Co praw da m ożna by w tym m iejscu bezpiecznie zastosować AtomicLong, ale niesie to ze sobą niniejsze korzyści niż w klasie C ountingFactorizer. Zmienne niepodzielne przydają się do wymuszenia niepodzielności operacji na jednej zmiennej. Ponieważ w przykładzie stosujemy bloki synchronizujące, stosowanie dwóch mechanizm ów synchronizacji nie ma sensu, bo nie zwiększy wydajności ani bezpie­ czeństwa. Nowa wersja serwletu stanowi kompromis między prostotą (synchronizacja całej metody) a współbieżnością (synchronizacją jak najkrótszych ścieżek kodu). Uzyskanie i zwolnie­ nie blokady niesie ze so b ą pew ien narzut, więc nie w arto zbyt m ocno rozdrabniać bloków synchronizujących (na przykład um ieszczając operację + + hits w osobnym bloku), nawet jeśli nie wpłynęłoby to na niepodzielność. Klasa CachedFactorizer za­ kłada blokadę w momencie dostępu do zm iennej stanowej i wykonywania operacji złożonych, ale zwalnia j ą przed rozpoczęciem potencjalnie długiej operacji rozkładu na czynniki. Uzyskujemy tym samym bezpieczeństwo wątkowe bez niepotrzebnego ograniczania wspólbieżności — wszystkie ścieżki kodu wewnątrz bloków są „wystar­ czająco krótkie”. Określenie, jak duże lub malc powinny być bloki synchronized, wymaga odpowied­ niego zrów now ażenia kwestii projektowych (bezpieczeństwa i prostoty) oraz wydaj­ ności. Czasem prostota i wydajność idą w zupełnie innych kierunkach, ale jak pokazuje klasa CachedFactorizer, na ogól można osiągnąć kompromis. j : Cżęsto' pojawia się: sprzeczność między prostotą i wydajnością. .W momencie imple- u ¡Cementacji strategii synchronizacjrnależy opierać się chęci poświęcenia prostoty (i byćr j ,może bezpieczeństwa) na rzecz wydajności. ' j

Gdy używasz blokad, zastanów się dobrze, jak długo będzie się wykonywał blok. kodu. Przetrzymywanie blokady długi czas, bo w ykonuje się złożone obliczenia lub operacje we-wy, znacząco zwiększa prawdopodobieństwo wystąpienia problemów z żywotnością i wydajnością. i Unikaj pizetrzymywania blokady w trakcie wykonywania żmudnych obliczeń l u b o p e r a c j i , ' 1; ! r które ze swej natury, mogą trwać długo,(operacje sieciowe i konsolowe wejście-wyjście). • )

44

' _________________

Część I ♦ Podstawy —---------—------------------

Rozdział

3.

Współdzielenie obiektów Na początku rozdziału 2. pojawiło się stwierdzenie, że poprawne programy współbieżne muszą przecie wszystkim właściwie zarządzać dostępem do współdzielonego, zm ien­ nego stanu. Tamten rozdział dotyczył użycia synchronizacji do zabezpieczenia się przed wieloma wątkami korzystającym i z tych sam ych danych w tym sam ym momencie. Ten prezentuje techniki współdzielenia i publikacji obiektów, by były bezpieczne do stosowania w wielu wątkach. Razem oba elementy stanow ią podstaw ę tworzenia klas bezpiecznych wątkowo i poprawnej konstrukcji współbieżnych aplikacji za pom ocą klas biblioteki ja v a . u ti 1. concurrent. W poprzednim rozdziale przedstawiliśmy, w jaki sposób bloki i metody synchronized zapew niają niepodzielność operacji. W ielu osobom w ydaje się, że te bloki dotyczą tylko niepodzielności i oznaczania sekcji krytycznych. Synchronizacja ma także inny istotny, choć subtelny, aspekt — w idoczność p am ięci. Chcemy nie tylko zapewnić, by gdy jeden w ątek m odyfikuje stan obiektu, inne mu w tym nie przeszkadzały, ale i to, by inne wątki rzeczywiście w idziały dokonaną zmianę. N ie m ożna tego osiągnąć bez synchronizacji. O biekty są bezpiecznie publikow ane czy to za pom ocą jaw nej synchronizacji, czy przez zastosowanie synchronizacji wybudowanej w klasy biblioteki.

3.1. W idoczność Widoczność to subtelny temat, bo zadania, które m ogą się nie udać, są mało intuicyjne. W środowisku jednowątkow ym , gdy zapisujem y w artość do zmiennej i później ją od­ czytujemy (w międzyczasie nie było innych zapisów), m ożemy się spodziewać otrzy­ mania tej samej wartości. Wydaje się to w miarę naturalne. Z tego względu początkowo trudno zaakceptow ać, że w przypadku w ielu odczytów i zapisów z w ielu w ątków p rzed staw io n e założenie m oże nie zaistn ieć. O gólnie nie ma gw arancji, iż wątek odczytujący zobaczy wartość zapisaną przez inny wątek w odpowiednim czasie, a nawet w ogóle. Aby zapewnić w idoczność zapisów do pam ięci w różnych wątkach, należy użyć synchronizacji.

f C zęść I ♦ Podstawy

46

L isting 3.1 przedstaw ia klasę NoVisibi 1 i t y w skazującą, co może pójść nic tale, jeśli wątki współdzielą dane bez synchronizacji. Dwa wątki, główny i odczytujący, korzystają ze w spółdzielonych zm iennych ready i number. Główny w ątek urucham ia w ątek odczytujący, a następnie ustawia number na 42 i ready na tru e . W ątek odczytujący czeka, aż ready będzie równe tru e , i dopiero wtedy wyświetla wartość number. Choć wydawaloby się oczywiste, że NoVisibi l i t y zawsze wyświetli 42, w praktyce może wyświetlić 0 lub w ogóle nie wyjść z pętli! Z powodu braku odpowiedniej synchronizacji nie mamy żadnej gw arancji, że wartości ready i number zapisane przez głów ny w ątek zobaczy w ątek odczytujący.

Rozdział 3 . ♦ W sp ó łd zielen ie o b ie k tó w

i i (• ! ;; ■■ ;■

i» )P f| 47

Klasa NoVi s i bi 1ity to chyba najprostsza postać programu współbieżnego — dwa wątki i dwie współdzielone zmienne — a mimo to zbyt łatwo wysnuć zle wnioski co do jego działania i sposobu opuszczenia pętli. Wysnucie odpowiednich wniosków co do kolejno- , ' ści działań w niepopraw nie zsynchronizow anym program ie w ielow ątkow ym je st wręcz niemożliwe. Wszystko to brzmi groźnie i rzeczywiście taicie jest. Na szczęście istnieje prosty środek zaradczy — k ażd orazow e użycie od p ow ied n iej syn ch ron izacji, gdy tylko dane są

i;

w sp ółdzielone p rzez w iele w ątków .

ii

Listing 3 .1 . W spółdzielenie zm iennych bez synchronizacji. N ie rób tak p u b lic class N o V is ib il i t y { p riv a te s t a t ic boolean ready; p riv a te s t a tic in t number; p riv a te s t a t ic class ReaderThread extends Thread { p u b lic void ru n () { w h ile (¡re a d y) T h re a d .y ie ld t): S yste m .o u t.p rin t!n (n u m b e r);

ij

}

p u b lic s t a t ic void m a in (S trin g [] -args) { new ReaderThreadt) . s t a r t ( >;

number - 42; ready = tru e ; -

} ) Koci klasy NoVisibi 1i t y może przebywać w pętli nieskończenie długo, bo wartość ready 'jj: m oże nigdy nie zostać zauw ażona przez w ątek odczytujący. Co jeszcze dziwniejsze, ;) kod może wyśw ietlić wartość 0, gdy zapis ready będzie widoczny w cześn iej niż zapis S number (tak zw ana zm ian a k olejn ości). N ie ma gwarancji, że operacje jednego wątlcu f w ykonają się w kolejności podanej przez program, jeśli tylko zmiana kolejności będzieK niezauważalna przez wątek dokonujący modyfikacji — naw et je śli oznacza to zmianę '! k o le jn o śc i z m ia n w in n y ch w ą tk a c h 1. Choć głów ny w ątek w kodzie źródłowym;'!: najpierw zapisuje number, a później ready, bez synchronizacji inny wątek może zauważyć;? te operacje w odwrotnej kolejności (lub nawet w cale ich nie widzieć). w . (lii : i

Przy braku synchronizacji kompilator, procesor i system wykonawczy mogą wykonywać j ;;; dziwne „przem eblow ania" operacji,; które mają wykonać.; Próby wcześnlejszego lo- ; ij gicznego wskazywania kolejn ości wykonania w pam ięci określonych działań przy ; i Ij braku synchronizacji wielowątkowej n iem a l na pewno będą niepoprawne. ; .•«,&

1 Mogłoby to wskazywać na zle zaprojektowanie systemu, ale tak naprawdę wynika to z faktu wykorzystywania przez maszynę wirtualną pełnej wydajności nowoczesnych systemów wieloprocesorowych. Przy braku synchronizacji model pamięci Javy dopuszcza, by kompilator zmieni! kolejność operacji i buforował wartości w rejestrach. Dopuszcza też zmianę kolejności wykonania działań przez procesor i jego bufory. Więcej informacji na ten temat znajduje się w rozdziale 16.

3.1.1. Nieświeże dane K lasa NoVisibi l i t y przedstaw ia je d e n z pow odów zw racania przez niepopraw nie zsynchronizowane programy zadziwiających wyników — nieśw ieżość danych. Gdy wątek odczytujący testuje wartość ready, może widzieć przedatow aną wartość. Jeśli synchronizacji nie stosuje się p rzy k ażd ym d o stęp ie do zm ien n ej, można uzyskać nieświeży odczyt. Co gorsza, taki odczyt nie odbyw a się na zasadzie wszystko albo nic — wątek jedną zmienną odczyta aktualną, a drugą nieważną (na w et jeśli teoretycznie została zapisana jako pierwsza). Czasem nieświeże jedzenie można spożyć — jest to tylko mniej przyjemne. Nieświeże dane byw ają bardzo groźne. Choć nieaktualny licznik odwiedzin aplikacji internetowej raczej nikomu nie zaszkodzi“, tak inna nieświeża wartość może doprowadzić do poważnych błędów. W klasie N oV isibility nieaktualne dane prowadzą czasem do wyświetlenia złego wyniku, a naw et do zablokow ania jednego z wątków. Sprawa kom plikuje się je s zc ze bardziej, gdy nieświeżość dotyczy referencji do obiektów, na przykład łączy listy jednokierunkow ej. N ieśw ieże d an e m ogą pow od ow ać pow ażn e i tajem nicze p om yłki, ja k n iesp od ziew an e w yjątk i, błęd y stru k tu ry danych, niedokładn e obli­ czenia oraz pętle niesk oń czon e.

Klasa M utablelnteger z listingu 3.2 nie je st bezpieczna wątkowo, bo z pola value ko­ rzystają metody get O i s e t( ) bez synchronizacji. Poza innymi hazardami, klasa jest narażona na nieświeżość danych: jeśli jeden w ątek wywołuje s e t ( ), drugi wątek wy­ wołujący g e t ( ) może nie widzieć aktualizacji. Listing 3 .2 . N iezabezpieczona p rze d w ątkam i klasa przechow ująca liczbę całkowitą (NtotThreadSa fe p u b lic class M utablelnteger { p riv a te in t value; p u b lic in t get.O { re tu rn value; ) p u b lic vo id s e t t in t value) { th is .v a lu e ■* value; }

Odczyt danych bez synchronizacji przypomina użycie poziomu izolacji READJJNCOMMITED (odczyt niczatwierdzony) w bazie danych, gdy staramy się zwiększyć wydajność kosztem dokładności. Odczyt niesynclironizowany to coś więcej niż tylko utrata dokładności, bo widoczna wartość współdzielonej zmiennej może być naprawdę poważnie przedatowana.

i 5I j jj )ii : Ij ; jj i (i

': ;i I1 1 1j , s. j

'

;

48

C zęść I ♦ Podstawy

Rozdział 3. ♦ W spółdzielenie obiektów

Bezpieczeństwo klasie Mutablelnteger zapewnimy, synchronizując metodę ustawiającą j i pobierającą. N o w ą w ersję przedstaw ia klasa S y n c h r o n iz e d ln te g e r z listingu 3.3. Synchronizacja tylko metody ustawiającej nie w ystarcza — w takiej sytuacji wątek j pobierający wartość nadal byłby narażony na nieświeżość. Listing 3 .3 . Zabezpieczona p rze d wątkam i klasa przechow ująca liczbę całkowitą PThreadSafe p u b lic cla ss S ynchronizedlnteger { QGuardedByC'this") p riv a te i n i value: p u b lic synchronized in t g e t() { re tu rn value: } p u b lic synchronized void s e t d n t value) { th is .v a lu e - value: }

)

R ysu n ek

3.1.

49

W ą te k A

W idoczność gw arantow ana p r z e z synchronizację

!'

, i • j

ii

3 .1 .2 . Niepodzielne operacje 64-bitowe Gdy w ątek odczytuje zm ienną bez synchronizacji, m oże w idzieć nieśw ieżą wartość, | ! ale przynajmniej jest to wartość umieszczona tam przez inny wątek, a nie jakaś losowa | wartość. M ówi się w takiej sytuacji o bezpieczeństwie poprawności. ■: To bezpieczeństwo dotyczy wszystkich zmiennych poza jednym wyjątkiem — 64-bito-:jj w ych zm iennych liczbow ych (double i long) niezadelclarow anych jak o v o la tile - ! (patrz punkt 3.1.4). Model pamięci Javy wymaga, by operacje pobrania i zapamiętania | były niepodzielne, ale dla nieulotnych zmiennych long i double maszyna w irtualna! może potraktować odczyt lub zapis 64-bitowy jako dwie operacje 32-bitowe. Jeśli zapis ! i odczyt takiej nieulotnej zm iennej odbyw a się w dwócli różnych w ątkach, w ątek! odczytujący może przeczytać niższe 32 bity nowej wartości i wyższe 32 bity starej3,! N aw et jeśli ktoś nie przejmuje się nieświeżymi danymi, powinien uważać na \vspół-.|f. dzielenie zmiennych typu long lub double w programach wielowątkowych, jeśli nie sąij,' one zadeklarowane jako v o la t ile lub chronione blokadą.

: '



Ponieważ stan programu zmienia się cały czas, wydawać by się mogło, że zastosowania obiektów niezmiennych są mocno ograniczone, ale w rzeczywistości tale nie jest. Istnieje bardzo poważna różnica między niezmiennym pbicktcni, a niezmienną referen cją do obiektu. Stan programu przechowywany w niezmiennych obiektach można uaktualnić, „zastępując” staiy obiekt now ą w ersją z nowym stanem. Kolejny podrozdział zawiera przykład tej techniki13.

•f.

O biekty niezm ienne są też bezpieczniejsze. Przekazanie zm iennego obiektu do nie-!§ znanego kodu (lub jego publikacja w taki sposób, że może go odnaleźć nieznany kod)i| bywa niebezpieczne — nieznany kod może zmodyfikować stan obiektu lub, co gorsza, zapamiętać referencję do obiektu i zmienić go w innym czasie, używając innego wątku.;]; Z drugiej strony, obiektu niezm iennego nie uszkodzi niebezpieczny ani błędny kod/f więc je st bezpieczny i m oże być publikow any bez stosowania dodatkowych zabezpie-f czeń [EJ Item 24]. i Ani specyfikacja języka Java, ani model pam ięci Javy nie definiuje niezmienności ; w sposób formalny. N ic jest ona równoważna prostemu zadeklarowaniu wszystkich!. pól obiektu jako fin a ł. Obiekt ze wszystkimi polami typu fin a l nadal może być zmień-; ny, bo nic nie stoi na przeszkodzie, by zawierał referencje do zmiennych obiektów, ł; Obiekt niezmienny nada! może używać wewnętrznie obiektów zmiennych do zarządzania^ własnym stanem , co ilustruje klasa ThreeStooges z listingu 3. i ł . Choć obiekt Set.. przechowujący imiona je st zm ienny, konstrukcja głównej klasy uniemożliwia modyy fikację zbioru po jego utworzeniu. Referencja stooges je st typu f in a l, więc caiy stan1 obiektu udostępnia pola tego typu. Ostatni wymóg (poprawnego utworzenia) został: spełniony, bo konstruktor w żaden sposób nie może spowodować wycieku referencji: th is .

j-

3.4.1. Pola typu finał Słowo kluczowe fin a ł, bardziej ograniczona wersja mechanizmu const z C++, obsługuje konstrukcję obiektów niezmiennych. Pól finalnych (ostatecznych) nie można modyfi­ kować (choć obiekty, do których zaw ierają referencje, m ogą się zmieniać). Co więcej, mają one specjalne znaczenie w modelu pamięci Javy. To użycie pól finalnych gwaran­ tuje bezpieczeństw o inicjalizaeyjiie (patrz punkt 3.5.2), które umożliwia swobodny dostęp i współdzielenie obiektów niezmiennych. 12r Technicznie możliwe jest uzyskanie obiektu niezmiennego bez wszystkich pól ustawionych na finał — przykładem jest klasa Stri rig — ale wykorzystuje to bardzo delikatne uwarunkowania wykluczające wyścigi i wymaga dobrego zrozumienia modelu pamięci Javy. Dla ciekawskich: klasa String leniwie wylicza skrót tekstu przy pierwszym wywołaniu metody hashCode() i buforuje ją w polu nicfinalnym. Wszystko działa poprawnie jedynie dlatego, że pole może uzyskać tylko jedną niedomyślną wartość, która zawsze jest taka sama, bo zostaje wyliczona na podstawie niezmiennego stanu (nie próbuj tego w domu). Wielu programistów obawia się, że to podejście powoduje problemy z wydajnością. W wielu sytuacjach są one bezpodstawne. Alokacja jest tańsza, niż może się wydawać, a obiekty niezmienne zwiększają wydajność, bo nie potrzebują blokad czy kopiowania defensywnego. Mają ograniczony wpływ na .szybkość mechanizmu odzyskiwania pamięci.

i

C zęść I ♦ Podstawy

60

Rozdział 3. ♦ W spółdzielenie obiektów

N awet jeśli obiekt jest zmienny, określenie kilku jego pól jako finalnych ułatwia analizę ' jeg o stanu, bo ograniczenie zm ienności niektórych pól zm niejsza liczbę kom binacji stanu obiektu. Obiekt, który je st w „większości niezm ienny”, ale m a jedno lub dwa; zmienne pola, okazuje się łatwiejszy do analizy niż obiekt z wieloma zmiennymi polami.-'! Dodatkowo deklaracja pola jako fin a ł informuje innych programistów, że wskazany1 element nie będzie się zmieniał. • , i ; i

Podobnie ja k zaleca się, by, wszystkie :pola określać jako. prywatne', je śli .nie wymagają i | , większej widoczności [EJ Item 12J, zaleca się też oznaczanie wszystkich pól nie- V zmieniających swego stanu m odyfikatorem f i n a ł . . ■ , ,

3 .4 .2 . Przykład — użycie volatile do publikacji obiektów niezmiennych

61

else re tu rn A rra ys.co p yO f(la stF a cto rs. la s tF a c to rs . le n g th ) ;

Wyścig dotyczący dostępu lub aktualizacji wielu pow iązanych zm iennych udaje się wyeliminować, używając obiektu niezmiennego przechowującego wszystkie zmienne. Ze zmiennym obiektem trzeba stosować blokady, by zapewnić niepodzielność; wykorzy­ stując niezmienny obiekt, w ątek uzyskuje do niego dostęp, nie m artwiąc się o 'in n y ! wątek modyfikujący jeg o stan. Jeśli konieczne staje się uaktualnienie zmiennych, po­ wstaje nowy obiekt stały, ale inne w ątki (widzące starszą wersję) nadal m ają dostęp do spójnego stanu. Klasa V olatileCachedfactorizer z listingu 3.13 używa ldasy OneValueCache do zapam ię- i tania wartości i rozkładu na czynniki. Jeśli wątek ustawia ulotne pole cache na referencję ; do obiektu OneValueCache, nowe dane od razu w idzą inne wątki.

W klasie UnsafeCachingFactorizer ze strony 36. staraliśmy się użyć dwóch obiektów AtoinicReference do zapam iętania ostatniej w artości i rozkładu, ale to podejście nie; okazyw ało się bezpieczne w ątkow o, bo niem ożliw e było jednoczesne pobranie lub; uaktualnienie obu wartości. Użycie zm iennych ulotnych również nie rozwiązałoby; problemu. Z drugiej strony obiekty niezmienne potrafią czasem zapewnić słabą formę niepodzielności. i Serwlet wyliczający rozkład na czynniki wykonuje dwie operacje niepodzielne: aktu-: alizaeję bufora i w arunek spraw dzający, czy bufor zaw iera w artość, k tó rą chcemy wyliczyć. Gdy grupa pow iązanych danych musi działać w sposób niepodzielny, wartorozważyć utworzenie dla nich klasy stanu niezmiennego, na przykład OneValueCache14: z listingu 3.12.

Listing 3 .1 3 . Buforowanie ostatniego wyniku w referencji ulotnej dotyczącej niezmiennego obiektu @ThreadSafe p u b lic class V o la tile C a ch e d F a cto rizer implements S e rv le t { p riv a te v o la t ile OneValueCache cache - new OneValueCacheinull, n u ll) : p u b lic void servicetS ervletR equest req, S e rv ietResponse resp) { B ig ln te g e r i = extractFrom R equest(req); 8 ig ln te g e r [] fa c to rs = cache.getFactorsG ) ; i f (fa c to rs == n u ll) { fa c to rs “ f a c t o r ( i) : cache » new OneValueCache(i. fa c to rs ) ;

} encodelntoResponseCresp. f a c t o r s ) ;

L is tin g 3 .1 2 . Niezmienny obiekt przechowujący buforowaną wartość i je j rozkład __________________ ^Immutable p u b lic class OneValueCache { p riv a te fin a l B ig ln te g e r lastNumber; p riv a te fin a l B ig ln te g e rf] la s tF a c to rs :

Operacje dotyczące bufora nie interferują między sobą, bo klasa OneValueCache jest nie­ zmienna, a pole cache za każdym razem jest udostępniane tylko raz w każdej z istotnych ścieżek. To połączenie kilku w artości pow iązanych niezm iennikiem w jednym nie­ zmiennym obiekcie oraz użycie referencji typu v o la ti le zapew nia klasie V olatileC achedFactorizer bezpieczeństwo wątkowe, choć w ogóle nie stosujemy jawnych blokad. ,

p u b lic OneValueCache(B1gInteger i , B ig ln te g e rt] fa c to rs ) ( lastNumber =. i ; la s tF a c to rs = A rra ys.co p yO f(fa cto rs. fa c to rs .le n g th );

} p u b lic B ig ln te g e rt] getFactorsC B iglnteger i ) { i f (lastNumber - - n u ll || ilastNum ber.e q u a ls (i)) re tu rn n u ll;

1,1Klasa OneValueCopy nie byłaby niezmienna, gdyby nie metody pobierające i wywołania copyOf (). Metoda A rra y s . copyOf ( ) pojawia się w javie 6, ale we wcześniejszych wersjach można użyć metody clonet).

i ;-

3.5. Bezpieczna publikacja Do tej pory skupialiśmy się na zapewnieniu braku publikacji obiektu, czyli zamknięciu w jednym w ątku lub w ew nątrz innego obiektu. O czyw iście są sytuacje, w których chcemy współdzielić obiekty między wątkami — wtedy musimy zatroszczyć się o bez­ pieczeństwo, Niestety, samo um ieszczenie referencji do obiektu w polu publicznym, patrz listing 3.14, nie w ystarcza do bezpiecznej publikacji obiektu.

C zęść I ♦ Podstawy f

Listing 3 .1 4 . P u b lik a c ja o b ie k tu bez o d p o w ie d n ie j xvn ch ro n iza c/i. N ie ró b tak



:

:

"

!f

'

/ / Niezabezpieczona publikacja. p u b lic Holder b o ld e r; p u b lic void i n i t i a l i z e d { h older = new H older(42):

R o z d z ia ł

3. ♦ W spółdzielenie obiektów

63

odczytać z niego nieświeży stan!C. Aby jeszcze bardziej udziwnić sytuację, wątek może za pierwszym razem odczytać nieaktualną wartość z wątku, a za drugim razem przeczytać wartość aktualną. Właśnie z tego powodu test z metody assertSanityC ) może się okazać prawdziwy i spowodować zgłoszenie wyjątku A ssertionE rror. Choć ryzykujemy powtarzanie się, przypomnijmy, iż bardzo dziwne rzeczy m ogą się dziać, jeśli data jest współdzielona przez w iele wątków bez należytej synchronizacji.

Zadziwiające, jak ten niewinny przykładowy kod potrafi zaszkodzić aplikacji. Z powodu jj, problemów z w idocznością obiekt Holder może w innym wątku pojawić się w niespój-|| nym stanie, nawet jeśli jego niezmienniki zostały poprawnie ustawione w konstruktorze! jf N iepopraw na publikacja um ożliw ia'innem u wątkow i zaobserw ow anie częściowo ) sk on stru o w a n eg o ob iek tu .

;i

ot 'i

3 .5 .1 . Nieodpowiednia publikacja — gdy dobre obiekty idą w złą stronę

j

.

N ie warto polegać na integralności częściowo skonstruow anego obiektu. Wątek; o!> jj serw ujący spostrzeże obiekt w niespójnym stanie, a następnie zauważy, źe jeg o stand nagle uleg! zmianie, choć lak naprawdę nic by 1 on modyfikowany od czasu publikacji. 1 j W rzeczywistości jeśli obiekt Holder z listingu 3.15 zostałby opublikowany w sposób| j niezabezpieczony (patrz listing 3.14), wątki inne niż publikujący po wywołaniu metody j; assertSan1ty( ) mogłyby otrzym ać w yjątek AssertionError!15 1: Listing 3 .1 5 . K la sa ry zy k u ją c a błąd, j e ś li n ie zo sta n ie p o p ra w n ie o p u b liko w a n a p u b lic class Holder { p riv a te in t ri; p u b lic H o ld e r(in t n) ( t.h is.n = n; } p u b lic void a s se rtS a n ityO { i f (n != n) throw new AssertionErrorCW arunek je s t praw dziw y.");

3.5.2. Obiekty niezmienne i bezpieczeństwo inicjalizacji Poniew aż obiekty niezm ienne są tak w ażne, model pam ięci Javy oferuje specjalną gwarancję bezpieczeństwa inicjalizacji współdzielonych obiektów niezmiennych. Przekonaliśmy się, że gdy referencja do obiektu staje się widoczna dla innych wątków, nie oznacza to jednocześnie, że to samo dzieje się zc stanem obiektu. Aby zapewnić spójny widok stanu obiektu, potrzebujem y synchronizacji.

:! !> i1 ; .;

Z drugiej strony, obiekty niezm ienne m ożna udostępniać bezpiecznie nawet wtedy, ; i gdy synchronizacja nic zabezpiecza publikacji referencji ilo obiektu. Aby zagwaran­ tować to bezpieczeństwo inicjalizacji, należy spełnić wszystkie wymogi niezmienności: j j niemodyfikowalny stan, wszystkie pola ustawione na fin a ł i odpowiednią konstrukcję : j \ (gdyby klasa Holder z listingu 3.15 była niezmienna, metoda assertSanityC ) nie mogłaby : i | zgłosić wyjątku nawet w momencie nieodpowiedniej publikacji). i {j Obiekty niezmienne mogą być bezpiecznie stosowane przez dowolne wątki bez dodat' fi 1 kowej synchronizacji, nawet je ś li synchronizacja nie służy do je j publikacji. ' ' V ‘i 1

, i, !j Ta gwarancja dotyczy wartości wszystkich pól finalnych poprawnie skonstruowanych obiektów. Pola finalne są dostępne w sposób bezpieczny bez dodatlcowej synchronizacji. Jeśli jednak dotyczą one zm iennych obiektów , dostęp do stanu tych obiektów nadal wymaga synchronizacji.

;j i : '*

3.5.3. Idiomy bezpiecznej synchronizacji Ponieważ nie użyliśmy synchronizacji do uwidocznienia obiektu Hol der innym wątkom,-j mówimy o n iep op raw n ej p u b lik acji. Niepoprawnie opublikowane obiekty narażamy j na dwie przypadłości. Inne wątki m ogą zauważyć nieśw ieżą wartość w polu hol der,,, , a tym samym zobaczyć referencję n u li lub inną wartość, choć w rzeczywistości została, ona ustawiona. Co gorsza, m ogą uzyskać popraw ną referencję do obiektu Hol der, alej;

15 Przedstawiany problem nie dotyczy klasy Holder jako takiej, ale sposobu upublicznienia jej obiektów. Klasę można zabezpieczyć przed niepoprawni) publikacją deklarując pole n jako f i hal, bo wtedy || obiekty klasy będą niezmienne, patrz punkt 3.5.2.

Obiekty, które nie są niezmienne, m uszą zostać opublikowane w sposób bezpieczny, co najczęściej oznacza synchronizację zarówno przez wątek tworzący, jak i konsumujący. Na razie skupm y się na zapew nieniu, by w ątek konsum ujący zaw sze widział stan obiektu po publikacji. Dopiero później zajmiemy się widocznością zmian po publikacji,

Choć może się wydawać, żc wartości podane w konstruktorze są pierwszymi zapisywanymi w polach, w rzeczywistości jest inaczej. Konstruktor O bject przed uruchomieniem właściwego konstruktora ustawia wszystkie zmienne na wartości domyślne. Kod wątku może odczytać domyślną wartość, która tak naprawdę jest przestarzała.

: ij ■;

C zęść I ♦ Podstawy Jjs

64

Rozdział 3. ♦ W sp ó łd zielen ie o b ie k tó w

65

I

Obiekty, które w sposób formalny nie są niezmienne, ale których stanu nie można zm ie­ nić po opublikow aniu, nazyw am y n iezm ien n y m i tech n iczn ie. N ie m uszą spełniać . ścisłej definicji niezm ienności z podrozdziału 3.4 — w ystarczy że są. przez program traktowane tak, jakby rzeczyw iście były niezm ienne po publikacji. W ykorzystanie obiektów niezmiennych technicznie upraszcza im plem entację i poprawia w ydajność., przez redukcję potrzeby synchronizacji. ;

i ,. Aby bezpiecznie opublikować obiekt, zarówno referencja do obiektu,,jak i jego-stan Ą ; muszą być widoczne .dla innychiwątltów dokładnie witymvsamymvmoraencie.. Popraw- ' ;> ’ nie skonstruowany obiekt bezpiecznie publikuj przez: ■ ' *, '

♦ inicjalizację referencji do obiektu z poziomu elementu s t a t ic ,



♦ ' przechowywanie referencji w polu v o lâ t i Ic lub obiekcie AtoniicR eference, ♦ przechowywanie referencji w polu f in a l poprawnie utworzonego obiektu, u,

♦ przechowywanie referencji w polu poprawnie chronionym blokadą.

• ; U l

U/.Bezpiecznie , opublikow anejobiekty niezmienne ■technicznie .mogąrbyć .bezpiecznie ’ j l używane przez dowolny '.‘«¡tek bez dodatkowej synchronizacji. .j ;

\\

W ew nętrzna synchronizacja w kolekcjach zabezpieczonych w ątkow o oznacza, że *| umieszczanie obiektu w takiej kolekcji (na przykład ldasie Vector lub synchronizedlist))! spełnia ostatni z wymienionych wymogów. Jeśli wątek A umieszcza obiekt X w kolekcji ,i| zabezpieczonej w ątkowo i w ątek B stara się j ą odczytać, gwarantuje się, że B otrzyma -| stan X w takiej wersji, w jakiej zostawił go A, mimo że kod obsługujący X nie stosuje żadnej ja w n e j synchronizacji. Biblioteka kolekcji zabezpieczonych wątkowo oferuje i| następujące gw arancje bezpieczeństw a publikacji (choć dokum entacja nie zawsze-ij wskazu je je dostatecznie mocno): ♦ Umieszczenie klucza lub wartości w H ashtable, synchronizedMap lub ConcurrerrtHap bezpiecznie publikuje ten element dowolnemu wątkowi, który pobiera go z Map (bezpośrednio lub za pom ocą iteratora). ♦ Umieszczenie elementu w Vector, CopyOnWriteArra.yL.ist, CopyOnWriteArraySët, s y n c h ro n iz e d lis t lub synchronizedSet bezpiecznie publikuje go dowolnemu wątkowi, który pobiera go z kolekcji.

Przykładowo obiekt Date je s t zm ienny11, ale gdy będziem y go stosować tak, jakby b y l; i ■ niezmienny, nie m usim y w prow adzać blokad, które w przeciwnym razie byłyby po- i ] ;• Irzebne do odpow iedniego w spółdzielenia obiektu i synchronizacji. Przypuśćmy, że i ; chcemy przechowywać obiekt Map zawierający daty ostatniego logowania użytkowników. j i p u b lic Map la s tL o g in Collections.synchronizedM aptnew HashMap ());

■’ h U

Jeśli wartości Date nie są m odyfikow ane po ich um ieszczeniu w Map, w tedy synchro- 5 ] nizacja zapewniana przez implementację synchronizedMap wystarcza do poprawnej HJ publikacji obiektu Date i nie jest potrzebna żadna dodatkowa synchronizacja w mo­ mencie dostępu do niej.

3.5.5. Obiekty zmienne

Inne mechanizmy biblioteki klas (na przykład Future i Exchanger) również wprowadzają vjf bezpieczeństwo publikacji. Zajmiemy się ich zaletami w momencie ich wprowadzania. %

Jeśli obiekt może zmieniać się po utworzeniu, bezpieczna publikacja zapewnia jedynie jego widoczność w stanie z momentu publikacji. Synchronizację trzeba wtedy stosować nie tylko w momencie publikacji obiektu, ale również przy każdym dostępie do niego, by w ten sposób zapewnić widoczność kolejnych modyfikacji. Bezpieczne w spółdzie­ lenie obiektu zm iennego w ym aga bezpiecznej publikacji o ra z albo bezpieczeństwa wątkowego, albo ochrony blokadą.

Inicjalizacja statyczna to często najprostszy i najbezpieczniejszy sposób publikacji ( ' obiektu, który może być w ykonany statycznie. :

I' Wymagania dotyczące publikacji obiektu zależą od jego zm ienności.

♦ Umieszczenie elementu w BlockingQueue lub ConcurrentlinkedQueue bezpiecznie publikuje go dowolnemu wątkowi, który pobiera go z kolejki.

♦ Obiekty ruezmieun.e m ogą być publikowane w. dowolny sposób.

p u b lic s t a t ic Holder h o ld e r *= new H older(42);

i' Inicjalizacja statyczna zachodzi w maszynie wirtualnej w momencie wczytywania klasy. ;( Z powodu wewnętrznej synchronizacji w maszynie wirtualnej mechanizm ten gwarantuje ; * bezpieczną publikację obiektu [JLS 12.4.2]. j;

3 .5 .4 . Obiekty technicznie niezmienne Bezpieczna publikacja wystarcza innym wątkom do bezpiecznego dostępu bez synchro-j i nizacji do obiektów, które nie będą modyfikowane po publikacji. Mechanizm bezpiecz-i|, nej publikacji gwarantuje, że opublikowany stan obiektu będzie widziany przez wszyst-A kie referencje w m om encie udostępnienia im referencji do obiektu. Jeśli tylko stan ten nie będzie ulegał zm ianom , wystarcza to, by dostęp do obiektu był bezpieczny. i;

* . , ♦ Obiekty niezmienne technicznie muszą być publikowane bezpiecznie. t :

"

.i

.

. ■.

,

'♦•■Obiekty zmienne muszą być publikowane bezpiecznie i być bezpieczne wątkowo lub chronione blokadą. ■ '

3.5.6. Bezpieczne współdzielenie obiektów Gdy uzyskujemy referencję do obiektu, pow inniśm y dokładnie wiedzieć, co możemy z nim zrobić. Czy musimy uzyskać blokadę przed jego użyciem? Czy m ożem y zmie­ nić jeg o stan czy tylko go odczytać? W iele błędów w spółbieżności pojaw ia się, bo

P raw dopodobnie je s t to błąd p ro je k to w y b ib lio tek i k las d o ty czący c h d a t — przvp. aut.

!

66

C zęść I ♦ Podstavvy!|

programista źle zrozumiał „reguły gry” współdzielonymi obiektami. Publikując obiekt, [ zawsze zaznaczaj, w jaki sposób powinien być obsługiwany. .11 Oto kilka najważniejszych strategii . korzystania ze ,współdzielonych obiektów, w ap lb kacjach współbieżnych. , • ' '

Odosobnienie w wątku. Obiekt odosobniony w wątku należy wyłącznie do jednego ,.!j wątku i je s t przez niego modyfikowany.

.

,

1Wspóklzielenie tylko z prawem do. odczytu. Obiekt współdzielony lylko do odczytu

'J jl

może być współbieżnie odczytywany przez wiele wątków bez dodatkowej' synchronizacji, ale żaden,wątek nie powinien go modyfikować. Obiektami tego typu są obiekty niezmienne i niezmienne technicznie.

jj /„ ",} d

W spółdzielenie z bezpieczeństw em wątkowym. Obiekt bezpieczny wątkowo

y

Rozdział

4.

Kompozycja obiektów

, . wewnętrznie synchronizuje dostęp, więc wiele; wątków może bezpiecznie korzystać z jego publicznego interfejsu bez potrzeby dodatkowej synchronizacji. ■

•Ochrona. Obiekt chroniony dostępny je s t dopiero po uzyskaniu, odpowiedniego..: klucza • Obiekty chronione to takie, które znajdują'; się w obiektach bezpiecznych, wątkowo lub zostały opublikowane i są chronione przez konkretną blokadę.

Do tej pory zajmowaliśmy się niskopoziomowymi kwestiami bezpieczeństwa wątko­ wego i synchronizacji. Nie chcemy analizować każdego dostępu do pamięci, by mieć pewność co do bezpieczeństw a programu. Chcem y skorzystać z zabezpieczonych wątkowo komponentów i bezpiecznie połączyć je w większe komponenty łub pro­ gramy. Niniejszy rozdział opisuje wzorce struktur klas, które ułatwiają zabezpieczanie pod kątem wątków i zachowanie tego bezpieczeństwa bez przypadkowego jego osła­ bienia.

4.1. Projektowanie klasy bezpiecznej wątkow o Choć m ożna napisać program bezpieczny w ątkow o przechow ujący cały swój stan w publicznych polach statycznych, bardzo trudno zweryfikować jego poprawność i zmo­ dyfikować go w taki sposób, by nada! pozostał bezpieczny (w porównaniu z programem poprawnie wykorzystującym herm etyzację). H erm etyzacja um ożliw ia sprawdzenie bezpieczeństwa wątkowego konkretnej klasy bez potrzeby analizy całego programu. ! I

Proces; projektowy;dla\klasy ¡bezpiecznej wątkowo powinien zawierać trzy podstawowe elementy: ' ; '

; ■ ♦ identyfikację zmiennych określających stan obiektu,



|

♦ identyfikację niezmienników ograniczających stan obiektu,

|

♦ określenie strategii zarządzania współbieżnym dostępem do stanu obiektu.'

'

;

j

• ' '

Stan obiektu rozpoczyna się od zaw artych w nim pól. Jeśli w szystkie są typu pod­ stawowego, pola w pełni określają stan. K lasa Counter z listingu 4.1 zawiera tylko jedno pole, value, które w pełni reprezentuje jej stan. Stan obiektu z n polami prosty­ mi, jest «-wartościową krotką utworzoną z tych pól. Stanem obiektu reprezentującego

C zęść I ♦ Podstawy

68

punkt w przestrzeni dwuwymiarowej jest (x,y). Jeżeli obiekt zawiera pola z referencjami do innych obiektów, stan dotyczy również wszystkich pól tych obiektów. Przykładowo stan obiektu LinkedList to stan samej listy i wszystkich zawartych w niej obiektów. Listing 4 .1 . Prosty licznik bezpieczny wątkowo stosujący wzorzec monitora ______________________ _ CT’ireadSafe . p u b lic fin a l cla ss Counter { @GuardedBy("this") p riv a te long value -- 0;



p u b lic synchronized long getValueO { re tu rn value;

} p u b lic synchronized long increm entO { i f (value Long .MAX_VAI_UE) throw new Ille g a lS ta te E x c e p tio n C p rz e p e ln ie n ie lic z n ik a " ) ; re tu rn ++value:

i ! S trategia synchronizacji definiuje sposób, w jaki obiekt koordynuje dostęp do swego stanu bez łamania określonych niezmienników i warunków końcowych. Określa, jaka kom binacja niezm ienności, odosobnienia w wątku i blokad zapewni bezpieczeństwo pod kątem w ątków , a także jak ie zm ienne pow inny być chronione blokadami. Aby ułatwić analizę i przyszłe modyfikacje klasy, dokumentuj strategię synchronizacji.

Rozdział 4 . ♦ K om pozycja o b ie k tó w

69

Ograniczenia założone na stany lub przejścia między nimi przez niezmienniki i warunki końcowe tworzą dodatkowe wymogi dla synchronizacji i hermetyzacji. Jeśli pewien stan jest niepoprawny, zmienne go przechowujące należy hermetyzować, bo. w przeciwnym razie klient mógłby (nawet nieświadomie) ustawić błędny stan. Jeśli operacja ma niepo­ prawne przejścia między stanami, musi być niepodzielna. Z drugiej strony, jeżeli klasa nie wprowadza tego rodzaju ograniczeń, m ożemy ograniczyć hermetyzację i wymogi szeregowalności, by uzyskać w iększą elastyczność i lepszą wydajność. Klasa może zawierać niezmienniki ograniczające wiele zm iennych stanowych. Klasa zakresu wartości, na przykład klasa NumberRange z listingu 4.10, typowo przechowuje zmienne stanu dla górnego i dolnego ograniczenia. Zmienne tc m uszą stosować ogra­ niczenie, które nakazuje, by dolne ograniczenie było m niejsze lub rów ne górnemu. Niezmienniki wielu zm iennych w ym uszają niepodzielność — pow iązane zmienne m uszą zostać pobrane lub uaktualnione w jednej niepodzielnej operacji. N ie m ożna uaktualnić jednej wartości, zw olnić blokady, ponownie założyć blokady i uaktualnić drugiej wartości, bo w ten sposób między modyfikacjami obiekt znajdzie się w stanie niespójnym. Gdy wiele zm iennych uczestniczy w niezmienniku, chroniąca je blokada musi trwać przez cały okres operacji korzystającej z tycli zmiennych. c;. ;Nię rnożna zapewnić bezpieczeństwa,wątkowego bez.dobrego zrozumienia niezmienni- ¡ i ków i warunków końcowych obiektu. Ograniczenia poprawnych wartości 1 przejść i ' stanów wymuszają hermetyzację i niepodzielność niektórych operacji.

4.1.2. Operacje zależne od stanu 4 .1 .1 . Zbieranie wymagań synchronizacyjnych Uczynienie klasy bezpiecznej wątkowo oznacza utrzymanie w mocy jej niezmienników nawet w przypadku współbieżnego dostępu. Wymaga to analizy stanu obiektu. Obiekty i zmienne m ają p rz e strz e ń sta n u — zakres możliwych stanów, które m ogą przyjąć. Im mniejsza przestrzeń, tym łatwiej j ą analizować. Stosując pola fin a ł, gdy tylko ma to sens, upraszczamy analizę, ograniczając liczbę zmiennych elementów stanu (w eks­ tremalnym przypadku w pełni niezmienny obiekt znajduje się zawsze w jednym stanie). Wiele klas posiada niezm ienniki w skazujące, czy konkretny stan je st poprawny czy błędny. Wartość pola w klasie Counter ma typ long. Przestrzeń stanu typu long zawiera się w przedziale od Long.MIN_VALUE do Long.MAXJ/ALUE, ale licznik nakłada dodatkowe ograniczenie na value — nie dopuszcza wartości ujemnych. Operacje m ogą również zawierać pewne warunki końcowe ograniczające możliwość p rz e jśc ia m iędzy sta n a n ii. Jeśli licznik zaw iera w artość 17, kolejnym możliwym stanem jest tylko wartość 18. Jeśli nowy stan określa się na podstawie bieżącego, ope­ racja ta jest typu złożonego. Nie wszystkie operacje wprowadzają ograniczenie przejścia m iędzy stanam i; uaktualnianie zm iennej przechow ującej tem peraturę nie korzysta z wcześniejszej wartości temperatury pamiętanej w zmiennej.

Niezm ienniki klas i warunki końcow e m etod ograniczają popraw ne stany i przejścia między nimi dia konkretnego obiektu. N iektóre obiekty m ają m etody z w a ru n k a m i w stępnym i bazującymi na stanie. Przykładow o nie m ożna usunąć obiektu z pustej kolejki. K olejka m usi znajdow ać się w „niepustym ” stanie p rzed p ró b ą usunięcia elementu. Operacje z warunkami wstępnymi bazującymi na aktualnym stanie nazywamy zależnymi od stanu [CPJ 3], ;

■j j’ f; iI J)

W programie jedno wątkowym, gdy w arunek w stępny nie zostanie spełniony, operacja nie ma wyboru — musi zgłosić błąd. Z drugiej strony, w program ie w spółbieżnym • ’ warunek wstępny może stać się praw dziw y nieco później w wyniku działania innego wątku. Program y w spółbieżne w prow adzają m ożliw ość oczekiw ania na uzyskanie i prawdziwości warunku wstępnego i dopiero po zajściu tej sytuacji wykonanie operacji. W budow any m echanizm w ydajnego oczekiw ania na zajście określonego w arunku . w ait i noti fy — jest ściśle powiązany z blokadą wewnętrzną i niełatwo zastosować go poprawnie. Aby uzyskać operacje oczekujące na spełnienie w arunku w stępnego przed rozpoczęciem działań, warto lepiej skorzystać z istniejących bibliotek klas, na przykład kolejek blokujących lub semaforów, by uzyskać pożądane zachowanie. Biblio­ teki z klasami blokującymi, BlockingQueue, Semaphore i innymi elementami synchro- ; nizującym i om awia rozdział 5. W ykorzystanie niskopoziom ow ych m echanizm ów zapew nianych przez platform ę i biblioteki klas w celu uzyskania klas zależnych od stanu opisuje rozdział 14.

Rozdział 4 . ♦ K om pozycja o b ie k tó w

C zęść I ♦ Podstawy

70

71

czesnym dostępem do stanu przez wieie wątków, m uszą stosować jaw ne blokady lub być niezm ienne1.

4 .1 .3 . Własność stanu W podrozdziale 4.1 w skazaliśm y, że stan obiektu może dotyczyć podzbioru pól gra, fu obiektów zaczynającego się od obiektu początkowego. D laczego to podzbiór?. W jakich warunkach pola osiągalne z poziomu głównego obiektu nie stanowią części jego 1 stanu?

4.2. Odosobnienie egzem plarza :■

Jeśli obiekt nie je st bezpieczny w ątkow o, istnieje kilka technik jeg o zabezpieczenia : ■' w programie wielowątkowym. M ożna wymusić stasow anie go tylko w jednym wątku i (odosobnienie w wątku) lub zapewnić jego ochronę za pomocą blokady.

Przy określaniu, które zmienne kształtują stan obiektu, chcemy rozważać tylko te dane, które obiekt posiada. W łasność nie je st jaw nie wskazywana przez języ k — wynika raczej ze sposobu zaprojektowania klasy. Jeśli alokujemy i wypełniamy odwzorowanie. ; tworzymy wiele obiektów: obiekt HashMap, obiekty Map.Entry używane w implementacji • odw zorow ania i być m oże inne wew nętrzne obiekty. Logiczny stan HashMap zawiera w sobie rów nież stan w szystkich obiektów wew nętrznych i Map.Entry, choć są one zaim plem entow ane jako niezależne byty. A utom atyczne odzyskiw anie pam ięci pozw ala nam uniknąć poważnego myślenia na tem at w łasności obiektów . Przekazując obiekt do metody w języku C++, należy się mocno zastanowić, czy przekazujem y własność, tylko wypożyczamy obiekt na krótki czas lub wprowadzamy długoterminowe współdzielenie. W Javie dostępne są wszystkie wymienione modele własności, ale odzyskiwanie pamięci redukuje koszt wielu typowych błędów we w spółdzieleniu referencji, a tym samym zachęca do mniej precyzyjnego myślenia o własności,

Hermetyzacja upraszcza tworzenie klas bezpiecznych wątkowo przez promowanie odosobnienia egzemplarza nazywanego często po prostu odosobnieniem [CJP 2.3.3]. Gdy obiekt zostanie hermetycznie zawarty w innym obiekcie, znamy wszystkie ścieżki wykonania korzystające z tego obiektu, więc łatwiej nam przeanalizować cale bezpieczeń­ stwo niż w sytuacji, gdy dostęp do obiektu ma cały program. Połączenie odosobnienia z odpowiednią dyscypliną w kwestii blokad zapewnia, że standardowo niezabezpieczo­ ny wąlkowo obiekt bywa wykorzystywany w sposób bezpieczny.

; (

; ! : r

Hermetyzacja danych wewnątrz obiektu ogranicza dostęp do danych tylko do metod ; -•obiektu, .co.ułatwia zapewnienie, by dane zawsze.byly chronione odpowiednią blokadą'

Odosobnione obiekty nie m ogą uciec zc swojego standardowego zasięgu. Obiekt można odosobnić na poziom ie egzem plarza klasy (stosując klasę prywatną), wykorzystując zakres leksykalny (zm ienną lokalną) lub w ątek (obiekt jest przekazywany między metodami, ale wykonywanymi w obszarze jednego wątku). Obiekty nie uciekają same — potrzebują w sparcia program isty — który pom aga im, publikując ich referencje poza standardowym zasięgiem.

W wielu sytuacjach własność i hermetyzacja idą ze sobą w parze — obiekt hermetyzuje , zaw arty w nim stan, w ięc jest jeg o w łaścicielem . To w łaściciel zm iennej stanowej ■ decyduje o sposobach blokow ania zapew niających integralność zm iennej. Własność implikuje kontrolę, ale gdy opublikuje się referencje do zmiennego obiektu, talc naprawdę ; przestaje się mieć j ą na wyłączność; w najlepszym razie mamy „własność współdzieloną”. ‘ Klasa najczęściej nie je st właścicielem obiektów przekazywanych do niej za pomocą : metod lub konstruktora, chyba że m etoda została tak zaprojektowana, by przejąć na ; własność przekazany obiekt (czynią tak na przykład metody fabryczne zapewniające : otoczkę synchronizującą dla kolekcji). Klasy kolekcji najczęściej w ykazują „własność dzieloną”, która oznacza, że to kolekcja:: ma stan całej infrastruktury kolekcji, ale do klienta należą obiekty przechowywane ; w kolekcji. Przykładem jest ServletContext ze szkieletu serwletowego. Zapewnia obiekt , . podobny do odwzorowania dla serwletów, w którym m ogą one rejestrować lub pobierać ' obiekty aplikacji na podstawie nazwy, używając metod s e tA itrib u te ( ) i g e tA ttrib u te ł ), Obiekt ServletContext implementowany przez kontener serwletów musi być bezpieczny • wąlkowo, bo korzysta z niego jednocześnie wiele wątków. Serwlcty nie muszą używać, synchronizacji, gdy w yw ołują metody se tA Ł trib u te ( ) i g e t.A ttrib u te ( ), ale m ogą jej : potrzebować do poprawnego k o rzy stan ia z obiektów zawartych w ServletContext. ; Obiekt te należą do aplikacji i są tylko „z uprzejmości” przechowywane przez obiekt , kontenera serw letów na rzecz aplikacji. Podobnie ja k wszystkie inne współdzielone1 obiekty, m uszą być odpowiednio zabezpieczone. Aby zapobiec problemom z jedno—

: •

Klasa PersonSet z listingu 4.2 ilustruje, w ja k i sposób odosobnienie i blokow anie w spółpracują by zapew nić bezpieczeństw o wątkowe klasy, gdy jej komponenty nie , są odpowiednio zabezpieczone. Stanem klasy PersonSet zarządza klasa HashSet, która nie je st bezpieczna w ątkow o. Poniew aż mySet je st składow ą p ryw atną i nigdy nic • , ucieknie, obiekt HashSet jest szczelnie zamknięty w PersonSet. Jedynie fragmenty metod addPersonł) i contai nsPersonC) wykorzystują obiekt mySet. Każda z nich zakłada przed jego użyciem odpow iednią blokadę. Cały stan chroni blokada wewnętrzna, więc klasa i PersonSet jest bezpieczna wątkowo.

:

. (:

1 Co ciekawe, obiekt HttpSession, który pełni w szkielecie podobną rolę, ntoże mieć bardziej wyśrubowane wymagania. Ponieważ kontener serwletów może skorzystać z obiektów zawartych w HttpSession, by je serializować w celu replikacji lub zapamiętać w inny sposób, obiekty te muszą być bezpieczne wątkowo, bo korzysta z nich nie tylko sama aplikacja internetowa. Słowo „może” występuje w replikacji i włączaniu pasywności, bo nie stanowią one części specyfikacji serwletów, ale często pojawiają się w kontenerach serwletów.

i, j i

}i , >! ;

72

C zęść I ♦ Podstawy

Listing 4 .2 . W ykorzystanie odosobnienia do zapew nienia bezpieczeństwa wątkowego @ThreadSafe p u b lic class PersonSet { @GuardedBy(" t h i s " ) p riv a te fin a l Set mySet - new HashSet(): p u b lic synchronized void addPersonCPerson p) { mySet. a d d (p ):

Rozdział 4 . ♦ Kompozycja obiektów

73 j

Odosabnianie czyni tworzenie klas bezpiecznych wątkowo prostszym, ponieważ klasę, , i : nleudostępnjającą swojego ■stanu, łatwiej analizować pod kątem >bezpieczeństwa bez f-:Ą 1 potrzeby sprawdzania catego programu. • ' / ’ 1 ,

4.2.1. Wzorzec monitora

} p u b lic synchronized boolean containsPerson(Person p) { re tu rn m yS e t.co n ta in s(p );

} in te rfa c e Person {

} } Przykład nie czyni żadnych założeń co do bezpieczeństw a w ątkow ego klasy Person, ale jeśli ta je st zmienna, aplikacja potrzebuje dodatkowej synchronizacji po pobraniu obiektu Person z PersonSet. Najpewniejszym sposobem wykonania tego zadania byłoby uczynienie klasy Person bezpiecznej w ątkow o; mniej pew nym rozw iązaniem je st ochrona obiektów Person blokadą i zapewnienie takiego protokołu uzyskiwania dostępu, by wszystkie klienty musiały uzyskać blokadę przed uzyskaniem obiektu. Odosobnienie egzemplarza to najprostszy sposób budowania Idas bezpiecznych wątkowo. Zapewnia elastyczność co do wyboru strategii blokowania; PersonSet wykorzystuje w łasną blokadę wewnętrzną, by chronić swój stan, ale równie dobrze można by zasto­ sować inną blokadę, o ile byłaby konsekwentnie stosowana. Odosobnienie egzemplarza dopuszcza stosow anie różnych blokad do ochrony różnych zm iennych stanowych. Przykład klasy, która stosuje kilka obiektów blokad do ochrony swego stanu, znajduje się na stronie 244. (klasa ServerStatus). Istnieje w iele przykładów odosabniania w bibliotekach klas p latform y, w łączając w to niektóre klasy, które istnieją tylko po to, by zam ienić w ersje klas niezabezpie­ czone w ątkow o w w ersje zabezpieczone. P odstaw ow e klasy kolekcji, na przykład. A rra y L is t lub HashSet, nie są zabezpieczone, ale istnieją specjalne m etody fabryczne (C o llection s.syn ch ro n ize d listO i inne podobne) włączające bezpieczeństwo wątkowe dla kolekcji. F abryki w ykorzystują w zorzec dekoratora (G am m a i inni, 1995), by otoczyć kolekcję klasą zapewniająca bezpieczeństwo wątkowe. Otoczka implementuje każdą metodę odpowiedniego interfejsu jako synchronizowaną, która kieruje właściwe wykonanie zadania do w łaściw ej kolekcji. O ile tylko otoczka przechow uje jedyną referencję do obiektu kolekcji (otoczka odosabnia kolekcję), obiekt otoczki je st bez­ pieczny wątkowo. Dokumentacja metod otoczek informuje, że by uzyskać pełne bez­ pieczeństwo, cały dostęp do metod kolekcji musi odbywać się za pomocą obiektu otoczki. O czywiście nadal nic nie stoi na przeszkodzie, by złam ać odosobnienie, publikując w cześniej zam knięty obiekt. Gdy obiekt pow inien być zam knięty (odosobniony) w konkretnym zasięgu, pozwolenie mu na ucieczkę stanowi poważny błąd w programie. Obiekty odosobnione m ogą uciec także w tedy, gdy opublikuje się inne obiekty, na przykład iteratory lub egzem plarze klas wewnętrznych, które pośrednio zaw ierają od­ osobniony obiekt.

Korzystając z zasady odosobnienia egzem plarza, m ożem y wysnuć logiczny wniosek, który prowadzi wprost do w zorca m o n ito ra Ja v y 2. Obiekt stosujący wzorzec monitora Javy hermetyzuje cały zmienny stan i chroni go, stosując blokadę wewnętrzną samego obiektu. Klasa Counter z listingu 4.1 przedstaw ia typow y przykład użycia takiego wzorca. Jedna hermetyzowana zmienna, value, przechow ująca cały stan obiektu je st dostępna wyłącznie przez metody synchronizowane. Wzorzec monitora Javy stosuje wiele klas bibliotecznych, na przykład Vector i Mashtable. Czasem potrzeba bardziej wyrafinowanej strategii synchronizacji. Rozdział 11. omawia, w jaki sposób poprawić skalowalność rozwiązania dzięki zastosowaniu bardziej szcze­ gółowej strategii blokowania. Podstaw ow ą zaletą w zorca je st jego prostota. Wzorzec monitora Javy to w zasadzie pewna konwencja. Dowolny obiekt może służyć do blokowania dostępu do stanu innego obiektu, o ile tylko wykorzystuje się go spójnie. Li­ sting 4.3 przedstawia klasę stosującą osobny, prywatny obiekt chroniący dostępu do stanu. Listing 4 .3 . Ochrona stanu pryw atną blokadą ___________ p u b lic class P riv a te lo c k { p riv a te f in a l Object myLock ■= new O b je c tO ; @GuardedBy(“myLock") Widget w idget; void someMethodO { synchronized (myLock) ( / / Dostęp lub modyfikacja stanu obiektu Widget.

Jest kilka zalet stosow ania blokady prywatnej zam iast blokady wewnętrznej obiektu 1 (lub innej publicznie dostępnej blokady). Prywatny obiekt blokady hermetyzuje sam ą i blokadę, więc kod klienta nie je st w stanie jej uzyskać w przeciwieństwie do blokady •‘ publicznej, w której kod klienta m oże uczestniczyć (dobrze lub źle). Klienty, które ; niepopraw nie u zyskują blokadę innego obiektu, m ogą doprow adzić do problem ów i ■ z żywotnością. Poza tym weryfikacja poprawności takiej blokady wymaga analizy całego ! programu, a nie tylko jednej klasy. ;i

' Wzorzec monitora Javy inspirują prace [-Ioarc’a na temat monitorów (I-Ioarc, 1974), choć istnieją znaczące różnice między wzorcem a rzeczywistymi monitorami. Instrukcje kodu bajtowego wejścia i wyjścia z bloku synchronizującego noszą nawet nazwy m on ito re n te r i m onitorexi t. Co więcej, wbudowane w Javę blokady wewnętrzne nazywane są czasem monitorami.

ir C zęść I ♦ Podstaw y ;

74

nnzdzial 4. ♦ Kompozycja obiektów throw new IIle g alA rgu m e n tE xce p tio nt“ No such ID: " + id ) : lo e .x » x; lo e .y - y:

4 .2 .2 . Przykład — śledzenie pojazdów floty K lasa Counter z listingu 4.1 jest poprawnym, ale bardzo prostym przykładem wzorca', monitora. W ykonajm y nieco mniej trywialny przykład'— klasę „śledzenia pojazdów"!. ułatwiającą kierowanie floty pojazdów (taksówek, wozów policyjnych lub dostawczych) ; . w określone m iejsce. N ajpierw zastosujem y w zorzec m onitora, a następnie nieco1 złagodzimy hcrmetyzację przy jednoczesnym zachowaniu pełnego bezpieczeństwa wąt-i ; kowego. jt K ażdy pojazd identyfikuje obiekt S trin g . Pojazd m a przypisane konkretne położenie] , (x, y). Klasa VehicleTracker hermetyzuje identyfikatory i położenia znanych pojazdów,]: co czyni j ą idealnym kandydatem na model danych w aplikacji z graficznym interfejsem] ■ użytkownika stosującej zasadę MVC. Oznacza to jednak, że zapewne będzie współ-]: dzielona przez wątek wyświetlający informacje i wiele wątków aktualizujących. Wątek!j widoku pobiera nazwy i pozycje pojazdów, a następnie w yśw ietla je na ekranie: ij Map lo c a tio n s - v e h ic le s .g e tlo c a tio n s t): fo r (S trin g key: lo c a tio n s .keyS etO ) ren d e rV e h icle (ke y, lo c a tio n s .g e t(k e y )):

75

j ; jj i

1

Podobnie w ątki aktualizujące m odyfikow ałyby lokalizacje pojazdów na podstawie!' danych utrzymywanych z odbiorników GPS lub wpisywanych ręcznie przez dyspozytora,;■

p riv a te s t a t ic Map deepCopy(Hap m) Map r e s u lt = new HashMap(): fo r (S trin g id : m.keySetO) r e s u lt.p u t( i d . new M u ta b le P o in ttm .g e t(id )) ) ; re tu rn Col le c tio n s . u n m o d ifia b le M a p (re s u lt);

Listin g

4 .5 . M odyfikowalna klasa punktu podobna do ja v a .awt.Point PNotThreadSafe p u b lic class MutablePoint { p u b lic in t X. y: p u b lic MutablePoint O { x » 0; y ■* 0; p u b lic M utablePoint(M utablePoint p) { t h is . x ~ p .x ; t h i s .y - p . y :

void vehicleHovedtVehicleHovedEvent e v t) {

)

Point loc = evt.getN ew L ocationO ; vehicles.setLocationtevt,getVehicleld(), loe.x, loe.y):

J1

¡j.'

■ J;

Ponieważ wątek widoku i wątki aktualizacyjne współbieżnie korzystają z modelu danych,! i musi on być zabezpieczony pod kątem wątków. Listing 4.4 przedstawia implementację]: klasy śledzenia pojazdów stosującą wzorzec monitora, który używa klasy MutablePoint-ą z listingu 4.5 do reprezentacji położeń. i: Listing 4 .4 . Im plem entacja klasy śledzenia p ojazdów stosująca wzorzec monitora __________________ ):s (PThreadSafe p u b lic cla ss Monito rV e hicle T ra cker { PG uardedB yt"this") p riv a te fin a l Map lo c a tio n s :

I :■

p u b lic M onitorVehicleTracker(M ap lo c a tio n s ) ( th is , lo c a tio n s = deepCop.y( lo c a tio n s ) ;

1



•i

p u b lic synchronized Map g e tL o c a tio n s () { re tu rn deepCopytlo c a tio n s ) :

p u b lic synchronized M utablePoint g e tL o c a tio n (S trin g id ) M utablePoint lo c - lo c a tio n s .g e t( i d ) : re tu rn lo c — n u ll ? n u ll : new M u ta b le P o in t(lo c);

'|j

{

.

Implementacja uzyskuje bezpieczeństwo wątkowe, kopiując zmienne dane przed ich zwróceniem klientowi. Najczęściej nie powoduje to utraty wydajności, chyba że zbiór śledzonych pojazdów jest bardzo duży4.'Inną konsekwencją kopiowania danych w każdym wywołaniu g e tto c a tio n ( ) je st to, żc zawartość zwróconej kolekcji nie zmieni się nawet wtedy, gdy zmianie ulegnie położenie pam iętane wewnątrz klasy śledzącej. To, czy efekt ten okaże się pożądany czy niepożądany, zależy od wymagań klasy. Może być zaletą, jeśli wymagamy wewnętrznej spójności zbioru lokalizacji (wtedy uzyskanie zestawu danych z tej samej chwili czasu je st wyjątkowo istotne), ale w a d ą gdy kod wywołujący wymaga najświeższych danych o każdym pojeździe i z tego powodu musi częściej wywoływać metodę pobierającą zbiór.

. f

Zauważ, że deepCopy () nie może lak po prostu otoczyć odwzorowania, używając unmodi f iabl eMap, ponieważ chroniłoby to jedynie kolekcję przez modyfikacją; żadną ochroną przed zmianami nie byłyby objęte obiekty zmienne zawarte w kolekcji. Z tych samych powodów nic zadziałałoby wypełnienie obiektu IlashMap za pomocą konstruktora kopiującego, ponieważ zostałyby skopiowane jedynie referencje do obiektów, a nie same obiekty lokalizacji.

'j 1 j j

Ponieważ metoda deepCopy () zostaje wywołana z metody synchronizowanej, wewnętrzna blokada obiektu śledzenia pojazdów zostaje zajęta przez okres operacji kopiowania. Gdy operacja ta trwa długo, pogorszeniu ulega szybkość odpowiedzi na działania użytkownika w interfejsie graficznym.

j j:

} p u b lic synchronized void s e tL o c a tio n (S trin g id , in t x. in t y) { M utablePoint lo c - lo c a tio n s .g e t ( id ) :. i f (lo c • n u ll)

Choć klasa MutablePoint nie jest zabezpieczona wątlcowo, odpowiednie zabezpieczenia posiada klasa śledząca. Zawarte w punktach zmienne ani odwzorowanie nigdy nie zostają opublikowane. Gdy musi zwrócić położenie pojazdu do metody wywołującej, stosuje konstruktor kopiujący dla MutablePoint lub metodę deepCopy() do utworzenia nowego obiektu Map zawierającego skopiowane klucze i wartości ze starego obiektu3.

;

76

.



Część I ♦ Podstawy!

1

“ “

R o z d z ia ł

4. ♦ Kompozycja obiektów

77

:

Klasa Point jest bezpieczna wątkowo, bo się nie nigdy nie zmienia. Wartości niezmienne1 można dowolnie w spółdzielić i publikow ać, w ięc nie potrzebujem y tw orzenia kopii; lokalizacji w momencie ich zwracania. . ■ ;

4.3. D elegacja bezpieczeństw a w ątkow ego N iem al w szystkie sytuacje poza tym i najprostszym i d otyczą obiektów złożonych, W zorzec monitora przydaje się, gdy tworzymy klasy od podstaw lub składamy nowe: klasy z istniejących klas niezabezpieczonych wątkowo. Co zrobić, gdy komponenty, klasy są już odpowiednio zabezpieczone? Odpowiedź brzmi: „to zależy”. Czasem zlożei nie wykonane z zabezpieczonych wątkowo komponentów również jest zabezpieczone1' (listingi 4.7 i 4.9), a innych razem jest zaledwie dobrym początkiem (listing 4.10). W klasie C ountingFactorizer ze strony 34. dodaliśmy obiekt AtomicLong do wcześniej/' szego bezstanowego obiektu. Wynikowy obiekt złożony nadal był bezpieczny wątkowo* Poniew aż stan klasy C ountingFactorizer to tak naprawdę stan bezpiecznej wątkowo1 klasy AtomicLong, a klasa C ountingFactorizer nie wprowadza żadnych dodatkowych' ograniczeń co do poprawności stanu licznika, łatwo stwierdzić jej poprawność w śra-j dow isku wielowątkow ym . M ożem y pow iedzieć, że klasa C o u n tin g F acto rizer dele-j' guje odpow iedzialność za sw oje bezpieczeństw o w ątkow e do AtomicLong — klasa; C ountingFactorizer jest bezpieczna, bo bezpiecznąjest klasa AtomicLong.5 ,

4 .3 .1 . Przykład — śledzenie pojazdów stosujące delegację

Klasa DelegatingVehicleTracker z listingu 4.7 nie stosuje żadnej jawnej synchronizacji.! Za cały dostęp do stanu odpowiada klasa ConcurrentHashMap. Poza tym wszystkie klucze; i wartości odwzorowania są niezmienne. Listing 4-7- Delegowanie bezpieczeństwa w ątkowego do ConcurrentH ashM ap PThreadSafe p u b lic class D elegatingV ehicleTracker { p riv a te fin a l ConcurrentMap lo c a tio n s : p riv a te f in a l Map unmodifiableMap; p u b lic DelegatingVehicleTracker(M ap p o in ts ) { lo c a tio n s - new ConcurrentHashMap (p o in ts ); unmodifiableMap - C o lle c tio n s .u n m o d ifia b le M a p !lo c a tio n s ):

1 p u b lic Map g e tLo ca tio n sO { re tu rn unmodifiableMap:

1 p u b lic P oint g e tto c a tio n tS trin g id ) { re tu rn lo c a tio n s .g e t( id ):

j

Jako bardziej rozbudow any przykład delegacji skonstruujm y w ersję klasy śledzenia! pojazdów, która deleguje odpowiedzialność za bezpieczeństwo wątkowe do innej kla­ sy. L okalizacje przechow ujem y w obiekcie Map, więc zacznijm y od jeg o bezpiecznej;] im plem entacji, ConcurrentHashMap. D odatkow o położenia będziem y przechowywać w niezmiennych obiektach Point zamiast w zmiennych obiektach MutablePoint. Nowi klasę przedstawia listing 4.6. Listing 4 .6 . N iezm ienna klasa P oint używ ana w klasie D elegatingVehicleTracker _________________ Plmmutable p u b lic cla ss P oint { p u b lic f in a l in t x, y; p u b lic P o in tC in t x, in t y ) { t h i s ,x ” x; t h is .y =■ y;

Gdyby count nie było zmienną typu f in a ł, analiza bezpieczeństwa klasy C ountingF actorizer byłaby | | znacznie trudniejsza. Gdyby klasa mogła zmodyfikować count, by wskazywało na inny obiekt AtomicLong, musielibyśmy zapewnić widoczność tej zmiany dla wszystkich.wątków korzystających :hi z obiektu oraz zagwarantować, by nie wystąpił wyścig w trakcie zmiany referencji pamiętanej : || w count. To kolejny przykład, że warto używać f in a ł, gdy tylko to możliwe. 'i||

} p u b lic vo id s e tto c a tio n iS tririg id , in t x, in t y ) { i f (lo c a tio n s .re p la c e (id . new P o in t(x . y ) ) -■> n u ll) throw new 11legalArgum entExceptioni“ niepoprawna nazwa pojazdu: “ + id ) ;

Gdybyśmy użyli oryginalnej klasy M utablePoint zam iast Point, złamalibyśm y zasady hermetyzacji przez um ożliw ienie m etodzie g etL o catio n sO zw rócenia referencji do zmiennego stanu niezabezpieczonego pod kątem w ątków. Zauw aż niew ielką zmianę zachowania klasy śledzącej pojazdy. Wersja z monitorem zwracała migawkę lokalizacji;, nowa w ersja zw raca niem odyfikow alny, ale dostępny „na żyw o” w idok lokalizacji: pojazdów. Oznacza to, że jeśli wątek A wywołuje getLocationsO i wątek B modyfikuje; nieco później położenia niektórych punktów, zmiany te zostaną uwzględniane w odwzo- . rowaniu dostępnemu wątkowi A. Jak wskazaliśmy wcześniej, elekt ten może być pożąda­ ny (bardziej aktualne dane) lub niepożądany (potencjalna niespójność widoku flo ty ): w zależności od wymagań. Jeśli wymagamy niezmiennego stanu floty, getL ocationsO może zwracać płytką kopię odwzorowania lo catio n s. Ponieważ zawartość obiektu Map jest niezmienna, wystarczy skopiować sam ą strukturę obiektu Map bez jego zawartości. Now e rozwiązanie przedstawia listing 4.8 (metoda zwraca zwykle HashMap, bo nie pojawia się nigdzie wymóg zwracania odwzorowania bezpiecznego wątkowo).

T

C z ę ś ć I ♦ Podstawy i I

78

Listing 4 .8 . Zw racanie statycznej kopii zbioru lokalizacji zam iast wersji „ na ż y w o '

R o z d z ia ł

4. ♦ Kompozycja obiektów

79

picczna w ątkow o, a poniew aż nie w ystępuje między nimi żaden związek, cala klasa 1 również jest bezpieczna wątkowo, bo deleguje obsługę do obiektów mouseListeners, ,

p u b lic Map g e tLo r.a tio ns() { re tu rn Col 1e c tio n s .unmodi fiableMap( new HasbMap (lo ca t1 o n s));

i keytisten ers.

}

4.3.3. Gdy delegacja nie zadziała

4 .3 .2 . Zmienne ze stanem niezależnym Przedstawione dotąd przykłady delegacji ograniczały się do pojedynczych zmiennych1; stanowych. N ic nie stoi na przeszkodzie, by delegować bezpieczeństwo wątkowe do) więcej niż jednej zmiennej stanowej, o ile te zmienne są od siebie niezależne (klasa je | zaw ierająca nie zakłada żadnych niezm ienników obejm ujących kilka zm iennych’} stanowych). h K lasa Visua Komponent z listingu 4.9 je st komponentem graficznym umożliwiającym1) klientom rejestrację nasłuchiwania zdarzeń myszy i klawiatury. Zawiera listę zareje-if strow anych obiektów nasłuchujących dla każdego typu, w ięc w m om encie zajścia) zdarzenia wywołani zostaną wszyscy, którzy chcieli zostać poinformowani o zdarzeniu,f| N ic ma żadnego związku miedzy nasłuchującymi zdarzeń myszy i zdarzeń klawiatury;'!1 listy są niezależne, w ięc klasa VisuaTComponent m oże delegow ać bezpieczeństwo) wątkowe do poszczególnych list. Listing 4.9. Delegowanie b e z p ie c ze ń s tw a wątkowego do kilku wewnętrznych zmiennych xlanowych p u b lic c la ss Visual Component ( p riv a te f in a l L is t< K e y lis te n e r> k e y tis te n e rs - new CopyOnWriteArrayList(): p riv a te f in a l List mouseListeners - new Ctyy0nWr1teArrayMst j i

Listing 4 .1 0. Klasa zakresu liczb, która niew ystarczająco dobrze chroni niezmiennik. Nie rób tak p u b lic class NumberRange { / / NIEZMIENNIK: lower upper, get O ) throw new ITlegalArgumentExceptionC " n ie mogę ustawić lower " + i + " > u p p er"): lo w e r.s e t ( i ):

)

p u b lic vo id s e tU p p erd n t i ) ( / / Uwago —niebezpieczne sprawdź i działaj i f ( i < lower, get O ) throw new ITlegaTArgumentExceptiont "n ie mogę ustawić upper ” + i + " < lo w e r"):

}j ' §

upper.sett i ):

'■%

} p u b lic vo id addKeyListener(KeyListener lis te n e r ) { keyLi s te n e rs . add(1is te n e r );

p u b lic boolean is In R a n g e tin t i ) { return Ci >= lo w e r.g e tO && i l i s t - C o l i e c t i o n s . s y n c h r o n i z e d l i s t t new A rrayL ist< E > ()); p u b l i c s y n c h r o n i z e d b o o l e a n p u t I f A b s e n t (E x ) boolean a bse n t = ! l i s t . c o n t a i n s ( x ) : i f (absent) list.a d d (x ): return absent;

boolean absent « ! 1ist.co rita in s(x); i f (absent) l i s t .add(x); return absent;

Blokowanie po stronie klienta m a w iele wspólnego z rozszerzaniem klasy — oba po­ dejścia wykorzystują zachowanie klasy bazowej do wykonania odpowiedniej podklasy. Podobnie jak rozszerzanie łamie hermetyzację implementacji [EJ Item 14], blokowanie po stronie klienta lamie herm etyzację strategii synchronizacji.

{

}

4.4.2. Kompozycja

)

Dlaczego przedstaw ione rozwiązanie nie działa? Przecież metoda put I f Absent () jeslf synchronizowana. Problem polega na tym, że do synchronizacji używa zlej blokady!! N iezależnie od tego, jaką blokadę w ykorzystuje klasa L is t, z pew nością nie jest toI blokada używająca obiektów klasy ListH elper. Klasa ListH elper oferuje jedynie iluzję | synchronizacji, bo różne operacje na liście, choć w szystkie poprzedzone slowed.! synchronized, stosują rozmaite blokady, więc metoda put I f Absent O nie je st niepo-l dzielna z punktu widzenia innych operacji listy. N ie mamy żadnej gwarancji, iż inny, f wątek nie zm odyfikuje listy, gdy będzie wykonywana metoda put I f Absent (). j Aby zapewnić poprawne działanie klasy pomocniczej, musimy użyć tej samej blokady co klasa L ist, stosując blokadę po stronie klienta lub blokadę zew nętrzną. Blokowanie; po stronie klienta używ a do blokow ania pew nego obiektu, z którego korzysta przy blokadach również sam X. A by móc skorzystać z tego podejścia, trzeba znać blokadę i używ aną przez X. >: w. Dokumentacja klasy Vector lub klas otoczek zapewniających synchronizację informuje,; że obiekty te sto su ją blokadę w ew nętrzną sam ych siebie, czyli,obiektu Vector hilj; otoczki kolekcji (ale nie otaczanej kolekcji). Listing 4.15 przedstawia poprawną implc: mentację metody put I f Absent () stosująca blokadę po stronie klienta. j Listing 4.15. Implementacja wsław, je śli brakuje z blokadą po stronie klienta __________________ @ThreadSafe class L is tH e lp e r { p u b lic List l i s t - Col le c tio n s . synchronizedListtnew A rra yL ist< E > () ) ;

'

\ ], i (i

Istnieje mniej w rażliw a alternatyw a do dodania operacji niepodzielnej do istniejącej k lasy —• kom pozycja. Klasa ImprovedList z listingu 4.16 im plem entuje operacje listy przez ich delegację do w ew nętrznego egzem plarza L is t oraz zastosow anie osobnej metody put I f Absent (). Podobnie ja k C o lle c tio n s . sy n ch ro n ized L ist i inne otoczki kolekcji kod klasy zakłada, że klient po przekazaniu obiektu listy do konstruktora p rze­ stanie używać obiektu listy bezpośrednio. Będzie jej używał zawsze przed ImprovedLi s t. : Listing 4.16. Implementacja wstaw, je ś li brakuje z użyciem kompozycji @ThreadSafe p u b lic class ImprovedList implements List { p riv a te f in a l List l i s t : p u b lic Im provedList(List l i s t ) { t h i s . l i s t ■» l i s t ;

}

p u b lic synchronized boolean p u tlfA b s e n td x) { boolean contains = lis t . c o n t a in s ( x ) : i f (c o n ta in s ) lis t . a d d ( x ) ; re tu rn ¡con ta in s;

1 p u b lic synchronized void c le a rO { l i s t . c le a r O ; } / / W podobny sposób delegacja innych m etod interfejsu List.

) r

■■ ; ;

C 'iiü S * C zęść I ♦ Podstaw/,

86

Klasa ImprovedList dodaje dodatkowy poziom blokowania przez zastosowanie własnej: blokady wewnętrznej. N ie ma znaczenia, czy przekazany obiekt L ist jest zabezpieczony, wątkowo, bo wszystkie metody stosują własny, spójny system blokowania także wtedy* gdy lista nie je st zabezpieczona lub ulegnie modyfikacji jej sposób blokowania. Cho{: ta dodatkowa warstwa synchronizacji może prowadzić do niewielkiej utraty wydajności^’, im plem entacja klasy ImprovedLi s t je s t mniej krucha niż próby dostosow ania się d0: strategii synchronizacji innego obiektu. W efekcie zastosowaliśmy wzorzec moniiofj, do hermetyzacji istniejącej listy. W ten sposób gwarantujemy jej bezpieczeństwo wąiko? we, jeżeli tylko klasa otaczająca zawiera referencję do listy.

4.5. Dokum entowanie strategii synchronizacji

:

Dokumentowanie to jedno z najważniejszych (i co bardzo przykre, słabo wykorzysta wanych) narzędzi zarządzania bezpieczeństwem wątkowym. Użytkownicy zaglądają do dokumentacji, by się dowiedzieć, czy klasa jest bezpieczna wątkowo. Osoby kon­ serwujące kod zaglądają do dokumentacji, by poznać strategię im plem entacyjną aby przypadkiem nie uszkodzić bezpieczeństwa wątkowego. Niestety, osoby te najczęściej, znajdują w dokumentacji mniej informacji, niż by chciały. ¡•9 ’

Dokumentuj gwarancję bezpieczeństwa,wątkowego użytkownikom, klasy; dokumęntajll strategię synchronizacji osobom konserwującym klasę. '

Każde użycie synchronized, v o la tile i dowolnej klasy bezpiecznej wątkowo powinni) zostać w yjaśnione, bo w skazuje strategię synchronizacji zapew niającą integralno^ danych w przypadku współbieżnego dostępu. Strategia stanowi element projektu pro­ gramu, więc należy j ą dokumentować. Oczywiście najlepszym czasem na dokument tow anie sw ych w yborów jest czas projektow ania. Tygodnie lub m iesiące później szczegóły m ogą zbyt mocno się rozmyć — zapisz je, zanim o nich zapomnisz. ę Określenie strategii synchronizacji wymaga podjęcia kilku decyzji: które zmienne powitji ny być typu v o la tile , które powinny być chronione blokadami, jakie blokady chronią poszczególne zm ienne, które operacje pow inny być niepodzielne itp. N iektóre mfori macje są ściśle implementacyjne i powinny zostać udokumentowane dla osób, które będą zajm ow ać się kodem w przyszłości. N iektóre w ypływ ają je d n ak na obserwowane, publiczne zachowanie klasy i z tego powodu ich opis warto zawrzeć w specyfikacji klasyj Najważniejsza jest informacja o gwarancjach bezpieczeństwa wątkowego zapewnianej przez klasę. Czy rzeczyw iście je s t bezpieczna? Czy w yw ołuje inne m etody, prze­ trzym ując blokadę? Czy istnieją inne blokady mogące wpływać na jej zachowanie! Niech klient klasy nie zgaduje, bo to wyjątkowo ryzykowne. Jeżeli nic godzisz sięi# obsługę blokowania po stronie klienta, dobrze o tym poinformuj. Gdy klient ma moź|j 7 Spowolnienie będzie niewielkie, bo synchronizacja wewnętrznej listy zawsze będzie nicblokująca, . a przez lo szybka. Patrz rozdział 11. .¡j

pnrriział 4. ♦ Kompozycja obiektów

87

wość tworzenia własnych operacji niepodzielnych na podstawie klasy (patrz podroz­ dział 4.4), poinformuj o rodzaju blokady do wykorzystania, by operację przeprowadzić bezpiecznie. Stosując błokady, poinform uj o nich przyszłych użytkowników i kon­ serwatorów, bo je st to takie proste — w ystarczy adnotacja (PGuardedBy. Gdy bezpie­ czeństwo wątkowe zapew niają nietypowe sztuczki, dokładnie je opisz. Obecny stan dokumentacji dotyczącej bezpieczeństwa wątkowego, nawet w klasach bibliotek platformy, nie je st najwyższych lotów. Ile razy zaglądamy do dokumentacji klasy i zastanawiamy się, czy jest bezpieczna w ątkow o?8 W iększość klas nie oferuje w tym względzie żadnej wskazówki. Wiele oficjalnych specyfikacji technologii Javy, na przykład serwlcty i JDBC, niedokładnie omawia wymagania dotyczące bezpie­ czeństwa wątkowego. Choć przyzwoitość nakazuje, by nie zakładać zachowań niestanowiących części spe­ cyfikacji, często i tale ich dokonujemy, co oznacza prawdopodobieństwo złego wyboru. Czy można założyć, żc obiekt je st bezpieczny w ątkow o, bo w ydaje się nam, że tak właśnie być powinno? Czy dostęp do obiektu zabezpieczymy wątkowo, jeśli najpierw pobierzemy jeg o blokadę? Ta ryzykow na technika zadziała, jeśli kontrolujem y cały kod korzystający z tego obiektu; w przeciwnym razie uzyskujemy tylko iluzję bezpie­ czeństwa wątkowego. Żadne z rozwiązań nie satysfakcjonuje. Niejednokrotnie intuicja co do bezpieczeństwa wątkowego danej klasy po prostu zawodzi. Przykładowo java.text.S im pleD ateF orm at nic je st bezpieczna wątkowo, ale doku­ mentacja inform uje o tym dopiero od JD K 1.4. Ile program ów błędnie utworzyło współdzielony egzemplarz klasy niezabezpieczonej wątkowo i stosowało go w wielu wątkach, czym nieświadomie narażało się na błąd w przypadku dużego obciążenia. Problem z SimpleDatel-orinat można rozwiązać, zakładając, żc jeśli klasa jawnie nie informuje o bezpieczeństwie wątkowym, nie je st odpowiednio zabezpieczona. Z drugiej strony, niemożliwe jest napisanie aplikacji stosującej scrwlety bez dokonywania w ąt­ pliwych założeń na temat bezpieczeństwa wątkowego klas zapewnianych przez kontener, na przykład HttpSession. Nie pozwól, by klienci lub koledzy musieli zgadywać.

4.5.1. Interpretacja nieścisłej dokumentacji Wiele specyfikacji technologii Javy milczy lub przynajm niej niedostatecznie dobrze określa gw arancje i wymogi bezpieczeństw a wątkow ego dla interfejsów takich jak ServietContext, HttpSession lub DataSource7. Ponieważ interfejsy implementuje kon­ tener lub dostawca bazy danych, często nie możemy zajrzeć do kodu, by się przekonać, jak działają. N ie chcem y też uzależniać się od im plem entacji proponow anej przez jednego z dostawców sterow nika JD BC — chcem y zachować poprawność kodu, by działał z każdym sterownikiem. N iestety, słowa „w ątek” lub „współbieżny” w ogóle nic pojawiają się w specyfikacji JD BC i zadziwiająco rzadko w ystępują w specyfika­ cji serwletów. Co możemy zrobić? Jeśli nigdy się nad tym nie zastanawiasz, podziwiamy Twój optymizm Wydaje nam się szczególnie irytujące to, że braki szczegółowego określenia bezpieczeństwa wątkowego Występują cały czas pomimo pojawiania się coraz to nowych wersji specyfikacji. ;

M usim y zgadyw ać. Jednym ze sposobów zwiększania prawdopodobieństwa właśc$ w ego odgadnięcia je st analiza specyfikacji z punktu widzenia osoby, która j ą m e n tu je (czyli producenta kontenera serwletów lub bazy danych), a nie osoby, którgi jej używa. Serw lety zaw sze są w yw oływ ane z poziom u wątku zarządzanego prze¿i kontener. M ożna więc bezpiecznie założyć, że jeśli istnieje więcej niż jeden taki wąte)^ kontener o tym wie. Kontener udostępnia kilka obiektów wykorzystywanych jednocz¿| śnie przez wiele serwletów: H ttpSession i ServietC ontext. Z tego względu p o w in ij się spodziew ać współbieżnego dostępu do nich, bo tworzy w iele w ątków i wywołuje metody takie ja k Servi e t. s e rv ic e ! ), które najprawdopodobniej odczytują zawartość S ervietC ontext.

| 'h

Ponieważ trudno sobie wyobrazić jednowątkowy kontekst, w którym obiekty te byfybjl! przydatne, m ożem y z ogrom ną dozą praw dopodobieństw a założyć, że powinny bj> () ) : / / Może zgłosić wyjątek CancitrreiuModificatiouException. fo r (Widget w : w id g e tL is t) doSomething(w);

Istnieje kilka powodów, dla których blokowanie kolekcji w trakcie iteracji okazuje się niepożądane. Inne wątki wykorzystujące kolekcję będą czekały na zakończenie iteracji; jeśii kolekcja jest duża lub wykonanie operacji dla jednego elementu trwa długo, mogą czekać naprawdę długo. Jeśli kolekcja jest zablokowana (patrz listing 5.4), doSomethingO zostaje w yw ołane w obrębie blokady, co m oże prow adzić do blokady wzajemnej (patrz rozdział 10.). N aw et przy braku ryzyka zagłoszenia lub blokady wzajemnej

2 W yjątek ConcurrentModi fic a tio n E x c e p tio n może pojaw ić się także w kodzie jednow ątkow ym . Stanie się lak wtedy, gdy obiekt zostanie z ko le k c ji usunięty bezpośrednio, a nie za pomocą Ite r a to r , remove ().



93: f



;

blokowanie kolekcji przez dłuższy cżas drastycznie zm niejsza skaiow alność aplikacji. : Im dłużej trzymamy blokadę, tym większe jest praw dopodobieństw o oczekiwania na , nią przez inny wątek. Im więcej takich wątków, tym mniejsze wykorzystanie mocy proce- : sora, bo wątki zamiast wykonywać użyteczne zadania, tylko czekają (patrz rozdział 11.). : Alternatywą dla blokowania kolekcji jest jej sklonowanie i iteracja przez kopię. Ponieważ > klonowanie będzie zamknięte w jednym wątku, żaden inny wątek nie zmodyfikuje kolek- : cji, więc w yelim inujem y m ożliw ość wystąpienia wyjątku ConcurrentM odificationException (kolekcja nadal powinna być zablokowana w trakcie klonowania). Klonowanie ■. kolekcji również niesie ze sobą pewien koszt: to, czy będzie opłacalny, zależy od wiciu . czynników: rozm iaru kolekcji, liczby zadań w ykonyw anych dla każdego elem entu, , częstości operacji iteracji względem innych operacji wykonywanych na kolekcji, wymo- ; • gów szybkości odpowiedzi i przepływności.

5.1-3. Ukryte iteratory Gdy blokada chroni iterację przed zgłoszeniem wyjątku C oncurrentM odification- • Exception, musimy pamiętać, by stosować j ą wszędzie, gdzie w spółdzielona kolekcja ; może być iterowana. Jest to trudniejsze niż się wydaje, bo iteratory są często ukryte, co przedstaw ia klasa H id d e n lte ra to r z listingu 5.6. Choć w kodzie nie w ystępuje jawna iteracja, pogrubiony fragment zawiera iterację. Złączenie tekstów zostaje zamie­ nione przez kompilator na wywołanie StringBuilder.append(O bject), który to wywołuje metodę to S trin g O kolekcji. Implementacja metody to S trin g O w standardowych kolek­ cjach przechodzi przez wszystkie elementy kolekcji i wywołuje ich metody to S trin g ( ), i ■by uzyskać ładnie sform atow aną reprezentację zawartości kolekcji. Listing 5 .6 . Iteracja ukryta w złączaniu tekstów. N ie rób lak p u b lic cla ss H id d e n lte ra to r { @GuardedBy("this“ ) p riv a te f in a ł Set se t = new HashSet(): p u b lic synchronized void addG nteger i ) { s e t.a d d ( i) : ) p u b lic synchronized void rem oveiIriteg e r i ) { s e t. rem oveti): } p u b lic vo id addTenThingsO { Random r = new RandomC): fo r ( i n t i - 0; i < 10: i++) a d d ( r .n e x tln t( ) ); System .out.printlnCDEBU G: dodano 10 elementów do " + s e t);

M etoda addTenThingsO m oże zgłosić w yjątek C o n cu rre n tM o d ifica tio n E xce p tio n , ponieważ kolekcja je st iterowana w m etodzie to S trin g C ) w trakcie przygotowywania komunikatu. Oczywiście rzeczywistym problemem jest brak bezpieczeństwa wątkowego klasy H id d e n lte ra to r; blokadę należałoby założyć przed użyciem s e t( ) w wywołaniu p r i n t l n i ), ale kod dzienników zdarzeń najczęściej tego nie czyni.

"IF

C zęść 1 ♦ PodstawyfS

94

R o z d z ia ł

5- ♦ Bloki budowania aplikacji

95

Z przedstawionego przykładu warto wynieść następującą lekcję: im większa odległo^' między stanem i chroniącą go synchronizacją tym większe prawdopodobieństwo, że ktoś zapomni użyć synchronizacji przy dostępie do stanu. Gdyby' H id d e n lte ra to r otacza) HashSet, przy użyciu synchronizedSet hermetyzującego synchronizację, tego rodzaju błąd nie m ia łb y praw a wystąpić. (

Interfejs BlockingQueue rozszerza interfejs Queue o operacje blokującego wstawiania i usuwania. Jeśli kolejka je st pusta, pobieranie blokuje się aż do momentu, gdy element będzie dostępny. Jeśli kolejka jest pełna, wstawianie blokuje się (w kolejkach o ograni­ czonym rozmiarze) aż do momentu, gdy zwolni się miejsce. Kolejki blokujące doskonale nadają się do projektow ania kodu typu producent-konsum ent. Są dokładniej opisane w podrozdziale 5.3.

i . Podobnie ja k hermetyzacja stanu obiektu’ułatwia utrzymanie .poprawności .niezm ienni !■ ków/ tak hermetyzacja synchronizacjfulatwia przestrzeganie strategii synchronizacji^

Podobnie ja k ConcurrentHashMap je st współbieżnym zastąpieniem synchronizowanej wersji Map, w Javic 6 ConcurrentSkipListMap i ConcurrentSkipListSet są współbieżnymi zastąpieniami SortedMap i SortedSet (implementowanymi przez otoczenie synchronizedMap klas TreeMap i TreeSet).

Iteracja często zostaje pośrednio wykonana przez metody hashCode() i e q u a łs() kolek-' cji, które m ogą zostać wywołane, gdy kolekcja znajdzie się we wnętrzu innej kolekcji;' M etody containsAl 1() , removeAl 1() ; re ta in A l 1O i konslruktory przyjmujące kolekcje rów nież stosują iteracje. W szystkie przedstaw ione pośrednie sposoby iteracji mogą prowadzić do zgłoszenia wyjątku ConcurrentModi f icat'ionException. . ;«

5.2. Kolekcje w spółbieżne Java 5.0 popraw ia obsługę kolekcji synchronizow anych przez w prow adzenie kilku klas kolekcji współbieżnych. Kolekcje synchronizowane uzyskują swoje bezpieczeństwo : przez szeregowanie całego dostępu do stanu kolekcji. Prowadzi to do słabej wspólbieżnośei. Gdy wiele w ątków oczekuje na globalną blokadę kolekcji, cierpi na tym prze­ pływ ność. r Kolekcje współbieżne zostały zaprojektowane do współbieżnego dostępu z wielu wąt­ ków. Java 5.0 wprowadza klasę ConcurrentHashMap zastępującą implementację Map bazJt ją cą n a otoczce synchronizującej oraz klasę CopyOnWri te A rra yL ist zastępującą synchroni­ zowane listy, g d y dominującą operacją jest iteracja. Nowy interfejs ConcurrentMap dodaje obsługę typow ych operacji złożonych, na przykład w staw, jeśli brak, zastąp i usuń warunkowo. t, ;

Zastąpienie kolekcji synchronizowanych kolekcjami współbieżnymi znacząco poprawił skalowalność przy niewielkim ryzyku. V|J|

Java 5.0 dodaje dw a now e typy kolekcji: Queue i BlockingQueue. Interfejs Queue ma a zadanie tym czasow o przechow yw ać zbiór elem entów oczekujących na wykonanie, Java udostępnia kilka implementacji, włączając w to ConcurrentL'inkedQueue (tradycyjni kolejka FIFO ) i P riorityQ ueue (niew spólbieżną kolejkę priorytetową). Operacje lit, Queue nie blokują działania aplikacji. Jeśli kolejka jest pusta, operacja pobrania od razi), zw raca w artość n u li. Choć można sym ulować działanie kolejki, używ ając obiekt«L ist — w rzeczywistości L inkedlist również implementuje interfejs Queue — specjał#! klasy kolejek dodano, by zapewnić bardziej współbieżne implementacje (bo lcolejm nie wym aga sw obodnego dostępu do jej elementów). ;j

5.2.1. Klasa ConcurrentHashMap ;i Synchronizowane klasy kolekcji przetrzym ują blokadę przez cały czas trwania każdej operacji. Pewne operacje, na przykład HashMap.getO lub L is t.c o n ta ln s O , niejednokrotnie w ym agają w ięcej pracy niż się początkow o wydaje: przejście przez kubełki tablicy mieszającej lub listy w celu odnalezienia obiektów zw racających tru e przy porównaniu m etodą equal s ( ) (sam e te operacje m ogą być bardzo kosztowne). Jeśli w kolekcji będącej tablicą m ieszającą m etoda hashCodeO niewystarczająco dobrze rozsiewa wartości skrótu, elementy m ogą być nierówno rozłożone w dostępnej prze­ strzeni tablicy mieszającej (w najgorszym przypadku tablica mieszająca zredukuje się do listy jednokierunkowej). Przejście przez listę i wywoływanie equa l s ( ) dla każdego obiektu potrafi zająć naprawdę dużo czasu — inne w ątki nie m ają wtedy dostępu do kolekcji.

i

. ;j ;

\i

: ;; .! ii : i: j 1i . : j. Ii ;i

1: > Klasa ConcurrentHashMap to bazująca na tablicy mieszającej implementacja interfejsu j Map podobna do HashMap, ale stosująca całkowicie inną strategię blokowania oferującą j większą wspólbieżność i skalowalność. Zamiast synchronizować każdą metodę osobną : j blokadą, ograniczając tym samym dostęp do tylko jednego wątku, używa bardziej > : ’ szczegółowego m echanizm u blokow ania nazyw anego b lo k o w an iem p ask o w y m i (patrz punkt 11.4.3) znacząco poprawiającego stopień współbieżności. Dowolnie duża liczba wątków może jednocześnie odczytywać kolekcję, wątki odczytujące mogą współ­ bieżnie odczytywać j ą z wątkami zapisującymi. Jedynie ograniczona liczba wątków zapisujących ma współbieżny dostęp do kolekcji. Zapewnia to znacząco większą prze­ pływność przy współbieżnym dostępnie przy niewielkiej utracie wydajności dla dostępu jednowątkowego.

Klasa ConcurrentHashMap, podobnie ja k inne kolekcje współbieżne, poprawia i w inny sposób implementację kolekcji synchronizowanych, na przykład przez zastosowanie iteratorów, które nie zwracają wyjątku ConcurrentModificationException i tym samym nie wymuszają blokady kolekcji w trakcie itetacji. I tera to ry zwracane przez Concur­ rentHashMap s ą słab o spójne, ale nie inform ują o błędzie m odyfikacji. Słabo spójny iterator toleruje współbieżne modyfikacje, przechodząc przez kolekcję w wersji z mo­ mentu uruchamiania iteracji (nie gwarantuje zauważenia zmian w kolekcji, które zaszły po rozpoczęciu iteracji).

.i ;■

C zęść I ♦ Podstawy' F

96

W iększość usprawnień niesie ze sobą pewne wady. Semantyka operacji działających na całym odwzorowaniu Map, na przykład size O lub isEmptyO, została nieco osłabionaw celu odniesienia się do współbieżnej natury kolekcji. Ponieważrozm iar może być prze-, starzały w momencie wyliczania, stanowi tak naprawdę jedynie przybliżenie — metoda s iz e O zw raca tylko przybliżenie rzeczyw istego rozmiaru. Choć początkowo może w ydaw ać się to straszne, w rzeczyw istości okazuje się, że m etody typu s iz e () luj, isEmptyO w środowiskach wielowątkowych są znacznie mniej użyteczne, bo dotyczą pewnej migawki stale zmieniającego się stanu. Wymogi dła tych operacji zostały osłabię, ne, by ułatwić optymalizację ważniejszych operacji, w szczególności g e t( ) , put() containsK ey() i removeO. Jedną z funkcji oferowanych przez synchronizowaną implementację Map, ale niedostępną w ConcurrentHashMap, jest możliwość zablokowania kolekcji na wyłączność. Uzyskanie blokady synchronizow anej wersji kolekcji H ashtable uniem ożliw ia innym wątkom ! otrzym anie dostępu do niej. M oże to być przydatne, gdy na przykład potrzebujemy w niepodzielny sposób dodać kilka odwzorowań lub kilkukrotnie iterować przez kolekcję ' przy zapewnieniu, że zawsze uzyskamy tę sam ą kolejność. Z drugiej strony jest to roz­ sądne „coś za coś” — od kolekcji współbieżnych oczekuje się stałej zmiany zawartości. Ponieważ ma tak wiele zalet i tak mało wad w porównaniu z Hashable i synchroni zedMap, zastąpienie synchronizowanej kolekcji ldasą ConcurrentHashMap w większości przypad­ ków znacząco zwiększa skalowalność aplikacji. Nowego rozwiązania nie należy stosować tylko w aplikacjach wymagających dostępu do kolekcji na w yłączność3.

5 .2 .2 . Dodatkowe, niepodzielne operacje dla Map Ponieważ klasy ConcurrentHashMap nie można zablokować w celu wyłącznego dostępu, nie uda nam się w prow adzić blokady po stronie użytkownika dającej nowe operacje niepodzielne, na przykład wstaw, jeśli brak (czyniliśmy tak dla klasy Vector w punkcie 4.4.1). Typow e operacje niepodzielne, takie jak wstaw, jeśli brak, usuń, jeśli równe, zastąp, jeśli równe, zostały zaw arte w interfejsie ConcurrentMap przedstawionym na listingu 5.7. Jeśli musisz samemu dodawać implementację takich metod do istniejącej, synchronizowanej implementacji, prawdopodobnie oznacza to, że warto rozważyć użycie interfejsu ConcurrentMap. Listing 5 .7 . Interfejs ConcurrentM ap __________________________________________________________ p u b lic in te rfa c e ConcurrentMap extends Map { / / Wsławia do odwzorowania tylko wtedy, gdy żadna wartość nie je s t odwzorowana z K. V-put IfA b se n t(K key. V va lu e ); / / Usuwa tytko wtedy, gdy K je s t odwzorowane na V. boolean remove(K key, V va lu e ); / / Zastitp wartość tylko wtedy, gdy K jest odwzorowane na oldValue. boolean replace(K key, V oldValue, V newValue):

3 Lub gdy polega się na efektach ubocznych synchronizacji zapewnianej przez synchroni zedMap.

Rozdział 5. ♦ Bloki budowania aplikacji

97

l / Zastcip wartość tylko wtedy, gdy K jest odwzorowane na pew ną wartość. V replace(K key. V newValue):

5.2.3. CopyOnWriteArrayList

j

Klasa CopyOnWriteArrayList to współbieżna wersja zastępująca synchronizowaną listę! i oferująca lepszą wspólbieżność w typowych sytuacjach i eliminująca potrzebę blokowa-t nia lub kopiowania kolekcji przed iteracją (istnieje równoważna klasa CopyOnWriteArraySet( zastępującą synchronizowany zbiór). ,f Kolekcje kopiow ania przy zapisie uzyskują sw oje bezpieczeństw o w ątkow e na pod- i stawie faktu, że jeśli tylko obiekt niezmienny technicznie je st poprawnie opublikowany, i nie potrzeba żadnej synchronizacji przy dostępie do niego. Zmienność kolekcji powstaje ; dzięki tworzeniu i ponownej publikacji kopii kolekcji przy każdej modyfikacji, iterator! takiej kolekcji zapam iętuje referencję do wersji, która istniała w czasie rozpoczynania iteracji. Ponieważ ona nigdy się nie zmieni, synchronizacja dotyczy tylko poprawnego i uwidocznienia zawartości tablicy. W elekcie wiele wątków może jednocześnie iterować kolekcję bez przeszkadzania sobie naw zajem . N ie zaszkodzi im nawet wątek chcący modyfikować kolekcję. Itcratory zw racane przez kolekcje kopiowania przy zapisie nie zgłaszają wyjątku ConcurrentModi f i cat1 onExcepti on i zw racają elem enty dokładnie ; w. takiej postaci, w jakiej były w momencie tworzenia iteralora (niezależnie od przyszłych modyfikacji). Oczywiście istnieje pewien koszt kopiowania tablicy za każdym razem, gdy jest mody- < filcowana, szczególnie dla dużej kolekcji. Z tego powodu stosowanie kolekcji kopiują- ! cych przy zapisie ma sens tylko wtedy, gdy operacje iteracji s ą znacznie częstsze od modyfikacji. To kryterium dokładnie pasuje do systemów powiadamiania o zdarzeniach: dostarczenie zdarzenia wym aga iteracji przez listę, by wyw ołać m etody w szystkich obiektów nasłuchujących. Rejestracja i w yrejestrow anie obiektów nasłuchujących najczęściej przeprowadza się znacznie rzadziej niż zachodzą zdarzenia. Więcej infor­ macji na temat kopiowania przy zapisie znajduje się w [CJP 2.4.4].

5.3. Kolejki blokujące oraz w zorzec producenta i konsum enta Kolejki blokujące w prow adzają blokujące operacje p u tO i ta k e O oraz ograniczone czasowo ekw iw alenty metod o f f e r t ) i poi ł O , Jeśli kolejka je st pełna, metoda p u t( ) blokuje się do momentu zwolnienia się miejsca. Jeśli kolejka je st pusta, metoda tak eO blokuje się do momentu udostępnienia choć jednego elementu. Kolejki m ogą mieć ogra­ niczoną pojemność. Kolejka nieograniczona nigdy nie je st pełna, więc metoda p u t() takiej kolejki nigdy się nie blokuje.

C zęść I ♦ Podstawy

98

K olejka blokująca obsługuje wzorzec projektow y p ro d u cen t-k o n su m en t. Wzorzec ten rozdziela kod w skazujący, co ma zostać zrobione na specjalnej liście zadań oil! kodu, k tó r y rzeczyw iście realizuje całą pracę. Wzorzec upraszcza tworzenie aplikacji bo usuwa zależności w kodzie między producentem i konsumentem ¡ .upraszcza z a r z ą ­ dzanie pracą, bo produkcja i konsum pcja m ogą odbywać się z różną szybkością. , We wzorcu producent-konsument wykonanym na podstawie kolejki blokującej producent/, um ieszcza dane w kolejce, gdy tylko sta n ą się dostępne. K onsum ent pobiera dane ■ z kolejki, gdy je st golowy przyjąć kolejne zlecenie. Producent nie musi nic wiedzieć 0 konsum entach i ich liczbie; nawet o tym, czy istnieje tylko jeden producent — m-p za zadanie jedynie umieszczać dane w kolejce. Podobnie konsument nie musi znać liczby producentów ani miejsca pochodzenia danych. Kolejka BlockingQueue upraszcza im pM m entację system u z w ielom a konsum entam i i producentam i. Najpopularniejszym, przykładem wzorca producent-konsument jest pula wątków połączona z kolejką zadań; wzorzec ten stosuje dodatkow o szkielet w ykonyw ania zadań Executor opisywanyw rozdziałach 6. i 8. Typow y przykład dwóch osób zm ywających naczynia doskonale oddaje sposób dzia­ łania wzorca producent-konsument. Jedna osoba zmywa naczynia i umieszcza je w okap-j triku, druga osoba zabiera naczynia z okapnika i je wyciera. W tym rozw iązaniu okap-ł nik stanowi kolejkę blokującą — jeśli nie ma w nim żadnych naczyń, druga osoba stoi:. 1czeka na ich nadejście; gdy olcapnik zapełni się w całości, pierwsza osoba musi czekać! na wolne miejsce. Analogię tę można rozwinąć na kilka producentów' (może być wiele zlewów') i wiele konsumentów, ale zawsze istnieje tylko jeden okapnik. Nikt tak napraw-'dę nie musi przejmować się tym, ile je st producentów i konsumentów', ani kto położył na okapniku konkretne naczynie. : Etykietki „producent” i „konsum ent” są względne: działanie, które w jednym kontekt ście jest konsumentem, w drugim może być producentem. Suszenie naczyń „konsumuje® czyste i mokre naczynia, by „wyprodukow ać” czyste i suche naczynia. Trzecia osoba chcąca pomóc mogłaby odkładać suche naczyniu na właściwe miejsce. W tym momencieosoba susząca staje się jednocześnie producentem i konsumentem, bo współdzieli dwie kolejki (każda z nich może zablokow ać suszącego). Kolejki blokujące upraszczają kod konsum enta, bo ta k e O blokuje się do momentu nadejścia danych. Jeśli producent nie będzie wystarczająco szybko generował danych,, by zająć konsumenta, będzie on czekał na zadania. Czasem taka sytuacja jest normalna i dopuszczalna (serwer aplikacji odczekuje na żądania od klientów), a czasem wskazuje, że warto zmienić współczynnik liczby producentów na konsumenta, by poprawić wyko­ rzystanie zasobów (jak w przypadku systemu przeszukiwania sieci, który w zasadzie ma nieskończenie wiele zadań do wykonania). o. Jeśli producent szybciej tworzy zadania, niż konsument potrafi je obsłużyć, w pewnym momencie aplikacji zapewne zabraknie pamięci, gdy kolejka nie ma żadnego graniczenia pojemności. W k o lejce o ogra n iczo n y m rozm iarze blokująca natura metody putfj znacząco upraszcza kod producenta, bo przestaje on działać, gdy nie ma miejsca na now e dane.

R o z d z ia ł

5. ♦ Bloki budowania aplikacji

99

Kolejki blokujące zaw ierają też metodę o f f e r ( ), która zwraca błąd, jeśli elementu nie można umieścić w kolejce. U łatwia to tworzenie bardziej elastycznych reguł radzenia sobie z przeciążeniem, na przykład przez zapisywanie nadchodzących danych na dysk twardy, redukcję liczby wątków produkujących lub ograniczenie producentów w inny sposób. i Kolejki o ograniczonej pojemności stanowią doskonałe narzędzie zarządzania zasobami aplikacji --.czyn ią program bardziej odpornym na przeciążenia przez ograniczenie zadań mogących wygenerować więcej zadań, niż można obsłużyć. ' :

Choć wzorzec producent-konsument umożliwia rozdzielenie kodu producenta i kon­ sumenta, zachow anie obu elementów nadal od siebie pośrednio zależy, bo współdzielą jedną kolejkę. M oże kusić założenie, że konsum enty zawsze będą nadążać, by nie zajmować się ograniczeniem kolejki, ale oznacza to najczęściej konieczność zmiany architektury, tyle żc rozłożoną w czasie. W budow uj zarządzanie zasobam i w projekt jak najszybciej — łatwiej zrobić to od razu niż w przyszłości w iele poprawiać. Kolej­ ki blokujące ułatwiają cale zagadnienie na wiele sposobów, ale gdy niełatwo dostosować je do konkretnej sytuacji, warto posiłkować się innymi blokującymi strukturami danych, semaforami (patrz punkt 5.3.3). Biblioteka klas zawiera kilka implementacji BlockintjOueue. Klasy LinkedBlockingCJueue i Arra.yB1ockingQueue to kolejki FIFO analogiczne do LinkedList i A rra yL ist, ale oferu­ jące lepszą wydajność współbieżną niż listy synchronizowane. Klasa P riori tyB lockingQueue to kolejka priorytetowa, gdy chce się przetwarzać elementy w innym porządku niż FIFO. Podobnie ja k inne kolekcje sortujące, klasa P rio ri tyB1ockingQueue potrafi porównywać elementy zgodnie z porządkiem naturalnym (jeśli implementują interfejs Comparabie) lub za pom ocą obiektu Comparator. Ostatnia implementacja BlockingOueue, SynchronousQueue, nie jest tak naprawdę żadną kolejką bo nie przechowuje jej elementów. Zamiast tego przechowuje listę slcolejkowanych w ątk ów czekających na um ieszczenie lub pobranie elem entu. W analogii ze zmywaniem naczyń przypominałoby to sytuację, w której nie używamy okapnika, ale od razu podajemy mokre naczynia do pierwszej wolnej osoby je suszącej. Choć wydaje się to dziwnym sposobem obsługi kolejki, redukuje czas potrzebny do przeniesienia danych od producenta do konsumenta, bo praca często zostaje w ykonana natychmiast (w tradycyjnej kolejce operacje wstawiania i usuwania m uszą zakończyć się w pełni, zanim jednostka zadaniowa będzie mogła się nimi zająć). Bezpośrednie przekazanie niesie ze sobą więcej informacji zwrotnych o stanie zadania do producenta. Gdy przyjmie je konsument, wie o jego wykonaniu — nie musi przejmować się pozostawieniem go na pastwę losu. Przypomina to różnicę m iędzy przekazaniem dokum entu osobiście a pozostawieniem go w skrzynce pocztowej. Ponieważ SynchronousQueue niczego nie przechowuje, metody p u t( ) i ta k e () blokują się do momentu, w którym będzie dostęp­ ny choć jeden niezajęty wątek. Kolejkę tego typu stosuje się tylko wtedy, gdy ma się pewność co do wystarczającej liczby konsumentów, by producent nie musiał czekać z danymi.

•_________ C zęść I ♦ Podstay^j"

100

5 .3 .1 . Przykład — przeszukiwanie komputera osobistego Przykładem programu, który doskonale nadaje się rozłożenia na część produkującą i sumującą, je st agent przeszukujący lokalne dyski twarde w poszukiwaniu dokumenty* i ich indeksow aniu w celu uproszczenia przyszłego w yszukiw ania — tak działają! aplikacje Google Desktop i usługa Windows Indexing. Klasa DiskCrawler z listingu j j ; zawiera zadanie producenta, które przeszukuje hierarchię plików w poszukiwaniu odpęd wiednich plików do indeksacji i umieszcza ich nazwy w kolejce. Klasa Indexer z listingi 5.S zawiera zadanie konsumenta, który pobiera nazwy plików z kolejki i je przetwarza. ;' Listing 5 .8 . Zadania producenta i konsum enta w aplikacji indeksacji zaw artości dysku twardego p u b lic cla ss F ile C ra w le r implements Runnable { p riv a te f in a l BlockingQueue fileQ ueue; p riv a te f in a l F i l e f i l t e r f i l e F i l t e r ; p riv a te f in a l F ile ro o t: p u b lic void ru n () { try ( c ra w l(ro o t); } catch (Irrte rru pte d E xce p tio n e) { Thread.cu rre n tT h re ad () . in t e r r u p t ( ) ;

1 p riv a te void crawl ( F ile ro o t) throws IriterruptedE xception { F i l e [ ] e n trie s *■ r o o t . lis t F i l e s C f i l e F i l t e r ) ; i f (e n trie s != n u ll) { fo r ( F ile e n try : e n trie s ) i f ( e n tr y .is D ir e c to r y !)) c r a w l(e n tr y ) ; else i f ( ¡a lre a d yIn d e xe d ie n try)) file Q u e u e .p u t(e n try ):

101

Rozdział 5. ♦ Bloki budowania aplikacji

Wzorzec producent-konsument oferuje bezpieczny wątkowo sposób rozdzielenia proble­ mu indeksacji lokalnego dysku twardego na dwa prostsze komponenty. W yszukiwanie plików i właściwa indeksacja rozdzielone na dwa osobne zadania czynią kod czytel­ niejszym i łatwiejszym do wielokrotnego użycia niż kod monolityczny. Każdy wątek wykonuje jedno konkretne zadanie. Kolejka blokująca zajmuje się całym przepływem sterowania. Uzyskane rozwiązanie je st proste i przejrzyste. Wzorzec dodatkowo ułatw ia uzyskanie w zrostu w ydajności. Producent i konsum ent mogą działać współbieżnie — jeśli ograniczeniem pierwszego są operacje we-wy, a dru­ giego procesor, wykonywanie ich współbieżnie zapewni lepsze w ykorzystanie mocy komputera niż w sposób sekwencyjny. Jeżeli producent i konsument m ogą być do pew ­ nego stopnia zrównoleglone, ścisłe ich powiązanie powoduje zanik tego zrównoleglenia i zrównuje wynikowe rozwiązania z w pełni szeregowym. Listing 5.9 uruchamia kilka wątków wyszukujących i indeksujących. W przedstawionej wersji wątki konsumentów nigdy się nie zakończą, więc program nigdy nie przestanie działać. Problemem tym zajmiemy się w rozdziale 7. Choć w przedstawionym rozdziale stosujemy jaw ne zarządzanie wątkami, wiele system ów producent-konsum ent można przekształcić do szkieletu wykonyw ania zadań Executor. ! Listing 5.9. Uruchamianie indeksacji dysku tw ardego

'■

p u b lic s t a t ic void s ta r t!n d e x in g ( F ile [] ro o ts ) { BlockingQueue queue “ new LinkedBlock1ngQueue(B0UND); F ile F ilt e r f i l t e r = new F ile F iI t e r O { p u b lic boolean accept(F11e f i l e ) { re tu rn tru e ; }

;.

}: fo r ( F ile ro o t : ro o ts ) new Thread (new Fi leCrawleriqueue. f i l t e r ,

ro o t.)), s t a r t ( ) :

fo r ( in t i = 0; i < N_CONSUMERS; i++) new Thread(new Indexer(queue)) . s t a r t ( );

1



j!

5.3.2. Szeregowe odosobnienie w w ątku p u b lic class Indexer implements Runnable { p riv a te f in a l B1ockingQueue queue; p u b lic Indexer(BlockingQueue queue) th is.q u e u e ■= queue;

} p u b lic vo id ru n () { try { w h ile (tru e ) in d e x F ile (q u e u e ,ta k e () ) : ) catch ( Iriterrup te d E xce p tio n e) { Thread.currentThreacK) . in te r r u p t( ) ;

Implementacje kolejek blokujących zaw arte w ja v a , u t i l .co n cu rren t stosują w ystar­ czająco m ocną synchronizację wewnętrzną, by bezpiecznie przekazać obiekt z wątku producenta do wątku konsumenta. Dla obiektów zm iennych w zorzec producent-konsum ent i kolejki blokujące tw orzą szeregowe odosobnienie w w ątk u , które ułatwia przekazyw anie w łasności obiektów od producentów do konsum entów . O biekt odosobniony w wątku je st w wyłącznym posiadaniu jednego wątku, ale jego własność można przenieść, bezpiecznie publikując go w nowym wątku i zapewniając, by stary w ątek więcej z niego nie korzystał. Bez­ pieczna publikacja zapewnia w idoczność stanu obiektu w nowym wątku. Ponieważ stary wątek nie korzysta więcej z obiektu, staje się on w yłączną w łasnością nowego wątku, co pozwala mu na jego sw obodną modyfikację.

102

C zęść I ♦ Podstawy S

Pule obiektów stosują szeregowe odosobnienie w wątku, bo wypożyczają obiekt żąda-ii jącem u go w ątkow i. O ile tylko pula stosuje w łaściw ą synchronizację wewnętrzną1? do bezpiecznej publikacji obiektów z pttli, a klienly nie publikują obiektów z pnjj |la) w łasną rękę, w łasność obiektu bezpiecznie wędruje między wątkami. i. Nic nie stoi na przeszkodzie, by skorzystać z innych mechanizmów publikacji do p rz e ­ kazywania własności obiektów zm iennych — należy przede wszystkim zapewnić, bytylko jeden w ątek otrzymał obiekt. Kolejki blokujące znacząco ułatw iają zagadnienie': ale przy odrobinie pracy to sa m o m o żn a osiągnąć, stosując niepodzielną w ersję metod/ remove!) z ConcurrentMap lub metodę compareAndSet!) z Atonii cReference.f

5 .3 .3 . Kolejki dwukierunkowe i kradzież zadań Java 6 dodaje dwa nowe typy kolekcji: Deque i BlockingDeque rozszerzające odpowiednim Queue i BlockingQueue. Interfejs Deque to kolejka dwukierunkowa umożliwiająca wydajne w stawianie i usuwanie danych z obu końców. Jej rzeczywiste implementacje to klasy; ArrayDeque i LinkedB'lockingDeque. ą •'ę Podobnie ja k kolejki blokujące idealnie nadają się do wzorca producent-konsument,; tak kolejki dwukierunkowe powiązane są z wzorcem kradzieży zadań. Wzorzec pro? ducenl-konsument stosuje je d n ą w spółdzieloną kolejkę dla w szystkich konsumentów?: We wzorcu kradzieży zadań każdy konsument stosuje własną kolejkę dwukierunkową.: Jeśli w yczerpie zadania z w łasnej kolejki, może podkradać zadania z ogona kolejki innego konsumenta. Kradzież zadań bywa bardziej skalowalna niż tradycyjny producent-', konsument, bo nie stosuje się jednej w spółdzielonej kolejki. Przez większość czasu konsumenty używ ają własnych kolejek dwukierunkowych. Ponieważ z innych kolejek podkradają dane z ogona, jeszcze bardziej zwiększa to równoległość operacji. Podkradanie zadań dobrze sprawdza się w problemach, w których konsumenty są rów­ nież producentami — wtedy wykonanie zadania zapewne objawi się pojawieniem innych zadań. Przykładowo system wyszukiwania sieci po przetworzeniu jednej strony znajduje zazwyczaj kolejne adresy stron do przetworzenia. Wiele algorytmów analizy grafów; między innymi system odzyskiwania pamięci w Javie, można wydajnie zrównolegiić za pomocą podkradania zadań. Gdy wątek wykryje nowe zadanie, umieszcza je na końcu własnej kolejki dwukierunkowej (lub przy system ie w spółdzielonym na końcu kolejki innego wątku). Gdy jego kolejka będzie pusta, poszuka zadań w innej kolejce, by nigdy - się nie nudzić.

Rozdział 5- ♦ B lo k i b u d o w a n ia a p lik a c j i

dłuższy okres, polega na tym, że w blokow aniu w ątek oczekuje na zdarzenie będące poza jego bezpośrednią kontrolą — zakończenie operacji we-wy, odblokowanie obiektu lub zakończenie zewnętrznych obliczeń. Gdy zajdzie to zewnętrzne zdarzenie, wątek ponownie przechodzi w stan RUNNABLE i jest uwzględniany przy harmonogramowaniu. Metody p u t() i ta ke !) klas BlockingQueue zgłaszają wyjątek weryfikowany In te rru p ­ tedException, podobnie ja k w iększość innych metod standardowej biblioteki, między innymi Thread.sleepO. Gdy m etoda potrafi zgłosić w yjątek InterruptedException, oznacza to, żc je st m etodą blokującą, która może w momencie jej zewnętrznego prze­ rwaniu nie dokończyć operacji blokującej. Klasa Thread zawiera metodę in te r r u p t!) wymuszającą przerwanie wątku oraz spraw ­ dzającą, czy wątek został przerw any. Każdy wątek zawiera właściwość logiczną wska­ zującą stan przerwania. Wymuszenie przerwania ustawia właściwość. Przerwanie to mechanizm wykorzystujący w spółpracę. Jeden wątek nie może wymusić na innym wątku, by ten przestał zajmować się swoim zadaniem i zajął się czymś innym. Gdy wątek A przerywa wątek 13, tak naprawdę A tylko prosi B, by zatrzymał wyko­ nywanie obecnej operacji w dogodnym m om encie (jeśli uzna to za stosowne). Choć interfejsy i specyfikacja języka nie wymuszają konkretnej semantyki przerwań na pozio­ mie aplikacji, najczęściej zauważenie przerwania skutkuje zaprzestaniem wykonywania aktualnej operacji. M etody blokujące reagujące na przerwanie ułatwiają anulowanie długo działających aktywności bez oczekiwania na ich zakończenie. Gdy kod wywołuje metodę m ogącą zgłosić w yjątek InterruptedException, również staje się kodem blokującym i musi odpowiednio reagować na przerwanie. W przypadku klas bibliotecznych w zasadzie istnieją dwa podejścia. P ro p ag acja w yjątku InterruptedE xception. Najczęściej je st to najsensowniej,sza strategia, jeśli tylko okazuje się dopuszczalna — wystarczy przekazać wyjątek do kodu wywołującego. M ożna albo w ogóle nie wyłapywać wyjątku InterruptedException, albo wyłapywać go i zgłaszać ponownie po przeprowadzeniu krótkiego czyszczenia zasobów. Przyjęcie i odbudow a w y jątk u . Czasem nie m ożna zgłosić wyjątku InterruptedExcept ion do kodu wywołującego, na przykład w momencie używania metody ru n !) z interfejsu Runnable. W takiej sytuacji wyłapuje się wyjątek i odbudowuje przerwanie, wywołując metodę in te r r u p t!) dla aktualnego wątku, więc wyższy kod zauważy anulowanie operacji. Rozwiązanie to ilustruje listing 5.10. Listing 5.10. Odbudowa pr.zerwania, by nie utracić informacji o nim

5.4. SVietociy blokujące i przerywane Wątki m ogą się blokować (czekać) z wielu powodów, czekając na: zakończenie operacji we-wy, uzyskanie blokady, obudzenie po wywołaniu Thread.sleepO lub wyniki oblicz«! innego wątku. G dy w ątek się blokuje, najczęściej zawiesza swe działanie i informuje, że znajduje się w jednym ze stanów blokujących (BLOCKED, WAITING lub TIMEDJJAlIIIii. Różnica m iędzy operacją blokującą a operacją, która ¡50 prostu w ykonuje się pi'®?

103

p u b lic class TaskRunnable implements Runnable { BlockingQueue queue: p u b lic void ru n t) f try { processTask(queue.take!) ) : } catch ( In te rru pte d E xce p tio n e) j

R o zd zia ł

/ / Odbudowa przerwania. Thread. currentThreadO . in t e r r u p t O ;

i } i Sposoby przerywania potrafią być bardziej zaawansowane, ale te dwa podejścia występ pują w zdecydowanej większości sytuacji. Jednej rzeczy nigdy nie rób z wyjątkiemInt.erruptedE xcept.ion — nie wyłapuj go, w żaden sposób na niego nie reagując Uniem ożliwia to kodowi w yw ołującem u dow iedzenie się o przerw aniu, bo dowód przerw ania traci się bezpow rotnie. S y tu acja je s t d o p u szczaln a tylko w tedy, gdyro zszerza się klasę Thread i m a się p ełn ą k o n tro lę nad kodem zn ajd u jący m się-; wyżej na stosie w yw ołań. Anulowanie i przerywanie wątków zostało szczegółowo opisane w rozdziale 7.

. Synchronizatory Kolejki blokujące to wyjątkowy element klas kolekcji, bo nie tylko slu żąjak kontenery: dla obiektów, ale również koordynują przepływ sterowania wątków producentów i koni sumentów, bo metody put() i takeO blokują się do momentu uzyskania odpowiedniego1j stanu (kolejki niepuslej i niepełnej). S y n c h ro n iz a to r to dow olny obiekt koordynujący przepływ sterow ania wątków na' podstawie jego stanu. Kolejka blokująca działa jak synchronizator. Innymi rodzajami synchronizatorów są: sem afory, bariery i zatrzaski. Biblioteki platlorm y zawierają w iele klas im plem entujących te elementy. Jeśli okazują się nieodpow iednie, nic niej stoi na przeszkodzie, by w ykonać w łasny mechanizm. D okładny opis tej procedury: zawiera rozdział 14. : T Wszystkie synchronizatory m ają pewne wspólne właściwości strukturalne. Hermetyzują: stan, który określa, czy w ątek przybywający do synchronizatora może przejść czy raczej musi zaczekać na w ykonanie zadania; zaw ierają metody modyfikujące stan i sposoby; wydajnego oczekiwania na zm ianę stanu przez kod. >(

5- ♦ Bloki budowania aplikacji

♦ Zapewnienie, że obliczenia nie rozpoczną się wcześniej niż inicjalizacja potrzebnych zasobów'. Prosty zatrzask binarny (dwustanow y) wskazuje, czy „zasób R został zainicjalizow any”. Każcie działanie wymagające R musi czekać w zatrzasku na popraw ną inicjalizację.

103

'

♦ Zapewnienie, że usługa nie rozpocznie swego działania dopóki inne usługi, od których je st uzależniona, nie uruchom ią się. K ażda usługa ma więc przypisany zatrzask binarny. Uruchomienie usługi S wymusza oczekiwanie w' zatrzasku na uruchomienie innych usług, od których S zależy. Po zakończeniu ich inicjalizacji zatrzask dla S otrzym uje odpowiedni sygnał i zwalnia wątek dotyczący S. ♦ Oczekiwanie, aż wszystkie elem enty wym agane dla określonego działania, na przykład wszyscy gracze gry, będą gotowe do startu. W takiej sytuacji zatrzask przechodzi do stanu końcowego, gdy w szyscy gracze potw ierdzą gotowość. Klasa CounLDownLatch to elastyczna im plem entacja zatrzasku mająca zastosow anie w każdej z wymienionych sytuacji. Umożliwia jednemu lub kilku wątkom oczekiwanie na zaistnienie pewnego zdarzenia. Zatrzask zawiera stan inicjalizowany pewną wartością dodatnią reprezentującą liczbę zdarzeń, na które się oczekuje. M etoda COUntOownO dekrementuje licznik, w skazując zajście zdarzenia. M etody aw aitO czekają aż do momentu osiągnięcia wartości 0 (czyli w ykonania w szystkich zaplanowanych zadań). Jeżeli licznik ma wartość niezerową, metoda aw a it() blokuje w ątek aż do osiągnięcia wartości 0, przerwania wątku lub przekroczenia czasu oczekiwania. Klasa łestllarness z listingu 5.11 ilustruje dwa typowe użycia zatrzasków. Klasa tworzy kilka wątków, które w spółbieżnie w ykonują określone zadanie. U żyw a dwóch za­ trzasków: bramy wejściowej i wyjściowej. Bram a w ejściow a ma początkowo licznik ustawiony na w artość 1. Bram a w yjściow a ma licznik ustaw iony na liczbę wątków wykonywania zadań. Początkowo w szystkie w ątki czekają przy branie wejściowej na jej otwarcie, W ten sposób żaden z nich nie zacznie być wykonywany, dopóki w szystkie, nie będą zainicjalizowane. N a końcu każdy z nich zm niejsza licznik bramy końcowej. W ten sposób główny w ątek czeka na w ykonanie poszczególnych zadań, zanim sam : będzie mógł policzyć czas, jaki zajęło ich wykonanie. Listing 5.11. Użycie klasy CountDownLatch to urucham iania i zatrzym yw ania w ątków w trakcie pomiarów szybkości

1. Zatrzaski Z atrzask to synchronizator opóźniający postęp w ykonania w ątku aż do moment]) osiągnięcia stanu kończącego [CPJ 3.4.2], Zatrzask działa jak brama — dopóki zatrzask nie znajdzie się w stanie końcowym, brama pozostaje zamknięta i nie przejdzie przez nią żaden wątek. W stanie kończącym brama zostaje otwarta i wszystkie wątki wcześniej oczekujące przez nią przechodzą. Po osiągnięciu stanu końcowego zatrzask nie mffżf ponow nie zam knąć bramy — pozostaje ona zaw sze otwarta. Zatrzaski zapewniają, że pewne działania nie zaczną się przed zakończeniem innych jednorazow ych działać Oto kilka przykładów: •,

p u b lic class TestHarness { p u b lic long tim eT a sksd n t nThreads, f in a l Runnable ta sk) throws In te rru pte d E xce p tio n { f in a l CountDownLatch s ta rtG a te = new CountDownLatch(l); f in a l CountDownLatch endGate » new CountDownLatchinThreads): f o r ( in t i = 0: i < nThreads; 1++) { Thread t - new Threadt) { p u b lic void ruriO { try { s ta rtG a te .a w a itO : try { ta s k . r u n t);

- ;

* mbhspw' R o z d z ia ł

107

5-12. Wykorzystanie.obiektu F utureTask do wstępnego wczytania danych, zanim będą irl,n-!v\\’iście potrzebne _______________________________________________________________________

Listing

} f in a lly ( endGate.countDown() :

)

^

p u b lic cla ss Preloader { p riv a te fin a l FutureTask fu tu re = new FutureTask(new C a lla b le < P rod u ctIn fo > () { p u b lic P roductInfo c a llO throws DataLoadException { re tu rn lo a d P ro d u c tln fo O ;

) catch (In te rru p te d E xce p tio n ignored) {

} } 1:

5. ♦ Bloki budowania aplikacji

.

t.s ta rtO :

}

} )): long s t a r t = System.nanoTimeO; s ta rtG a te . couritDownt ) ;

p riv a te f in a l Thread thread “ new Ih re a d ifu tu re ) ;

e n d G a te .a w a itO ;

p u b lic void s t a r t o

long end = System.nanoTimeO; re tu rn end - s t a r t ;

Dlaczego w ogóle wprowadziliśmy zatrzaski do kodu klasy? Przecież mogliśmy wykona! wątki zaraz po ich utworzeniu. Bo chcieliśmy zmierzyć, ile czasu zajmie współbieżne' wykonanie zadania przy n wątkach. Gdybyśmy po prostu tworzyli wątki i je uruchamiali; wcześniej w ykreowane miałyby przewagę nad późniejszymi, więc zmniejszyłaby sif w spólbieżność w ykonania i uzyskalibyśm y nieprecyzyjny wynik. Brama wejściową zapewnia jednoczesny start wszystkich wątków. Brama wyjściowa wymusza oczekiwani! wątku głównego na zakończenie ostatniego wątku zadaniowego zamiast na oczekiwanie sekwencyjne na zakończenie poszczególnych wątków. -ą \

5 .5 .2 . Klasa FutureTask

{ th re a d .s t a r t o ; }

p u b lic P roductInfo g e t() throws DataLoadException. Interru pte d E xce p tio n { try { re tu rn fu tu re .g e tO ; } catch (ExecutionException e) { Throwable cause = e.getC auset): i f (cause in s ta n c e o f DataLoadException) throw (DataLoadException) cause; else throw LaunderThrowable.launderThrowabletcause):



i

1

Klasa FutureTask działa podobnie ja k zatrzask. Jest im plem entacją interfejsu Futur!, który stanow i abstrakcję w yliczeń przynoszących w ynik [CPJ 4.3.3]. Wyliczeni! reprezentow ane p rzez FutureTask je st im plem entow ane przez klasę z interfejseil Callable, przynoszącym wyniki odpowiednikiem interfejsu Runnable, i może znajdowiif się w jednym z trzech stanów: oczekiwania na uruchomienie, wykonywania i zakończe­ nia. Zakończenie dotyczy wszystkich możliwych sposobów zakończenia: normalnego, anulowania i pojawienia się wyjątku. Gdy FutureTask znajdzie się w stanie zakończenia, pozostaje w nim na zawsze. jj

Zachowanie F u tu re .g e t() zależy od stanu zadania. Jeśli jest zakończone, wynik zwróci od razu. W przeciwnym razie blokuje się aż do momentu przejścia zadania w stan zakoń­ czenia. Dopiero w tedy zwraca wynik lub zgłasza wyjątek. Klasa FutureTask przekaztijt dane z wątku wyliczającego do wątków pobierających wynik. Specyfikacja FutureTask gwarantuje, że przekazanie zapewnia bezpieczną publikację wyniku. j i| K lasę FutureTask stosuje szkielet Executor do reprezentacji zadań asynchroniczny^ M oże być rów nież użyta do reprezentacji potencjalnie długo wykonywanego zadan| które m ożna rozpocząć przed m om entem , w którym b ęd ą potrzebne wyniki. KlaSi] ' Preloader z listing 5.12 używa FutureTask do przeprowadzenia kosztownych wylicze| których w ynik będzie potrzebny w przyszłości. Rozpoczynając wyliczenia wcześni«* redukujemy czas potrzebny na oczekiwanie na icli zakończenie, gdy wynik rzeczywiści będzie potrzebny.

IClasa Preloader tworzy obiekt FutureTask, który opisuje zadanie wczytania informacji o produkcie z bazy danych, oraz wątek, który zajmie się pobraniem danych. Zapewnia metodę s ta r tO , bo nie zaleca się urucham iania w ątku z poziomu konstruktora lub inicjalizacji statycznej. Gdy kod potrzebuje danych ProductInfo w późniejszym okresie, wywołuje metodę g e tO , która zwraca dane od razu, gdy są gotowe, lub czeka na ich załadowanie. Zadania opisywane przez interfejs Cal la b ie m ogą zgłaszać wyjątki weryfikowane i nie weryfikowane. Poza tym dowolny kod może zgłosić w yjątek E rror. Cokolwiek kod zadania zgłosi, zostaje otoczone w yjątkiem ExecutionExcept1on i przekazane wyżej przez metodę F u tu rę .g e t( ). K om plikuje to kod w yw ołujący g e t() nie tylko dlatego, że musi radzić sobie z ew entualnym w yjątkiem ExecutionException (i nieweryfikowanym CancaletaionException), ale również z tym, że ExecutionException zostaje zgłoszone jako podtyp Throwable, co czyni dostęp do niego mniej wygodnym. Gdy uzyskam y w yjątek Execut ionException z poziom u Preloader, zaw ierał się o n ! będzie w jednej z trzech kategorii: wyjątku w eryfikow anego zwracanego przez Cal la b ie ,: wyjątku RuntimeException lub błędu E rror. Każdym z przypadków musimy zająć się oddzielnie, ale do obsługi najmniej przyjemnych szczegółów wykorzystamy metodę narzędziową launderThrowableO przedstawioną na listingu 5.13. Przed jej wywołaniem klasa Preloader sprawdza znane sobie wyjątki weryfikowane i ponownie je zgłasza. Pozostają więc wyjątki nieweryfikowane, które klasa obsługuje, wywołując launder- TbrowableO i zgłaszając jak o w yjątek uzyskany wynik. Jeśli obiekt Throwable prze­ kazany do metody jest typu Error, metoda zgłasza go ponownie. Jeśli nie jest to w yjątek,

; ,

1 ;

1 0 8 _______________________________ ____________________________________

C z ę ś ć I ♦ P o d st ą p i

f

RuntimeException, zgłasza wyjątek I I legaIStateException. Pozostaje do obsługi jedyni w yjątek RuntimeException, który m etoda przekazuje do kodu w yw ołującego (a te|-: zgłasza go ponownie). Ą -V;

Listing 5 .1 3 . O graniczenie niew eryfikowanych błędów Throwable do wyjątków RuntimeException /** ;L.in 'fhrowablc to Error, zgłoś go; jeśli to * RuntimeException, zwróć go; w przeciwnym razie zgłoś IilegalSlateExcep(ion. */ p u b lic s t a t ic RuntimeException launderThrowableGhrowable t ) { j f ( t instanceof RuntimeException) re tu rn (RuntimeException) t: e lse i f ( t in sta n ceo f E rro r) throw (E rro r) t ; e l se throw new I ł ł egałS ta teException( “ Nie n ie w e ryfiko w a ln y", t ) :

,



1

5 .5 .3 . Semafory

$ Ą , -v,| i ¡ijs

I £

zasobu lub m ogą działać w określonym czasie [CPJ 3.4.1], Semafory te służą do implei mentacji pul zasobów łub nakładania ograniczeń na kolekcje. 4 K lasa Semaphore zarządza zbiorem w irtualnych p r z ep u stek ; ich liczbę określa sti; w konstruktorze. Działanie uzyskuje przepustkę (o ile są dostępne) i oddaje j ą po doki naniu odpow iednich działań. Jeżeli w szystkie przepustki zostały w ydane, metodt a c q u ire () blokuje się aż do udostępnienia przepustki (lub do upłynięcia okreśjLonega czasu łub przerw ania w ątku). M etoda r e le a s e O zw raca przepustkę semaforowi Szczególnym przypadkiem semafora jest semafor binarny (czyli taki z ustawionąpocząk kowo w artością 1). Sem afor binarny służy jak o mutelcs z w yłączoną możliwości^ wielokrotnego wejścia. Kto ma przepustkę, ma muteks. : c; {

li| 4j

,i

)

t

p u b lic synchronized V computetA arg) throws Interru pte d E xce p tio n V r e s u lt - c a c h e .g e t(a rg ); i f ( r e s u lt — n u ll) { result. = c.com pute(arg); ca ch e .p u t(a rg , r e s u lt) ;

1

re tu rn r e s u lt;

u

ffl)

}

p u b lic Hemoizerl(Computable c) t lr is . c = c;

zw raca zb u fo ro w a n e

{

' '-’I | j| 3

'it t

pu b lic class Memoizer'2 implements Computable { p riv a te f in a l Map cache = new ConcurrentHashMap (); p riv a te f in a l Computable"^. V> c;

public Memoizer2(Computable c) { th is .c ■= c; } p u b lic V computeCA a rg ) throws In te rru pte d E xce p tio n { V r e s u lt = c a c h e .g e t(a rg ); i f ( re s u lt { i p riv a te fin a l Map. Serwer internetowy z klasy TaskExecutionWebServer używa szkieletu Executor oraz puli wątków zadaniowych. Zlecenie zadania metodą ex ecuted dodaje zadanie kolejki. Wątki zadaniowe pobierają zadania z kolejki i je wykonują. w V Zmiana strategii z jedno zadanie, jeden w ątek na pulę wątków ma duży wpływ na sta­ bilność aplikacji — serwer będzie działał poprawnie nawet w przypadku dużego obcią­ żeniu5. Co w ięcej, system taki działa wydajniej, bo nie tworzy tysięcy wątków, które konkurują o ograniczone zasoby pamięci i mocy procesora. Użycie szkieletu Executor otwiera drzwi do wielu innych operacji dotyczących poprawy wydajności, monitorowa­ 4 Jednowątkowy system w ykonaw czy zapewnia wystarczającą synchronizację wewnętrzną, ; b y zagwarantować w idoczność każdego zapisu w pamięci wykonanego przez wcześniejsze zadanie. Oznacza to, że obiekty są bezpiecznie zamknięte w „w ą tku w ykonaw czyni” , choć wątek ten od czasu do czasu może ulec zmianie. I 5 Choć serwer zapewne nie przestanie działać z powodu utworzenia zbyt dużej liczby wątków, je śli szybkość nadchodzenia nowych zadań będzie znacznie większa od szybkości ich obsługi, m o żliw y je st brak pamięci z powodu w zrostu rozm iaru k o le jk i zadań oczekujących na wykonanie. M ożna temu zaradzić stosując szkielet Executor z ograniczoną rozm iarow o kole jką zadań; patrz punkt 8.3.2.

pozttóał 6 - * Wykonywanie zadań

129

nia, tworzenia dzienników, raportowania błędów itp., które byłoby znacznie trudniej osiągnąć w inny sposób.

6.2.4» Cykl życia szkieletu Executor P rz e d s ta w iliś m y m e to d ę tw o rz e n ia o b ie k tó w Executor, ale n ie sp o só b ic h n is z c z e n ia . Im ple m e n tacja Executor w y k o rz y s tu je w ą tk i do w y k o n a n ia z le c o n y c h zadań. M a s z y n a w irtu a ln a n ie m o ż e się w y łą c z y ć , d o p ó k i is tn ie je c h o ć b y je d e n n ie d e m o n o w y w ą te k ,, w ię c n ie w y lą c z e n ie z p o z io m u a p lik a c ji o b ie k tu Executor s k u tk u je c ią g ły m d z ia ła n ie m m aszyny w ir tu a ln e j. :

Ponieważ szkielet Executor przetwarza zadania asynchronicznie, w dowolnym momencie trudno stwierdzić, jaki je st stan zleconych zadań. N iektóre mogły się zakończyć, inne właśnie działają, a jeszcze inne dopiero oczekują na wykonanie. W przypadku zamykania aplikacji m ożna to zrobić m iło (zakończyć aktualnie w ykonyw ane zadania, ale nie przyjmować żadnych nowych) lub stanow czo (odłączyć zasilanie kom putera) — je st też wiele kroków pośrednich. Poniew aż szkielet Executor je st w ykonaw cą usług dla aplikacji, powinien dopuszczać wyłączenie miłe i stanowcze oraz informować aplikację o postępach w wykonywaniu zadań, na które wpłynie wyłączanie. Aby rozwiązać wszystkie związane z tą kw estią problem y, interfejs ExecutorService rozszerza standardowy interfejs Executor o metody zarządzania cyklem życia (i kilka dodatkowych m etod zlecania zdań). M etody zarządzania cyklem życia określone w interfejsie ExecutorService w skazuje listing 6.7. Listing 6.7. Metody cyklu życia z interfejsu ExecutorService p u b lic in te rfa c e ExecutorService extends Executor { void shutdown!); List shutdownNowO ; boolean isShutdownO; boolean is le rra in a te d !); boolean awaitTerm inationO ong tim eo u t, TimeUnit u n it) throws In te rru pte d E xce p tio n ; / / ...dodatkowe metody zlecania zadań...

Cykl życia w prow adzany w ExecutorService składa się z trzech stanów: d ziałan ia, wyłączania i zakończenia. Początkowo obiekt ExecutorService znajduje się w stanie działania. Metoda shutdown!) inicjuje miły sposób wyłączenia — przestają być przyjm o­ wane now e zlecenia, ale stare m ają szansę się zakończyć (także te, które o czek u ją jeszcze w kolejce). M etoda shutdownNow() w ym usza szybkie zakończenie — stara się anulować w szystkie w ykonyw ane zadania i w ogóle nie podejm uje się rozpoczęcia zadań z kolejki. Zadania przekazane do obiektu ThreadPool Executor (implementującego ExecutorService) po jego wyłączeniu obsługuje specjalna p ro ce d u ra odrzuceń w ykonania (patrz punkt 8.3.3), która może po cichu pominąć zadanie lub spow odow ać, że m etoda executeO zgłosi nieweryfikowany wyjątek RejectedExecutionException, Po zakończeniu wszystkich zadań obiekt ExecutorService przechodzi do stanu zakończenia. Można oczekiwać na

C zęść II ♦ Struktura aplikacji współbiełnil-

128

,

—_ _ —_

-------—

m szybkość odpowiedzi. Odpowiednio dostrajając rozmiar puli wątków, można zapcw,'v stale zajęcie dla procesora przy jednoczesnym uniknięciu zajęcia całej pamięci open, i nej lub wyłączenia systemu z powodu braku zasobów. Biblioteka klas zawiera elastyczne implementacje pul wątków z kilkoma predeFiniowj> nymi konfiguracjami. Pulę wątków tworzy się, wywołując jedną ze statycznych me® fabrycznych klasy Executors. newFixedThreadPool () — pula wątków o stałym rozmiarze tworzy wątki przy

r

nadchodzeniu nowych zadań aż do osiągnięcia maksymalnego rozmiaru, N astępnie stara się utrzym ać stały rozmiar puli (dodaje nowy wątek, gdy poprzedni wyłączył się lub zgłosił wyjątek).

»

newCachedThreadPool () — buforowana pula wątków zwiększa elastyczność

przypisywania bezrobotnych wątków, gdy aktualny rozmiar puli przekroczy aktualne zapotrzebowanie na moc obliczeniową. Dodaje nowć wątki, gdy wzrasta zapotrzebowanie na moc, ale nie ogranicza rozmiaru puli.

j

newSingleThreadExecutor!) — jednowątkow y system wykonawczy tworzy jeden

w ątek zadaniowy przetw arzający zlecenia. Wątek zosta je zastąpiony nowym, ’ gdy poprzedni nieoczekiwanie przestanie działać. Zapewnia się i sekwencyjność wykonania zadań w kolejności narzucanej przez kolejkę zadań (LIFO, FIFO, kolejka priorytetowa)'1. newSchedul edlhreadPooł () — pula wątków o stałym rozmiarze obsługująca zadania opóźnionej okresowe. W działaniu przypomina klasę Timer

(patrz punkt 6.2.5).

j

t

Metody newFixedThreadPoolO i newCachedThreadPool () zwracają egzemplarze ogólnego obiektu ThreadPool Executor, który może posłużyć także do konstrukcji bardziej w yspecjalizow anych pul. Szczegółow y opis konfiguracji puli w ątków znajduje się w rozdziale 8. Serwer internetowy z. klasy TaskExecutionWebServer używa szkieletu Executor oraz puli wątków zadaniowych. Zlecenie zadania metodą execute () dodaje zadanie kolejki. Wątki zadaniowe pobierają zadania z kolejki i je wykonują.

¿¿at 6. ♦ Wykonywanie zadań

nia tw o rz e n ia d z ie n n ik ó w , ra p o rto w a n ia b łę d ó w it p ., k tó re b y ło b y zn a c z n ie tru d n ie j osiągnąć w in n y sposób.

6 2.4- Cykl życia szkieletu Executor P rz e d s ta w iliś m y m eto d ę tw o rz e n ia o b ie k tó w Executor, ale n ie sposób ic h n iszcze n ia . Im ple m e n tacja Executor w y k o rz y s tu je w ą tk i do w y k o n a n ia z le c o n y c h zadań. M a s z y n a w irtu a ln a n ie m oże się w y łą c z y ć , d o p ó k i is tn ie je c h o ć b y je d e n n ie d e m o n o w y w ą te k, w ięc n ie w y lą c z e n ie z p o z io m u a p lik a c ji o b ie k tu Executor s k u tk u je c ią g ły m d z ia ła n ie m m aszyny w irtu a ln e j.

Ponieważ szkielet Executor przetwarza zadania asynchronicznie, w dowolnym momencie trudno stwierdzić, jaki jest stan zleconych zadań. N iektóre mogły się zakończyć, inne właśnie działają a jeszcze inne dopiero oczekują na wykonanie. W przypadku zamykania aplikacji można to zrobić m iło (zakończyć aktualnie w ykonyw ane zadania, ale nie przyjmować żadnych nowych) lub stanowczo (odłączyć zasilanie komputera) — jest też wiele kroków pośrednich. Poniew aż szkielet Executor jest wykonawcą usług dla aplikacji, powinien dopuszczać wyłączenie miłe i stanowcze oraz informować aplikację o postępach w wykonywaniu zadań, na które wpłynie wyłączanie. Aby rozwiązać wszystkie związane z tą kw estią problemy, interfejs ExecutorService rozszerza standardowy interfejs Executor o metody zarządzania cyklem życia (i kilka dodatkowych m etod zlecania zdań). M etody zarządzania cyklem życia określone w interfejsie ExecutorService wskazuje listing 6.7. Listing 6 .7. Metody cyklu życia z interfejsu ExecutorService _______________________________ p u b lic in te rfa c e ExecutorService extends Executor { void shutdow n!); List shutdowriNowi); boolean isShutdownO: boolean isT erm in a te d O : boolean aw aitTerm inationdong tim eout. Tirnellnit u n it) throws InterruptedE xception; / / ...dodatkowe metody zlecania zadań...

)

Zmiana strategii z jedno zadanie, jeden wątek na pulę wątków ma duży wpływ na sta­ bilność aplikacji — serwer będzie działał poprawnie nawet w przypadku dużego obcią­ żenia5. Co więcej, system taki działa wydajniej, bo nic tworzy tysięcy wątków, które konkurują o ograniczone zasoby pamięci i mocy procesora. Użycie szkieletu Executor otwiera drzwi do wielu innych operacji dotyczących poprawy wydajności, monitorowa­ 4 Jednowątkowy system w ykonaw czy zapewnia wystarczającą synchronizację w ew nętrzną by zagwarantować w idoczność każdego zapisu w pamięci wykonanego przez wcześniejsze zadanie. Oznacza to, że obiekty są bezpiecznie zamknięte w „w ą tku w ykonaw czym ” , choć wątek ten od czasu do czasu może ulec zmianie. 5 Choć serwer zapewne nie przestanie działać z powodu utworzenia zbyt dużej liczby wątków, jeśli szybkość nadchodzenia now ych zadań będzie znacznie większa od szybkości ich obsługi, m o żliw y jest brak pamięci z powodu wzrostu rozm iaru ko le jk i zadań oczekujących na wykonanie. M ożna temu zaradzić,stosując szkielet Executor z ograniczoną rozm iarowa ko le jką zadań; patrz punkt 8.3.2.

129

'

.

Cykl życia w prow adzany w ExecutorService sldada się z trzech stanów: d ziałan ia, w yłączania i zakończenia. Początkowo obiekt ExecutorService znajduje się w stanie działania. Metoda shutdown!) inicjuje miły sposób wyłączenia — przestają być przyjmo­ wane nowe zlecenia, ale stare m ają szansę się zakończyć (także te, które oczekują jeszcze w kolejce). M etoda shutdownNow!) wymusza szybkie zakończenie — stara się anulować w szystkie w ykonyw ane zadania i w ogóle nie podejm uje się rozpoczęcia zadań z kolejki. Zadania przekazane do obiektu ThreadPool Executor (implementującego ExecutorService) po jego wyłączeniu obsługuje specjalna p ro ced u ra odrzuceń w ykonania (patrz punkt 8.3.3), która może po cichu pominąć zadanie lub spowodować, że metoda execute!) zgłosi nieweryfilcowany wyjątek RejectedExecutionException. Po zakończeniu wszystkich zadań obiekt ExecutorService przechodzi do stanu zakończenia. Można oczekiwać na

C zęść II ♦ Struktura aplikacji współbież^

130

osiągnięcie tego stanu przez obiekt, wywołując jeg o metodę awai tT e rn iin a tio n i ) co jakiś czas odpytywać go o stan przy użyciu metody isTerminatedC). Najczęściej ?.aril2 po wywołaniu metody shutdownO stosuje się metodę awaitTenninationO, zapewniając tym samym efekt synchronicznego kończenia działania przez ExecutorService. \VyląCZa, nie obiektów Executor i anulowanie zadań omawiamy bardziej szczegółowo w rozdziale 7,: Klasa L i fecycleWebServer z listingu 6.8 rozszerza przedstaw ianą wcześniej wersję serw era o obsługę cyklu życia. S erw er w yłącza się na dw a sposoby: programowo wywołując metodę stopO , lub żądaniem klienta wysianym do serwera zawierąjącyoj specjalne dane HTTP. L is tin g 6 .8 . Serwer internetowy z obsługą wyłączenia _________________________________________ p u b lic cla ss L ifecycleWebServer { p riv a te f in a l ExecutorService exec ■ * . . . ; p u b lic void s ta rtC ) throws lOException { ServerSocket socket = new ServerSoeket(80): w h ile ( lexec.isShutdownO ) ( try { f in a l Socket conn'“ socket .acceptO ; exec.executetnew RunnableO { p u b lic void ru n t) j hand!eRequesttconn}: }

}): } catch (RejectedExecutionException e) { i f O exec.isShutdownO ) logi"odrzucono zle ce n ie zadania", e ):

} }

Rozdział 6. ♦ Wykonywanie zadań

131

Obiekt Timer tworzy tylko jeden wątek do wykonywania zaharmonogramowanych zadań, Jeśli któreś z zadań zajmuje zbyt dużo czasu, cierpi na tym dokładność czasowa wykonania innych zadań TimerTask. Jeżeli cyklicznie wykonywane zadanie powinno rozpoczynać się co 10 ms, a inne zadanie umieszczone w harmonogramie trwa 40 ms, zadanie okresowe albo wykona się czterokrotnie po długim zadaniu, albo pominie cztery wykonania (w zależności od ustawień: stałe opóźnienie lub stały odstęp czasu). Harmonogramowane pule wątków rozw iązują przedstawiony problem, korzystając z wielu wątków do wykonywania zleconych zadań okresowych i opóźnionych.

Klasa OutOfTime z listingu 6.9 ilustruje, w jaki sposób obiekt Timer potrafi niepoprawnie zadziałać w przedstawianej sytuacji. Co gorsza, to błędne działanie propaguje na kod wywołujący system hnnnonogramowania. M ożna oczekiwać, że program będzie działał 6 sekund i się wyłączy. W rzeczywistości przestanie działać już po 1 sekundzie, zgłasza­ jąc wyjątek I ł legalStateExeeption wraz z tekstem informującym o wyłączeniu systemu harmonogramowania. Klasa ScheduledThreadPoolExecutor poprawnie radzi sobie ze źle zachow ującym i się zadaniam i, więc nie ma powodów , by stosow ać klasę Timer w aplikacjach Javy 5.0 lub nowszych. Listing 6 .9 . Klasa ilustrująca błędne zachowanie obiektu Timer

p u b lic void stopO { exec.shutdown( ) : } p u b lic class OutOfTime { p u b lic s t a t ic void m a in (S trin g [] args) Timer tim e r = new TimerC); tim er.scheduletnew ThrowTaskO, 1 ); SECONDS.sleep(l); tim er.scheduletnew ThrowTaskO. 1): SECONDS.sieep(5 ):

}

}

}

s t a tic class ThrowTask extends TimerTask { p u b lic void ru n i) { throw new RuntiineExceptioriO : }

6 .2 .5 . Zadania opóźnione i okresowe O biekt Timer zarządza w ykonyw aniem zadań opóźnionych („w ykonaj zadanie za 100 ms”) lub okresowych („wykonuj zadanie co 10 ms”). Niestety, obiekt ten ma swoje w ady, w ięc w now szych w ersjach program ów w arto zastąp ić go rozwiązaniem ScheduledThreadPoolExecutor6. O biekt ScheduledThreadPoolExecutor powstaje za pom ocą konstruktora lub metody fabrycznej newScheduledThreadPool (). 6 O biekty Timer obsługująhurm onognim ow anie bazujące na czasie bezwzględnym, a nie relatywnym , więc są czule na zm iany zegara systemowego. Rozwiązanie ScheduledThreadPoolExecutor obsługuje tylko czas relatywny.

s ? j

; !

Dodatkowy problem z Timer polega na tym, że słabo reaguje na sytuacje, w których : obiekt TimerTask zgłasza wyjątek nieweryiikowany. Obiekt Timer nie wyłapuje takiego wyjątku, co powoduje przerwanie działania wątku hannonograinującego. N ie zostanie ; on wskrzeszony do dalszego działania, ale zniknie wraz ze wszystkimi zleconymi za­ daniami, więc inne zawarte w nim i zlecone zadania po prostu się nie wykonają. Problem ten nazywany „wyciekiem w ątku” wraz z technikami jego zapobiegania opisuje pod­ rozdział 7.3.

}

void haridleRequesttSocket connection) { Request req = readRequest(connection); i f ( i sShutdownReques t ( req )) s to p (): el se dispatchR equest(req);

i

} } Jeśli trzeba w ykonać w łasn ą usługę harm onogram ow ania, w arto skorzystać z klas. oferowanych przez bibliotekę, na przykład klasy OelayQueue implementującej interfejs BlockingQueue i oferującej działanie podobne do ScheduledThreadPoolExecutor. Klasa OelayQueue zarządza kolekcją obiektów Delayed. Obiekty te m ają związany z sobą czas opóźnienia-— obiekt z kolejki można pobrać tylko wtedy, gdy minął jego czas oczekiwa­ nia. Innymi słowy, obiekt OelayQueue zwraca obiekty w kolejności powiązanej z czasem ich opóźnienia.

"/.Ml C zęść II ♦ Struktura aplikacji wspófo|g g |

132

6.3. Znajdowanie sen so w n eg o zrów noleglenia Szkielet Executor ułatw ia określenie strategii wykonania, ale by m ó c u ży ć obiej^ Executor, zadania muszą znajdować się w obiektach implementujących interfejs Runnabljl W w iększości aplikacji serwerowych istnieje oczywista granica zadań — pojedync| żądanie klienta. Czasem granice nie są równie oczywiste, szczególnie w aplikacjach klienckich. N iejednokrotnie w aplikacjach serw erow ych obsługujących pojedyn(ta sks. s iz e ( ) ) : Iterator ta s k lte r = tasks. ile r a to r O : . fo r (Fut.ure f : fu tu re s ) { QuoteTask task = ta s k lte r .n e x tO ; try { quotes ,a d d ( f.g e t( ) ) : } catch (ExecutionException e) {

141

quotes. add(task.getF ailureQ uote(e,getC ause() ) ) ; } catch (C ancellationE xception e) ( quotes. a d d (ta sk. getTim eoutQuote(e));

} } C o lle c tio n s .s o rt(q u o te s , ra n k in g ); re tu rn quotes;

1

podsumowanie Układanie struktury aplikacji wokół w ykonyw ania zadań upraszcza programowanie i wspomaga wspólbieżność. Szkielet Executor ułatwia rozdzielenie zgłaszania zadań do wykonania od strategii ich w ykonania. Dopuszcza stosowanie różnych strategii wykonania. Z a każdym razem , g d y tw orzysz wątki w celu wykonania określonych zadań, zastanów się, czy nie lepiej skorzystać ze szkieletu Executor. Aby zmaksyma­ lizować korzyści płynące z podzielenia aplikacji na zadania, zidentyfikuj sensowne granice zadań. W niektórych aplikacjach granice narzucają się same, w innych trzeba icli długo szukać, analizując najlepsze sposoby zapewnienia szczegółowego zrównolcglenia.

X42 i

Część II ♦ Struktura aplikacji współbiein6(. '

.

Rozdział

7.

Anulowanie j wyłączanie zadań Łatwo uruchomić zadanie i wątek. YV w iększości przypadków same decydują, kiedy mają się wyłączyć (najczęściej po wyliczeniu wszystkich danych), czasem jednak zacho- i dzi potrzeba wcześniejszego zatrzym ania zadań lub wątków, niż wynikałoby to z ich domyślnego sposobu pracy. N a ogól w ynika to z kliknięcia przycisku A n u lu j p r z e z . użytkownika wykorzystującego aplikację. Uzyskanie bezpiecznego, szybkiego i pew nego zatrzym yw ania w ątków i zadań nie : zawsze jest łatwe. ,lava nie zapewnia żadnego mechanizm u bezpiecznego wymuszania zatrzymania wątku, gdy wykonuje własne zadania1. Zam iast tego warto stosować m e­ chanizm przerw ań, który ułatwia grzeczne poinformowanie innego wątku, że powinien ; zakończyć swe działanie. Wykorzystanie przerwań wymaga współpracy od przerywanego wątku, bo bardzo rzadko : chcemy n aty ch m iasto w eg o przerw ania zadania, w ątku lub usługi, gdyż m ogłoby to ; spowodować pozostawienie obiektu w niespójnym stanie. Zamiast tego warto tak napisać, kod zadań i wątków, by po zarządzaniu tego od nich potrafiły szybko posprzątać po sobie i dopiero później zakończyć sw e działanie. Zapewnia to w iększą elastyczność, bo najczęściej to kod zadania potrafi w najlepszy sposób zapew nić spójność danych, nad którymi pracował przed nadejściem przerwania. Kwestie związane z kończeniem, cykłu życia potrafią skomplikować projekt i imple­ mentację zadali, usługi i całych aplikacji. N iestety, zbyt często pom ija się na etapie i projektowania tę bardzo istotną kwestię. Odpowiednie radzenie sobie z anulowaniem, wyłączeniem lub błędem odróżnia dobre aplikacje od tych, które tylko działają poprawnie. ! Niniejszy rozdział zajmuje się mechanizmami anulowania i przerywania oraz sposobami takiego układania kodu usługi i zadań, by ułatwić obsługę żądań anulacji.

|

Zabronione metody T h r e a d . s t o p ( ) i su sp e n c K ) były próbą stworzenia takiego mechanizmu, ale wkrótce zdano sobie sprawę, że mają bardzo poważne wady i należy zabronić ich użytkowania. Dokładny opis problemów powodowanych przez te metody i motywów zabraniających ich stosowania znajduje się pod adresem http://java.sun.eom/j2se/1.5.0/docs/guide/>nisc/ihreadPrimitiveDeprecation.html.

Część II ♦ Struktura aplikacji współbieżnej";

144

Rozdział

7. ♦ Anulowanie i wyłączanie zadań

7.1. Anulowanie zadań

p u b lic void ru n t) { B ig ln te g e r p ■■ Biglnteger.ONE; w h ile (¡c a n c e lle d ) { p “ p.nextP robablePrim eO ; synchronized ( t h is ) { prim es.add( p);

Działanie można anulować, gdy zewnętrzny kod może doprowadzić je do zakończenia zanim samo zakończy się w normalnym trybie. Istnieje wiele powodów, dia których można zechcieć anulować zadanie:

} )

U ży tk o w n ik żąda a n u low an ia. Użytkownik klika przycisk A n u lu j w aplikacji

}

z interfejsem graficznym lub przesyła żądanie anulowania za pom ocą innego 1 , interfejsu, na przykład JMX (ang. J a v a M an a g em en t E xtensions).

p u b lic void ca ncel() ( cancelled = tru e ; }

Z ak oń czy ł się czas p rzezn a czo n y na zad an ie. /Aplikacja przeszukuje przestrzeń

rozwiązań przez określony przedział czasu i wybiera znalezione w tym czasie f najlepsze rozwiązanie. Po upływie wyznaczonego czasu należy anulować wszystkie zadania zajmujące się jeszcze poszukiwaniem rozwiązania. Z d arzen ia ap lik acji. A plikacja przeszukuje przestrzeń rozwiązań przez rozkład

!

głównego zadania na kilka mniejszych obejmujących mniejsze przestrzenie. Gdy jedno z zdań znajdzie rozwiązanie, pozostałe powinny zostać anulowane. B łędy. Algorytm przeszukiwania internelu szuka istotnych stron, zapamiętując

cale strony lub ich podsum ow ania na dysku twardym. Gdy wykryje błąd (bo przykładowo dysk je st zapełniony), powinien anulować wszystkie inne zadania w yszukujące i, być może, zapam iętać ich aktualny stan, by od tego miejsca wznow ić działanie w przyszłości.

145

,/

Z ak oń czen ie ap lik acji. Gdy aplikacja lub usługa kończy swe działanie, należy

p u b lic synchronized lis t< B ig In te g e r> g e t() { re tu rn new A rra y L is t< B ig Irite g e r> (p rim e s );

) ) Listing 7.2 przedstawia kod używający klasy generatora do wyliczenia liczb pierwszych w ciągu jednej sekundy. Generator niekoniecznie przerwie swe działanie dokładnie po jeden sekundzie, ale raczej z pewnym opóźnieniem wynikającym z konieczności przej­ ścia do testu sprawdzającego zawartość znacznika. M etoda cancel () zostaje wywołania również z poziomu bloku fin a l ly, by mieć pewność, że generator zostanie wyłączony nawet w momencie przerwania operacji s le e p ( ). Gdyby metoda cancel () nie została użyta ani razu, wątelc poszukiwania liczb pierwszych działałby nieskończenie długo, konsumując zasoby procesora i uniemożliwiając wyłączenie maszyny wirtualnej. Listing 7.2. Generowanie liczb pierw szych przez je d n ą sekundę

w jakiś sposób zająć się aktualnie wykonywanymi zadaniami. Gdy kończenie i aplikacji jest łagodne, zadania m ogą mieć szansę się zakończyć; w przeciwnym razie pow inny zostać od razu anulowane. N ie istnieje bezpieczny sposób zatrzymania wątku Javy z wykorzystaniem wywłasz­ czenia, więc nie istnieje również bezpieczny sposób zatrzymania w ten sposób zadania, Pozostaje użycie m echanizm ów w spółpracy, w których to zadanie i kod żądający anulowania w spółpracują ze sobą, używając uzgodnionego protokołu.

s ta tic L is t< B ig In teg e r> aSecondOfPrimesO throws InterruptedE xception { PrimeGenerator generator - new Prim eGeneratorO ; e xec.execute(generator); tr y { SECONDS.sleep(l): } f in a l ly { generator.cancel ( ) ;

) re tu rn g e n e ra to r.g e tO ;

Jednym z takich mechanizmów współpracy jest ustawianie znacznika „żądania anulowa­ nia”, który zadania sprawdza okresowo. Jeśli zauważy jego ustawienie, zadanie powinno m ożliw ie szybko zakończyć aktywność. Technikę tę ilustruje ldasa PrimeGenerator z listingu 7.1, która w yśw ietla liczby pierw sze aż do momentu anulowania. Metoda cancel O ustaw ia znacznik cancelled. G łówna pętla spraw dza zawartość znacznika przed rozpoczęciem liczenia kolejnej liczby pierwszej. Pam iętaj, że do poprawnego działania zm ienną cancel 1ed należy oznaczyć jako vol a ti 1e. Listing 7 .1 . Wykorzysta n ie pola volatile do przechow yw ania stanu anulowania ___________________ _

' ■ '

OThreadSafe p u b lic cla ss PrimeGenerator implements Runnable { . @GuardedBy(“ t h is " ) p riv a te fin a l List primes ■» new A rra y L is t< B ig ln te g e r> (): p riv a te v o la t ile boolean cancelled;

-1

Zadanie, które chce być anulowane, określa strategię anulowania w skazującą ,ja k ”, „kiedy” i „który” anulow ania— jaki inny kod może zażądać anulowania, kiedy zada­ nie spraw dza, czy pow inno się zakończyć i które podejście podejm ie zadanie, by się wyłączyć. Rozważmy rzeczyw isty przykład anulow ania płatności czekiem. Banki m ają ściśle określone zasady anulowania płatności wskazujące sposób składania takiego żądania, gwarantowanej szybkości reakcji na zgłoszenie i procedury wykorzystywane do zapo­ bieżenia wypłacie pieniędzy (wymaga to powiadomienia innych banków i często opłaty za anulowanie). Połączenie tych trzech elem entów zapew nia spraw ne anulow anie płatności czekiem.

C zęść II ♦ Struktura aplikacji współbieżnej ;

146

K lasa PrimeGenerator używ a prostej strategii anulow ania: kod w yw ołuje metod.'' cancel (), by poinform ować o chęci anulowania, klasa sprawdza zawartość zmiennej,: anulowania po znalezieniu każdej liczby pierwszej i kończy swe działanie, gdy wykryje anulowanie,

7 .1 .1 . Przerywanie Mechanizm anulowania z PrimeGenerator spowoduje w pewnym momencie zakończenie.zadania, ale może to zająć chwilę, jeśli wyliczenie jednej liczby pierwszej trwa długo. Jeżeli zadanie wywołuje metodę blokującą, na przyldad B lo c k in g Q u e u e .p u tO , problem' staje się poważny — zadanie może nigdy nie sprawdzić znacznika anulowania i tym , samym nigdy nie zakończyć swego działania. Problem ten ilustruje klasa BrokenPrimeProducer z listingu 7.3. Wątek producenta gene­ ruje liczby pierw sze ,i um ieszcza je w kolejce blokującej. Jeśli producent znacznie wyprzedzi konsumenta, kolejka wypełni się i metoda put O zablokuje działanie wątku. Co się stanie, jeśli konsument spróbuje anulować producenta, gdy ten ugrzązł w metodzie p u t( )? Choć ustawi znacznik anulowania m etodą cancel (), producent nigdy lego nie: zauważy, bo nie potrafi wy jść z metody blokującej put O (gdyż w tym samym czasie: konsument przestał przyjm ować liczby pierwsze z kolejki). Listing 7 .3 . Niepewne anulowanie, które może napotkać wątek producenta w operacji blokującej. Nie rób lak class BrokenPrimeProducer extends Thread { p riv a te f in a l BlockingQueue queue; ■ p riv a te v o la t ile boolean cancelled = fa ls e : BrokenPrimeProducer(BlockirigQueue queue) { th is.q u e u e - queue:

} p u b lic vo id ru n t) { try { B ig ln te g e r p » Big Integer.ONE: w h ile (¡ca n c e lle d ) queue.put(p = p.nextP robablePrim et) ) : J catch (In te rru p te d E xce p tio n consumed) ( }

} p u b lic void ca ncel() { cancelled = tru e ; }

} void consumePrimes() throws Interru pte d E xce p tio n { BlockingQueue primes » . . . ; BrokenPrimeProducer producer = new ProkenPrimeProducer(primes); p ro d u c e r .s ta r t( ); try { w h ile (needMorePrimesO) consum eiprim es.taket) ) ; } fin a lly { pro d u ce r.ca n ce l( ) :

R o zd z iał

7. ♦ Anulowanie i wyłączanie zadań

147

W rozdziale 5. wspomnieliśmy, że pewne metody blokujące bibliotek obsługują prze­ rywanie. Przerywanie wątku jest mechanizmem współpracy inform ującym jeden wątek z poziomu innego wątku, że ten pierwszy powinien w miarę możliwości się zatrzymać, jeśli zajmuje się czymś innym. i , h ;

- W dnterfejsie programistycznym ani; śpficyfikacjłjęzykainielm a.żadnego.pow ićizaniasiii przerywania z konkretnym system em an ulow a nia,'a le. w praktyce, zastosowanie, i .przerywania do.czegokolwiekrinnego:poza¡.anulowaniem-bywa bardzo .trudne ,w:więk-siś szych aplikacjach.’ , “ . "‘j

Każdy wątek zawiera w sobie sta tu s p rze rw an ia; w yw ołanie metody przerwania dla wątku ustaw ia zm ienną statu so w ą na w artość tru e . K lasa Thread zaw iera m etody przeryw ania w ątku i odpytyw ania o jeg o stan, co p rzedstaw ia listing 7.4. M etoda in te rr u p t! ) przerywa docelowy wątek natomiast metoda isln terrupted C ) zwraca status aktualnego wątku. Wyjątkowo niefortunnie nazwana m etoda statyczna in trru p te d O czyści status przerwania i zwraca jego wcześniejszy stan. To jedyny sposób wyczyszcze­ nia stanu wątku. Listing 7.4. Melody przerwań klasy Thread p u b lic class Thread { p u b lic void in te rr u p t^ ) ( . . . ) p u b lic boolean isInterruptedO ( . . . ) p u b lic s t a t ic boolean in te rru p te d O { . . .

}

. ) Metody blokujące biblioteki Javy, na przykład Thread. s le e p ( ) łub O b je c t.w a itO , próbują wykryw ać przerw anie w ątku i m ożliw ie szybko na nie reagow ać. C zyszczą status przerwania i zgłaszają w yjątek InterruptedE xception wskazujący wcześniejsze zakończenie blokującej operacji z powodu jej przerwania. M aszyna wirtualna w żaden sposób nie gwarantuje, jak szybko metoda blokująca wykryje przerwanie, ale najczęściej czas ten jest krótki. Przerw anie w ątku, gdy nie znajduje się w operacji blokującej, ustaw ia jeg o status przerwania. Od kodu wykonywanego przez w ątek zależy, czy zm iana statusu zostanie wykryta i ja k szybko to nastąpi. Próba przerw ania je st pam iętana — je śli nie zgłosi wyjątku InterruptedException, dowód zgłoszenia istnieje tak długo, aż ktoś go jaw nie , nie usunie. i . Wywołanie metody inte rrup bC ) . niekoniecznie powoduje,-zatrzymanie iaktualnycha-i |.¡-.działań wątku. Powoduje, jedynie przekazanie -odpowiedniej-wiadomości o żądaniu-wi i przerwania.

Warto mieć na uwadze, że przerwanie tak naprawdę nie powoduje przerwania działające­ go wątku, a jedynie poinform owanie go, by sam siebie przerwał przy pierwszej nada­ rzającej się okazji (okazje te nazyw a się m ie jsc am i an ulow ania). N iektóre metody, : w a itO , sleepO i jo in O , bardzo poważnie traktują informację o przerwaniu, zgłaszając

C zęść II ♦ Struktura aplikacji współbleĘna|'.'

148

w yjątek kodow i je w yw ołującem u, gdy w ykryją j ą w m om encie wykonywania |Uo' wywołania. Dobrze zachowujące się metody m ogą ignorować żądanie, o ile tylko pozQ. stawią w spokoju znacznik informujący o jego zajściu, by kod wywołujący mógł wykryj chęć przerw ania wątku. Źle zachow ujące się metody ukryw ają żądanie przerwania, więc nie może ono przenieść się wyżej w stosie wywołań, by zostać obsłużone. Statyczną metodę in te rru p te d !) warto stosować ostrożnie, bo zawsze anuluje status pr»>. rwania aktualnego wątku. Jeśli wywoła się tę metodę, a ona zwróci wartość tru e i niczamierza się pominąć przerwania, należy albo wywołać wyjątek In te rru p te d E x c e p tio n albo ponow nie zainicjow ać przerw anie dla aktualnego w ątku (patrz listing 5.10 ze strony 103.). K lasa B ro k e n P rim e P ro d u c e r pokazuje, że mechanizm anulowania nie zawsze dobrze współpracuje z m etodam i blokującym i. Jeśli kod zadania powinien szybko reagować: na przerwanie, warto używać przerwań jako mechanizmu anulowania, by wykorzystać testy anulowania znajdujące się w większości metod blokujących biblioteki Javy. Przerwanie to na jpraw dopodobniej najbardziej sensow ny sposób impfementacjj’S anulowania. •

Klasę BrokenPrimeProducer łatwo je st napraw ić (i uprościć), używ ając przerwania' zam iast zmiennej logicznej w skazującej chęć anulowania. N ow ą w ersję przedstawia listing 7.5. W każdej iteracji pętli istnieją dwa miejsca, w których można wykryć prze­ rwanie: wywołanie blokujące put O i jawne odpytywanie statusu przerwania w nagłówki) pętli. Ten jaw ny test nie jest tak naprawdę potrzebny, bo wykona go również metoda p u t ( ) , ale popraw i szybkość reakcji zadania na anulowanie, bo nie spow oduje roz­ poczęcia być może długo trwającego liczenia liczby pierwszej. Jeśli wywołania metod blokujących nie są dostatecznie częste, by uzyskać zadow alającą szybkość reakcji, warto w prowadzić własne, jaw ne testy. Listing 7.5. Wykorzystanie przerwania do emulowania zadania

______________

p u b lic class PrimeProducer extends Thread { p riv a te f in a l BlockingQueue queue;

149

7,1 .2 . Strategie przerywania Podobnie ja k zadania pow inny mieć strategie anulowania, tak wątki pow inny mieć strategie przeryw ania. Strategia określa, w jak i sposób w ątek interpretuje żądanie przerwania — co robi (jeśli w ogóle reaguje) w m om encie w ykrycia takiej sytuacji, .jakiego rodzaju działania traktuje się jako niepodzielne pod kątem przerwań, i jak szybko wątek zareaguje na zgłoszone przerwanie. Najbardziej sensowne strategie przerywania przyjmują postać anulowania na poziomie wątku lub usługi — kończą działanie tak szybko, ja k to dopuszczalne, czyszczą po sobie zasoby i inform ują kod nimi zarządzający o sytuacji. M ożna zastosow ać inne strategie przeryw ania, na przykład w strzym yw anie i wznawianie usługi, ale wątki i pule wątków używające niestandardowych strategii zapewne byłyby ograniczone tylko do zadań wymagających tych strategii. Warto rozdzielić sposób reakcji wątków i zadań na przerwanie. Pojedyncze przerwanie może mieć więcej niż jednego odbiorcę — przerwanie wątku wykonawczego puli wąt­ ków może oznaczać „anuluj aktualny w ątek” lub „wyłącz cały wątek wykonawczy”. Zadania nie działają w w ątkach, które są ich w łasnością, ale raczej pożyczają je od zewnętrznych usług, na przykład puli wątków. Kod, który nie posiada wątku (w przypad­ ku puli wątków jest to dowolny kod nieslanowiący części puli) powinien dbać o zacho­ wanie statusu przerwania, by kod wywołujący mógł na niego zareagować, nawet jeśli kod działający „gościnnie” również na niego reaguje. Gdy pilnujesz czyjegoś mieszkania, nie wyrzucasz poczty, która przyszła do tej osoby — zachowujesz ją, nawet jeśli pota­ jemnie przeczytasz korespondencję i zaprenumerowane czasopisma. Właśnie z tego powodu większość metod blokujących bibliotek Javy po prostu zgłasza wyjątek In te r r u p te d E x c e p tio n w odpow iedzi na zgłoszenie przerwania. N igdy nie działają one w wątku, który należy do nich, więc stosują najbardziej sensowną strategię anulowania dla zadania — jak najszybciej zejść z drogi i pozwolić przerwaniu przedostać się wyżej stosu wywołań. Zadanie nie od razu musi wszystko rzucić, gdy wykryje żądanie przerwania — nic szko­ dzi, jeśli opóźni je do momentu nadejścia bardziej odpowiedniej chwili (na przykład za­ kończenia aktualnego algorytmu) i dopiero później zgłosi wyjątek In te rru p te d E x c e p tio n lub w inny sposób poinform uje o błędzie. U łatw ia to ochronę struktur danych przed uszkodzeniem spowodowanym przerwaniem działań w połowic aktualizacji.

PrimeProducer(BlockingQueue queue) { th is.q u e u e = queue:

} p u b lic void ru n t) { try { B ig ln te g e r p “ Biglnteger.ONE: w h ile ( iT hread.currentThreadO . is ln te r r u p te d t ) ) queue.put(p = p .nextP robableP rim eO ); ) catch {In te rru p te d E xce p tio n consumed) { I'k Umozihvia zakończenie wątku. */

Rozdział 7. ♦ Anulowanie i wyłączanie zadań

3, 5; f

.'■)

Zadanie nie powinno niczego zakładać na temat strategii przerwania wykonującego go wątku, chyba że zostało zaprojektow ane do działania tylko w w ątkach cechujących się bardzo konkretną strategią. Gdy zadanie interpretuje przerwanie jaico anulowanie lub podejmuje inne działania, pow inno zachow ać status przerw ania wątku. Jeśli nie może w prosty sposób zgłosić wyjątku I n t e r r u p te d E x c e p tio n do kodu wywołującego, powinno przywrócić status przerwania, wykonując poniższy kod: Thread.currenŁThreadi) . in t e r r u p t f );

p u b lic vo id cancel () { in te r r u p tO : )

Podobnie jak kod nie powinien dokonywać żadnych założeń co do znaczenia przerwa­ nia w wątku go wykonującym, kod anulowania nie powinien zakładać żadnej konkretnej

150

C zęść II ♦ Struktura aplikacji w sp ółb ieżn i

strategii anulowania dowolnych wątków. Watek powinien zostać anulowany tylko prze|C'i swojego właściciela; to on może udostępnić metodę anulowania odpowiednio interp^lf.! tującą strategię przerwania wątku, na przykład przeprow adzającą jego zakończenie, | ( I v Ponieważ każdy wątek: stosuje własną strategię przerwania; nie- należy przeryw a^:' ’ wątku, je śli się nie wie, czy takie właśnie je s t życzenie właściciela wątku. 1 ¡'¡'¿li

Z nalazło się w iele osób krytykujących przerw ania Javy, bo z jednej strony język njj4 dopuszcza do wywłaszczenia w ątku w celu jego zatrzymania, a z drugiej wymusza jji program iście obsługę w yjątków In te rru p te d E x c e p tio n . W arto pam iętać, że opeja: i odw leczenia żądania przerw ania daje programiście szansę na elastyczny sposób jeg0;i obsługi, który zapew nia zarówno dobry czas reakcji, ja k i poprawność danych całej- v aplikacji.

R o z d z ia ł

7. ♦ Anulowanie i wyłączanie zadań

-

151

Jedynie kod im plementujący strategię przerwania wątku może pom inąć,żądanie przerwania. Ogólne zadania i biblioteki nigdy nie powinny ukrywać przerwania.

D ziałania, które nie o bsługują anulow ania, a m im o to w y w o łu ją m etody blokujące z możliwością przerywania, powinny te metody wywoływać w pętli, ponawiając wywoła­ nie po wykryciu przerwania. Powinny lokalnie zapamiętywać status przerwania i przy­ wracać go po wykonaniu zadania, co przedstawia listing 7.7, zamiast od razu wyłapywać wyjątek InterruptedE xception. U staw ienie statusu przerw ania zbyt w cześnie może skutkować powstaniem pętli nieskończonej, bo w iększość metod blokujących od razu sprawdza status i zgłasza wyjątek, gdy wykryje włączenie przerwania. Metody najczę­ ściej spraw dzają status przecł rozpoczęciem w łaściw ej operacji błokującej łub innej dłuższej operacji, by ja k najszybciej odpow iedzieć na przerwanie. Listing 7.7. Zadanie bez możliwości anulowania, które przywraca status przerwania przed swym zakończeniem______ __________ ___________ _______________________________________________

7 .1 .3 . Odpowiadanie na przerwanie Jak wspomnieliśmy w podrozdziale 5.4, gdy wywołujemy metodę blokującą z możliwąt ) ścią przerwania, na przykład Thread.s le e p ( ) lub BlockingQueue.putO, istnieją dwie*- i sensowne strategie obsługi wyjątku InterruptedE xception: ,, i ♦ Propagacja wyjątku (prawdopodobnie po podstawowym czyszczeniu danych),, u;- \ co jednak czyni kreow aną metodę m etodą blokującą z m ożliwością : przerwania. i . ♦ O dtworzenie statusu przerwania, by kod wyżej na stosie wywołańodpowiednio sobie z nim poradził.

’ O i jij' i

Propagacja wyjątku to nic innego jak dodanie InterruptedException w klauzuli throws! j metody. Przykład takiego podejścia zawiera metoda getNextTaskt) z listingu 7.6. -f; j Listing 7.6. Propagacja wyjątku InterruptedException do kodu wywołującego____________

p u b lic Task getNextTaskt) throws InterruptedE xception { re tu rn q u e u e .ta ke t):

p u b lic Task get.NextTask(BlockingQueue queue) { boolean in te rru p te d - fa ls e : try { w h ile (tru e ) ( try { re tu rn queue. ta ke O ; ) catch (In te rru p te d E x c e p tio n e) { in te rru p te d “ tru e : / / Ponownie podejm ij probe pobrania danych,

} } } f in a lly { i f (in te rru p te d ) Thread.currentThreacK ) . in t e r r u p t ( ) ;

)

-

B1ockingQueue queue,, . :

} Jeśli nie można lub nie chce się zgłaszać wyjątku InterruptedException (bo na przykład | zadanie znajduje się w obiekcie Runnable), trzeba poszukać innego sposobu poinfon j litowania o przerw aniu. N ajczęściej w ystarczy w tym celu p onow nie zgłosić chęć: v przerwania, wywołując metodę in te r r u p tt) . Nie wolno wyłapywać wyjątku IrilerrtlptedExceptiori i nic nie robić w bloku catch, chyba że kod implementuje inny sposób, radzenia sobie z przerwaniem. Klasa PrimeProducer pomijała przerwanie, ale wiedziała; że wątek zostanie przerwany i żaden inny kod stosu wywołań nie musi wiedzieć o zakoń­ czeniu obsługi. W większości sytuacji nie wiemy, jaki wątek będzie wykonywał kod; więc powinniśm y zachować status przerwania. Ą

t -

Jeśli kod nie wywołuje przerywanej metody blokującej, nadał powinien w jakiś sposób uwzględniać sprawdzanie statusu przerwania wątku. Określenie odpowiedniej częstości sprawdzeń wymaga znalezienia złotego środka między wydajnością i szybkością reakcji. Gdy ważniejsza jest szybkość reakcji, nie pow inno się wykonywać długo działających operacji, które nie uwzględniają sprawdzania przerwania, co niejednokrotnie ogranicza liczbę dostępnych klas. Anulowanie może dotyczyć stanu innego niż status przerwania; przerwanie m a na celu przyciągnięcie uwagi wątku. Informacje przechowywane w innym miejscu m ogą zapew­ nie bardziej szczegółowy sposób obsługi anulowania (pamiętaj o synchronizacji przy dostępie do tych danych). Przykładowo, gdy wątek wykonawczy należący do ThreadPool Executor wykryje przerwanie, spraw dza, czy pula m a zostać wyłączona. Jeśli tak, przeprowadza dodatkowe czyszczenie przed wyłączeniem puli. W przeciwnym razie może utworzyć nowy wątek, by przyw rócić puli pożądany rozmiar.

152

Część II ♦ Struktura aplikacji współbie*

zdzia* 7- ♦ Anulowanie i wyłączanie zadań

wątek zadania, metoda timedRunC) wykonuje czasow ą operację j o i n t ) z nowym w ąt­ kiem. Po powrocie z j o i n t ) sprawdza, czy został zgłoszony wyjątek z poziomu zadania. Jeśli tak, zgłasza go ponownie w wątku wywołującym metodę timedRunO. Zapisany obiekt Throwable je st współdzielony przez dwa wątki, więc w celu zapewnianie bez­ pieczniej publikacji został zadeklarowany jako v o la tile .

7 .1 .4 . Przykład — działanie czasowe W iele problem ów rozw iązuje się nieskończenie długo (na przykład wyświcllcńj^. wszystkich liczb pierwszych); w innych odpowiedź można podać bardzo szybko, agi najczęściej również m ogą trwać całą wieczność. Możliwość powiedzenia-„spędź hji minut na szukaniu odpowiedzi” lub „przedstaw wszystkie wyniki, które uda się tVylic^ - :. w 10 minut” bywa jedynym ratunkiem. I ••!,i,V M etoda aSecondOfPrimesO z listingu 7.2 uruchamia generator liczb pierwszych i pr^ć; • rywa jego działanie po sekundzie. Choć generator zapewne będzie działał nieco dłużej'* niż sekundę, w pewnym momencie zauważy sygnał przerwania i zatrzyma się, dopro­ wadzając do zakończenia wątku. Jeśli klasa PrimeGenerator zgłosi wyjątek nieweryfikóJ: wany przed upływem czasu zatrzymania, prawdopodobnie pozostanie on niezauważony, bo generator działa w osobnym wątku, który nie obsługuje wyjątków w sposób jawny, ą Listing 7.8 przedstawia próbę wykonywania dowolnego obiektu Runnab le precz określony"okres. U rucham ia zadanie i harm onogram uje jego anulowanie po określonym czasie; W ton sposób unika problemu nieweryfilcowanego wyjątku zgłoszonego przez zadanie,: bo może on zostać złapany przez kod wywołujący timedRunO. Listing 7 .8 . H arm onogram ow anie przerw ania pożyczonego wątku. Nie rób tak ___________________ ipp riv a te s t a t ic f in a l ScheduledExecutorService cancelExec -

153

(Jsting

Przerywanie zadania w dedykowanym wątku p u b lic s t a t ic void tim edR untfinal Runnable r. long tim eout, TimeUnit u n it) throws InterruptedE xception { class RethrowableTask implements Runnable { p riv a te v o la t ile Throwable t : p u b lic void ru n () { try {

r.runO ; } catch (Throwable t ) { th is .t = t:

} ) void re th ro w n { i f ( t != null) throw la u n d erT hro w a b le (t):

...;

p u b lic s t a t ic void LimedRuntRunnable r . long tim eout, TimeUirit u n it) { f in a l Thread taskThread = T hre a d .cu rren tT h re a d i); cancel Exec, schedule; new RunnableO { p u b lic void ruriO { ta s k T h re a d .in te rru p t;); } } . tim eo u t, u n it ) ; r .r u n O ;

To kusząco proste podejście, ale niestety — lamie zasady: należy znać strategię przerwań wątku, zanim się go wyłączy. Poniew aż timedRunO może wywołać dowolny wątek;; kod nie jest w stanie znać strategii przerw ania wątku. Jeśli zadanie zakończy się przed, upływem czasu, zadanie anulowania, które przerywa w ątek wywołujący t itnedRunOjmoże zadziałać po powrocie z metody timedRunO do kodu wywołującego. Nie wiadomo;, jaki kod będzie wykonywany, gdy do tego dojdzie. Istnieje pewna zadziwiająca sztuczki eliminująca ryzyko — wystarczy użyć obiektu ScheduledFeature zwróconego praf schedul e, by anulować zadanie w yłączające wątek. r|. Jeżeli zadanie nie odpow iada na przerwania, timedRunO nie powróci, dopóki zadanij! się nie zakończy, co m oże trwać znacznie dłużej niż założony przedział czasu (ta# w ogóle się nie zakończyć). Okresowo uruchamiana usługa, która nie chce się zakończyć w przewidzianym przedziale czasu, z pewnością będzie denerwować jej użytkowników Listing 7.9 rozw iązuje problem obsługi w yjątków w metodzie a S e c o n d O f P r im e s | i oraz wcześniej omówione zagadnienia. Wątek utworzony w calu wykonania zadań® może stosować w łasną strategię wykonania i nawet jeśli zadanie nie odpowiada na prat rwanie, metoda czasowego uruchomienia wróci do kodu wywołującego. Uruchamiaj^

RethrowableTask task = new RethrowableTaskO: fin a l Thread taskThread = new T h re a d (ta s k ): taskThread. s t a r t O ; cancel Exec, schedule (new RunnableO { p u b lic void ru n () { ta s k T h re a d .in te rru p t;); ). tim eout, u n it ) ; taskThread. jo i n ( uni t . to M i11i s ( t i meout) ) : ta s k .re th ro w ;);

Nowa wersja rozwiązuje problemy wcześniejszych przykładów, ale z racji zastosowania czasowej wersji metody jo in O ma w łasną wadę — nie wiemy, czy sterowanie zostało zwrócone z pow odu norm alnego zakończenia w ątku czy z pow odu upływ u czasu wskazanego w jo in ( )2.

7.1.5. Anulowanie przy użyciu Futurę Już wcześniej zastosowaliśmy abstrakcję, by zarządzać cyklem życia zadania, rozwiązać problem z wątkami i anulowaniem — obiekt Futurę. Stosując podstawą zasadę, że lepiej polegać na istniejących i sprawdzonych klasach bibliotecznych niż kreować własne To wada interfejsu programistycznego klasy Tbread, ponieważ to, czy jo in t) zakończyło się poprawnie czy też nie, ma konsekwencje co do widoczności pamięciowej w modelu pamięci Juvy. Niestety, metoda jo in t) nie wskazuje, jaki byl powód jej zakończenia.

154

C zęść II ♦ Struktura aplikacji wspóftie^ |

>!• rozwiązania, wykonajmy metodę timedRunO, używając Future i szkieletu.wykonywa/ zadań. . yi M etoda E xecutorService. subrni t ( ) zwraca obiekt Future opisujący zadanie., Obfejj zaw iera m etodę cancel O przyjm ującą w artość logiczną (m a yln te rru p tlfR u n n iy' i zw racającą inform ację o pow odzeniu lub niepowodzeniu anulowania. W zasady m etoda inform uje jedynie o popraw nym dostarczeniu przerw ania, a nie o tym, c zadanie w ogóle zareagow ało. Przekazanie jak o argumentu w artości fa ls e oznaczą.' „nie uruchamiaj zadania, jeśli jeszcze się nie zaczęło”. Powinno być s to s o w a n e j zadań, które nie zostały zaprojektowane do przerywania obsługi. Skoro nie należy przerwać wątku, gdy nie zna się jego strategii przerywania, czy można zadanie anulować, wywołując metodę cancel O z wartością true? Watki wykonywania zadań tw orzone przez standardow e im plem entacje szkieletu Executor stosują taką strategię przeryw ania, że m ożna bezpiecznie przekazać w artość tru e jak o argument w momencie anulowania zadań Future działających wewnątrz podstawowego szkielety Executor. Nie wolno przeryw ać wątku puli bezpośrednio w anulowanym zadaniu; bo nie wiemy, co zadanie wykonuje w momencie dostarczenia przerwania — wykorzystuj w tym celu metodę obiektu Future. Oto jeszcze jeden powód, by tak pisać kod zadań, by traktowały przerw anie jak o anulow anie — można przeprow adzić anulowanie za pom ocą obiektu Future. Listing 7.10 przedstawia w ersję timedRunO, która zleca zadania obiektowi Executor­ Service i pobiera wyniki czasową w ersją Future, get O. Jeśli g e t() zakończy się wyją), kiem T'imeoutException, zadanie zostaje anulowane za pom ocą obiektu Future. Aby uprościć kod, wywołujemy m etodę Future.cancel O bezwarunkowo w bloku finally, korzystając z faktu, iż anulowanie wykonanego zadania nie przynosi żadnego efektu. Jeśli rzeczywiste obliczenia zgłoszą wyjątek przez anulowaniem, zostaje on ponownie zgłoszony przez metodę timedRunt), bo to kod wywołujący powinien najlepiej poradzii sobie z obsługą wyjątku. Listing 7.10 ilustruje również inną dobrą praktykę — anulo. wania zadań, których wyniki nie są dłużej potrzebne. Podobne podejście zastosowano w kodzie z listingów 6.1.3 i 6.16. L is tin g 7 .1 0 . Anulowanie zadania za p omocą obiektu Future_________________________________ p u b lic sta t ic 'v o id timedRun(Runnab1e r . long tim eo u t, TimeUnit u n it) throws In te rru pte d E xce p tio n { Future task = ta s k E x e c .s u b m it(r); try

{

ta sk.g etC tim e o u t, u n it ) : } catch (TimeoutException e) { / / Zadania zostanie anulowane poniżej. } catch (ExecutionException e) { I I Wyjątek zgłoszony w zadaniu, zgłoś go ponownie. th row 1aunderlhrowab1e( e .ge tCause( ) ) : ) f in a lly { / / Nie ma znaczenia, je śli zadanie sig zakończyło. ta sk, cancel ( t r u e ) : / / Przerywa zadanie w toku.

Rozdział 7. ♦ Anulowanie i wyłączanie zadań

155

! Gdy metoda F u tu re .g e t() zgłasza wyjątek In te rru p te d E x c e p tio n ,lub Timeout- j | Exception, a ma się pewność, że wynik nie będzie już potrzebny, warto anulować' -, : zadanie metodą Future.'canceK). 1 • ■ • ' ■ • ■ . .j

7.1.6. Radzenie sobie z blokowaniem niedającym szans na przerwanie Wiele metod blokujących bibliotek Javy po otrzym aniu przerw ania bardzo szybko kończy swe działanie i zgłasza w yjątek InterruptedE xception. Ułatwia to tworzenie zadań szybko reagujących na anulowanie. Niestety, nie w szystkie metody blokujące reagują na przerwanie. Jeśli wątek został zablokowany przez operację synchronicznego dostępu we-wy, która czeka na uzyskanie blokady, przerwanie jedynie ustawi status przerwania (nie będzie miało żadnego innego efektu). W pewnych sytuacjach można przekonać wątek zablokowany na nieprzery walnej akcji, by jednak się zatrzymał, ale wymaga to dokładnej znajomości przyczyny zablokowania. Synchroniczna o p eracja we-wy dla g n iazd a z java. io. Częstą postacią bloku jącego we-wy w aplikacjach serwerowych je st odczyt lub zapis danych z gniazda. Niestety, metody read( ) i w rite ( ) z InputStream i OutputStream nie reagują na przerwanie, ale zam knięcie gniazda spowoduje zgłoszenie przez te blokujące metody wyjątku SocketException. S ynchroniczna o p eracja we-wy z ja v a . nio. Przerwanie wątku oczekującego na InterrupLibleChannel powoduje zgłoszenie wyjątku C losedB ylnterruptException i zam knięcie kanału (co w konsekwencji oznacza zgłoszenie w yjątku ClosedB ylnterruptE xception przez pozostałe wątki zablokowane na kanale). Zam knięcie kanału powoduje zgłoszenie przez zablokowane na nim wątki wyjątku AsynchronousCloseException. Większość standardowych kanałów implementuje w ersję In terru p tib leC h an n el. A synchroniczne wc-wy z użyciem Selector. Jeśli wątek je st zablokowany na operacji S ele cto r.se le c t O (z pakietu ja va. n io . channels), metoda closeO powoduje jego natychm iastowy pow rót przez zgłoszenie wyjątku ClosedSelectorE xcep tion.

Przejęcie blokady. Jeśli wątek został zablokow any w oczekiwaniu na blokadę w ew nętrzną nie można nic zrobić, by go odblokować w prosty sposób. Pozostaje się postarać, by szybko uzyska! blokadę i wykonał wystarczająco dużo poleceń, by zauważył przerwanie. Klasy jaw nych blokad (Lock) oferują metodę lo c k ln te rru p tib ly l ), która dopuszcza oczekiwanie na blokadę i przyjmowanie przerwań. Więcej informacji na ten temat znajduje się w rozdziale 13. Klasa ReaderThread z listingu 7.11 przedstawia technikę hermetyzacji niestandardowego anulowania zadania. Klasa zawiera jedno połączenie gniazdowe, odczytuje synchro­ nicznie dane z gniazda i przekazuje dane do obiektu processB uffer. A by ułatw ić przerwanie operacji przez użytkownika lub przy wyłączaniu serwera, wątek przysłania metodę interrupfcO , by jednocześnie dostarczyć standardowe przerwanie i zamknąć

156

■dział 7- ♦ Anulowanie i wyłączanie zadań

C zęść II ♦ Struktura aplikacji współbleż^j

D ostosow yw anie obiektu Future zadania um ożliw ia przesłonięcie m etody Future. to*-canceK). Własny kod anulowania potrafi zapisać informacje do dziennika zdarzeń, zbierać statystyki albo anulować zadania, które słabo reagują na standardowe pr/urywa­ nie. Klasa ReaderThread hermetyzuje anulowanie pobierania danych z. gniazda przez modyfikację działania metody in te rr u p tO wątku. To samo zadanie można uzyskać, modyfikując metodę Future.cancel O.

gniazdo. W ten sposób anulowanie pobierania możliwe jest również wtedy, gdy wąteji oczekuje na dane w m etodzie read(). j, Listing 7 .1 1 . H erm etyzacja niestandardow ego sposobu anulow ania wątku p rzez przesłonięcie' m etody interruptQ ' ;__________________________ 4 p u b lic class ReaderThread extends Thread { p riv a te f in a l Socket socket; p riv a te f in a l InputStream in :

• >'

}

. ;1

p u b lic void in te r r u p tO { try { s o c k e t.c lo s e !); } catch (IOException ignored) { } f in a lly { super. i nterrupt O;

!

Interfejs Cancel lableTask z listingu 7.12 definiuje nową wersję interfejsu rozszerzającą interfejs C allable przez dodanie metody cancel () i metody fabrycznej newTaskO do konstruowania RunnableFuture. K lasa CancellingExecutor rozszerza klasę ThreadPoolExecutor i przesłania metodę newTaskForO, by CancellableTask mógł utworzyć własną wersję F uture. cancel ().

pf

p u b lic ReaderThreadtSocket socket) throws IOException { th is .s o c k e t « socket; t h is . in = socket.getlnputS tream O :

!

Listing

a v |. iii-T

•:;".i

}

i

7.12. Herm etyzacja niestandardowego anulowania zadania za pom ocą newTaskForQ

p u b lic vo id ru n !) {

try { ■

■ .

-¡if

)

if f

}

ij}

-

)

t .■ $

..

- ff

7 .1 .7 . Hermetyzacja niestandardowego anulowania za pomocą newTaskForO R ozw iązanie zastosow ane w klasie ReaderThread do hermetyzacji niestandardowego anulowania m ożna wprowadzić, używając metody newTaskForO dodanej do ThreadPoolExecutor w Javie 6. Gdy obiekt Cal la b ie umieszczamy w obiekcie ExecutorServ1tt, metoda submitO zw raca obiekt Futurę, który może posłużyć do anulowania zadanił Metoda newTaskForO to specjalna metoda fabryczna tworząca obiekt Futurę dla zadani ■ Zwraca obiekt implementujący interfejs RunnableFuture, który rozszerza zarówno fejs Futurę, ja k i Runnable (interfejs implementuje klasa FutureTask). 1|

!

}

;

OThreadSafe class C ancel!i ngExecutor extends ThreadPoolExecutor {

f

protected RunnableFuture newTaskFor(Callable c a lla b le ) { i f (c a lla b le in sta n ceo f CancellableTask) re tu rn ((CancellableTask) c a lla b le ).n e w T a s k O ; el se re tu rn super.new TaskFor(callable);

’I

} c a tc h -( IOException e) { / * Umożliwia opuszczenie wątku. * /

}

^

:

p u b lic in te rfa c e CancellableTask extends Callable { void c a n c e l!); RunnableFuture newTaskO:

I

byteCJ b u f = new byteCQUFSZ]; w h ile '( tr u e ) { in t count - in .re a d (b u f); i f (count < 0) break; else i f (count > 0 ) processBuffer(buf. co u n t);

157

p u b lic a b s tra c t class SocketUsingTask implements Cancel!ableTask @GuardedBy("this") p riv a te Socket socket; protected synchronized void setSockettSocket s) { socket = s; } p u b lic synchronized void c a n ce l() { tr y { i f (socket !“ n u l l ) s o c k e t.c lo s e !): } catch (IOException ignored) {

} p u b lic RunnableFuture newTaskO { re tu rn new FutureTask(this) { p u b lic boolean cancel(boolean m aylnte rru p tlfR u n n in g ) { try { SocketUsingTask. th is .c a n c e l( ) ; } fin a lly { re tu rn super .cancel (may In te rr u p t I FRunni rig):

158

C zęść II ♦ Struktura aplikacji współbjgj^j

Klasa SocketUsingTask implementuje interfejs CallableTask i definiuje metodę FuiW'. '♦ c a n c e lO zam ykającą gniazdo i w yw ołującą metodę s u p e r.cancel O . Jeśli Sockety UsingTask zostanie anulowane na podstawie obiektu Futurę, giiiazdo zostanie zamknięć' a wątek wykonujący ustawiony w stan przerwania. Zwiększy to szybkość reakcji zadani” na anulowanie — może bezpiecznie wywoływać nie tyiko metody blokujące pozwalają^’na przerywanie, ąłc także metody blokujące związane z we-wy, które tego nie o ferują*

.2. Zatrzymanie usługi wykorzystującej wątki 0 A plikacje często tw orzą usługi, które zaw ierają wątki na własność, na przykład pule", wątków. Czas życia tych usług najczęściej je st dłuższy niż metody, za pomocą której', powstały. Jeśli aplikacja wyłącza się w sposób miły, wątki należące do usług powinny zostać przerwane. Ponieważ nie istnieje żadne rozwiązanie wywłaszczające i zatrzymałjące wątek, kod musi użyć łagodnej perswazji, by zatrzymać wątki. -#

Rozdział17. ♦ Anulowanie i wyłączanie zadań

podejście nie wymaga jawnej synchronizacji3. Jednakże w podrozdziale 11.6 przekonamy się, że tworzenie dziennika za pom ocą jawnych wywołań wstawionych w kodzie potrafi zmniejszyć wydajność w dużych aplikacjach. Alternatywne podejścia najczęściej stosują metodę lo g (), która kolejkuje komunikat do zapisu przez inny wątek. Klasa LogWriter z listingu 7.13 przedstaw ia prostą usługę dziennika, w której właści­ wy zapis dziennika znalazł się w osobnym wątku. Zam iast od razu zapisywać komuni­ katy do pliku za pom ocą strum ienia w yjściow ego, klasa zapam iętuje je w obiekcie BlockingOueue i osobnym wątkiem pobiera dane z kolejki. Powstaje rozwiązanie wielu producent, jeden konsument — dowolny kod wywołujący metodę lo g () je st producen­ tem, wątek pobierający dane z kolejki to konsument. N iestety, podejście to m a pew ną wadę — blokuje wszystkie producenty w momencie zapełnienia kolejki do momentu jej opróżnienia przez wątek konsumujący. Listing 7.13. Usługa dziennika na zasadzie producent-konsument bez obsługi wyłączania p u b lic class LogW riter ( p riv a te fin a l BlockingQueue queue; p riv a te f in a l LoggerThread logger; p u b lic LogW riterCW riter w r it e r ) { th is.q u e u e - new LinkedBlockingQueueiCAPAClTY); th is .lo g g e r “ new L o g g e rT h re a d (w rite r);

Zasady hermetyzacji zalecają unikanie modyfikacji wątków — wysyłania przerwania;1 zm iany priorytetu itp. — których nie je st się właścicielem . Z drugiej strony interfejs; programistyczny wątków nie zawiera formalnej definicji własności wątku. Wątek repre? zentowany przez obiekt Thread m ożna dow olnie w spółdzielić ja k każdy inny obiekiF Warto w sposób mniej formalny wskazać właściciela wątku -— najczęściej jest to klasa tworząca wątek. W ynika stąd, że pula wątków posiada wątki wykonawcze i jeśli mają one zostać przerwane, musi się tym zająć pula. 1“'

} p u b lic void s t a r t O

{ lo g g e r.s ta rtC ); j

p u b lic void logC S tring msg) throws In te rru pte d E xce p tio n { queue.put(msg);

}

Podobnie ja k z każdym innym herm etyzow anym obiektem w łasność wątku nie jest przechodnia — do aplikacji należy usługa, ale to do usługi, a nie do aplikacji, należą wątki wykonawcze. Z tego powodu aplikacja nie powinna sama próbować ich zalrzj“.' mywać. N a usłudze spoczywa obowiązek udostępnienia metod cyklu życia do wyłączani samej siebie i w szystkich innych należących do niej wątków. W tedy aplikacja zleca w yłączenie usługi, a ta w yłącza wątki. Interfejs ExecutorService definiuje metody shutdownt) i shutdownNowt). Inne usługi posiadające wątki powinny w podobny sposób udostępniać mechanizm wyłączania. ;'s

p riv a te class LoggerThread extends Thread { p riv a te fin a l P rin tW rite r w r it e r ; p u b lic void runO { tr y { w h ile (tru e ) wr i t e r . p r in tlr K queue. ta k e ( ) ) ; j catch (In te rru p te d E x c e p tio n ignored) { ) f in a ll y {

w riter.closet): Zdefiniuj metody cyklu życia, jeśli do usługi należą inne wątki, których' czas.żyćJS 1 je st dłuższy'niż metody, która je tworzy. ■ ' , • ■• -

7 .2 .1 . Przykład — usługa dziennika zdarzeń

:«S

4

} } } )

4 1

W iększość aplikacji serw erow ych w ykorzystuje dzienniki zdarzeń, które niekiedyti| tak proste, że w ykorzystują instrukcje p r in tln C ) wstawione w prost w kodzie. KM strumieni, na przykład P rin tW ri te r , są bezpieczne w ątkow o, więc to bardzo pro|S

159

Jeśli w jednym komunikacie umieszcza się wiele wierszy tekstu, może pojawić się konieczuość dodatkowych blokad po stronie klienta, by uniknąć przemieszania się komunikatów z wielu wątków. Jeśli w tym samym czasie dwa wątki wysyłają do dziennika wielowicrszowe teksty, stosując jedno polecenie println() na każdy wiersz, plik dziennika zapewne będzie zawierał przemieszane dane.

160

C zęść II ♦ Struktura aplikacji współb|ej.

Aby usługa typu LogWriter była przydatna w systemie produkcyjnym, musimy w jay^ sposób doprowadzać do w yłączenia wątku zapisującego, by maszyna wirtualna niog|ai w yłączyć się bez problem ów . Z atrzym anie wątku nie pow inno stanowić problem« ponieważ wątek cały czas wywołuje metodę takeO zapewniającą reakcję na przerwani» Gdyby zm odyfikować kod w taki sposób, by wyłączał w ątek w momencie w y lap ^ wyjątku InterruptedE xception, uzyskamy możliwość przerwania tworzenia dziennj, ka przez przesłanie przerwania. Z drugiej strony proste wyłączenie wątku dziennika nie jest zbyt satysfakcjonujący^' m echanizm em kończenia działania. M oże doprowadzić do pominięcia przy zapis|komunikatów, które znajdują się w kolejce, ale nie zostały jeszcze przekazane dalej! Co gorsza, gdy w momencie wyłączania kolejka byki pełna, wątki zablokowane w opęta? cji log() nigdy się nie odblokują. Anulowanie działalności producencko-konsumencki|:' wymaga anulowania zarówno konsumenta, jak i producentów. Przerwanie wątku zapilisującego rozwiązuje problem konsumenta, ale z racji tego, że producenly. nie są dedyko­ wanymi w ątkami, ich wyłączenie okazuje się znacznie trudniejsze. K olejna próba poradzenia sobie z wyłączeniem usługi LogWri te r mogłaby polegać ni ustawieniu znacznika „żądania wyłączenia”, by uniemożliwić przyjmowanie kolejnycf: kom unikatów . N ow ą w ersję m etody lo g () przedstaw ia listing 7.14. Konsument^ wykryciu taktu wyłączania opróżniłby kolejkę z ju ż znajdujących się w niej komuniki tów, czym odblokowałby producenty. Niestety, taki kod zawiera wyścig, który czytą. go mało pewnym. Implementacja metody log O zawiera sekwencję sprawdź i wykonaj — producent m oże zaobserw ow ać, że dziennik nie je st jeszcze wyłączany, ctójj w rzeczywistości został ju ż zamknięty na przyjmowanie nowych zleceń. Oznacza;)«; że producent ponownie ryzykuje zablokowaniem się w metodzie lo g (), z której nigdy nie wyjdzie. Istnieją sztuczki zmniejszające prawdopodobieństwo zajścia takiego zdamy nia (konsument czeka kilka sekund, zanim po raz ostatni opróżni kolejkę), ale nie zmieji to samej istoty problemu, więc pewne prawdopodobieństwo wystąpienia problenk nadal będzie istnieć. - iff Listing 7 .1 4 . N iepew ny sposób dodania obsługi w yłączania do usługi dziennika zdarzeń _________ -A p u b lic vo id lo g (S tririg msg) throws InterruptedE xception { i f ( ! shutdownRequested) queue.put(m sg): else throw new IIle g a lS ta te E x c e p tio n t"usługa została wyłączona");

dział 7.

Anulovyanie i wyłączanie zadań

161

■7.15- Dodanie pew nej m etody wyłączania usługi dziennika zdarzeń public class LogService { p riv a te f in a l BlockingQueue queue; p riv a te fin a l LoggerThread loggerThread: p riv a te f in a l P rin tW rite r w r it e r ; @Guarded8y("this” ) p riv a te boolean isShutdown; PGuardedByC'this") p riv a te in t re s e rv a tio n s ; p u b lic void s ta r tO

{ lo g g e rT h re a d .s ta rtO ; }

p u b lic void s to p t) { ■synchronized ( t h is ) { isShutdown = tru e ; } lo g g e rT h re a d .in te rru p t!);

p u b lic void lo g (S trin g msg) throws InterruptedE xception synchronized ( t h is ) { i f (isShutdown) throw new Ille g a lS ta te E x c e p tio n i. . . ) ; ++reservations; queue.put(m sg):

} p riv a te class LoggerThread extends Thread { p u b lic void ru n !) { tr y { w h ile (tru e ) { try { synchronized (L o g S e rv ic e .th is ) { i f (isShutdown && rese rva tio n s break;

0)

S trin g msg » q u e u e .ta k e t); synchronized (L o g S e rv ic e .th is ) { --re s e rv a tio n s ; } w r it e r . p r in t In(m sg); catch (In te rru p te d E x c e p tio n e) { / * próbuj ponownie */

f i n a ll y { w r it e r . c lo s e ( );

By zapew nić bezpieczny sposób w yłączania usługi LogW riter, m usimy pozbyć.|| wyścigu przez zapewnienie niepodzielności operacji zgłaszania nowego koiminika|; Z drugiej strony nie chcemy zakładać blokady, bo metoda p u t() jest blokująca. Zamil tego zapewniamy jedynie niepodzielne sprawdzenie warunku wyłączenia i inkremenfil licznika dopuszczającego do przesiania komunikatu. N ow ą wersję przedstawia kM LogService z listingu 7.15. -;;il -

' %

7.2.2. Wyłączanie ExecutorService .W punkcie 6.2.4 poinform owaliśmy o dw óch sposobach wyłączania usługi ExecutorService, jak im i są w yłączenie łagodne shutdownO i w yłączenie natychm iastow e shutdownNowt). W przypadku wyłączenia natychmiastowego metoda shutdownNow!) zwraca listę zadań, które jeszcze się nie rozpoczęły w momencie zgłaszania zakończenia wykonywania.

162

C zęść il ♦ Struktura aplikacji współty^

« Anulowanie i wyłączanie zadań

Dwa rodzaje sposobów wyłączania oferują różne stopnie bezpieczeństwa i szybj^ reakcji: natychmiastowe wyłączenie jest szybsze, ale ryzykowniejsze, bo zadaniami' zostać przerwane w połow ie wykonywania; wyłączanie łagodne działa wolniej pieczniej, bo obiekt ExecutorService nie wyłącza się aż do momentu przetworzeni wszystkich zalcolejkowanych zadań. Inne usługi przechowujące własne wątki Powiną oferować dwa podobne tryby wyłączania.

7,17. Zamykanie usługi za pom ocą pig u łki z trucizną p u b lic class IndexingService { p riv a te s t a t ic fin a l F ile POISON ® new F i le t " " ) : p riv a te f in a l IndexerThread consumer ~ new IndexerThreadt): p riv a te fin a l C rawlerlhread producer ■= new Craw lerThreadt); p riv a te f in a l BlockingQueue queue; p riv a te fin a l F ile F ilt e r f i l e F i l t e r : p riv a te fin a l F ile ro o t:

Proste programy m ogą działać poprawnie, uruchamiając i wyłączając globalny ExecutorService z poziomu metody m ain(). Bardziej wyrafinowane programy zapewji' ukrywają obiekt ExecutorService za bardziej ogólnym systemem usług z a p e w n ia ją własne metody wyłączania. Odmiana klasy LogService z listingu 7.16 deleguje wlaści^ w yłączenie do obiektu E xecutorService, bo sam a nie zarządza wykorzystywany^ w ątkami. H erm elyzacja E xecutorService rozszerza łańcuch w łasności od a p litó przez usługę do wątku przez dodanie kolejnego węzła. Każda składowa węzła zarząd cyklem życia usług lub wątków, których je st właścicielem. Listing 7 .1 6 . Us/uga dziennika zdarzeń wykorzystująca obiekt E xecutorService p u b lic class LogService { p riv a te f in a l ExecutorService exec

class CrawlerThread extends Thread { / * Listing 7.IS. * / } class IndexerThread extends Thread j / * Listing 7.19, * / } p u b lic void s t a r t t ) { p ro d u c e r ,s ta r tt); c o n s u m e r.s ta rtt):

p u b lic v o id s to p t) { producer, in t e r r u p t ( ) : }

.y

p u b lic vo id aw aitTerm lnationO throws Iriterrup te d E xce p tio n { c o n s u m e r.jo in t):

newSingleThreadExecutorO :

} p u b lic void s t a r tO

{}

p u b lic void s to p t) throws Iriterrup te d E xce p tio n { try { exec.shutdownO : exec.awaitTerminationtTIHEOUT, UNIT): } f in a lly { w r it e r . c lo s e t ):

Ï I i l

il

} p u b lic void lo g tS tr in g msg) { . try { exec.execute(new W riteTask(msg)); } catch (RejectedExecutionException ignored) {

Listing 7.18. Wątek producenta cl/a klasy IndexingService pu b lic class CrawlerThread extends Thread { p u b lic void ru n t) { try { c r a w l(r o o t) ; } catch (In te rru p te d E xce p tio n e) { / * przejdź dalej */ } fin a lly { w h ile ( tru e ) { tr y { queue,put(POISON) : break ; } catch (In te rru p te d E x c e p tio n e l) { / * próbuj ponownie * /

)

7 .2 .3 . Pigułki z trucizną

J Kolejnym sposobem przekonania usługi producent-konsument do zatrzymania się^

p riv a te void c ra w H M le ro o t) throws Iriterrup te d E xce p tic

.

p ig u łk a z tru c iz n ą — specjalny obiekt um ieszczony w kolejce, którego obecnol oznacza: „gdy tu dotrzesz, zatrzymaj się” . W kolejce F1FO pigułka zapewnia, żepń|j wyłączeniem usługa w ykona wszystkie zadania zlecone przed wstawieniem pigtij^ producent po umieszczeniu pigułki nie powinien umieszczać w kolejce żadnych nov|f, zadań. Klasy IndexingService z listingów 7,17, 7.18 i 7.19 przedstawiają wersję dynczy producent; wersję pojedynczy konsument usługi indeksacji dysku przedstaw iono wcześniej na listingu 5.8 na stronie 100. Nowe podejście używa pigjf z trucizną do zatrzymania usługi. j|j ,i! S

Hg*lng_7.19. Wątek konsum enta ella klasy IndexingService p u b lic class IndexerThread extends Thread { p u b lic void ru n t) { tr y { w h ile (tru e ) { F ile f i l e “ q u e ue .ta ke t):

164

C zęść II ♦ Struktura aplikacji wspólbie^j

pozttóał 7- ♦ Anulowanie i wyłączanie zadań

165

} f i n a l ly { exec.shutdown!); exec.aw a1tTerm ination(tim eout. u n i t ) :

i f ( f i l e = POISON) break; else 'in d e x F ile ( f ile ) ;

) re tu rn hasNewMail,get():

} } catch (In te rru p te d E xce p tio n consumed) {

}

} }



)

A

Pigułki z trucizną działają poprawnie tylko wtedy, gdy znana je st liczba producentów i konsumentów. Podejście przedstawione w klasie IndexingService można rozszerzyć na w iele producentów, jeśli każdy producent będzie umieszczał w kolejce tylko jedą pigułkę, a konsument zatrzyma się dopiero po otrzymaniu tylu pigułek, ile jest klientów. Podobnie można postąpić przy jednym producencie i wielu konsumentach — producent umieszcza wtedy w kolejce tyle pigułek, ile jest konsumentów. Niestety, trudno znaleźć pewny system zatrzymywania stosujący pigułki przy wielu producentach i konsumentach. Poza tym pigułki można stosować tylko dla kolejek o nieograniczonej pojemności.

7 .2 .4 . Przykład — jednorazowa usługa wykonawcza Jeśli metoda musi przetw orzyć zadania wsadowo i nie może wrócić, kiedy wszystkie nie zostaną w ykonane, m oże sobie ułatw ić zadanie, w ykorzystując pryw atny obiekt Executor, którego czas życia powiązany jest z metodą. W takich sytuacjach najczęściej korzysta się z metod invokeA.l 1 () lub invokeAn,y(). M etoda checkM ailO z listingu 7.20 spraw dza równolegle pocztę z wielu serwerów. Tworzy prywatny obiekt Executor i zleca osobne zadanie dla każdego serwera. Następnie czeka na w ykonanie w szystkich zadań sprawdzania poczty'1. Listing 7 .2 0 . W ykorzystanie pryw atnego obiektu Executor, którego czas życia pow iązany je s t z wywołaniem m etody ______________________________________ ______________________________ _ p u b lic boolean checkMai1(S e t< 5trin g > hosts, long tim eo u t. TimeUrńt u n it) throws In te rru pte d E xce p tio n { ExecutorService exec = Executors.newCachedThreadPool ( ) ; f in a l AtomicBoolean hasNewMail » new A to m icB oolean(false); tr y { f o r ( f in a l S trin g host : hosts) exec.execute(new Runnable() { p u b lic void runO { i f (ch e ckM a il(h o st)) ha sN e w M a il.se t(tru e );

)

7.2.5. Ograniczenia metody shutdownNow() Gdy obiekt ExecutorService zostaje wyłączony w sposób natychmiastowy przy użyciu metody shutdownNowO, próbuje anulować zadania obecnie wykonywanie i zwraca listę zadań, które zostały zlecone, ale jeszcze się nie rozpoczęły. W ten sposób mamy możliwość ich ponownego uruchom ienia później lub wpisania informacji o nich do dziennika zdarzeń5. Niestety, nie można w prosty sposób się dowiedzieć, które zadania się zaczęły, ale nie skończyły. Innymi słowy, nie można w momencie wyłączania poznać stanu zadań, jeśli zadania te same w żaden sposób nie zapewniają punktów kontrolnych. By się dowiedzieć, które zadania nic dotarły do końca, nie tylko trzeba poznać zadania, które się nie rozpo­ częły, ale także zadania trwające w momencie zgłoszenia wyłączania usługi6. Klasa TrackingExecutor z listingu 7.21 przedstaw ia technikę dowiadywania się, które zadania działały w momencie wyłączania usługi. Hermetyzując obiekt ExecutorService i polecając metodzie execute( ) (oraz submit(), czego nie pokazaliśmy) zapamiętywanie zadań anulowanych po rozpoczęciu wyłączania, klasa TrackingExecutor potrafi wskazać zadania rozpoczęte, ale niezaicończone. Po przerwaniu usługi metoda getCancelledTasksO zwraca listę anulowanych zadań. Aby to podejście działało poprawnie, zadania powinny zachowywać status przerwania w momencie powrotu (czynią tak wszystkie poprawnie napisane metody). Listing 7 .2 1 . K lasa implem entująca E xecutorService śledzi anulow ane zadania p o zleceniu wyłączenia usługi p u b lic class TrackingExecutor extends AbstractExecutorService { p riv a te f in a l ExecutorService exec; p riv a te fin a l Set tasksCancelledAtShutdown = C ollections.synchron1zedSet(new HashSet() ) : p u b lic List getCancelledTasksO { i f ( !exe c.isT e rm in a te d ()) throw new Ille g a lS ta te E x c e p tio n t. . . ) ; re tu rn new ArrayList(tasksCancel ledAtShutdown);

}

}): Obiekty Runnabl e zwrócone przez metodę shutdownNowt) m ogą nie być tym i samymi obiektami, które zostały przekazane do obiektu ExecutorService. M o g ą być oto czon ym i egzemplarzami zleconych zadań. 4 K od stosuje obiekt Atomi cBool ean zamiast zmiennej vol a ti 1e bool ean, bo z powodu korzystania ze znacznika hasNewHail z poziom u wewnętrznego obiektu Ruriner zmienna m usiałby być finalna, co u n ie m o żliw iło b y je j zmianę.

Niestety, nie ma żadnej m etody dopuszczającej zwrócenie do w yw ołującego metod jeszcze tnerozpoczętych z jednoczesnym um ożliw ieniem zakończenia aktualnie uruchom ionych. Taka metoda w ye lim inow ałaby niepewny stan pośredni.



166

'1

C zęść U ♦ Struktura aplikacji wspólbie^'

dział 7- ♦ Anulowanie i wyłączanie zadań p riv a te void submitCrawlTasktURL u) { exec. execute!new CrawlTask(u));

p u b lic void e x e c u te (fin o l Runnable runnable) { exec.execute(new RunnableO { p u b lic void runC) { try { ru n n a b le .ru n (); } f i n a lly { i f (isShutdownO && T h re a d .c u rre n tlh re a d O . is In te rru p te d O ) tasksCancelledAtShutdown.add(runnable);

} .

p riv a te class CrawlTask implements Runnable { p riv a te fin a l URL u r l; p u b lic void ru n () { fo r (URL lin k : processP age(url)) { i f (Thread.c u rre n tThreadO . is ln t e r r u p t e d t )) re tu rn ; subm itC raw lT ask(link);

} }.):

}

■'1: . „

1

) }



p u b lic URL getPageO { re tu rn u r l ; }

/ / Delegowanie wykonania innych metod interfejsu do klasy abstrakcyjnej.

}

'

i



K lasa WebCrawler z listingu 7.22 przedstaw ia zastosow anie klasy TrackingExecutor, Zadanie przeszukiwania sieci Internet najczęściej jest nieograniczone, więc algoryt® przeszukujący pow inien wyłączać się w takim stanie, by móc w przyszłości wzuowii działanie od punktu przerw ania. Klasa CrawlTask zaw iera m etodę getPageO, która identyfikuje aktualnie przetwarzaną stronę. W momencie wyłączania algorytmu zadania, które się nie rozpoczęły, i te, które się nie zakończyły, m uszą zostać zapamiętane, by móc powrócić do ich obróbki po wznowieniu pracy. L is tin g 7 .2 2 . Wykorzystanie klasy TrackingExecutor do zapisania niedokończonych zadań w celu ich ‘ późniejszego uruchomienia . __________________________________________________ p u b lic a b s tra c t class WebCrawler { p riv a te v o la t ile TrackingExecutor exec; @ GuardedBy("this") p riv a te fin a l Set urlsToCrawl - new HashSet0; p u b lic synchronized vo id s t a r t o { exec = new TrackingExecutortExecutors.newCachedThreadPoolO); fo r (URL u rl ; urlsToC raw l) s u l» itC ra w lT a s k (u rl); urlsToCrawl . c le a r O ;

p u b lic synchronized void stopO throws InterruptedE xception { try { saveUncrawled(exec.shutdownNowO); i f (exec.awaitTerminationCTIHEOUT, UNIT)) saveUncrawledCexec.getCancelledTaskst) ) : } f in a lly { exec - n u l1:

pro te cte d a b s tra c t List processPageUIRL u r l ) ; p riv a te void saveUncrawled(List uncrawled) { fo r (Runnable task : uncrawled) u rlsT o C ra w l. add(( (CrawlTask) task) .getPaget) ) :

167

v V

Klasa TrackingExecutor zaw iera niem ożliw y do uniknięcia wyścig, który w pewnych sytuacjach może błędnie wskazywać zakończone zadania jako anulowane. Pojawia się on, ponieważ pula wątków może zostać w yłączona między ostatnią instrukcją wyko­ nywaną w zadaniu a faktem w ykrycia zakończenia zdania przez pulę. N ie stanowi to problemu, jeśli zadania są idem p o ten ty ezn e (dwukrotne uruchomienie daje taki sam efekt ja k uruchom ienie raz) — taka sytuacja występuje w typowym przeszukiwaniu sieci Internet. W przeciwnym razie aplikacja pobierająca anulowane zadania powinna być świadoma ryzyka i uwzględniać go w dalszych działaniach.

3. Obsługa nietypow ego zakończenia wątku Zachowanie jednow ątkow ej aplikacji konsolow ej przy w ystąpieniu niewyłapanego wyjątku jest oczywiste — program się wyłącza, przedstawiając stos w yw ołań metod zależny od sposobu działania aplikacji. Błąd w ątku w aplikacji współbieżnej rzadko udaje się zauważyć tak łatwo. Choć stos w yw ołań zostanie wyświetlony na konsoli, nikt może na nią nie zaglądać. N aw et jeśli jeden z wątków się wyłączy, wrażenie może być takie, jakby aplikacja nadal działała poprawnie. N a szczęście są sposoby wykrywania „wycieków” wątków z aplikacji i zapobiegania im. Głównym powodem zbyt wczesnej śmierci w ątków byw a wyjątek R u n t i m e E x c e p t i o n . Ponieważ błąd ten w skazuje najczęściej błąd program isty łub inny problem , który trudno naprawić, najczęściej się go nie w yłapuje. W ten sposób w yjątek przechodzi przez cały stos w ywołań i w reszcie napotka na działanie domyślne, które pow oduje wyświetlenie całego stosu wywołań i zakończenie wątku. Konsekwencje nieprzew idzianego zakończenia w ątku byw ają różne — od niem alże pomijalnych po katastrofalne. Wszystko zależy od roli wątku w aplikacji. Utrata wątku z puli wątków potrafi wpłynąć na wydajność, ale aplikacja, która działała dobrze przy 50 wątkach, zapewne będzie działała dobrze również przy 49. Z drugiej strony utrata

' ' ••Ü'Vr 168

C zęść 11 ♦ Struktura aplikacji współbieżnej

wątku zdarzeń interfejsu graficznego z pew nością zostanie zauw ażona — aplikacja przestanie reagować na zdarzenia, więc interfejs graficzny pozostanie zamrożony. IC]asa O utom me ze strony 131. przedstawia poważną konsekwencję wycieku wątku — uslnga reprezentowana przez Timer nie daje się właściwie zatrzymać.

poztlział 7. * Anulowanie i wyłączanie zadań

7,3-1- Procedury obsługi niewyłapanych wyjątków Poprzedni podrozdział oferuje aktywne podejście do problemu wyjątków nieweryfiIcowanych. Interfejs programistyczny wątków zapewnia dodatkowo opcję UncaughtExceptionHandler, która pozwala wykryć moment kończenia wątku z powodu niewyłapanego wyjątku, Oba przedstawiane podejścia wzajemnie się uzupełniają. Ich połączenie oferuje doskonale zabezpieczenie przed wyciekaniem wątków.

W zasadzie dowolny fragment kodu potrafi zgłosić wyjątek RuntimeException. Wywołą. jąc inną metodę, wierzy się, że zadziała poprawnie lub zgłosi jeden z weryfikowanych wyjątków. Tm gorzej zna się wywoływany kod, tym warto być bardziej sceptycznym co do jego zachowania. Wątki przetw arzające zadania z puli w ątków lub w ątek przyjm ow ania i rozdzielania zdarzeń Swing cale swoje życie w ykonują nieznany im kod, używając abstrakcyjnych interfejsów typu Runnable. Wątki te powinny być bardzo sceptyczne co do kodu, który wykonują. Byłoby bardzo niedobrze, gdyby wątek obsługi zdarzeń przestawał działać, bo jakaś źle napisana metoda obsługi zdarzenia zgłosiła wyjątek Nu 11PointerLxception. Koci wątku powinien wywoływać metody w blokii try -c a tc h , by móc wyłapać nieweryfilcowany w yjątek, lub w bloku t r y - f i n a l ly, by' poinform ow ać inną część koda o swoim wyłączeniu. To jeden z niewielu przypadków, w których warto rozważyć wy­ łapywanie wyjątków RuntimeException — czyń tak, gdy wywołujesz nieznany sobie kod przez abstrakcje typu Runnable7. Listing 7.23 ilustruje sposób zabezpieczania wątku wykonawczego w puli zadań. Jeśli zadanie zgłasza wyjątek nieweryfikowany, wątek ma szansę się wyłączyć, ale wcześniej 0 swej śmierci informuje szkielet aplikacji. Szkielet ma wtedy' szansę zastąpić wąlek wykonawczy nowym wątkiem, choć nie musi tego czynić, jeśli ma zostać wyłączony lub istnieje wystarczająca liczba innych wątków zadaniowych. Klasa ThreadPooIExecutor 1 szkielet Swing w ykorzystują tę technikę, by zapewnić, że źle zachowujące się zadania zlecone do wykonania nie „położyły” całej aplikacji. Gdy piszesz wątek wykonawczy, który wykonuje zlecone zadania lub wykonuje nieznany kod (na przykład dynamiczne wczytywane moduły), wykorzystuj je d n ą z przedstawionych technik, by zabezpieczyć aplikację przed błędem występującym we wczytanym module. Listing 7.23. Typowa struktura wątku wykonawczego z puli wątków ______________________ ■ p u b lic void ru n () { Throwable thrown - n u ll; try j w h ile ( lis In te r r u p te d O ) runTask(getTaskfromWorkQueue() ) ; } catch (Throwable e) { thrown “ 6; • } f in a lly { th re a d E x ite d (th is . thrown);

169

Jeśli wątek kończy swe działanie z powodu niewylapanego wyjątku, maszyna wirtu­ alna zgłasza to zdarzenie kodowi dostarczonemu przez aplikację w postaci wywołania UncaughtExceptionHandler (patrz listing 7.24), Jeżeli nie istnieje żadna procedura obsługi, Java dom yślnie w yśw ietla stos wy w ołań metod na standardow ym w yjściu błędów System, e rr8. Listing

7.24. Interfejs UncaughtExceptionHandler p u b lic in te rfa c e UncaughtExceptionHandler ( void UncaughtExceptiontThread t , Throwable e );

To, co procedura obsługi powinna zrobić z niewyłapanym wyjątkiem, zależy od wymagań jakości usług. Typowa odpowiedź polega na zapisie informacji o błędzie i stosu wywołań metod do dziennika aplikacji, co przedstaw ia listing 7.25. Procedury m ogą również podejmować bardziej bezpośrednie akcje, na przykład ponow nie urucham iać wątek, wyłączać całą aplikację, inform ować operatora lub przeprowadzać szczegółową dia­ gnostykę. Listing 7.25. Procedura obsługi UncaughtExceptionHandler zapisująca informacje do dziennika aplikacji p u b lic class UEHLogger implements Thread.UncaughtExceptionHandler { p u b lic void uncaughf.Exception(Thread t . Throwable e) { Logger logger “ Logger.get.AnonymousLogger(); logger. !og(Level .SEVERE. "Watek wstrzymany przez w yjątek: ” + t.getMameO. e );

}

i W długo . działających aplikacjach zawsze, używaj procedur obsługi niewyłapanych : i; j : wyjątków dla wszystkich wątków, by przynajmniej zapisać informację o "zaistniałej A j j- sytuacji do dziennika a p lik a c ji.. : ■

7 Istnieją pewne spory co do bezpieczeństwa tej techniki; gdy wątek zgłasza wyjątek nieweryfikowany, m ożliw e jest, że od razu cala aplikacja będzie działać niepoprawnie. Z drugiej strony alternatywa — wyłączenie całej a p lika cji — wydaje się mało praktyczna.

Przed Javą5.0 jedynym sposobem sterowania UncaughtExceptionHandler b yło wykonanie podklasy klasy ThreadGroup. W Javie 5.0 można ustawić UncaughtExceptionHandler osobno dla każdego wątku, stosując metodę Thread.setUncaughtExceptionHandler(). Powrót do domyślnej procedury obsługi zapewnia metoda Thread. setDefau1tUncaught.ExceptionHandler(). W danym momencie zostaje wywołana tylko jedna z procedur obsługi. Java najpierw poszukuje wersji wskazanej przez użytkownika. i ; Ody jej nie odnajdzie, wykonuje tę określoną dla ThreadGroup. Dom yślne rozwiązanie dla ThreadGroup \ .; kieruje obsługę do nadrzędnego ThreadGroup, aż któraś grupa wreszcie wszystko obsłuży. Grupa wątków najwyższego poziomu w yw o łu je dom yślną systemową procedurę obsługi (jeśli istnieje, domyślnie mc jest określona) lub w yśw ietla na konsoli stos w yw ołań metod. ;

170

C zęść II ♦ Struktura aplikacji w sP ó łb ie^ |

pozdzirił 7. ♦ Anulowanie i wyłączanie zadań

Aby ustaw ić procedurę UncaughtExceptionHandler dla puli wątków, warto p r z e k u ł obiekt ThreadFactory do konstruktora ThreadPool Executor. Podobnie jak we \vszysj"i kich innych sytuacjach, tylko w łaściciele w ątków powinni m odyfikow ać procedyj^ UncaughtExceptionHandler. Standardowa pula wątków umożliwia niewyłapancmu w y jj kowi zadania przerwanie wątku z puli, ale wykorzystuje blok t r y - f i n a l ly do poinf0ri mowania o tym zdarzeniu samej puli. Niestety, w tej sytuacji zadanie może zostać p0 cielili pominięte, czego nie zalecamy. Gdy chcemy się dowiedzieć, że zadanie przestało działać z powodu zgłoszenia wyjątku, by móc odpowiednio zareagować, warto otoczyć zadanie w obiekcie Runnable lub C allable kodem wyłapującym wyjątki lub przysłonić'dowiązanie afterExecute w kodzie ThreadPool Executor.

Dowiązania wyłączenia powinny być bezpieczne wątkowo: m uszą stosować synchro­ nizację w momencie dostępu do współdzielonych zasobów i powinny uważać na blokady wzajemne, podobnie jak każdy inny działający współbieżnie kod. Nić powinny zakładać żadnego konkretnego stanu aplikacji (zakończenia działania innych usług lub wyłączenia pozostałych w ątków aplikacji) ani pow odu w yłączania m aszyny w irtualnej. Z tego względu warto ich kod pisać wyjątkowo defensywnie. D odatkowo powinny działać możliwie krótko, gdyż im dłużej działają, tym bardziej opóźniają wyłączenie maszyny wirtualnej (użytkownik może spodziewać się szybkiego wyłączenia). Dowiązania wyłączenia przydają się do wyłączania usługi i czyszczenia aplikacji, na przykład usuwania plików tymczasowych lub zwalniania zasobów, które nie są auto­ matycznie uwalniane przez system operacyjny. Listing 7.26 przedstawia, w jaki sposób klasa LogService z listingu 7.16 mogłaby zarejestrować dowiązanie wyłączenia w meto­ dzie s t a r t O , by zapew nić zam knięcie dziennika w m om encie w yłączania maszyny wirtualnej.

Co ciekawe, wyjątki zgłaszane z zadań, m ogą pozostać nicwylapane tylko wtedy, zadania zostaną zgłoszone za pom ocą metody e xe cu te d . W zadaniach zgłoszonych m etodą submit () wszystkie zgłoszone wyjątki, weryfikowane lub nie, traktuje się jako część statusu wyniku zadania. Jeśli zadanie zgłoszone dzięki submi t O zgłosi wyjątekp metoda Future, get () zgłasza je ponownie, ale otoczone wyjątkiem ExecutionExceptionjt Af

7.4. W yłączanie maszyny wirtualnej

7 .4 .1 . Dowiązania wyłączenia W wyłączaniu normalnym maszyna wirtualna najpierw uruchamia wszystkie zarejej; strowane dow iązania w yłączenia. Dowiązania wyłączenia to nieuruchomione wątłą zarejestrowane przy użyciu metody Runtime.addShutdownHooki). M aszyna wirtuaiąa nie gwarantuje żadnej konkretnej kolejności wykonania tych wątków. Jeśli w momeno| wyłączania maszyny wirtualnej działają inne wątki (demonowe lub tradycyjne), bę9| działały współbieżnie względem wątków wyłączenia. Po zakończeniu wątków do\vi| zanych do w yłączenia m aszyna w irtualna decyduje się w ykonać Analizatory Ofjft runFinalizersO nE xit je st równe tru e ) i zatrzymuje swe działanie. M aszyna nie.sllj się zatrzym ać ani przesiać przerw ania do żadnego z jeszcze działających wątkof aplikacji. Zostają one nagle przerw ane w momencie zatrzymania maszyny wirtualtł| Jeżeli wątki w yłączenia lub Analizatory nie chcą się zakończyć, cały proces norm| nego wyłączenia „zaw iesza się” i konieczne je st wyłączenie natychmiastowe. W \w łączeniu natychm iastow ym m aszyna w irtualna proszona je st jedynie o zatrzymaj! się — nie w ykonają się w tedy żadne wątki wyłączenia.

i i 1’

Listing 7.26. Rejestracja wątku wyłączenia zatrzym ującego usługę dziennika zdarzeń

|

M aszyna w irtualna m oże w yłączać się w sposób n o rm a ln y lub natychmiastowy? Sposób normalny inicjuje zakończenie ostatniego zwykłego wątku (niedemonowegojj: w yw ołanie m etody S y s te m .e x it() lub inne zdarzenie w yłączenia inicjowane prźez system (wysianie sygnału SIGINT lub naciśnięcie klawiszy Ctri+C). Choć jest to standar-; dowy i preferowany system kończenia działania maszyny wirtualnej, istnieje również wyłączanie natychmiastowe inicjowane wywołaniem metody Runtime, h a l t ( ) lub zaję­ ciem procesu maszyny wirtualnej przez system operacyjny (wysłanie sygnał«1 SIGKIIL). ' l |f ’'if!

171

p u b lic void shutdown( ) { Runtime.getRuntlme!) .addShutdownHookinew Thread!) { p u b lic void ru n !) { t r y { L o g S e rv ic e .th is .s to p O ; } catch (In te rru p te d E xce p tio n ignored! {)

}

,:

Ponieważ w szystkie wątki w yłączające d ziałają w spółbieżnie, zam knięcie dziennika mogłoby spraw ić problem y, gdyby inne w ątki w yłączające chciały jeszcze z niego skorzystać. By uniknąć błędów, warto w kodzie dowiązań wyłączenia pominąć wszystkie odniesienia do innych usług wyłączanych przez aplikację lub inne wątki wyłączające. Przykładowe rozwiązanie może polegać na zastosowaniu jednego wątku wyłączającego wszystkie usługi zam iast wielu w ątków nakierow anych na w yłączanie tylko jednej usługi. Zapewnia to sekwencyjne w yłączanie usług, a tym samym uniknięcie wyścigu łub blokady wzajemnej poszczególnych akcji. Technikę tę .warto stosować nawet wtedy, gdy nie korzysta się z dowiązania wyłączenia. Stosowanie sekwencyjnego wyłączania usług (niezależnie od m iejsca jeg o przeprow adzania) elim inuje poten cjaln e źródło błędów. W aplikacji jawnie określającej zależności między usługami technika ta zapewnia również wykonywanie akcji w yłączających w odpowiedniej kolejności.

7.4.2. W ątki demonowe Czasem chcem y utw orzyć w ątek, który w ykonuje p ew n ą p o m o cn iczą funkcję, ale jednocześnie nie chcemy, by jego działanie blokowało zakończenie działania maszyny wirtualnej. W łaśnie w tym cełu wynaleziono w ą tk i dem onow e. Wątki dzieli się na dwa rodzaje: zwykłe i demonowe. W momencie uruchamiania maszy­ ny wirtualnej wszystkie powstające wątki (system odzyskiwania pamięci i inne pomocni­ cze systemy) są wątkami demonowymi. W yjątkiem je st w ątek główny. N ow y w ątek

jflülí»C zęść II ♦ Struktura aplikacji wsjiótbiej

172

dział 7- ♦ Anulowanie i wyłączanie zadań

dziedziczy status wskazujący, czy jest demonem, po wątku go tworzącym. Z tego powójj« domyślnie wszystkie wątki tworzone przez wątek główny są typu normalnego. ¡gj W ątki normalne od demonowych różnią się tylko tym, co dzieje się w momencie ią kończenia. W momencie kończenia wątku maszyna wirtualna przegląda inne działają wątki. Jeśli pozostały tylko wątki dcm onowe, rozpoczyna w yłączenie standardom W momencie zatrzymania maszyny wirtualnej wątki demonowe są po prostu porzucajjj (nie zostają w ykonane bloki f i na"11y, stos nie je st rozw ijany) — maszyna wirtual^ zatrzymuje je w aktualnym stanie i wychodzi. Wątki demonowe należy stosować oszczędnie, bo niewiele procesów można bezpieczni porzucić bez dodatkowego czyszczenia w dowolnym momencie ich wykonywanltf Szczególnie niebezpieczne bywa wykorzystywanie wątków demonowych do przepij wadzania dowolnych operacji we-wy. Wątki te nadają się najlepiej do zadań porządź wych, czyli zadań wykonywanych w tle, na przykład okresowo usuwających z bufon przestarzałe wpisy. i;|;» ............................................... ^ -Mil a Wątki demonowe nie najlepiej nadają się do zarządzania cyklem życia Usług 1 kacjach. ' ";|§ 8

7 .4 .3 . Finalizatory

>

»»i System odzyskiwania pamięci wykonuje dobrą pracę przy odzyskiwaniu zasobów, gdy nie są ju ż nikomu potrzebne, ale pewne rodzaje zasobów, na przykład uchwyty plików i gniazd, m uszą zostać jaw nie zwolnione, by system operacyjny mógł zastosowaćjc ponownie. By pomóc w ich zwalnianiu, system odzyskiwania pamięci w sposób szeze-: gólny traktuje obiekty z inną m etodą f i na 1i ze () niż domyślna — wywołuje tę metolif, by dać obiektowi ostatnią szansę na zwolnienie zasobów. "ii' . Ponieważ finalizatory działają w wątku zarządzanym przez maszynę wirtualną, dowolny stan wykorzystywany przez finalizatory może być odczytywany przez więcej mzj wątek, więc trzeba pam iętać o synchronizacji. W przypadku Analizatorów nie mamy gw arancji, czy i kiedy zostaną w ykonane (jeśli w ogóle zostaną). Co, goisza, trudra napisać je popraw nie9. W w iększości sytuacji popraw nie użyte bloki fin a lly Wia z metodami c lo s e t) lepiej radzą sobie ze zwalnianiem niepotrzebnych zasobów niż f lizatory. W yjątek stanow ią obiekty zarządzające zasobam i wykorzystywanymi pąH metody rodzime. Z wymienionych powodów, i wielu innych, warto unikać pisania i sto» wania klas z Analizatorami (nie dotyczy to klas głównej biblioteki Javy) | EJ Item 6]| Unikaj finalizalorów.

,

.

9 W pracy fBoehm, 2005] znajduje się kilka przykładów wyzwań stojących przed programistami Analizatorów.

!

173

podsumowanie Kwestie końca cyklu życia zadań, w ątków , usług i aplikacji zw iększają złożoność projektu i implementacji kodu. Java nie dostarcza w yw łaszczającego mechanizmu anulowania zadań i przerywania wątków. Zam iast tego oferuje mechanizm przerwań wymagający współpracy obu stron (anulującego i anulowanego). Od programisty zależy, jaki protokół anulowania zastosuje i czy będzie go stosował w sposób spójny. Obiekty FutureTask i szkielet Executor ułatw iają tworzenie zadań i usług dopuszczających anulowanie.

174

Część II ♦ Struktura aplikacji wspóibjQ^ :l | r

,t -. ; ‘S .■■ "■i-';-. :-K

.

.

~Tfc

I

:

f Rozdział 8 . ■ :}r

Zastosowania pul wątków W rozdziale 6. poznaliśm y szkielet w ykonyw ania zadań, który ułatw ia zarządzanie zadaniami i cyklem życia wątków. Dodatkowo zapewnia proste i elastyczne rozdzielenie zgłaszania zadań od ich wykonywania. Rozdział 7. opisał ki lica istotniejszych szczegółów cyklu życia usług, które dotyczą wykorzystania szkieletu w rzeczywistych aplikacjach. Niniejszy rozdział zajm uje się zaaw ansow anym i opcjam i konfiguracji i dostrajania pul wątków, opisuje hazardy, na które warto uważać, używając szkieletu wykonywania zadań. Przedstawia kilka zaaw ansow anych przykładów użycia obiektów Executor.

¡1 ■m

lii1

8.1. Niejawnie sp lecio n e zadania i strategie wykonania

.

Wspomnieliśmy wcześniej, że szkielet Executor oddziela zgłaszanie zadań od ich w yko­ nywania. Niestety, założenie to często okazuje się zbyt optymistyczne przy bardziej, złożonych procesach. Choć szkielet Executor oferuje znaczącą elastyczność w określaniu) i modyfikacji strategii wykonania, nie wszystkie rodzaje zadań są zgodne ze wszystkimi strategiami. Zadaniami wymagającymi konkretnej strategii wykonania są między innymi: Zadania zależne. W iększość poprawnie zachowujących się zadań to zadania niezależne, niewykorzystujące wyników i efektów ubocznych innych wątków. Gdy wykonujemy niezależne zadania w puli w ątków, możemy dowolnie zmieniać rozm iar i konfigurację puli — wpływa to jedynie na wydajność rozwiązania. Z drugiej strony, jeśli zgłaszam y zadania uzależnione od innych zadań, niejawnie wprowadzamy ograniczenia co do strategii wykonania, która musi zostać odpowiednio dobrana w celu uniknięcia problem ów z żywotnością (patrz punkt 8.1.1).

i. k :"

U 1

Zadania w ykorzystujące odosobnienie w w ątk u . Jednowątkowe systemy wykonywania mocniej zapew niając w spółbieżności niż dowolne pule wątków. G warantują sekwencyjne w ykonanie zadań, co ułatwia uzyskanie bezpieczeństwa wątkowego. Obiekty m ożna odosobnić w wątku zadania, więc kod pisany z myślą o działaniu w tym wątku nie musi wykorzystywać

V-: i

U

'l i '

:

.. i



C zęść II ♦ Struktura aplikacji wspótjjiej

176

zdział 8. ♦ Zastosowania pul wątków

sytuacja wystąpi w puli z wieloma wątkami, gdy wszystkie wykonywane zadania czekają na inne zadania znajdujące się w kolejce. Mówimy w takiej sytuacji o zagłodzeniu w ą t­ ku blokadą w zajem ną. Występuje ono wtedy, gdy zadanie z puli inicjuje nieograni­ czone, blokujące oczekiwanie na zasób lub spełnienie warunku, gdy zasób ten jest w po­ siadaniu innego zadania z puli (aktualnie niewykonywanego lub czekającego na jeszcze inny zasób), a pula ma niewystarczający rozmiar, by uruchomić wszystkie zadania.

synchronizacji, naw et jeśli zasoby nie są zabezpieczone pod kątem wątków. !■ W prow adza to jednak niejawne powiązanie między zadaniem i strategią w ykonania — zadania wymagają, by strategia wykorzystywała pojedynczy , w ątek1. Zmiana strategii z jednowątkowej na pule wątków spowoduje utratę bezpieczeństwa wątkowego. Z a d a n ia czule na czas reak cji. Aplikacje z graficznym interfejsem użytkownika . są czułe na czas reakcji — użytkowników denerwu je długi czas reakcji m iędzy kliknięciem a jego wizualnym przedstawieniem. Zlecenie długo x działającego zadania do szkieletu Executor z jednym wątkiem lub kilku < ¡fi długich zadań do niewielkiej puli wątków może negatywnie wpłynąć na szybkość reakcji interfejsu. Ast Z a d a n ia używ ające ThreadLocal. Obiekty T hreadlocal ułatw iają posiadanie prywatnych wersji zm iennych przez każdy wątek. Z drugiej strony szkielet y w ykonania może dowolnie (czytaj wielokrotnie) wykorzystywać zawarte 'i w nim wątki. Standardowa implementacja szkieletu Executor dodaje nowe wątki, gdy są potrzebne, i usuwa je, gdy obciążenie spada. Dodatkowo .¡szastępuje nowym wątkiem stary, jeśli zadanie zgłosiło wyjątek. Obiekty T hrea dlocal warto stosować tylko wtedy, gdy czas życia wątku równy jest T czasowi wykonania zadania. Obiektów tych nie należy używać w pulach , l’1 w ątków do komunikacji między zadaniami.

Klasa ThreadDeacfiock z listingu 8.1 ilustruje zagłodzenie wątku. Klasa RenderPageTask zgłasza dwa zadania do w ykonania w obiekcie E xecutor, które pobierają nagłówek i stopkę, renderujc ciało strony, czeka na wyniki z nagłówka i stopki, by na końcu wszystkie elem enty złączyć w je d n ą całość. Stosując pulę z jednym wątkiem, kod zawsze doprowadzi do blokady wzajemnej. Co gorsza, w wielu sytuacjach nawet jeśli pula ma wiele wątków, może dojść do zagłodzenia, jeśli wszystkie czekają na zwolnienie zasobu przez zadanie czekające w kolejce do wykonania. I Isting 8.1. Zadanie z blokadą wzajemną w przypadku zastosowania puli jednowątkowej. Nie rób tak p u b lic class ThreadDeadlock { ExecutorService exec - Executors.newSingleThreadExecutori); p u b lic class RenderPageTask implements C a lla b le < S tring > { p u b lic S trin g caVIO throws Exception { Future header, fo o te r; header - exec, submit (new LoadFileTaskCheader.htm l " ) ) ; fo o te r = exec, submit (new LoadFileTaskC’fo o te r.h tm V 1) ) ; S trin g page = renderBodyt); / / Zawiera blokadą wzajemną —zadanie czeka na wykonanie podzadań. re tu rn header.g e tO + page + f o o te r .g e t t ) ;

. ■’•b Pule zadań działają najlepiej, gdy zadania są homogeniczne i niezależne. Zmieszanie zadań działających długo i krótko stw arza ryzyko „zablokow ania” puli, jeśli niejesl duża. Zlecanie zadań zależnych od innych zadań stwarza ryzyko blokady wzajemnej, jeśli pula ma ograniczony rozmiar. Na szczęście żądania przesyłane siecią w typowycji aplikacjach serwerowych — serwerach WWW, serwerach poczty i plików — najczęściej spełniają te żądania. , j"T i Niektóre zadania m ają charakterystykę wymagającą lub zakładającą kopkrdH ' /strategię wykonania; Zadania zależne od innych .zadan wymuszają stosowanie 'diaffl puli, by nigdy nie stać. w kolejce; zadania, wykorzystujące odosobnienie, ,w 5w o t | .wymagają’ sekwencyjnego wykonywania. Informuj w dokumentacji o.tyclt. wyrnoga|l • by inne osoby zajmujące się konserwacją kodu przypadkowo nie zmniejszyly;bj| ; pieczeństwa Wątkowego. ’ , ’

) )

(, ;

'i

8 .1 .1 . Zagłodzenie w ątku Jeśli zadanie zależne od innych zadań zostaje wykonane przy użyciu puli wątków, mo| wystąpić blokada wzajemna. W puli jednowątkowej zadanie zlecające wykonanie injgt zadania w tej samej puli i oczekujące na jego wyniki zawsze spowoduje blokadę 1# jemną. Drugie zadanie czeka w kolejce na zakończenie pierw szego, ale pierwsze« może się zakończyć, dopóki nie otrzyma wyników wykonania drugiego zadania. Tai|j!

177

l : Zgłaszając do wykonania w szkielecie lxecu! o r zadania zależne, pamiętaj o możliwo- ,• ! - -ści zagłodzenia, przez blokadę wzajemną. ¡Dobrzeudokumentuj wymagania co do liczby -; ] , wątków i innych ograniczeń dotyczących konfiguracji puli. I

Poza jaw nym ograniczeniem m inim alnego rozm iaru kolejki może istnieć większe, niejawne ograniczenie wynikające z innych zasobów. Jeżeli aplikacja używa puli połączeń JDBC z 10 połączeniami, a każde zadanie potrzebuje połączenia z bazą danych, w efekcie aplikacja działa tak, jakby stosowała tylko 10 wątków, bo pozostałe zawsze czekają na uzyskanie obiektu połączenia z bazą danych.

8.1.2. Długo działające zadania Pule wątków m ogą mieć problemy szybkością reakcji, jeśli zadania blokują się przez dłuższy czas, nawet gdy nie występuje prawdopodobieństwo blokady wzajemnej. Pula wątków zatkana długo działającymi zadaniami zwiększa czas odpowiedzi nawet bardzo krótkich zadań. Jeśli pula je st zbyt m ała w porów naniu z liczbą zlecanych zadań o dłuższym czasie działania, w pew nym m om encie w szystkie wątki zostaną zajęte przez długie zadania i ucierpi na tym czas odpowiedzi.

1 W zasadzie wymóg ten nie jest aż tak silny — wystarczy, że zadania nie działają współbieżnie i zapewniają na tyle dobrą synchronizację, by zmiany w jednym z nich były zawsze widoczne w drugim. Dokładnie taką gwarancję daje klasa zwraca przez newSingleThreadExecutor().

r£ -

,zdział 8. ♦ Zastosow ania pul wątków

Jedną z technik niwelacji efektu długo działających zadań jest stosowanie oczekiwani' ' 1 z określonym czasem reakcji zamiast oczekiwania bezwarunkowego. Większość metoj ' blokujących w klasach standardowej biblioteki zawiera dwie wersje: nieograniCZ0|! " i i ograniczoną czasowo. Przykładam i takich metod są: Thread. jo in ( ), BłockingQueile^ ' p u t(), CountOownLatch. awai t () i S e l e c t o r .s e ł e c t O . Jeżeli minie czas oczekiwanjąf i: w blokadzie, można oznaczyć zadanie jako niewykonane łub zaharmonograniowaóg'' ? ponow nie do wykonania, ale w późniejszym terminie. Gwarantuje to, że każdy w ^ ' j będzie czynił postępy, czy to zakończone sukcesem czy porażką, zwalniając wątki ^ zadaniom, które m ogą wykonać się sprawniej. Jeśli pula często zawiera dużo zabloy, wanych wątków, równie dobrze może to oznaczać, że je st za mala. t

8.2. Określanie rozmiaru puli wątków | ■ii Idealny rozm iar puli wątków zależy od rodzaju zgłaszanych do niej zadań i charakty rystyki docelowego systemu komputerowego. Nie warto na stałe umieszczać w kodzig' rozmiaru puli. Warto przenieść to ustawienie do pliku konfiguracyjnego lub roztnia puli dostosowywać dynam icznie do liczby dostępnych procesorów, używając metody Runtime. ava i 1ab łeP ro cesso rs(). • " a: Rozm iar puli nie musi być idealnie dopasowany do potrzeb. Najczęściej wystarczy unikać ekstrem ów , czyli rozm iarów „zbyt dużych” lub „zbyt m ałych”. Gdy pula jest za duża, wątki konkurują o czas procesora i pamięć, przez co często zużyw ająjej więcej niż potrzeba. Gdy je st za mała, procesory m ogą się nudzić, czekając na inno zadania; choć zleceń je st dużo. ty By ja k najlepiej dobrać rozm iar puli, dobrze poznaj docelowe środowisko, dostępne zasoby i naturę zlecanych zadań. Ile procesorów ma system produkcyjny? Ile ma pamif ci? Czy zadania są przede w szystkim typu obliczeniow ego, typu w e-w y czy raczej kombinacją obu typów. Czy wymagają cennych zasobów, na przykład połączeń JDBCj Jeżeli istnieje kilka kategorii zadań z bardzo różnym i zachow aniam i, zastanów ^ nad wprowadzeniem kilku niezależnych pul wątków, by odpowiednio zrównowa^ obciążenie. ,'f| ii-i?

P rz y

następującej definicji:

M pt l F H

i ¡.j

fi

■.1■; ■ : i

Ucpu — docelowe wykorzystanie mocy procesorów, 0 < Ucpu < 1,

— stosunek czasu oczekiwania do czasu działania, optymalny rozmiar puli wątków zapewniający zadane wykorzystanie mocy procesorów! ma wartość:

Do określenia liczby procesorów służy następujący kod: • ■:

i' Oczywiście cykle procesora to n ie jed y n y zasób, na który w pływ ma pula wątków. 1 Innymi ograniczeniami są: dostępna pam ięć operacyjna, uchwyty plików i połączenia bazodanowe. W yliczanie rozmiaru puli uwzględniającego te ograniczenia okazuje się znacznie prostsze — wystarczy wyliczyć, ile pamięci i zasobów wymaga jedno zadanie, , a następnie podzielić ilość dostępnej pamięci przez uzyskaną liczbę. Wynikiem będzie i i górne ograniczenie rozmiaru puli. ■H Gdy zadanie wymaga zasobu z osobnej puli zasobów , na przykład połączenia bazo- ity danowego, rozmiary puli w ątków i puli zasobów są ze so b ą pow iązane. Jeśli każde ¡ ii zadanie wymaga osobnego połączenia bazodanowego, efektywny rozmiar puli wątków , ty zależy od rozmiaru puli połączeń. Podobnie, gdy jedynym konsumentem połączeń je st ; pula wątków, efektywny rozmiar puli połączeń określa pula wątków.

4

fe ;' b:

ą

‘iF i;’-'

Zadania czysto obliczeniowe w systemie z/V procesorami najczęściej osiągają oplymalęt jk wykorzystanie zasobów przy puli wątków zawierającej JV+1 wątków. Nawet zadani) wykonujące intensywne obliczenia od czasu do czasu otrzymują błąd strony lub wstny- i imiją swe działanie z innych powodów, więc „nadmiarowy” wątek ułatwia wykorzystani) i;,;, dodatkowych cykli. Gdy zadania wykonują operacje we-wy lub inne operacje blokują«, ki warto zw iększyć rozm iar puli, bo nie wszystkie zadania będą działały jednocześni jk By poprawnie przybliżyć zalecany rozm iar puli, warto wyliczyć stosunek czasu oczew ;■ wania zadań do czasu ich działania. Uzyskana wartość nie musi być dokładna — dojej u, w yliczenia nie trzeba stosow ać testów w ydajnościow ych. A lternatyw ne podejścif j , polega na dostrojeniu rozmiaru puli na kilkukrotnym uruchomieniu aplikacji przy użyęj )■■■„ kilku różnych rozmiarów pul, stosując program do generowania zadań i sprawdzają ty obciążenie procesora. J j !)» i,'.’.

U

N c,„i — liczba procesorów,

in t N_CPUS = Runtime.getRuntiineO .a v a ila b le P ro c e s s o rs O ;

"■ F

r.

i; w

8.3. Konfiguracja klasy ThreadPoolExecutor ' tytyity Klasa ThreadPoolExecutor zapewnia podstawową implementację dla obiektów wykony­ wania zadań zwracanych przez metody fabryczne newCachedThreadPooł () , newFixed- ty TbreadPoolO i newScheduledThreadExecutorO. K lasa ThreadPoolExecutor stanow i ■1 " elastyczną i bardzo przydatną im plem entację podatną na wiele modyfikacji. i ; ; i. Gdy domyślne strategie w ykonania nie sp ełn iają oczekiw ań, m ożna utw orzyć obiekt 1; FhreadPooł Executor i dostosow ać go do w łasnych potrzeb. Przeglądając kod klasy , ! Executor, można poznać ustawienia domyślnych strategii w ykonywania i ewentualnie v. ty, zastosować je jako punkt początkowy. K lasa ThreadPoolExecutor zaw iera kilka kon- jf c struktorów. Główny z nich przedstawia listing 8.2.. i i Jtyr

.jllif 180

C zęść II ♦ Struktura aplikacji wspćłbiejn6j'

Listing 8 .2 . Ogólny k o m tn ik lo r klasy ThreaciPoolExecutor_________________________ p u b lic T hreadPoolExecutor(int corePoolSize, in t maximumPoolSize, long keepAliveTime. TlmeUnlt u n it, BIockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionl-landler handler) { . . .

Rozctóał 8. ♦ Zastosowania pul wątków

181

8,3.2. Zarządzanie zadaniami w kolejce Ograniczona rozmiarowo pula wątków ogranicza liczbę współbieżnie wykonywanych zadań. Przypadkiem szczególnym jest pula jednowąlkowa, która gwarantuje sekwencyjne wykonywanie zadań, oferując bezpieczeństwo wątkowe przez odosobnienie w wątku.

}

8 .3 .1 . Tworzenie i kończenie wątków Podstaw ow y rozm iar puli, m aksym alny rozm iar puli i czas utrzym ywania określają zasady tworzenia i kończenia wątków. Rozmiar podstawowy to docelowy rozmiar puli;implementacja stara się zachować ten rozmiar nawet wtedy, gdy nie są zlecane żadne zadania do w ykonania2. Więcej w ątków wykona tylko wtedy, gdy kolejka zostanie zapełniona3. M aksymalny rozmiar puli określa górną granicę liczby jednocześnie akty. wowanych wątków. Watek, który nie wykonywał zadań przez określony czas, staje się kandydatem do wyłączenia, jeśli aktualny rozmiar puli przekracza rozmiar podstaw y Dostrajając podstawowy rozmiar puli i czas życia dodatkowych wątków, zmusza się pulę do oddania niektórych zasobów niewykorzystywanych wątków, by mogły zostać użyte przez inny kod. Oczywiście, jak wszystko, ma to również swoje wady — zwiększa, opóź­ nienie, jeśli w przyszłości potrzebne będą dodatkowe wątki, bo pula musi je utworzyć. M etoda fabryczna newFixedThreadPool O ustawia podstaw ow y i m aksymalny roz­ m iar puli na podaną w artość, tworząc efekt nieograniczonego czasu życia. Metoda newCachedlhreadPool () ustawia maksymalny rozmiar puli na In te g e r. MAX_VALUE i pod­ staw ow y rozm iar na O przy jednom inutow ym czasie bezczynności. Powstaje więc pula o niemalże nieograniczonej pojemności, która zmniejsza swój rozmiar przy mniej­ szej liczbie zadań. Inne rozwiązania uzyskuje się, ręcznie modyfikując opcje konstruk­ tora klasy ThreadPoołExecutor.

2 Po in ic jn liza cji ThreaciPoolExecutor w ą tki nie są tworzone od razu, ale dopiero przy zlecaniu zadań, . chyba że wcześniej w yw o ła się metodę presta rtA U C o re T h rea d st). . 3 Programiści czasem chcą ustawić rozm iar podstawowy na O, by wątki wykonawcze zostały ewentualnie wyłączone i nie przeszkadzały w zam knięciu maszyny w irtualnej. Niestety, może to spowodować dziwne zachowanie w pulach w ą tkó w , które nic używ ają obiektów SynchronousQueue ja k o ko lejki (czyni tak obiekt zwracany przez metodę newCachedThreadPool O ). Jeśli pula osiągnęła swój rozmiar podstawowy, klasa ThreadPooł Executor tw o rzy nowe w ątki tylko wtedy, gdy kolejka jest pełna. '■Oznacza.to, że zgłaszane w ątki będą wldadane do ko le jki, ale nieuruchamiane, dopóki kolejka się nic zapełni. Z pewnością nie je st to pożądany efekt. W Javie 6 metoda ałlawCoreThreadTimeOutO u m o ż liw ia określenie czasu działania wątku. W łącz tę funkcję p rzy niezerowym rozm iarze podstaw ow ym , je ś li chcesz m ieć ograniczoną pulę z ograniczoną długością k o le jki i jednocześnie chcesz, by w ą tki b y ły usuwane, gdy nie ma żadnych zadań do wykonania.

W punkcie 6.1.2 pokazaliśmy, w jaki sposób nieograniczone w żaden sposób tworzenie wątków potrafi doprowadzić do niestabilności. Problem rozwiązaliśmy, tworząc pulę wątków o stałym rozmiarze i zastępując nią tworzenie nowego wątku w każdym żądaniu. Niestety, to tylko połowiczne rozwiązanie, bo aplikacja nadal naraża się na wyczerpanie zasobów przy dużym obciążeniu. Jeśli przybywanie nowych zadań przekracza czas ich obsługi, rośnie długość kolejki. W puli zadania oczekują na wykonanie jako obiektu Runnable zarządzane przez szkielet E xecutor — nie są jeszcze osobnymi wątkami. Oznacza to mniejsze niż w przypadku czekającego wątku zajęcie zasobów pamięciowych, ale nadal ryzykuje się wyczerpaniem zasobów, gdy żądania będą nadchodziły znacznie szybciej, niż serwer potrafi je obsłużyć. Żądania często nadchodzą falami, naw et jeśli średnia częstość pojawiania się zadań pozostaje stabilna. Kolejka pomaga wygładzić falę, ale gdy zadania przybywają zbyt szybko, trzeba ograniczyć szybkość nadchodzenia, by nie zabrakło pamięci operacyjnej'1. Nawet przed zapełnieniem pam ięci szybkość reakcji znacząco maleje wraz z wypeł­ nianiem się kolejki. Klasa ThreadPooł Executor przyjm uje obiekt BloclcingQueue, którego zadaniem jest przechowywanie zadań oczekujących na wykonanie. Istnieją trzy podstawowe metody kolejkowania zadań: nieograniczone, ograniczone i synchroniczne przekazanie. Wybór kolejki zależy od innych parametrów konfiguracyjnych, w szczególności od rozmiaru puli. Domyślne implementacje z metod newFixedThreadPool () i newSingleThreadExecutorO stosują nieograniczona kolejkę LinkedBlockingQueue. Zadania trafiają do kolejki, jeśli wszystkie wątki wykonawcze są zajęte. Kolejka bez ograniczenia rozmiaru urośnie do niebotycznych rozmiarów, jeśli nowe zadania nadchodzą szybciej niż są wykonywane. Bardziej stabilna strategia zarządzania stosuje kolejkę o ograniczonym rozmiarze, na przykład ArrayBlockingQueue, ew entualnie ograniczone rozmiarowo wersje Linked­ BłockingOueue i P rio rityB lockingQueue. Kolejki o ograniczonej pojemności ułatwiają uniknięcie drenażu zasobów, ale staw iają trudne pytanie: co zrobić z zadaniami, gdy kolejka je st pełna? Różne stra te g ie nasycenia przedstawiamy w punkcie 8.3.3. Dla kolejek o ograniczonym rozmiarze trzeba odpowiednio dobrać icii rozmiar względem rozmiaru puli. Duża kolejka z niewielką pulą redukuje zajętość pamięci, użycie procesora i przełączanie kontekstu, ale kosztem zwiększenia czasu reakcji.

Istnieje lu ścisła analogia do sterowania przepływem w sieciach komunikacyjnych — można buiorować ograniczoną liczbę danych, ale gdy nadchodzą one zbyt szybko, trzeba w jakiś sposób poinformować wysyłającego, by zwolnił, lub po prostu odrzucać nowe dane, dopóki bufor nie będzie miał wolnego miejsca.

C zęść II ♦ Struktura aplikacji wsp niezależna od pozostałych, a koszt każdego z zadań Jest na tyle duży, że opłaca się ' i tworzyć nowo zadania, '

' ¡0

8.5. Zrów noleglenie algorytmów \ rekurencyjnych i Przykłady dotyczące renderingu strony z podrozdziału 6.3 przedstaw iająą kolelt usprawnienia wyszukiwania poprawiające zrównoleglenie operacji. Pierwsze podejioi! było w pełni sekwencyjne. Drugie stosowało co prawda dwa wątki, ale całe pobieranie obrazów odbywało się sekwencyjnie. Ostatnia w ersja pobierała poszczególne obr® strony w osobnych w ątkach, by osiągnąć lepsze zrównoleglenie. Pętle zawierają« niebanalne obliczenia lub w ykonujące blokujące operacje we-wy są często dobrymi kandydatami do zrów noleglenia, jeśli tylko kolejne iteracje są ód siebie niezależnej , d .leżeli mamy pętlę, której iteracje są niezależne, i nie chcemy czekać na zakończeń# icli wszystkich przed wykonaniem dalszych akcji, skorzystajmy zc szkieletu Executor, by przekształcić pętlę sekw en cy jn ą w zrów nolegloną, co przedstaw iają metojj p ro ce ssS e q u en tially O i p ro ce ssIn P ara llel O z listingu 8.10. |,t Listing 8.10. Przekształcenie pętli sekwencyjnej »' zrównolegloną ________________ _ voi d processSequerit. i al l y ( L i s t el einen t s ) fo r (Element e : elements) p rocess(e);

Zrównoleglenie pętli udaje się zastosować także w niektórych algorytmach rekurencyj­ nych. W samych algorytmach niejednokrotnie zdarzają się pętle podobne do tych z listin­ gu 8.10, które łatwo zrównoleglić. Najprostsza sytuacja jest wtedy, iteracje nie potrzebują wyników w cześniejszych iteracji rekurencyjnych. M etoda se q u en tialR ec u rslv ei) z listingu 8.11 w ykonuje przejście przez drzew o typu „w głąb”, przeprowadzając w każdym węźie obliczenia i umieszczając wyniki w kolekcji. Przekształcona wersja, p arallel R ecursive!) także stosuje przejście przez drzewo typu „w głąb”, ale zamiast wyliczać wyniki przy każdym odwiedzanym węźle, zleca je jako zadania do wykonania przez pulę wątków.

listing 8 .11. Przekształcenie sekwencyjnej rekurencjl końcówkowej net rekurcncję zrównolegloną public void sequentialRecursive(List nodes. C ollection r e s u lts ) j fo r (Node n : nodes) { r e s u lts . a d d in .compute() ) ; sequentialR ecursive(n.getC h11dren(). r e s u lt s ) ;

public < D void pa r a i l el Recurs iv e ( fin a l Executor exec, List1 p riv a te fin a l Set

seen = new HashSet

(); Zdefiniujmy „puzzle” jako kombinację pozycji początkowej, pozycji docelowej i rzbiora p u b lic SequentialPuzzleSolver(Puzz1e puzzle) reguł definiujących poprawne ruchy. Zbiór reguł składa się z dwóch części: obliczeni) th is .p u z z le - puzzle; listy ruchów poprawnych dla danej pozycji i wyników powstających po ich zastosowania Interfejs Puzzle z listingu 8.13 przedstawia abstrakcję tego rodzaju puzzli. Parameią typu P i Mreprezentują klasy położenia i mchu. Wykorzystując interfejs, możemy napisał p u b lic List s o lv e d ( prosty, sekwencyjny algorytm przeszukujący przestrzeń puzzli w celu odnalezienia P pos - p u z z le .in itia lP o s itio n O ; rozwiązania lub jego niewykrycia po sprawdzeniu wszystkich dopuszczalnych kom­ re tu rn search (new PuzzleNode(pos, n u ll, n u ll) ) ; } binacji. L is tin g 8 .1 3 . xl bstrakcja puzzli z przesuwnymi blokami_______________________________________ 4 p u b lic in te rfa c e Puzzle { P ir r it ia lP o s it io n O ; boolean isGoaKP p o s itio n ); Set legalMoves(P p o s itio n ); P move(P p o s itio n , M move);

-

.t

} Klasa Node z listingu 8.14 reprezentuje położenie osiągnięte po określonej serii ruchów.! Przechow uje referencję do ruchu, który utworzył położenie i poprzedniego obiekl*! Node. Przechodząc precz wcześniejsze obiekty Node, uzyskujemy całą konstrukcję ruchów prow adzącą do uzyskania aktualnego wyniku.

p riv a te List search(PuzzleNode node) { i f (¡se e n .co nta in s(n o de .p o s)) ( seen.add(node.pos); i f (pu z z le .is G o a l(n o d e .p o s )) re tu rn n o d e .a sM o ve listO ; fo r (M move : p u z z le .le g a l Moves(node.pos)) { P pos = puzzle.move(node.pos, move); PuzzleNode c h ild = new PuzzleNode(pos. move, node); List r e s u lt = s e a rc h (c h ild ); i f ( r e s u lt ! " n u l l ) re tu rn r e s u lt;

re tu rn n u ll;

s t a t ic cla ss Node { !* Listing 8.14. */ ) 7 Przykłady tego rodzaju puzzli dostępne są pod adresem http://www.puzzleworlcl.org/SlidmgBlockPuzzlesl!

.— _ — _ ---------------- .



C zęść II ♦ S tru k tu ra a p lik a c ji w s p a fc u J f “

———

■ —

U idria* 8. ♦ Zastosowania pul wątków

■ Modyfikacja algorytmu w taki sposób, by wykorzystywał wspólbieżność, dałaby S2a I na równoległe obliczanie następnego ruchu i określanie warunku zakończenia, bo ¡w obliczania jed n eg o ruchu w zasadzie nie zależy od ruchu poprzedniego. PiszeiA „w zasadzie”, bo ruchy współdzielą pewien wspólny stan, na przykład przeanalizowana I w cześniej pozycji.' G dy m am y do dyspozycji kilka procesorów , zredukuje tę C2aj potrzebny na uzyskanie rozwiązania. K lasa ConcurrentPuzzleSolver z listingu 8.16 używa wewnętrznej klasy SolverTaciktóra rozszerza klasę Node i implementuje interfejs Runnable. Większość pracy wykonuj m etoda ru n (): w ylicza zbiór następnych możliwych ruchów, pomija ju ż sprawdza® położenia, spraw dza odnalezienie rozw iązania (przez aktualne lub inne zadanie) przekazuje jeszcze nieprzeszukane położenia do obiektu Executor. L is tin g 8 .1 6 . Współbieżny algorytm rozwiązania puzzli _________________________________ p u b lic class ConcurrentPuzzleSolver { p riv a te f in a l Puzzle puzzle; p riv a te f in a l ExecutorService exec; p riv a te f in a l ConcurrentMap seen; p ro te cte d f in a l ValueLatch