[英]What is the difference between atomic / volatile / synchronized?
原子/易失/同步如何在內部工作?
以下代碼塊有什么區別?
代碼 1
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
代碼 2
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
代碼 3
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
volatile
是否按以下方式工作? 是
volatile int i = 0;
void incIBy5() {
i += 5;
}
相當於
Integer i = 5;
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
我認為兩個線程不能同時進入一個同步塊......我對嗎? 如果這是真的,那么atomic.incrementAndGet()
在沒有synchronized
情況下如何工作? 它是線程安全的嗎?
內部讀取和寫入 volatile 變量/原子變量有什么區別? 我在一些文章中讀到該線程具有變量的本地副本 - 那是什么?
您特別詢問他們的內部工作方式,所以您在這里:
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
它基本上從內存中讀取值,增加它並放回內存。 這在單線程中有效,但現在,在多核、多 CPU、多級緩存的時代,它無法正常工作。 首先它引入了競爭條件(多個線程可以同時讀取值),但也引入了可見性問題。 該值可能僅存儲在“本地”CPU 內存(某些緩存)中,對其他 CPU/內核(以及線程)不可見。 這就是為什么許多人在線程中引用變量的本地副本。 這是非常不安全的。 考慮這個流行但損壞的線程停止代碼:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
將volatile
添加到已stopped
變量中,它可以正常工作 - 如果任何其他線程通過pleaseStop()
方法修改了已stopped
變量,您可以保證在工作線程的while(!stopped)
循環中立即看到該更改。 順便說一句,這也不是中斷線程的好方法,請參閱:如何停止永遠運行而沒有任何使用的線程和停止特定的 java 線程。
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
AtomicInteger
類使用 CAS(比較和交換)低級 CPU 操作(不需要同步!)它們允許您僅在當前值等於其他值(並成功返回)時修改特定變量。 因此,當您執行getAndIncrement()
它實際上在循環中運行(簡化的實際實現):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
所以基本上:閱讀; 嘗試存儲遞增的值; 如果不成功(該值不再等於current
),請閱讀並重試。 compareAndSet()
在本機代碼(程序集)中實現。
volatile
不同步private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
此代碼不正確。 它修復了可見性問題( volatile
確保其他線程可以看到對counter
所做的更改)但仍然存在競爭條件。 這已被多次解釋:前/后增量不是原子的。
volatile
的唯一副作用是“刷新”緩存,以便所有其他方看到最新版本的數據。 這在大多數情況下過於嚴格; 這就是為什么volatile
不是默認值。
volatile
不同步 (2)volatile int i = 0;
void incIBy5() {
i += 5;
}
與上述相同的問題,但更糟糕的是因為i
不是private
。 競爭條件仍然存在。 為什么會出現問題? 例如,如果兩個線程同時運行此代碼,則輸出可能是+ 5
或+ 10
。 但是,您一定會看到更改。
synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
令人驚訝的是,此代碼也不正確。 事實上,這是完全錯誤的。 首先,您正在同步i
,它即將更改(此外, i
是一個原始類型,所以我猜您正在同步通過自動裝箱創建的臨時Integer
......)完全有缺陷。 你也可以這樣寫:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
沒有兩個線程可以使用相同的鎖進入同一個synchronized
塊。 在這種情況下(以及在您的代碼中類似),每次執行時鎖定對象都會更改,因此synchronized
無效。
即使您使用 final 變量(或this
)進行同步,代碼仍然不正確。 兩個線程可以首先同步讀取i
到temp
(在temp
本地具有相同的值),然后第一個為i
分配一個新值(例如,從 1 到 6),另一個線程執行相同的操作(從 1 到 6) .
同步必須跨越從讀取到賦值。 您的第一次同步沒有效果(讀取int
是原子的),第二次也是如此。 在我看來,這些是正確的形式:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}
將變量聲明為volatile意味着修改其值會立即影響變量的實際內存存儲。 編譯器無法優化掉對變量的任何引用。 這保證當一個線程修改變量時,所有其他線程立即看到新值。 (對於非易失性變量,這不能保證。)
聲明原子變量可保證對變量進行的操作以原子方式發生,即操作的所有子步驟都在執行它們的線程內完成,並且不會被其他線程中斷。 例如,增量和測試操作需要將變量遞增,然后與另一個值進行比較; 原子操作保證這兩個步驟都將完成,就好像它們是單個不可分割/不可中斷的操作一樣。
同步對變量的所有訪問一次只允許一個線程訪問該變量,並強制所有其他線程等待該訪問線程釋放對變量的訪問。
同步訪問類似於原子訪問,但原子操作通常在較低的編程級別實現。 此外,完全有可能只同步對變量的某些訪問,而允許不同步其他訪問(例如,同步對變量的所有寫入,但不同步對變量的讀取)。
原子性、同步性和易變性是獨立的屬性,但通常結合使用以強制執行適當的線程協作以訪問變量。
附錄(2016 年 4 月)
對變量的同步訪問通常使用監視器或信號量來實現。 這些是低級互斥(互斥)機制,允許線程以獨占方式獲取對變量或代碼塊的控制,如果所有其他線程也嘗試獲取相同的互斥,則強制它們等待。 一旦擁有線程釋放互斥鎖,另一個線程可以依次獲取互斥鎖。
附錄(2016 年 7 月)
同步發生在對象上。 這意味着調用類的同步方法將鎖定調用的this
對象。 靜態同步方法將鎖定Class
對象本身。
同樣,進入同步塊需要鎖定方法的this
對象。
這意味着如果多個線程鎖定不同的對象,則同步方法(或塊)可以同時在多個線程中執行,但對於任何給定的單個對象,一次只能有一個線程執行同步方法(或塊)。
易揮發的:
volatile
是一個關鍵字。 volatile
強制所有線程從主內存而不是緩存中獲取變量的最新值。 訪問 volatile 變量不需要鎖定。 所有線程可以同時訪問 volatile 變量值。
使用volatile
變量可以降低內存一致性錯誤的風險,因為對 volatile 變量的任何寫入都會與對該同一變量的后續讀取建立先發生關系。
這意味着對volatile
變量的更改始終對其他線程可見。 更重要的是,這也意味着當線程讀取volatile
變量時,它不僅會看到volatile
的最新更改,還會看到導致更改的代碼的副作用。
何時使用:一個線程修改數據,其他線程必須讀取數據的最新值。 其他線程將采取一些行動,但他們不會更新數據。
原子XXX:
AtomicXXX
類支持對單個變量進行無鎖線程安全編程。 這些AtomicXXX
類(如AtomicInteger
)解決了內存不一致錯誤/修改已在多個線程中訪問的 volatile 變量的副作用。
何時使用:多個線程可以讀取和修改數據。
同步:
synchronized
是用於保護方法或代碼塊的關鍵字。 通過使方法同步有兩個效果:
首先,對同一對象的synchronized
方法的兩次調用不可能交錯。 當一個線程正在為一個對象執行synchronized
方法時,所有其他調用synchronized
一個對象的synchronized
方法的線程都會阻塞(掛起執行),直到第一個線程完成對對象的處理。
其次,當一個synchronized
方法退出時,它會自動建立一個發生在同一個對象的synchronized
方法的任何后續調用之前的關系。 這保證了對象狀態的更改對所有線程都是可見的。
何時使用:多個線程可以讀取和修改數據。 您的業務邏輯不僅更新數據,還執行原子操作
即使實現不同, AtomicXXX
也相當於volatile + synchronized
。 AmtomicXXX
擴展了volatile
變量 + compareAndSet
方法,但不使用同步。
相關 SE 問題:
值得閱讀的好文章:(以上內容摘自這些文檔頁面)
https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
我知道兩個線程不能同時進入 Synchronize 塊
兩個線程不能兩次進入同一個對象上的同步塊。 這意味着兩個線程可以在不同的對象上進入同一個塊。 這種混亂會導致這樣的代碼。
private Integer i = 0;
synchronized(i) {
i++;
}
這不會像預期的那樣表現,因為它每次都可能鎖定不同的對象。
如果這是真的,那么 atomic.incrementAndGet() 在沒有 Synchronize 的情況下如何工作? 線程安全嗎??
是的。 它不使用鎖定來實現線程安全。
如果您想更詳細地了解它們的工作原理,可以閱讀它們的代碼。
內部讀取和寫入易失性變量/原子變量有什么區別??
原子類使用可變字段。 領域沒有區別。 不同之處在於執行的操作。 Atomic 類使用 CompareAndSwap 或 CAS 操作。
我在一些文章中讀到線程具有變量的本地副本,那是什么??
我只能假設它指的是每個 CPU 都有自己的內存緩存視圖,這可能與其他每個 CPU 不同。 為了確保您的 CPU 具有一致的數據視圖,您需要使用線程安全技術。
這只是在共享內存時至少有一個線程更新它時的問題。
同步 Vs 原子 Vs 易失性:
如果我遺漏了什么,請糾正我。
易失性 + 同步是一個簡單的解決方案,用於完全原子的操作(語句),其中包括對 CPU 的多條指令。
比如說:volatile int i = 2; i++,就是 i = i + 1; 這使得 i 為執行此語句后內存中的值 3。 這包括從內存中讀取 i(即 2)的現有值,加載到 CPU 累加器寄存器中,並通過將現有值加一(累加器中的 2 + 1 = 3)來進行計算,然后寫回增加的值回到記憶中。 盡管 i 的值是可變的,但這些操作還不夠原子。 我是 volatile 只保證從內存中的 SINGLE 讀/寫是原子的,而不是 MULTIPLE。 因此,我們還需要在 i++ 周圍進行同步,以使其成為萬無一失的原子語句。 請記住,一個語句包含多個語句。
希望解釋足夠清楚。
Java volatile修飾符是保證線程間通信的特殊機制的一個例子。 當一個線程寫入 volatile 變量,而另一個線程看到該寫入時,第一個線程會告訴第二個線程有關內存中的所有內容,直到它執行對該 volatile 變量的寫入。
原子操作在單個任務單元中執行,不受其他操作的干擾。 原子操作在多線程環境中是必要的,以避免數據不一致。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.