李娟
摘 要:Java多線程同步機制的應用有利于提高系統資源的利用率,改善系統的安全性。但是在多線程中最重要的問題是線程的同步和共享資源的訪問保護。本文通過具有意義的售票系統的并發同步實例,對同步進行了探索。
關鍵詞:Java 多線程 同步
中圖分類號:G420 文獻標識碼:A 文章編號:1673-9795(2014)03(a)-0183-02
至今,隨著計算機技術的飛速發展和互聯網的大面積普及,多處理器計算機已經司空見慣,在這種前景下,Java虛擬機(JVM)提供了一個多線程機制。在Java語言的編程設計中使用多線程運行機制來支持多任務和并行處理,可以讓在同一地址空間中執行多控制流,顯著的提高程序效率。但是線程的同步問題和共享資源的訪問保護是非常復雜的問題。
1 線程的同步機制
多線程的應用程序中,兩個或兩個以上的線程可以共享同一片存儲空間,這帶來方便的同時,也導致線程共享資源發生沖突,此時我們可以使用Java語言提供的同步機制(又叫互斥鎖機制)來解決此沖突問題。該同步機制是使用synchronized關鍵字控制一段程序代碼,這代碼段稱為互斥區或臨界區。定義臨界區的目的是在任一時間只有一個線程使用共享資源,保證多線程的并發執行。Java語言的每個對象(即類實例)都對應一把鎖(Lock),臨界區使用鎖來互斥多線程進入臨界區。每次只有一個線程獲得鎖進入臨界區,其它沒有獲得鎖的線程必須在就緒隊列中等待,直到該鎖被釋放。synchronized關鍵字的使用方式有synchronized方法和塊兩種。
(1)synchronized方法:將訪問共享資源的方法都標記為synchronized,然后該標記的方法來控制對類成員變量的訪問。類實例和鎖是一一對應的,當獲得需要調用synchronized方法的類實例鎖時,synchronized方法才可以執行,而且它開始執行直到完畢為止獨占鎖。這時其它調用synchronized方法的線程進入阻塞,一直到獲得釋放鎖為止。定義同步方法語法格式如下:
public synchronized void 方法名(參數列表){
…//省略代碼
}
(2)synchronized塊:java語言中除了使用synchronized方法來設置同步,還可以使用synchronized塊來設置同步。如果使用前者來修飾一個比較大的方法時,也會鎖住了不需要鎖住的字段,導致程序運行效率降低。后者是把程序的某段代碼使用synchronized塊來修飾,跟前者比它可以減少程序的同步區域。所以我們可以使用synchronized塊來修飾語句塊,能夠彌補synchronized方法修飾的缺陷。定義同步塊的語法格式如下:
synchronized(表達式)//表達式的結果是當前對象{
…//省略代碼
}
從以上兩種方法能夠看出,關鍵字synchronized用來與對象的鎖聯系,當某個對象使用synchronized修飾時就意味著同步機制已啟動,任一時刻只有讓一個線程訪問臨界區資源,阻止其他線程訪問該對象,即使出現阻塞和死鎖現象,該對象的被鎖定狀態也不會解除。
2 同步機制在售票系統的實現
在現實生活當中也經常遇到多個線程共享同一個數據資源,典型的例子是火車票售票系統,來講解線程共享資源。假設在售票廳內設10個售票窗口,每個售票窗口相當于一個線程,這些線程的共同訪問資源為售票廳的100張票。若不設置同步機制代碼如下:
public class Ticket {
public static void main(String[] args) {
Sell_Ticket st = new Sell_Ticket();//創建10個線程,每個線程代表一個售票口
for (int i = 0; i < 10; i++) new Thread(st, "第" + i + "個窗口").start();
}
}
class Sell_Ticket implements Runnable {
int trainTicket = 100;//預售的票數
boolean flag = false;//循環控制標志
public void run(){
while (!flag) {// 當還有剩余票時繼續售票
sellTicket(); }
}
public void sellTicket(){
if (trainTicket > 0) {
System.out.println(Thread.currentThread().getName()+ "售票成功,剩余票數:"
+ trainTicket);
trainTicket--;
} else flag = true;
}
}
運行的結果是多個窗口同時售票,會出現剩余票數變為負數的情況,即10個線程從100張票賣到1張票的時候還沒有停止賣,系統出現繼續賣出負數票的現象。
下面通過Java的多線程同步機制的synchronized方法和塊兩種方式來分別解決以上出現的問題。為了便于觀察到運行錯誤,特意添加Thread.sleep(10)方法,讓每個線程在售票階段睡眠10 ms。
(1)使用synchronized方法:在售票方法sellTicket()的前面添加synchronized關鍵字,就相當于使用一把鎖鎖住該方法。修改后的程序代碼如下:
public synchronized void sellTicket(){
if (trainTicket > 0) {
try {
Thread.sleep(10);//睡眠10毫秒
} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+ "售票成功,剩余票數:"+ trainTicket);
trainTicket--;
} else flag = true;
}
以上是使用synchronized關鍵字修飾sellTicket()方法,對該方法實現了多線程的互斥訪問。程序運行run()方法以后,當判斷出還有剩余票時調用被加鎖的sellTicket()方法,這時的sellTicket()方法,在同一個時間段內只能被一個線程訪問。
若synchronized關鍵字修飾靜態方法sellTicket(),鎖住的就是類本身。因為靜態方法是所有類實例對象所共享的,因此線程對象在訪問此靜態方法時是互斥訪問的,從而可以實現線程的同步。實現方法如下所示:
public static synchronized void sellTicket() {}
(2)使用synchronized塊:在程序中的if語句外面加synchronized關鍵字,就相當于使用一把鎖鎖住了這段代碼。它不同于同步方法,傳遞一個對象進行同步。修改后的程序如下:
Object object =new Object();
public void run() {
while (!flag) { // 當還有剩余票時繼續售票
synchronized(object){
if (trainTicket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+ "售票成功,剩余票數:"
+ trainTicket);
trainTicket--;
} else flag = true;
}
}
}
以上是使用synchronized關鍵字修飾了售票代碼段,對該代碼段實現了多線程的互斥訪問。同步塊不像同步方法修飾整個方法,而修飾一段代碼即可。synchronized(object)傳遞的是一個對象object,如果多線程想使用該對象的方法和變量,首先判斷有沒有加鎖,若已加鎖,等待鎖的釋放;若沒有鎖,先將給它加鎖,然后去執行代碼。在同一個時間段內只能有一個線程能夠獲得這把鎖。
同步塊的關鍵是多個線程對象競爭同一個共享資源即可,上面的代碼中是通過外部創建共享資源,然后傳遞到線程中來實現。我們也可以利用類成員變量被所有類的實例所共享這一特性,因此可以將object對象用靜態成員對象來實現,如下所示:
static Object object =new Object();
使用synchronized方法和塊兩種方式修改后的運行結果相同,剩余票數每次減1,從100減到0,到0時flag=true,while循環結束,即不能售票。
3 結語
Java程序中通過synchronized關鍵字來實現互斥訪問。本文引用的售票系統,通過synchronized方法和塊兩種方式實現了線程的同步互斥,避免了車票售完以后還能繼續售票導致數據混亂的問題。總之,合理使用多線程同步機制才能讓數據資源得到安全保障。
參考文獻
[1] 明日科技.Java從入門到精通[M].3版.北京:清華大學出版社,2013.
[2] 沈祥玖,李作緯.操作系統原理與應用[M].3版.北京:高等教育出版社,2013.
[3] 路勇.Java多線程同步問題分析[J].軟件,2012(4):31-33.