簡體   English   中英

原子/易失性/同步之間有什么區別?

[英]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 )進行同步,代碼仍然不正確。 兩個線程可以首先同步讀取itemp (在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是用於保護方法或代碼塊的關鍵字。 通過使方法同步有兩個效果:

  1. 首先,對同一對象的synchronized方法的兩次調用不可能交錯。 當一個線程正在為一個對象執行synchronized方法時,所有其他調用synchronized一個對象的synchronized方法的線程都會阻塞(掛起執行),直到第一個線程完成對對象的處理。

  2. 其次,當一個synchronized方法退出時,它會自動建立一個發生在同一個對象的synchronized方法的任何后續調用之前的關系。 這保證了對象狀態的更改對所有線程都是可見的。

何時使用:多個線程可以讀取和修改數據。 您的業​​務邏輯不僅更新數據,還執行原子操作

即使實現不同, AtomicXXX也相當於volatile + synchronized AmtomicXXX擴展了volatile變量 + compareAndSet方法,但不使用同步。

相關 SE 問題:

Java中volatile和synchronized的區別

揮發性布爾值與原子布爾值

值得閱讀的好文章:(以上內容摘自這些文檔頁面)

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 易失性:

  • Volatile 和 Atomic 僅適用於變量,而同步適用於方法。
  • 揮發性確保可見性而不是對象的原子性/一致性,而其他兩者都確保可見性和原子性。
  • 可變變量存儲在 RAM 中,訪問速度更快,但我們無法實現線程安全或同步而沒有同步關鍵字。
  • 同步實現為同步塊或同步方法,但兩者都不是。 我們可以在 synchronized 關鍵字的幫助下對多行代碼進行線程安全,而兩者都無法實現相同的目標。
  • Synchronized 可以鎖定同一個類對象或不同的類對象,而兩者都不能。

如果我遺漏了什么,請糾正我。

易失性 + 同步是一個簡單的解決方案,用於完全原子的操作(語句),其中包括對 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM