[英]When to use volatile and synchronized
我知道有很多問題,但我仍然不太明白。 我知道這兩個關鍵字的作用,但我無法確定在某些情況下使用哪個。 以下是一些我正在嘗試確定最適合使用的示例。
例1:
import java.net.ServerSocket;
public class Something extends Thread {
private ServerSocket serverSocket;
public void run() {
while (true) {
if (serverSocket.isClosed()) {
...
} else { //Should this block use synchronized (serverSocket)?
//Do stuff with serverSocket
}
}
}
public ServerSocket getServerSocket() {
return serverSocket;
}
}
public class SomethingElse {
Something something = new Something();
public void doSomething() {
something.getServerSocket().close();
}
}
例2:
public class Server {
private int port;//Should it be volatile or the threads accessing it use synchronized (server)?
//getPort() and setPort(int) are accessed from multiple threads
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
任何幫助是極大的贊賞。
一個簡單的答案如下:
synchronized
可以始終用於為您提供線程安全/正確的解決方案,
volatile
可能會更快,但只能用於在有限的情況下為您提供線程安全/正確。
如有疑問,請使用synchronized
。 正確性比表現更重要。
表征可以安全使用volatile
的情況涉及確定每個更新操作是否可以作為單個volatile變量的單個原子更新來執行。 如果操作涉及訪問其他(非最終)狀態或更新多個共享變量,則只能使用volatile進行安全操作。 你還需要記住:
long
或double
可能不是原子的,並且 ++
和+=
這樣的Java運算符不是原子的。 術語:如果操作完全發生,或者根本不發生,則操作是“原子的”。 術語“不可分割的”是同義詞。
當我們談論原子性時,我們通常從外部觀察者的角度來看原子性; 例如,與執行操作的線程不同的線程。 例如,從另一個線程的角度來看, ++
不是原子的,因為該線程可能能夠觀察到在操作過程中遞增的字段的狀態。 實際上,如果場是long
或double
,甚至可能觀察到既不是初始狀態也不是最終狀態的狀態!
synchronized
關鍵字
synchronized
表示變量將在多個線程之間共享。 它用於通過“鎖定”對變量的訪問來確保一致性,這樣一個線程就無法修改它而另一個線程使用它。
經典示例:更新指示當前時間的全局變量
incrementSeconds()
函數必須能夠不間斷地完成,因為它在運行時會在全局變量time
的值中創建臨時不一致。 如果沒有同步,另一個函數可能會看到“12:60:00”的time
,或者在標有>>>
的注釋中,當時間真的是“12:00:00”時,它會看到“11:00:00”因為小時數還沒有增加。
void incrementSeconds() {
if (++time.seconds > 59) { // time might be 1:00:60
time.seconds = 0; // time is invalid here: minutes are wrong
if (++time.minutes > 59) { // time might be 1:60:00
time.minutes = 0; // >>> time is invalid here: hours are wrong
if (++time.hours > 23) { // time might be 24:00:00
time.hours = 0;
}
}
}
volatile
關鍵字
volatile
只是告訴編譯器不要對變量的常量做出假設,因為它可能會在編譯器通常不期望它時發生變化。 例如,數字恆溫器中的軟件可能具有指示溫度的變量,其值由硬件直接更新。 它可能會在正常變量不會發生變化的地方發生變化。
如果未將degreesCelsius
聲明為volatile
,則編譯器可以自由優化:
void controlHeater() {
while ((degreesCelsius * 9.0/5.0 + 32) < COMFY_TEMP_IN_FAHRENHEIT) {
setHeater(ON);
sleep(10);
}
}
進入這個:
void controlHeater() {
float tempInFahrenheit = degreesCelsius * 9.0/5.0 + 32;
while (tempInFahrenheit < COMFY_TEMP_IN_FAHRENHEIT) {
setHeater(ON);
sleep(10);
}
}
通過將degreesCelsius
聲明為volatile
,您告訴編譯器每次運行循環時都必須檢查其值。
摘要
簡而言之, synchronized
允許您控制對變量的訪問,因此您可以保證更新是原子的(即,一組更改將作為一個單元應用;沒有其他線程可以在半更新時訪問該變量)。 您可以使用它,以確保數據的一致性。 另一方面, volatile
是承認變量的內容超出了你的控制范圍,所以代碼必須假設它可以隨時改變。
您的帖子中沒有足夠的信息來確定發生了什么,這就是為什么您獲得的所有建議都是關於volatile
和synchronized
一般信息。
所以,這是我的一般建議:
在編寫 - 編譯 - 運行程序的循環期間,有兩個優化點:
所有這些意味着指令很可能不會按照您編寫它們的順序執行,無論是否必須維護此順序以確保多線程環境中的程序正確性。 您將在文獻中經常發現的一個典型例子是:
class ThreadTask implements Runnable {
private boolean stop = false;
private boolean work;
public void run() {
while(!stop) {
work = !work; // simulate some work
}
}
public void stopWork() {
stop = true; // signal thread to stop
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
Thread t = new Thread(task);
t.start();
Thread.sleep(1000);
task.stopWork();
t.join();
}
}
根據編譯器優化和CPU架構,上述代碼可能永遠不會在多處理器系統上終止。 這是因為stop
的值將被緩存在CPU運行線程t
的寄存器中,這樣線程將永遠不會再次從主內存讀取值,即使主線程已經同時更新了它。
為了對抗這種情況,引入了記憶圍欄 。 這些是特殊說明,不允許在圍欄之后使用圍欄后的說明重新排序圍欄之前的常規指令。 一個這樣的機制是volatile
關鍵字。 標記為volatile
變量未由編譯器/ CPU優化,並且將始終直接寫入/讀取主存儲器。 簡而言之, volatile
確保了跨CPU核心的變量值的可見性 。
可見性很重要,但不應與原子性相混淆。 即使變量聲明為volatile
兩個遞增相同共享變量的線程也可能產生不一致的結果。 這是因為在某些系統上,增量實際上被轉換為可在任何點上中斷的匯編指令序列。 對於這種情況,需要使用關鍵部分,例如synchronized
關鍵字。 這意味着只有一個線程可以訪問synchronized
塊中包含的代碼。 關鍵部分的其他常見用途是對共享集合的原子更新,當通常迭代集合而另一個線程正在添加/刪除項目時將導致拋出異常。
最后兩點有趣:
synchronized
和一些其他結構如Thread.join
將隱式引入內存柵欄。 因此,在synchronized
塊內增加變量不要求變量也是volatile
,假設它是唯一被讀/寫的地方。 AtomicInteger
, AtomicLong
等中的那些。這些方法比synchronized
快得多,因為它們不會在鎖定時觸發上下文切換已經被另一個線程占用了。 它們在使用時也會引入內存柵欄。 volatile
解決了CPU內核的“可見性”問題。 因此,本地寄存器的值被刷新並與RAM同步。 但是,如果我們需要一致的值和原子操作,我們需要一種機制來保護關鍵數據。 這可以通過synchronized
塊或顯式鎖來實現。
注意:在第一個示例中,字段serverSocket
實際上從未在您顯示的代碼中初始化。
關於同步,它取決於ServerSocket
類是否是線程安全的。 (我假設它是,但我從未使用它。)如果是,你不需要圍繞它同步。
在第二個示例中, int
變量可以原子更新,因此volatile
可能就足夠了。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.