Java 中的易失變量與原子變量
一、概述
在本教程中,我們將了解volatile
關鍵字和原子類之間的區別以及它們解決的問題。首先,有必要了解 Java 如何處理線程之間的通信以及可能出現哪些意外問題。
線程安全是一個至關重要的話題,它可以深入了解多線程應用程序的內部工作。我們還將討論競爭條件,但我們不會深入探討這個主題。
2.並發問題
我們通過一個簡單的例子來看看原子類和volatile
關鍵字的區別。想像一下,我們正在嘗試創建一個將在多線程環境中工作的計數器。
理論上,任何應用程序線程都可以增加這個計數器的值。讓我們開始用一種天真的方法來實現它,並檢查會出現什麼問題:
public class UnsafeCounter {
private int counter;
int getValue() {
return counter;
}
void increment() {
counter++;
}
}
這是一個完美工作的計數器,但不幸的是,它僅適用於單線程應用程序。這種方法在多線程環境中會遇到可見性和同步問題。在大型應用程序中,跟踪錯誤甚至損壞用戶數據可能會造成困難。
3.能見度問題
可見性問題是在多線程應用程序中工作時的問題之一。可見性問題與 Java 內存模型緊密相關。
在多線程應用程序中,每個線程都有其共享資源的緩存版本,並根據事件或計劃更新主內存中的值或從主內存中更新值。
線程緩存和主內存值可能不同。因此,即使一個線程更新主內存中的值,這些更改也不會立即對其他線程可見。這稱為可見性問題。
volatile
關鍵字通過繞過本地線程中的緩存來幫助我們解決這個問題。因此, volatile
變量對所有線程都是可見的,並且所有這些線程將看到相同的值。因此,當一個線程更新值時,所有線程都會看到新值。我們可以將其視為低級觀察者模式,並可以重寫之前的實現:
public class UnsafeVolatileCounter {
private volatile int counter;
public int getValue() {
return counter;
}
public void increment() {
counter++;
}
}
上面的示例改進了計數器並解決了可見性問題。但是,我們仍然有一個同步問題,我們的計數器在多線程環境中無法正常工作。
4. 同步問題
儘管volatile
關鍵字可以幫助我們提高可見性,但我們還有另一個問題。在我們的增量示例中,我們使用變量count.
首先,我們讀取這個變量,然後給它分配一個新值。這意味著增量操作不是原子的。
我們在這裡面臨的是一個競爭條件。每個線程應首先讀取該值,將其遞增,然後將其寫回。當多個線程開始使用該值並在另一個線程寫入它之前讀取它時,就會出現問題。
這樣,一個線程可能會覆蓋另一個線程寫入的結果。 synchronized
關鍵字可以解決這個問題。但是,這種方法可能會造成瓶頸,並且它不是解決這個問題的最優雅的解決方案。
5. 原子值
原子值提供了一種更好、更直觀的方式來處理這個問題。它們的界面允許我們在沒有同步問題的情況下與值進行交互和更新。
在內部,原子類確保,在這種情況下,增量將是原子操作。因此,我們可以使用它來創建線程安全的實現:
public class SafeAtomicCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public int getValue() {
return counter.get();
}
public void increment() {
counter.incrementAndGet();
}
}
我們的最終實現是線程安全的,可以在多線程應用程序中使用。它與我們的第一個示例沒有太大區別,只有通過使用原子類,我們才能解決多線程代碼中的可見性和同步問題。
六,結論
在本文中,我們了解到在多線程環境中工作時應該非常謹慎。錯誤和問題很難追踪,並且在調試時可能不會出現。這就是為什麼必須了解 Java 如何處理這些情況的原因。
volatile
關鍵字可以幫助解決可見性問題並通過本質上的原子操作解決問題。設置標誌是volatile
關鍵字可能有用的示例之一。
原子變量有助於處理非原子操作,如遞增-遞減或任何需要在分配新值之前讀取值的操作。原子值是解決我們代碼中的同步問題的一種簡單方便的方法。
與往常一樣,示例的源代碼可在 GitHub 上獲得。