2016-12-04

Java Synchronization

在多執行緒的環境(例如 Web)如果有同時寫入的需求,就得用 Java synchronization,同時讀取則不用(唯讀)。

synchronized method

最簡單的作法就是在 method 上加 synchronzed。
public static void main(String[] args) {
  Sync s1 = new Sync("S1");
  Thread s1aThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod();
    }
  };
  Thread s1bThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod();
    }
  };
  s1aThread.start();
  s1bThread.start();
}

public void normalMethod() {
  System.out.println(">>> " + this.id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + this.id);
}
在尚未使用 synchronized 前,兩個 Thread 會交錯執行。

>>> S1
>>> S1
<<< S1
<<< S1
在 method 上加 synchronized。
public synchronized void normalMethod() {
  System.out.println(">>> " + this.id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + this.id);
}
就可以看到第一個 method 執行完,才會執行第二個,不會交錯在一起。
>>> S1
<<< S1
>>> S1
<<< S1
synchronized 對同一個 object 的不同 method 也是有用的。
public static void main(String[] args) {
  Sync s1 = new Sync("S1");
  Thread s1aThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod1("S1");
    }
  };
  Thread s1bThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod2("S2");
    }
  };
  s1aThread.start();
  s1bThread.start();
}

public synchronized void normalMethod1(String id) {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}

public synchronized void normalMethod2(String id) {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}
因為 synchronized 是鎖在 object 上,依序執行如下。
>>> S1
<<< S1
>>> S2
<<< S2
但是對不同物件的同一個 method 呢?
public static void main(String[] args) {
  Sync s1 = new Sync("S1");
  Sync s2 = new Sync("S2");
  Thread s1aThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod();
    }
  };
  Thread s1bThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s2.normalMethod();
    }
  };
  s1aThread.start();
  s1bThread.start();
}

public synchronized void normalMethod() {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}
結果是沒有用的,又交錯執行了。
>>> S2
>>> S1
<<< S1
<<< S2
原因剛提過了,synchronized method 是鎖在 object 上。

雖然是同一個 class,但是兩個獨立不相關的 object,synchronized 當然起不了作用。

既然是同一個 class,那 static method 就可行了!
public static void main(String[] args) {
  Thread s1aThread = new Thread() {

    @Override
    public void run() {
      super.run();
      Sync.staticMethod("S1");
    }
  };
  Thread s1bThread = new Thread() {

    @Override
    public void run() {
      super.run();
      Sync.staticMethod("S2");
    }
  };
  s1aThread.start();
  s1bThread.start();
}

public static synchronized void staticMethod(String id) {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}
Synchronized static method 是鎖在 class 上,而且 static method 是透過 class 呼叫,而不是透過 object,即使透過不同的 object 去呼叫 synchronized static method,也會得到一樣的結果。
>>> S1
<<< S1
>>> S2
<<< S2

但是同時呼叫 synchronized method 與 synchronized static method 有作用嗎?
public static void main(String[] args) {
  Sync s1 = new Sync("S1");
  Sync s2 = new Sync("S2");
  Thread s1aThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s1.normalMethod("S1");
    }
  };
  Thread s1bThread = new Thread() {

    @Override
    public void run() {
      super.run();
      s2.staticMethod("S2");
    }
  };
  s1aThread.start();
  s1bThread.start();
}

public synchronized void normalMethod(String id) {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}

public static synchronized void staticMethod(String id) {
  System.out.println(">>> " + id);
  try {
    Thread.sleep(1000);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("<<< " + id);
}
結果是無效的,還是一樣的原因,鎖的對象不同,一個是 class,另一個是 object,這是一般最常見的錯誤。

在繼續看 synchronized block 之前,得先來好好認識一下 volatile 修飾字。

volatile

volatile 修飾字早就存在於 Java 中,但我從來沒用過,其實也不知道它是做什麼用。

volatile 從字面上來看是「易揮發的、不穩定的」,還有 volatile 只能用在屬性上,不能用在方法上。

在揭曉謎底之前,再看一個有關效能優化的作法,啟動 Java VM 後,初始化的所有變數都存在「記憶體」中,但為了效能優化,Java VM 會將變數存一份到「暫存」或者「CPU 快取」。

看到問題了嗎?同一個變數被存到兩個不同的地方,表示可能會出現「不一致」的狀況。

這在多工的環境就可能造成變數不一致的狀況,那要怎麼避免這個可能發生而且超難追蹤的問題呢?

答案就是「易揮發的、不穩定的」volatile,只要在變數加上 volatile 修飾字,Java VM 就不會對它進行效能優化,永遠只會有一份存在記憶體裡。

所以只要是多工環境共用的變數,建議加上 volatile。

synchronized block

以下是以前我 Singleton 的寫法,當然要先加上 volatile 修飾字,不然剛做啥講半天。

這在單工環境是沒有問題的,但是遇到多工就可能出錯。
public class Singleton {

  private static volatile Singleton INSTANCE;

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
    return INSTANCE;
  }

}
原因在於檢查發現 INSTANCE 是 null 之後,在 new Singleton() 並指派給 INSTANCE 之前,這中間可能被其他 thread 插隊,導致 new Singleton() 被呼叫多次,而失去 Singleton 的原意。

最簡單的解法就是像上面 synchronized method 的作法,為整個 method 加上 synchonized,這樣當然可以解決所有問題,但會有「效能問題」。

因為對於 synchronized static method,每次呼叫都得拿到 Singleton.class 的鎖才行,導致所有的 request 或 thread 去呼叫 Singleton.getInstance() 時都得等等等...,即使該 INSTANCE 已經呼叫過 new Singleton() 也一樣。

Singleton pattern 目的只要防止「第一次」存取,結果用了 synchronized static method 導致每次存取都得付出一點代價,這太不合理了。

所以就是 synchronized block 上場了。
public static Singleton getInstance() {
  synchronized (Singleton.class) {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
  }
  return INSTANCE;
}
上面是完全錯誤的示範,用 synchronzed block 包住 method 裡的所有 statement,那跟 synchronized method 有什麼差別!

synchronzed block 就是只包「需要」的部份就好。
public static Singleton getInstance() {
  if (INSTANCE == null) {
    synchronized (Singleton.class) {
      INSTANCE = new Singleton();
    }
  }
  return INSTANCE;
}
這樣對了吧?不好意思,還是錯。

這樣還是有漏洞,原因是第一個人過了 INSTANCE == NULL,然後進到 synchronized block 去呼叫 new Singleton(),但是在將 new Singleton() 指派給 INSTANCE 前,又有另一個人也過了 INSTANCE == null,然後等在 synchronized block 門外了,一旦第一個人離開 synchronized block,第二個人就會跟著進去,然後又 new Singleton() 了一次。

Double-checked Locking

正解就是 Double-checked locking。
public static Singleton getInstance() {
  if (INSTANCE == null) {
    synchronized (Singleton.class) {
      if (INSTANCE == null) {
        INSTANCE = new Singleton();
      }
    }
  }
  return INSTANCE;
}
在進入 synchronized block 之後,還要再檢查一次 INSTANCE == null,這樣就不怕第二個人尾隨進來搗蛋了。

synchronized block 要鎖誰?

上面的用法都是鎖在 class 上,這是最簡單的作法,也不會出什麼問題。

但有幾種情況就有可能出問題,第一個就是鎖在「字串」上。

請先參考 Java String 有個 Pool!,就可以知道你手上的 String 物件可能和別人手上的 String  物件是同一個 instance,如果不幸出現這種情況,又好巧不巧遇到別隻程式也拿同樣的字串做 synchronized block 的鎖,那就真的有的等了。

第二個就是鎖在「非 final」的變數上,因為非 final,所以可能被重新指派到不同的變數 instance 上,導致前後兩個執行緒是鎖在不同的物件 instance 上,那當然 synchronization 就破工了。
---
---
---

沒有留言:

張貼留言