1. 程式人生 > >Java高併發學習(六)

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限制的多個執行緒是序列執行的。 ---------------------