選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
8 分鐘閱讀時間 (1500 個字)

Java Concurrency #4 - Sharing Objects[1]

shutterstock_198004562
  • 前一篇提到正確的並行程式其實就是針對 mutable shared state 作好存取的管控(synchronized blocks)當時的重點放在如何避免多個執行緒同時存取。接下來幾篇的重點會放在一些共享/公開物件的技巧,使得多個執行緒不會有存取的問題

    • 時常有個錯誤的認知以為同步就是透過原子操作或是針對重點部份作區隔。然而同步還有另一個更為重要的面向,也就是記憶體的可視性(memory visibility)

      • 同步不只是要避免多個執行緒存取 shared state,同時還要確保其中一個執行緒修改 state 時,其他的執行緒都可以看見這項修改

      • 可視性是個不易察覺的議題,因為它並不直覺。在單執行緒中,寫入一個值到變數後,如果中間沒任何其他的寫入,讀取到的值應該會等同於先前寫入的值

      • 然而,如果是多執行緒,上面陳述的狀況就不見得會成立了,以下面的程式為例:

          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;
              }
          }
        • 直覺上程式應該會印出 42 並結束執行,但實際上有可能印出 ,或是程式永遠不會結束
        • 這是因為在另一個執行緒有可能因為時間差而使得主執行緒中的 number = 42ready = true 不可視
        • 上面的現象叫作 reordering,指的是 readynumber 的變化不見得來得及被另一個執行緒看到,因此跟預期的執行順序不同
        • 然而,上述的現象令人意外地難以發現。不過只要掌握一個大原則,基本上就不會有太大的問題,那就是只要有 shared mutable state,就要處理 synchronization
      • 上述的 NoVisibility 所表達的是一種不新鮮的狀態(stale state)

        • 而且還不只是如此,它不會是全無或全有的情形,也有可能 number 的異動被看見了,但 ready 並無
        • 不新鮮的食物(stale food)還是可以吃,只是沒那麼好吃了,但不新鮮的資料(stale state)可是會造成不可預期的嚴重錯誤,或是造成檔案資料內容毀損…等等
        • 未作同步就讀資料就好比在資料庫系統中的 READ_UNCOMMITTED,代表著願意以效能交換精確度,但在多執行緒的世界中,可不單單只有失去精確度
      • 以下的程式片段,各位看看有什麼問題

          public class MutableInteger {
              private int value;
              public int get() {
                  return value;
              }
              public void set(int value) {
                  this.value = value;
              }
          }
        • 都看到第四篇了,應該能答得出來吧?就是沒作同步
        • 只不過這次要多記得的是,各執行緒讀到的可能是不新鮮的資料,而我們可以如下調整:
          public class SynchronizedInteger {
          private int value;
          public synchronized int get() {
            return value;
          }
          public synchronized void set(int value) {
            this.value = value;
          }
          }
      • 這麼小範圍的 synchronized method 其實就比較不用擔心效能問題了

      • 也許你會覺得只要針對 setter 作同步就好了啊,但其實 getter 未作同步還是有機會看到 stale state

        • 雖然有機會看到 stale state,但至少是某一個執行緒 set 的結果(其他的執行緒做的異動還未可視),這樣的保證可稱之為不知哪生出來的安全性(Out-of-thin-air safety)
        • Out-of-thin-air safety 在大部份的變數都是成立的,除了一個例外,未被宣告成 volatile 的64 bit 的數值型態(double/long)
        • Java memory model 強制所有的存取都必須符合原子性,可是 long/double 就是個例外,JVM 被允許以兩個 32 bit 的操作對其值作存取
        • 在未處理同步的情況下,很有可能讀到 A Thread 針對高位的 32 bit 的操作與 B Thread 對低位 32 bit 的操作混合起來的結果
        • 因此,即時你不在乎讀到舊值(stale state),long/double 的這種現象會超乎你的想像 ,所以最好還是透過 volatile 或是 lock 的機制作同步
      • 內建鎖(Intrinsic locking) 可以讓 Thread A 對 shared mutable state 做的事在 Thread B 可見

      • 所以小結一下,同步(synchronization),是為了:

        • 不讓多個執行緒同時變更值
        • 確保其中一個執行緒的變更在其他執行緒可視
        • 以上都是針對 shared mutable state
        • 換言之,同步不只是為了 mutex 同時也是為了 visibility
    • Java 提供另一個確保 Visibility 的方法,就是 volatile

      • 它是一個保留字,只能加在 state 前面,不過接下來的部份就很艱難了,因為跟 JVM 的處理有關
        • compiler 及 runtime 時,被標記 volatile 的 state 不會被 reorder
        • 此外,volatile 的 state 不會被放在暫存器(register) 或是 cache 中,因此其值被讀取時永遠是會是最近一次被任何的執行緒所修改的最終值
        • 不過 volatile 的 state 並不需要透過 synchronized block 的搭配來達成 Thread safety,因此是一種輕量的同步方式
        • 然而不建議過度仰賴 volatile,因為相較於 locking,它比較脆弱且不易理解
      • 以下是一個 volatile 的典型用法
          //...(略)
          volatile boolean asleep;
          while(!asleep)
            countSheep();
        • 這裡一定要用 volatile 加在 asleep 前,否則其他的執行緒改動這個 state 的值的時候,目前的執行緒也不會知道
        • 其實也可以用 synchronized 但感覺就殺雞用牛刀
      • volatile 大部份都用在狀態的檢查之類的邏輯,它不是萬能的,像 read-then-write 操作就無法單靠 volatile 來確保 visibility,因此 volatile 的使用情境大致如下:
        • 寫入變數的值不相依於它本身的值,除非能保證寫入這件事是單執行緒的(很繞口,但想想 ++count 是不是就相依於自己的值,遞增後又寫入值)
        • 變數本身與其他變數共同構成 invariant 的要素
        • 當不需要 locking 的時候
      • 小結一下:
        • locking 確保 visibility 跟 atomicity
        • volatile 只確保 visibility
Java Concurrency #5 - Sharing Objects[2]
[JavaScript] 裝置通知

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/05/06, 週一

Captcha 圖像