Java併發編程:synchronizedhtml
雖然多線程編程極大地提升了效率,可是也會帶來必定的隱患。好比說兩個線程同時往一個數據庫表中插入不重複的數據,就可能會致使數據庫中插入了相同的數據。今天咱們就來一塊兒討論下線程安全問題,以及Java中提供了什麼機制來解決線程安全問題。java
如下是本文的目錄大綱:數據庫
一.何時會出現線程安全問題?編程
二.如何解決線程安全問題?安全
三.synchronized同步方法或者同步塊網絡
如有不正之處,請多多諒解並歡迎批評指正。多線程
請尊重做者勞動成果,轉載請標明原文連接:併發
http://www.cnblogs.com/dolphin0520/p/3923737.htmlide
在單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現同時訪問同一個資源的狀況,這種資源能夠是各類類型的的資源:一個變量、一個對象、一個文件、一個數據庫表等,而當多個線程同時訪問同一個資源的時候,就會存在一個問題:this
因爲每一個線程執行的過程是不可控的,因此極可能致使最終的結果與實際上的願望相違背或者直接致使程序出錯。
舉個簡單的例子:
如今有兩個線程分別從網絡上讀取數據,而後插入一張數據庫表中,要求不能插入重複的數據。
那麼必然在插入數據的過程當中存在兩個操做:
1)檢查數據庫中是否存在該條數據;
2)若是存在,則不插入;若是不存在,則插入到數據庫中。
假如兩個線程分別用thread-1和thread-2表示,某一時刻,thread-1和thread-2都讀取到了數據X,那麼可能會發生這種狀況:
thread-1去檢查數據庫中是否存在數據X,而後thread-2也接着去檢查數據庫中是否存在數據X。
結果兩個線程檢查的結果都是數據庫中不存在數據X,那麼兩個線程都分別將數據X插入數據庫表當中。
這個就是線程安全問題,即多個線程同時訪問一個資源時,會致使程序運行結果並非想看到的結果。
這裏面,這個資源被稱爲:臨界資源(也有稱爲共享資源)。
也就是說,當多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。
不過,當多個線程執行一個方法,方法內部的局部變量並非臨界資源,由於方法是在棧上執行的,而Java棧是線程私有的,所以不會產生線程安全問題。
那麼通常來講,是如何解決線程安全問題的呢?
基本上全部的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問。
一般來講,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其餘線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。
本文主要講述synchronized的使用方法,Lock的使用方法在下一篇博文中講述。
在瞭解synchronized關鍵字的使用方法以前,咱們先來看一個概念:互斥鎖,顧名思義:能到達到互斥訪問目的的鎖。
舉個簡單的例子:若是對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其餘線程便只能等待。
在Java中,每個對象都擁有一個鎖標記(monitor),也稱爲監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
在Java中,可使用synchronized關鍵字來標記一個方法或者代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便得到了該對象的鎖,其餘線程暫時沒法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程纔會釋放該對象的鎖,其餘線程才能執行這個方法或者代碼塊。
下面經過幾個簡單的例子來講明synchronized關鍵字的使用:
1.synchronized方法
下面這段代碼中兩個線程分別調用insertData對象插入數據:
public class Test { public static void main(String[] args) { final InsertData insertData = new InsertData(); new Thread() { public void run() { insertData.insert(Thread.currentThread()); }; }.start(); new Thread() { public void run() { insertData.insert(Thread.currentThread()); }; }.start(); } } class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public void insert(Thread thread){ for(int i=0;i<5;i++){ System.out.println(thread.getName()+"在插入數據"+i); arrayList.add(i); } } }
此時程序的輸出結果爲:
說明兩個線程在同時執行insert方法。
而若是在insert方法前面加上關鍵字synchronized的話,運行結果爲:
class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public synchronized void insert(Thread thread){ for(int i=0;i<5;i++){ System.out.println(thread.getName()+"在插入數據"+i); arrayList.add(i); } } }
從上輸出結果說明,Thread-1插入數據是等Thread-0插入完數據以後才進行的。說明Thread-0和Thread-1是順序執行insert方法的。
這就是synchronized方法。
不過有幾點須要注意:
1)當一個線程正在訪問一個對象的synchronized方法,那麼其餘線程不能訪問該對象的其餘synchronized方法。這個緣由很簡單,由於一個對象只有一把鎖,當一個線程獲取了該對象的鎖以後,其餘線程沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized方法。
2)當一個線程正在訪問一個對象的synchronized方法,那麼其餘線程能訪問該對象的非synchronized方法。這個緣由很簡單,訪問非synchronized方法不須要得到該對象的鎖,假如一個方法沒用synchronized關鍵字修飾,說明它不會使用到臨界資源,那麼其餘線程是能夠訪問這個方法的,
3)若是一個線程A須要訪問對象object1的synchronized方法fun1,另一個線程B須要訪問對象object2的synchronized方法fun1,即便object1和object2是同一類型),也不會產生線程安全問題,由於他們訪問的是不一樣的對象,因此不存在互斥問題。
2.synchronized代碼塊
synchronized代碼塊相似於如下這種形式:
synchronized(synObject) { }
當在某個線程中執行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其餘線程沒法同時訪問該代碼塊。
synObject能夠是this,表明獲取當前對象的鎖,也能夠是類中的一個屬性,表明獲取該屬性的鎖。
好比上面的insert方法能夠改爲如下兩種形式:
class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); public void insert(Thread thread){ synchronized (this) { for(int i=0;i<100;i++){ System.out.println(thread.getName()+"在插入數據"+i); arrayList.add(i); } } } }
class InsertData { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Object object = new Object(); public void insert(Thread thread){ synchronized (object) { for(int i=0;i<100;i++){ System.out.println(thread.getName()+"在插入數據"+i); arrayList.add(i); } } } }
從上面能夠看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。由於也許一個方法中只有一部分代碼只須要同步,若是此時對整個方法用synchronized進行同步,會影響程序執行效率。而使用synchronized代碼塊就能夠避免這個問題,synchronized代碼塊能夠實現只對須要同步的地方進行同步。
另外,每一個類也會有一個鎖,它能夠用來控制對static數據成員的併發訪問。
而且若是一個線程執行一個對象的非static synchronized方法,另一個線程須要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,由於訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是對象鎖,因此不存在互斥現象。
看下面這段代碼就明白了:
public class Test { public static void main(String[] args) { final InsertData insertData = new InsertData(); new Thread(){ @Override public void run() { insertData.insert(); } }.start(); new Thread(){ @Override public void run() { insertData.insert1(); } }.start(); } } class InsertData { public synchronized void insert(){ System.out.println("執行insert"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("執行insert完畢"); } public synchronized static void insert1() { System.out.println("執行insert1"); System.out.println("執行insert1完畢"); } }
執行結果;
第一個線程裏面執行的是insert方法,不會致使第二個線程執行insert1方法發生阻塞現象。
下面咱們看一下synchronized關鍵字到底作了什麼事情,咱們來反編譯它的字節碼看一下,下面這段代碼反編譯後的字節碼爲:
public class InsertData { private Object object = new Object(); public void insert(Thread thread){ synchronized (object) { } } public synchronized void insert1(Thread thread){ } public void insert2(Thread thread){ } }
從反編譯得到的字節碼能夠看出,synchronized代碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執行時會讓對象的鎖計數加1,而monitorexit指令執行時會讓對象的鎖計數減1,其實這個與操做系統裏面的PV操做很像,操做系統裏面的PV操做就是用來控制多個線程對臨界資源的訪問。對於synchronized方法,執行中的線程識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設置,而後它自動獲取對象的鎖,調用方法,最後釋放鎖。若是有異常發生,線程自動釋放鎖。
有一點要注意:對於synchronized方法或者synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,所以不會因爲異常致使出現死鎖現象。
參考資料:
《Java編程思想》
http://ifeve.com/synchronized-blocks/