java 并发的原子性_Java并发教程–原子性和竞争条件
java 并發的原子性
原子性是多線程程序中的關鍵概念之一。 我們說一組動作是原子的,如果它們都以不可分割的方式作為單個操作執行。 認為多線程程序中的一組操作將被串行執行是理所當然的,可能會導致錯誤的結果。 原因是由于線程干擾,這意味著如果兩個線程對同一數據執行多個步驟,則它們可能會重疊。
下面的交織示例顯示了兩個線程執行多個操作(循環中的打印)以及它們如何重疊:
執行時,將產生不可預測的結果。 舉個例子:
Thread 2 - Number: 0 Thread 2 - Number: 1 Thread 2 - Number: 2 Thread 1 - Number: 0 Thread 1 - Number: 1 Thread 1 - Number: 2 Thread 1 - Number: 3 Thread 1 - Number: 4 Thread 2 - Number: 3 Thread 2 - Number: 4在這種情況下,不會出錯,因為它們只是打印數字。 但是,當您需要在不同步的情況下共享對象的狀態(其數據)時,這會導致競爭條件的存在。
比賽條件
如果由于線程交織而有可能產生不正確的結果,則您的代碼將處于競爭狀態。 本節描述了兩種競爭條件:
為了消除競爭條件并增強線程安全性,我們必須通過使用同步使這些操作成為原子操作。 以下各節中的示例將顯示這些競爭條件的影響。
先檢查后實際比賽條件
當您有一個共享字段并期望順序執行以下步驟時,會出現此競爭條件:
這里的問題是,當第一個線程在上次檢查后要執行操作時,另一個線程可能已經插入并更改了字段的值。 現在,第一個線程將基于不再有效的值執行操作。 通過示例更容易看出這一點。
UnsafeCheckThenAct應該一次更改字段號 。 在調用changeNumber方法之后,應導致執行else條件:
public class UnsafeCheckThenAct {private int number;public void changeNumber() {if (number == 0) {System.out.println(Thread.currentThread().getName() + " | Changed");number = -1;}else {System.out.println(Thread.currentThread().getName() + " | Not changed");}}public static void main(String[] args) {final UnsafeCheckThenAct checkAct = new UnsafeCheckThenAct();for (int i = 0; i < 50; i++) {new Thread(new Runnable() {@Overridepublic void run() {checkAct.changeNumber();}}, "T" + i).start();}} }但是由于此代碼未同步,因此(無法保證)可能會導致對該字段進行多次修改:
T13 | Changed T17 | Changed T35 | Not changed T10 | Changed T48 | Not changed T14 | Changed T60 | Not changed T6 | Changed T5 | Changed T63 | Not changed T18 | Not changed這種競爭條件的另一個示例是延遲初始化 。
解決此問題的一種簡單方法是使用同步。
SafeCheckThenAct是線程安全的,因為它已通過同步對共享字段的所有訪問來消除競爭條件。
public class SafeCheckThenAct {private int number;public synchronized void changeNumber() {if (number == 0) {System.out.println(Thread.currentThread().getName() + " | Changed");number = -1;}else {System.out.println(Thread.currentThread().getName() + " | Not changed");}}public static void main(String[] args) {final SafeCheckThenAct checkAct = new SafeCheckThenAct();for (int i = 0; i < 50; i++) {new Thread(new Runnable() {@Overridepublic void run() {checkAct.changeNumber();}}, "T" + i).start();}} }現在,執行此代碼將始終產生相同的預期結果。 只有一個線程會更改該字段:
T0 | Changed T54 | Not changed T53 | Not changed T62 | Not changed T52 | Not changed T51 | Not changed ...在某些情況下,還有其他機制會比同步整個方法更好,但是我不會在本文中討論它們。
讀-修改-寫競爭條件
在這里,當執行以下一組操作時,會出現另一種競爭條件:
在這種情況下,還有另一種危險的可能性,那就是丟失了對該字段的某些更新。 一種可能的結果是:
Field’s value is 1. Thread 1 gets the value from the field (1). Thread 1 modifies the value (5). Thread 2 reads the value from the field (1). Thread 2 modifies the value (7). Thread 1 stores the value to the field (5). Thread 2 stores the value to the field (7).如您所見,值5的更新丟失了。
讓我們看一個代碼示例。 UnsafeReadModifyWrite共享一個數字字段,每次都會遞增:
public class UnsafeReadModifyWrite {private int number;public void incrementNumber() {number++;}public int getNumber() {return this.number;}public static void main(String[] args) throws InterruptedException {final UnsafeReadModifyWrite rmw = new UnsafeReadModifyWrite();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(6000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());} }您能發現引起比賽狀況的復合動作嗎?
我敢肯定你做到了,但是為了完整起見,我還是會解釋一下。 問題出在增量( number ++ )中。 這似乎是一個動作,但實際上,它是三個動作的序列(get-increment-write)。
執行此代碼時,我們可能會看到丟失了一些更新:
2014-08-08 09:59:18,859|UnsafeReadModifyWrite|Final number (should be 10_000): 9996由于無法保證線程如何交織,因此取決于您的計算機,將很難重現此更新丟失。 如果無法重現上面的示例,請嘗試UnsafeReadModifyWriteWithLatch ,它使用CountDownLatch來同步線程的啟動,并重復測試一百次。 您可能應該在所有結果中看到一些無效值:
Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 997 Final number (should be 1_000): 999 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000 Final number (should be 1_000): 1000這個例子可以通過使所有三個動作原子化來解決。
SafeReadModifyWriteSynchronized在對共享字段的所有訪問中使用同步:
public class SafeReadModifyWriteSynchronized {private int number;public synchronized void incrementNumber() {number++;}public synchronized int getNumber() {return this.number;}public static void main(String[] args) throws InterruptedException {final SafeReadModifyWriteSynchronized rmw = new SafeReadModifyWriteSynchronized();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(4000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());} }讓我們看另一個刪除此競爭條件的示例。 在這種特定情況下,由于字段號與其他變量無關,因此我們可以使用原子變量。
SafeReadModifyWriteAtomic使用原子變量來存儲字段的值:
public class SafeReadModifyWriteAtomic {private final AtomicInteger number = new AtomicInteger();public void incrementNumber() {number.getAndIncrement();}public int getNumber() {return this.number.get();}public static void main(String[] args) throws InterruptedException {final SafeReadModifyWriteAtomic rmw = new SafeReadModifyWriteAtomic();for (int i = 0; i < 1_000; i++) {new Thread(new Runnable() {@Overridepublic void run() {rmw.incrementNumber();}}, "T" + i).start();}Thread.sleep(4000);System.out.println("Final number (should be 1_000): " + rmw.getNumber());} }以下帖子將進一步說明鎖定或原子變量之類的機制。
結論
這篇文章解釋了在非同步多線程程序中執行復合操作時隱含的一些風險。 為了強制執行原子性并防止線程交織,必須使用某種類型的同步。
- 您可以在github上查看源代碼。
翻譯自: https://www.javacodegeeks.com/2014/08/java-concurrency-tutorial-atomicity-and-race-conditions.html
java 并發的原子性
總結
以上是生活随笔為你收集整理的java 并发的原子性_Java并发教程–原子性和竞争条件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓手机旗舰机排行榜(安卓手机旗舰)
- 下一篇: 泄爆钉如何安装(泄爆钉备案)