synchronized用法的分析和總結
在多執行緒環境中併發訪問資源時會產生不可預料的結果,防止這種衝突的產生是當資源被一個任務使用時,在其上加鎖。第一個訪問某個資源的任必須鎖定這個資源,使其它任務在資源未被解鎖之前無法訪問它。而其在被解鎖之時另一個任務就可以鎖定並使用它,每個任務鎖定的資源都是最新的結果。
Java提供Synchronized關鍵字,為防止資源衝突提供了內建支援
鎖機制有如下兩種特性
- 互斥性,同一時間只允許一個執行緒訪問共享資源
- 可見性,鎖在被釋放之前對共享變數的修改,對隨後獲得該鎖的另一個執行緒是可見的
可以根據鎖的作用範圍通俗的把鎖分為類鎖和物件鎖
類鎖:
它表示的是給Class類上鎖,類鎖對類的所有物件例項都起作用,Class類是一個特殊的類,它包含了與類有關的資訊,使用如下方式加鎖時就是類鎖,它表示這個類同一時間只能被一個執行緒使用。
Synchronized(類.class){}
物件鎖:
所有的物件都含有單一的鎖(也稱為監視器鎖),當在物件上呼叫其任意synchronized方法時,此物件都被加鎖。它表示一個物件例項在同一時間只能被一個執行緒使用。
Synchronized(this|object){}
物件鎖的驗證
下圖兩個執行緒共用一個Runnable物件,test1和test2是兩個普通的方法
public class TestMain { public static void main(String arg[]) { SyncThread target = new SyncThread(); Thread thread1 = new Thread(target, "SyncThread1"); Thread thread2 = new Thread(target, "SyncThread2"); thread1.start(); thread2.start(); } }
public class SyncThread implements Runnable { private static int count; private final byte[] lock = new byte[0]; public SyncThread() { count = 0; } @Override public void run() { if (Thread.currentThread().getName().contains("1")) { test1(); } else if (Thread.currentThread().getName().contains("2")) { test2(); } } private void test1() { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ": " + (count++)); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } } } private void test2() { for (int i = 0; i < 5; i++) { try { System.out.println(Thread.currentThread().getName() + ": " + (count++)); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } } } }
從列印結果可以看出test1和test2方法執行是不同步的。
SyncThread2: 0 SyncThread1: 0 SyncThread1: 1 SyncThread2: 2 SyncThread1: 3 SyncThread2: 4 SyncThread2: 5 SyncThread1: 5 SyncThread1: 6 SyncThread2: 6
當把test1和test2寫成普通的同步方法時,我們再來看看列印的結果
private synchronzied void test1() { ... }
private synchronzied void test2() { ... }
SyncThread1: 0 SyncThread1: 1 SyncThread1: 2 SyncThread1: 3 SyncThread1: 4 SyncThread2: 5 SyncThread2: 6 SyncThread2: 7 SyncThread2: 8 SyncThread2: 9
可以看出兩個執行緒是按順序執行的,thread1執行完了才輪到thread2執行。當test1還沒執行完時,target物件被鎖住,test2處於等待的狀態,直到test1釋放鎖的時候test2才能執行。所以對於某個特定的物件而言,其所有Synchronized方法共享同一個鎖。
當test1和test2改成是同步控制塊時,列印結果和上面是一樣的,這種情況鎖住的也是物件本身。使用同步控制塊而不是同步方法,可以使多個任務訪問物件的時間效能得到提高,平時使用時建議儘量使用同步控制塊。
private void test1() { synchronzied(this|lock){ ... } }
private void test2() { synchronzied(this|lock){ ... } }
synchronized鎖重入
上面的測試中還應用到了synchronized鎖重入的功能,它表示的是當一個執行緒得到一個物件鎖後,再次請求次物件鎖時是可以再次得到該物件當鎖的。如果沒有鎖重入的話,就會造成死鎖。
當一個物件上的不同方法鎖住的是不同的物件時,各自的鎖互相獨立,下面把test1在this物件上同步,test在lock物件上同步。
private void test1() { synchronzied(this){ ... } }
private void test2() { synchronzied(lock){ ... } }
從列印結果可以看出,test1和test2的執行變成不同步了。因此在使用synchronized時要注意同步的物件。
SyncThread1: 0 SyncThread2: 1 SyncThread1: 2 SyncThread2: 3 SyncThread1: 4 SyncThread2: 5 SyncThread1: 6 SyncThread2: 6 SyncThread1: 7 SyncThread2: 8
類鎖的驗證
需要注意的是,以上同步的條件是test1和test2處於同一個物件,當它們不是處於同一個物件時是不同步的
public static void main(String arg[]) { SyncThread target = new SyncThread(); Thread thread1 = new Thread(new SyncThread(), "SyncThread1"); Thread thread2 = new Thread(new SyncThread(), "SyncThread2"); thread1.start(); thread2.start(); }
把測試方法改成thread1和thread2不共用一個Runnable,列印結果如下
SyncThread1: 0 SyncThread2: 1 SyncThread1: 2 SyncThread2: 2 SyncThread1: 3 SyncThread2: 3 SyncThread1: 4 SyncThread2: 4 SyncThread1: 5 SyncThread2: 5
要想在不同物件也能同步,要實現的是類鎖。針對每個類,也有一個鎖(作為類的Class物件的一部分)
在main方法裡還是繼續用不同的Runnable物件
Thread thread1 = new Thread(new SyncThread(), "SyncThread1"); Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
把test1和test2改成如下靜態的同步方法
private static synchronized void test1() { ... }
private static synchronized void test2() { ... }
列印結果如下,這個就印證了static synchronized方法的鎖是類鎖,所以static synchronized可以防止在類的範圍內防止併發資料對static資料的併發訪問
SyncThread1: 0 SyncThread1: 1 SyncThread1: 2 SyncThread1: 3 SyncThread1: 4 SyncThread2: 5 SyncThread2: 6 SyncThread2: 7 SyncThread2: 8 SyncThread2: 9
test1和test2寫成如下形式時和靜態同步方法的結果和上面的static方法是一樣的,這種用法鎖住的是Class類
private void test1() { synchronzied(SyncThread.class|靜態物件){ ... } }
private void test1() { synchronzied(SyncThread.class|靜態物件){ ... } }
最後說一下什麼時候應該使用同步機制,如果你正在寫一個變數,它可能接下來被另一個執行緒讀取,或者正在讀取一個上一次已經被另一個執行緒寫過的變數,那麼你必須使用同步方法。