同步的重要性有兩個方面:

  • 實現原子性:防止某個執行緒正在使用物件狀態而另一個執行緒同時在修改改狀態
  • 記憶體可見性:確保一個執行緒修改了物件狀態後,其他執行緒能夠看到發生的狀態變化

失效資料

  • 缺乏同步的程式可能會產生的一種錯誤情況就是——失效資料
  • 失效資料舉例
    //在沒有同步的情況下共享資料
    public class NoVisibility {
        private static boolean ready;
        private static int number;
    
        private static class ReaderThread extends Thread {
            public void run() {
                while (!ready)
                   Thread.yield();
                System.out.println(number);
            }
        }
    
        public static void main(String[] args) {
            new ReaderThread().start();
            number = 42;
            ready = true;
       }
    }

引發的問題有

  1. NoVisibility 可能會持續迴圈下去,因為ReaderThread可能永遠看不到ready寫入的值
  2. NoVisibility 可能會輸出0,因為ReaderThread可能看到了ready寫入的值,但是沒有看到寫入的number的值(這種現象叫重排序)

非原子的64位操作

  • 非volatile型別的64位數值變數(double和long)在多執行緒程式中使用也是不安全的
  • 如果對該變數的讀操作和寫操作在不同的執行緒中執行,那麼可能會讀取某個值的高32位和另一個值的低32位
  • 用關鍵字volatile來宣告,或者用鎖保護

加鎖與可見性

  • 為了確保所有執行緒都能看到共享變數的最新值,所有執行讀操作或者寫操作的執行緒都必須在同一個鎖上同步

volatile變數

  • 稍弱的同步機制
  • 編譯器和runtime都會注意到這個變數是共享的,所以不會將該變數重排序,不會被快取在暫存器或者其他處理器不可見的地方
  • 行為類似於以下程式清單,但是使用volatile時不會執行加鎖操作,因此也不會阻塞執行緒,比synchronized更輕量級
    @ThreadSafe
     public class SynchronizedInteger {
        @GuardedBy("this") private int value;
    
        public synchronized int get() { return value; }
        public synchronized void set(int value) { this.value = value; }
     }

  • 僅當volatile變數能簡化程式碼的實現以及對同步策略的驗證時,才應該使用它們,不建議過度依賴volatile變數提供的可見性
  • 通常用作某個操作完成、發生中斷或者狀態的標誌
  • 典型用法:檢查某個狀態標記以判斷是否退出迴圈。以下示例中asleep必須為volatile變數,否則當asleep被另一個執行緒修改時,執行判斷的執行緒卻發現不了。
  • 加鎖機制可以確保可見性和原子性,而volatile變數只能確保可見性
    //數綿羊
    volatile boolean asleep;
     ...
        while (!asleep)
            countSomeSheep();