一.什麼時候會出現線程安全問題?
在單線程中不會出現線程安全問題,而在多線程程式設計中,有可能會出現同時通路同一個資源的情況,這種資源可以是各種類型的的資源:一個變量、一個對象、一個檔案、一個資料庫表等,而當多個線程同時通路同一個資源的時候,就會存在一個問題:
由于每個線程執行的過程是不可控的,是以很可能導緻最終的結果與實際上的願望相違背或者直接導緻程式出錯。
舉個簡單的例子:
現在有兩個線程分别從網絡上讀取資料,然後插入一張資料庫表中,要求不能插入重複的資料。
那麼必然在插入資料的過程中存在兩個操作:
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同步方法或者同步塊
在了解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();
}
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的話,運作結果為:
public synchronized void insert(thread thread){
從上輸出結果說明,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方法可以改成以下兩種形式:
synchronized (this) {
for(int i=0;i<100;i++){
private object object = new object();
synchronized (object) {
從上面可以看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。因為也許一個方法中隻有一部分代碼隻需要同步,如果此時對整個方法用synchronized進行同步,會影響程式執行效率。而使用synchronized代碼塊就可以避免這個問題,synchronized代碼塊可以實作隻對需要同步的地方進行同步。
另外,每個類也會有一個鎖,它可以用來控制對static資料成員的并發通路。
并且如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因為通路static synchronized方法占用的是類鎖,而通路非static synchronized方法占用的是對象鎖,是以不存在互斥現象。
看下面這段代碼就明白了:
new thread(){
@override
insertdata.insert();
insertdata.insert1();
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 {
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會自動釋放目前線程占用的鎖,是以不會由于異常導緻出現死鎖現象。
最新内容請見作者的github頁:http://qaseven.github.io/