在單線程程序中,每次只能做一件事情,後面的事情需要等待前面的事情完成之後纔可以進行,但是如果使用多線程程序,就會發生兩個線程搶佔資源的問題,如兩個人同時說話、兩個人同時過同一個獨木橋等。所以在多線程編程中需要防止這些資源訪問的衝突。
5.線程安全
線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不行進行訪問直到該線程讀取完,其他線程纔可使用。不會出現數據不一致或者數據污染。
如果不提供數據訪問保護,有可能出現多個線程先後更改數據所得到的數據是髒數據。
線程不安全的情況,以較爲常見的售票窗口爲例:
//實現Runnable接口的售票系統
public class ThreadSafeTest implements Runnable{
int num = 10;//全局變量:總票數
public void run(){//多線程具體實現的內容
while(true){
if(num > 0){
try{
Thread.sleep(100);
//使線程休眠0.1秒,增加出錯的可能性
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
public static void main(Sting[] args){
ThreadSafeTest t = new ThreadSafeTest();
Thread a = new Thread(t);
Thread b = new Thread(t);
Thread c = new Thread(t);//創建三個線程
a.start();
b.start();
c.start();//線程啓動
}
}
運行結果:
tickets10
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0
tickets-1
tickets-2
在該例子中,最後打印售票剩下的票爲負值,是因爲同時創建了3個線程,這3個線程執行run()方法,在num變量爲1時,線程1、線程2、線程3都對num變量有存儲功能,當線程1執行run()方法時,還沒有來得及做遞減操作,就指定它條調用了sleep()方法進入就緒狀態,這是線程2、線程3都進入了run()方法,發現num變量依然大於0,各自進入線程中的run()方法,並休眠。之後線程1休眠時間到,將num變量值遞減,接着線程2、線程3也都對num變量進行遞減操作,從而產生了負值。
6.線程同步
解決多線程資源衝突問題的方法大多都是採用給定時間只允許一個線程訪問共享資源,這時就需要給共享資源上一道鎖。
方法一:同步代碼塊(同步塊)
1、共享數據:多個線程共同操作的同一個數據(變量);
2、同步監視器:由一個類的對象來充當。哪個線程獲取此監視器,誰就執行。
方法二:同步方法
6.1 同步塊
在Java中提供了同步機制,可以有效地防止資源衝突。同步機制使用synchronized關鍵字。
//實現Runnable接口的售票系統
public class ThreadSafeTest implements Runnable{
int num = 10;//全局變量:總票數
public void run(){
//多線程具體實現的內容
Object obj = new Object();//創建任意一個對象
synchronized(obj){
//將需要同步的代碼使用synchronized關機鍵字包括起來。
//對象可使用this關鍵詞,表示該對象爲當前對象。
while(true){
if(num > 0){
try{
Thread.sleep(100);
//使線程休眠0.1秒,增加出錯的可能性
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}//synchronized結束
}
public static void main(Sting[] args){
ThreadSafeTest t = new ThreadSafeTest();
Thread a = new Thread(t);
Thread b = new Thread(t);
Thread c = new Thread(t);//創建三個線程
a.start();
b.start();
c.start();//線程啓動
}
}
運行結果:
tickets10
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0
這樣,我們就避免了原先出現負數的情況。
通常我們將共享資源的操作放置在synchronized定義的區域內,這樣當其他線程也獲取到這個鎖時,必須等待鎖被釋放時才能進入該區域。
其中Obj爲任意一個對象,每個對象都存在一個標誌位,並具有兩個值,分別爲0和1(類似Sql數據庫中的bit數據類型)。一個線程運行到同步塊時,首先檢查該對象的標誌位,如果爲0狀態,表明此同步塊中存在其他線程在運行。這是該線程處於就緒狀態,知道處於同步塊中的線程執行完同步塊中的代碼爲止。這時該對象的標誌位被設置爲1,該線程才能執行同步塊中的代碼,並將Object對象的標誌位設置爲0,防止其他線程執行同步塊中的代碼。
注意:需要同步數據的線程必須使用同一把鎖。使用this關鍵詞,調用當前對象作爲同步鎖,當創建該線程時,如果多次實例化了當前對象,則會創建多個同步鎖,即各個線程使用各自的鎖,該同步鎖將會失去作用。
6.2 同步方法
同步方法就是在方法前面修飾synchronized關鍵詞的方法。
當某個對象調用了同步方法時,該對象上的其他同步方法必須等待該同步方法執行完畢後才能被執行。必須將每個能訪問共享資源的方法修飾爲synchronized,否則就會出錯。
public synchronized void doit(){//定義同步方法
if(num > 0){
try{
Thread.sleep(100);
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
public void run(){
while(true){
doit();//在run()方法中調用該同步方法
}
}
將共享資源的操作放置在同步方法中,運行結果與使用同步塊的結果一致。
6.3 死鎖的問題
死鎖:不同的線程分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。
解決方法:
特定的算法、原則;
儘量減少同步資源的定義。
7.線程通信
java.lang.Object中定義了三個只能在synchronized方法或synchronized代碼塊中才能使用的方法:
wait():令當前線程掛起並放棄CPU、同步資源,使別的線程可訪問並修改共享資源,而當前線程排隊等候再次對資源的訪問。
notify():喚醒正在排隊等待同步資源的線程中優先級最高者結束等待。
notifyAll():喚醒正在排隊等待資源的所有線程結束等待。
若沒有在synchronized方法或synchronized代碼塊中才能使用,則會報:java.lang.IllegaMonitorStateException異常。
管道通信:使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通信。
分佈式系統中說的兩種通信機制:共享內存機制和消息通信機制。
文章中的synchronized關鍵字就“屬於” 共享內存機制。而管道通信,更像消息傳遞機制,也就是說:通過管道,將一個線程中的消息發送給另一個。