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

Java Concurrency #2 - Thread Safety

shutterstock_198004562
  • 寫多執行緒的程式重點不在 thread 或 lock,重點在如何使得 shared, mutable 的 state 能夠正確且安全地被存取
    • shared 指的是某個物件的 state 會被多個 thread 存取
      • 當物件的 state 被多個物件存取時,同步是非常重要的
      • 除了透過 synchronized 之外,還能透過 volatile, explicit lock, 或是 Atomic variable 來達到同步的效果(不要只會 synchronized 的原因,後面陸續會說明)
    • mutable 指的是某個物件的 state 其值會在程式運行的過程中變化
      • immutable 就是其相反,這樣的 state 前面會加上 final 這個 keyword
      • immutable 也可以是類別像是 java.lang.String,它的定義就是 public final class,每一次建立一個 String 物件之後,它的內容都不能再改變
    • 千萬別輕忽同步這件事
      • 多執行緒的程式在未處理同步問題的情況下或許都能通過測試,而且好幾年都不會有問題,但…那只是時候未到,總有一天會爆發的
      • 承上,它不是沒問題,而是隨時都可能有問題
    • 如果多個執行緒會存取同一個物件的 state,有以下三種方式可以處理:
      • 不要分享那個 state
      • 讓那個 state 變成 immutable(final)
      • 處理同步(synchronization)
  • 什麼是 Thread Safety?
    • 當一個 class 被多個 thread 存取,且使用那個 class 的那一段程式沒作任何同步處理,程式依然能正常運作時,那個 class 就叫 Thread-safe class
    • 當那個 class 在單執行緒的情況下都無法正常運作了,就不用談 safe了
    • 當一個 class 是 stateless,它就會是 Thread safe,因為不管多少執行緒存取它都不會有 state 被改變,以下面的 Servlet 為例:
        public class StatelessServlet implements Servlet {
            public void service (ServletRequest req, ServletResponse resp) {
                BigInteger i = extractFromResponse(resp);
                BigInteger[] factors = factor(i);
                encodeIntoResponse(resp, factors);
            }
        }
      • 它本身沒有任何的屬性
      • 它也沒有參考到其他的類別
      • 唯一暫時的 state 是以 local variable 的型式存在的,這些變數會是存在該 thread 的 stack 中,不會有共享的問題
    • 小結: Stateless 的物件不會有 Thread safety 的問題
  • Atomicity
    • 如果上面的 stateless servlet,想加入一個 state,那會發生什麼事?
      • 程式碼調整如下:
          public class StatelfulServlet implements Servlet {
              private long count = 0;
              public long getCount() {
                  return count;
              }
              public void service (ServletRequest req, ServletResponse resp) {
                  BigInteger i = extractFromResponse(resp);
                  BigInteger[] factors = factor(i);
                  ++count;
                  encodeIntoResponse(resp, factors);
              }
          }
        • 不覺得似曾相識嗎?
        • 是的,這個一加進去一定會造成 race condition,可能會有多個 request 取到同樣的 count 的現象發生
        • 此外 ++count 看起來像是一條指令,但實際上它不符合原子性(Atomicity),也就是它並非單一、無法分割的指令,它實際上是三條指令的綜合:
          • 先取得 count 的值
          • 將 count 加上 1
          • 把上面的結果放回 count
          • 以上簡稱 read-modify-write operation
      • Race condition 就是某個運算的結果要看運氣才會正確,而且是因為多個 Thread 因為時間差相互交織(interleave) 造成的,就可以這麼稱呼這個現象
        • 現實生活中也可能會有 Race Condition
        • 比方說某甲與某乙約在博愛路上的麥當勞見面,結果博愛路上有三間麥當勞…
        • 約定的時間到了,兩人沒碰面,此時甲、乙都以為自己走錯分店了,於是就各自跑到另一間分店…
        • 但運氣不好,某甲到的時候某乙剛好離開沒多久,兩人就此擦身而過
        • 兩人都以為被放鴿子了,但如果有一個更明確的協定,也許兩人就不用浪費時間找彼此...
        • 上面的真實情境其實就是 check-then-act 這一類的 Race Condition,重點在於:
          • 「某甲到換到另一間分店時乙不在」這件事本身可能就因為時間的關係不成立
          • 承上,以程式的角度來看,某段程式檢查某個條件成立,等到該執行緒真的因為條件成立執行條件成立的區塊時,該條件搞不好早就處在不成立的狀態了(是不是很有即視感!)
      • 最常見的 check-then-act 就是在 Lazy Initialization
        • 比方說以下的程式片段,要在真的用到物件的時候才初始化(因為很耗效能,一開始先不初始化)
            public class LazyInitRace {
                private ExpensiveObject instance = null;
                public ExpensiveObject getInstance() {
                    if (instance == null)
                        instance = new ExpensiveObject();
                    return instance;
                }
            }
          • 有機會發生 race condition 的情境是 A thread 檢查 instance 發現是 null
          • 然就切換到 B thread,並也檢查 instance 發現是 null
          • 結果有可能就會是 2 個 instance 被建立
          • 這一方面會有效能問題,另一方面如果這個 class 建出來的 instance 是需要為 singleton 的話(e.g. registry),那兩個 instance 就會造成 registry 無統一管理的問題發生
    • 不論是 read-write-operate 或是 check-then-act,都有一個特性,它們都不是單一且不可分割的操作,因此稱作複合操作(Compound Operations)
      • 如果要能達到執行緒安全(thread safety),所有的操作都必須是 Atomic(沒錯,就是資料庫 ACID 裡的 Atomic)
      • 指令 A 與 指令 B 對彼此來說都是 Atomic 的原則就是,不管哪個指令被任何執行緒執行時,另一個指令在 context switch 到另一個執行緒時只有被執行跟被執行兩種情形而已
      • 還是看實例好了,假設我們修改 servlet 如下:
          public class ThreadSafeStatefulServlet implements Servlet {
              private final AtomicLong count = new AtomicLong(0);
              public long getCount() {
                  return count.get();
              }
              public void service (ServletRequest req, ServletResponse resp) {
                  BigInteger i = extractFromResponse(resp);
                  BigInteger[] factors = factor(i);
                  count.incrementAndGet();
                  encodeIntoResponse(resp, factors);
              }
          }
        • 差異有三,第一點就是 long 被換成 AtomicLong(java.util.concurrent.atomic 這個 package 裡有很多類似的類別)
        • 第二點就是所有對 state 的操作都從 compound operation 變成 Atomic(incrementAndGet()),
        • 第三點就是 Servlet 的 state,也就是 count 的 state 變成執行緒安全,所以 ThreadSafeStatefulServlet 這個類別也是執行安全的類別(Thread safe class)
      • 上面的例子只是其中一種解法,有空不妨看一下 AtomicLong 的原始碼,看 JDK 是如何達成的
×
Stay Informed

When you subscribe to the blog, we will send you an e-mail when there are new updates on the site so you wouldn't miss them.

Java Concurrency #3 - Locking
Java Concurrency #1 - Concurrency?

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
2026年1月02日, 星期五

Captcha 圖像