簡體   English   中英

何時使用volatile和synchronized

[英]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進行安全操作。 你還需要記住:

  • 對非易失性longdouble可能不是原子的,並且
  • +++=這樣的Java運算符不是原子的。

術語:如果操作完全發生,或者根本不發生,則操作是“原子的”。 術語“不可分割的”是同義詞。

當我們談論原子性時,我們通常從外部觀察者的角度來看原子性; 例如,與執行操作的線程不同的線程。 例如,從另一個線程的角度來看, ++不是原子的,因為該線程可能能夠觀察到在操作過程中遞增的字段的狀態。 實際上,如果場是longdouble ,甚至可能觀察到既不是初始狀態也不是最終狀態的狀態!

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是承認變量的內容超出了你的控制范圍,所以代碼必須假設它可以隨時改變。

您的帖子中沒有足夠的信息來確定發生了什么,這就是為什么您獲得的所有建議都是關於volatilesynchronized一般信息。

所以,這是我的一般建議:

在編寫 - 編譯 - 運行程序的循環期間,有兩個優化點:

  • 在編譯時,編譯器可能會嘗試重新排序指令或優化數據緩存。
  • 在運行時,當CPU有自己的優化時,比如緩存和亂序執行。

所有這些意味着指令很可能不會按照您編寫它們的順序執行,無論是否必須維護此順序以確保多線程環境中的程序正確性。 您將在文獻中經常發現的一個典型例子是:

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 ,假設它是唯一被讀/寫的地方。
  • 對於諸如值交換,遞增,遞減之類的簡單更新,您可以使用非阻塞原子方法,如AtomicIntegerAtomicLong等中的那些。這些方法比synchronized快得多,因為它們不會在鎖定時觸發上下文切換已經被另一個線程占用了。 它們在使用時也會引入內存柵欄。

volatile解決了CPU內核的“可見性”問題。 因此,本地寄存器的值被刷新並與RAM同步。 但是,如果我們需要一致的值和原子操作,我們需要一種機制來保護關鍵數據。 這可以通過synchronized塊或顯式鎖來實現。

注意:在第一個示例中,字段serverSocket實際上從未在您顯示的代碼中初始化。

關於同步,它取決於ServerSocket類是否是線程安全的。 (我假設它是,但我從未使用它。)如果是,你不需要圍繞它同步。

在第二個示例中, int變量可以原子更新,因此volatile可能就足夠了。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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