Java高併發學習(六)
Java高併發學習(6)
執行緒安全的概念與synchronized
並行程式開發的一大關注點是執行緒安全問題。由於讀寫者問題產生的錯誤,會導致資料不一致。雖然在使用volatile關鍵字後這種錯誤情況有所改善。但是,volatile並不能真正的保證執行緒安全。他只能保證一個執行緒修改資料後其他執行緒能看到這個改動。但當兩個執行緒同時修改一個數據時,依然會產生衝突。
下面程式碼演示了一個計數器,兩個執行緒同時對i進行累加操作,個執行100000次。我們希望當兩個執行緒執行結束後i的值為200000,但事實往往並非如此。如果你多次執行下面的程式碼,你會發現i的值總是小於200000。這就是兩個執行緒同時對i寫入,其中一個執行緒結果會覆蓋另一個(雖然這時候i被宣告為volatile變數)。
public class fist{ static int i =0; public static class MyThread_Write extends Thread{ @Override public void run(){ for(int j=0;j<100000;j++){ i++; } } } public static void main(String args[]) throws InterruptedException { MyThread_Write t1 = new MyThread_Write(); MyThread_Write t2 = new MyThread_Write(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
3次輸出結果如下:
上述結果產生的原因:假設執行緒1和執行緒2同時讀取i為0,並各自計算得到了i=1,並先後寫入到這個結果,因此,雖然i++被執行了兩次,但是實際i的值只增加了1。
要從根本上解決這個問題,我們就要保證多個執行緒在對i進行操作時完全同步,也就是說,當A執行緒在寫入時,執行緒B不僅不能寫入,同時也不能讀。因為線上程A寫完之前,執行緒讀取的一定是一個過期資料。Java中提供了一個重要的關鍵字synchronized來實現這個功能。
關鍵字synchronized關鍵字的作用是實現執行緒間的同步。他的工作是對同步程式碼加鎖,使得每一次,只能有一個執行緒進入同步塊,從而保證執行緒將的安全性。
關鍵字synchronized可以有多重種用法。這裡做一個簡單的整理:
·指定加鎖物件:對給定物件加鎖,進入同步程式碼前要獲得指定的鎖。
·直接作用於例項方法:相當於對當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖。
·直接作用於靜態方法:相當於對當前類加鎖,進入同步程式碼前要獲得當前類的鎖。
下面程式碼,將synchronized作用於本類,因此,沒當執行緒進入synchronized包裹的程式碼段,就都會要求請求fist.class的鎖。如果其他執行緒持有這把鎖,那麼新到的執行緒就必須等待。這樣,就保證了每一次只能有一個執行緒進行i++操作。
public class fist{ static int i =0; public static class MyThread_Write extends Thread{ @Override public void run(){ synchronized (fist.class) { for(int j=0;j<100000;j++){ i++; } } } } public static void main(String args[]) throws InterruptedException { MyThread_Write t1 = new MyThread_Write(); MyThread_Write t2 = new MyThread_Write(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
執行結果:
當然,也可以讓關鍵字作用於一個例項方法。這就是對物件中的方法加鎖,但這裡先給出一種錯誤的加鎖方式。
public class fist{ static int i =0; public static class MyThread_Write extends Thread{ public synchronized void increase(){ i++; } @Override public void run(){ for(int j=0;j<100000;j++){ increase(); } } } public static void main(String args[]) throws InterruptedException { MyThread_Write t1 = new MyThread_Write(); MyThread_Write t2 = new MyThread_Write(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
上述程式碼就犯了個嚴重的錯誤。雖然聲明瞭increase()方法是一個同步方法。但很不幸的是,這段程式碼指向的是不同的increase()方法,這是什麼意思呢?其實我們在建立執行緒t1和t2時分別用了兩次new MyThread_Write(),然後java虛擬機器在記憶體中建立了兩個 MyThread_Write物件,這兩個物件都有increase()方法,也就是說,在記憶體中有兩個increase()方法。然而我們只讓每一個物件對自己的increase()方法上了鎖。
那怎麼解決這個問題呢?我們會想要是記憶體中只有一個increase()方法就好了,無論我們創造多少個 MyThread_Write物件,他們的increase()方法都指向記憶體中唯一的increase()方法就好了!那這不就是靜態方法嗎!其實我們只要把increase()方法宣告為靜態方法就好了。
public class fist{ static int i =0; public static class MyThread_Write extends Thread{ public static synchronized void increase(){ i++; } @Override public void run(){ for(int j=0;j<100000;j++){ increase(); } } } public static void main(String args[]) throws InterruptedException { MyThread_Write t1 = new MyThread_Write(); MyThread_Write t2 = new MyThread_Write(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
除了用於執行緒同步,確保執行緒的安全之外,synchronized還可以確保執行緒間的可見性和有序性。從可見性的角度上講,synchronized完全可以代替volatile的功能,只是使用上沒有那麼方便。就有序性而言,由於synchronized每一次只有一個執行緒可以訪問同步塊,因此,無論同步塊內的程式碼如何被亂序執行,只要保證序列語義一致,那麼執行的結果總是一樣的。換而言之,被synchronized限制的多個執行緒是序列執行的。 ---------------------