Konstrukcja zapewniająca bezpieczeństwo gwintów

Sześć miesięcy temu rozpocząłem cykl artykułów o projektowaniu klas i obiektów. W tym miesiącu w kolumnie Techniki projektowania będę kontynuować tę serię, przyglądając się zasadom projektowania, które dotyczą bezpieczeństwa gwintów. W tym artykule dowiesz się, czym jest bezpieczeństwo nici, dlaczego go potrzebujesz, kiedy go potrzebujesz i jak się do niego zabrać.

Co to jest bezpieczeństwo nici?

Bezpieczeństwo wątków oznacza po prostu, że pola obiektu lub klasy zawsze zachowują prawidłowy stan, jak obserwują inne obiekty i klasy, nawet gdy są używane jednocześnie przez wiele wątków.

Jedną z pierwszych wskazówek, które zaproponowałem w tej kolumnie (zobacz „Projektowanie inicjalizacji obiektów”), jest to, że należy projektować klasy w taki sposób, aby obiekty zachowywały prawidłowy stan od początku ich życia do końca. Jeśli zastosujesz się do tej rady i utworzysz obiekty, których wszystkie zmienne instancji są prywatne i których metody dokonują tylko poprawnych przejść stanu na tych zmiennych instancji, to jesteś w dobrej formie w środowisku jednowątkowym. Ale możesz mieć kłopoty, gdy pojawi się więcej wątków.

Wiele wątków może oznaczać problemy dla obiektu, ponieważ często, gdy metoda jest w trakcie wykonywania, stan obiektu może być tymczasowo nieprawidłowy. Gdy tylko jeden wątek wywołuje metody obiektu, tylko jedna metoda naraz będzie wykonywana, a każda metoda będzie mogła zakończyć się przed wywołaniem innej metody. Tak więc w środowisku jednowątkowym każda metoda będzie miała szansę upewnić się, że każdy tymczasowo nieprawidłowy stan zostanie zmieniony na prawidłowy, zanim metoda powróci.

Jednak po wprowadzeniu wielu wątków maszyna JVM może przerwać wykonywanie jednej metody przez wątek, podczas gdy zmienne instancji obiektu nadal znajdują się w tymczasowo niepoprawnym stanie. JVM może wtedy dać szansę na wykonanie innemu wątkowi, a ten wątek mógłby wywołać metodę na tym samym obiekcie. Cała Twoja ciężka praca nad tym, aby zmienne instancji były prywatne, a metody przeprowadzały tylko prawidłowe przekształcenia stanu, nie wystarczą, aby zapobiec obserwowaniu obiektu w nieprawidłowym stanie przez drugi wątek.

Taki obiekt nie byłby bezpieczny dla wątków, ponieważ w środowisku wielowątkowym obiekt mógłby zostać uszkodzony lub mieć nieprawidłowy stan. Obiekt bezpieczny dla wątków to taki, który zawsze utrzymuje prawidłowy stan, co obserwują inne klasy i obiekty, nawet w środowisku wielowątkowym.

Po co martwić się o bezpieczeństwo nici?

Są dwa ważne powody, dla których warto pomyśleć o bezpieczeństwie wątków podczas projektowania klas i obiektów w Javie:

  1. Obsługa wielu wątków jest wbudowana w język Java i interfejs API

  2. Wszystkie wątki wewnątrz wirtualnej maszyny języka Java (JVM) współużytkują tę samą stertę i obszar metod

Ponieważ wielowątkowość jest wbudowana w Javę, jest możliwe, że każda zaprojektowana przez Ciebie klasa może być ostatecznie używana jednocześnie przez wiele wątków. Nie musisz (i nie powinieneś) czynić każdej klasy, którą projektujesz, bezpieczną dla wątków, ponieważ bezpieczeństwo wątków nie jest darmowe. Jednak za każdym razem, gdy projektujesz klasę Java , powinieneś pomyśleć o bezpieczeństwie wątków. W dalszej części artykułu znajdziesz omówienie kosztów bezpieczeństwa wątków i wskazówki dotyczące tego, kiedy uczynić klasy bezpiecznymi wątkami.

Biorąc pod uwagę architekturę maszyny JVM, musisz zajmować się zmiennymi instancji i klas tylko wtedy, gdy martwisz się o bezpieczeństwo wątków. Ponieważ wszystkie wątki współużytkują tę samą stertę, a sterta jest miejscem, w którym przechowywane są wszystkie zmienne instancji, wiele wątków może próbować współbieżnie używać zmiennych instancji tego samego obiektu. Podobnie, ponieważ wszystkie wątki współużytkują ten sam obszar metody, a obszar metody jest miejscem, w którym przechowywane są wszystkie zmienne klas, wiele wątków może próbować jednocześnie używać tych samych zmiennych klas. Kiedy zdecydujesz się uczynić klasę bezpieczną dla wątków, Twoim celem jest zagwarantowanie integralności - w środowisku wielowątkowym - zmiennych instancji i klas zadeklarowanych w tej klasie.

Nie musisz martwić się wielowątkowym dostępem do zmiennych lokalnych, parametrów metod i wartości zwracanych, ponieważ te zmienne znajdują się na stosie Java. W JVM każdemu wątkowi przypisywany jest własny stos Java. Żaden wątek nie może wyświetlać ani używać żadnych zmiennych lokalnych, zwracanych wartości ani parametrów należących do innego wątku.

Biorąc pod uwagę strukturę maszyny JVM, zmienne lokalne, parametry metod i wartości zwracane są z natury „bezpieczne wątkowo”. Ale zmienne instancji i zmienne klas będą bezpieczne dla wątków tylko wtedy, gdy odpowiednio zaprojektujesz klasę.

RGBColor # 1: Gotowy na pojedynczy wątek

Jako przykład klasy, która nie jest bezpieczna wątkowo, rozważ RGBColorklasę pokazaną poniżej. Instancje tej klasy reprezentują kolor przechowywane w trzech prywatnych zmiennych instancji: r, g, i b. Biorąc pod uwagę klasę pokazaną poniżej, RGBColorobiekt zaczynałby swoje życie w stanie prawidłowym i doświadczałby tylko przejść między stanami prawidłowymi, od początku swojego życia do końca - ale tylko w środowisku jednowątkowym.

// W pliku thread / ex1 / RGBColor.java // Instancje tej klasy NIE są bezpieczne dla wątków. klasa publiczna RGBColor {private int r; prywatne int g; prywatne int b; public RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); this.r = r; this.g = g; this.b = b; } / ** * zwraca kolor w tablicy trzech int: R, G i B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; return retVal; } public void invert () {r = 255 - r; g = 255 - g; b = 255 - b; } private static void checkRGBVals (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {throw new IllegalArgumentException (); }}}

Ponieważ trzy zmienne instancji, ints r, gi b, są prywatne, jedynym sposobem, w jaki inne klasy i obiekty mogą uzyskać dostęp lub wpływać na wartości tych zmiennych, jest RGBColorużycie konstruktora i metod. Projekt konstruktora i metody gwarantuje, że:

  1. RGBColorKonstruktor zawsze nada zmiennym właściwe wartości początkowe

  2. Metody setColor()i invert()zawsze wykonują prawidłowe transformacje stanu na tych zmiennych

  3. Metoda getColor()zawsze zwróci prawidłowy widok tych zmiennych

Zwróć uwagę, że jeśli do konstruktora lub setColor()metody zostaną przesłane złe dane , zostaną one nagle zakończone rozszerzeniem InvalidArgumentException. checkRGBVals()Metoda, która rzuca ten wyjątek, w efekcie określa, co to oznacza dla RGBColorobiektu ważność: wartości wszystkich trzech zmiennych r, gi bmusi być pomiędzy 0 a 255 włącznie. Ponadto, aby był poprawny, kolor reprezentowany przez te zmienne musi być najnowszym kolorem przekazanym do konstruktora lub setColor()metody albo wyprodukowanym przez invert()metodę.

Jeśli w środowisku jednowątkowym wywołasz setColor()i przekażesz na niebiesko, RGBColorobiekt po setColor()zwróceniu będzie niebieski . Jeśli następnie wywołasz getColor()ten sam obiekt, otrzymasz niebieski. W społeczeństwie jednowątkowym przypadki tej RGBColorklasy są grzeczne.

Wrzucenie równoczesnego klucza do prac

Niestety, ten szczęśliwy obraz dobrze wychowanego RGBColorobiektu może przerazić się, gdy inne wątki pojawią się na nim. W środowisku wielowątkowym wystąpienia RGBColorklasy zdefiniowanej powyżej są podatne na dwa rodzaje złego zachowania: konflikty zapisu / zapisu oraz konflikty odczytu / zapisu.

Konflikty zapisu / zapisu

Wyobraź sobie, że masz dwa wątki, jeden o nazwie „czerwony”, a drugi o nazwie „niebieski”. Obie nici próbują ustawić kolor tego samego RGBColorobiektu: Czerwona nić próbuje zmienić kolor na czerwony; niebieska nić próbuje ustawić kolor na niebieski.

Oba te wątki próbują jednocześnie zapisywać do zmiennych instancji tego samego obiektu. Jeśli program do planowania wątków przeplata te dwa wątki we właściwy sposób, dwa wątki nieumyślnie będą ze sobą kolidować, powodując konflikt zapisu / zapisu. W trakcie tego procesu dwa wątki uszkodzą stan obiektu.

RozsynchronizowanyRGBColor aplet

Poniższy aplet o nazwie Niezsynchronizowany RGBColor przedstawia jedną sekwencję zdarzeń, które mogą spowodować uszkodzenie RGBColorobiektu. Czerwona nić niewinnie próbuje ustawić kolor na czerwony, podczas gdy niebieska nić niewinnie próbuje ustawić kolor na niebieski. Ostatecznie RGBColorobiekt nie reprezentuje ani czerwonego, ani niebieskiego, ale niepokojący kolor, magenta.

Z jakiegoś powodu Twoja przeglądarka nie pozwoli Ci zobaczyć w ten sposób fajnego apletu Java.

Aby przejść przez sekwencję zdarzeń, które prowadzą do uszkodzenia RGBColorobiektu, naciśnij przycisk Krok apletu. Naciśnij Wstecz, aby wykonać kopię zapasową kroku, lub Resetuj, aby cofnąć się do początku. W miarę postępów wiersz tekstu na dole apletu wyjaśni, co dzieje się na każdym kroku.

For those of you who can't run the applet, here's a table that shows the sequence of events demonstrated by the applet:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes setColor(255, 0, 0) 0 0 0  
red checkRGBVals(255, 0, 0); 0 0 0  
red this.r = 255; 0 0 0  
red this.g = 0; 255 0 0  
red this.b = 0; 255 0 0  
red red thread returns 255 0 0  
blue later, blue thread continues 255 0 0  
blue this.b = 255 255 0 0  
blue blue thread returns 255 0 255  
none object represents magenta 255 0 255  

As you can see from this applet and table, the RGBColor is corrupted because the thread scheduler interrupts the blue thread while the object is still in a temporarily invalid state. When the red thread comes in and paints the object red, the blue thread is only partially finished painting the object blue. When the blue thread returns to finish the job, it inadvertently corrupts the object.

Read/write conflicts

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

Synchronizacja oznacza, że ​​tylko jeden wątek na raz będzie w stanie wykonać fragment kodu (sekcja krytyczna). Więc nawet jeśli są to pola, do których chcesz koordynować dostęp między wieloma wątkami, mechanizm Java, który to robi, faktycznie koordynuje dostęp do kodu. Oznacza to, że tylko jeśli ustawisz dane jako prywatne, będziesz mógł kontrolować dostęp do tych danych, kontrolując dostęp do kodu, który manipuluje danymi.