java語言

當前位置 /首頁/計算機/java語言/列表

Java多執行緒的定義狀態和屬性

同步一直是java多執行緒的難點,在我們做android開發時也很少應用,但這並不是我們不熟悉同步的理由。希望這篇文章能使更多的人能夠了解並且應用java的同步。

Java多執行緒的定義狀態和屬性

在多執行緒的應用中,兩個或者兩個以上的執行緒需要共享對同一個資料的存取。如果兩個執行緒存取相同的物件,並且每一個執行緒都呼叫了修改該物件的方法,這種情況通常成為競爭條件。

競爭條件最容易理解的例子就是:比如火車賣票,火車票是一定的,但賣火車票的視窗到處都有,每個視窗就相當於一個執行緒,這麼多的執行緒共用所有的火車票這個資源。並且無法保證其原子性,如果在一個時間點上,兩個執行緒同時使用這個資源,那他們取出的火車票是一樣的(座位號一樣),這樣就會給乘客造成麻煩。解決方法為,當一個執行緒要使用火車票這個資源時,我們就交給它一把鎖,等它把事情做完後在把鎖給另一個要用這個資源的執行緒。這樣就不會出現上述情況。

1. 鎖物件

synchronized關鍵字自動提供了鎖以及相關的條件,大多數需要顯式鎖的情況使用synchronized非常的方便,但是等我們瞭解ReentrantLock類和條件物件時,我們能更好的理解synchronized關鍵字。ReentrantLock是JAVA SE 5.0引入的, 用ReentrantLock保護程式碼塊的結構如下:

();try{...}finally{ck();}

這一結構確保任何時刻只有一個執行緒進入臨界區,一旦一個執行緒封鎖了鎖物件,其他任何執行緒都無法通過lock語句。當其他執行緒呼叫lock時,它們則被阻塞直到第一個執行緒釋放鎖物件。把解鎖的操作放在finally中是十分必要的,如果在臨界區發生了異常,鎖是必須要釋放的,否則其他執行緒將會永遠阻塞。

2. 條件物件

進入臨界區時,卻發現在某一個條件滿足之後,它才能執行。要使用一個條件物件來管理那些已經獲得了一個鎖但是卻不能做有用工作的執行緒,條件物件又稱作條件變數。

我們來看看下面的例子來看看為何需要條件物件

假設一個場景我們需要用銀行轉賬,我們首先寫了銀行的類,它的建構函式需要傳入賬戶數量和賬戶金額

public class Bank {private double[] accounts; private Lock bankLock; public Bank(int n,double initialBalance){ accounts=new double[n]; bankLock=new ReentrantLock(); for (int i=0;i<th;i++){ accounts[i]=initialBalance; } } }

接下來我們要提款,寫一個提款的方法,from是轉賬方,to是接收方,amount轉賬金額,結果我們發現轉賬方餘額不足,如果有其他執行緒給這個轉賬方再存足夠的錢就可以轉賬成功了,但是這個執行緒已經獲取了鎖,它具有排他性,別的執行緒也無法獲取鎖來進行存款操作,這就是我們需要引入條件物件的原因。

public void transfer(int from,int to,int amount){ (); try{ while (accounts[from]<amount){ //wait } }finally { ck(); } }

一個鎖物件擁有多個相關的`條件物件,可以用newCondition方法獲得一個條件物件,我們得到條件物件後呼叫await方法,當前執行緒就被阻塞了並放棄了鎖

public class Bank {private double[] accounts; private Lock bankLock; private Condition condition; public Bank(int n,double initialBalance){ accounts=new double[n]; bankLock=new ReentrantLock(); //得到條件物件 condition=ondition(); for (int i=0;i<th;i++){ accounts[i]=initialBalance; } } public void transfer(int from,int to,int amount) throws InterruptedException { (); try{ while (accounts[from]<amount){ //阻塞當前執行緒,並放棄鎖 t(); } }finally { ck(); } }}

等待獲得鎖的執行緒和呼叫await方法的執行緒本質上是不同的,一旦一個執行緒呼叫的await方法,他就會進入該條件的等待集。當鎖可用時,該執行緒不能馬上解鎖,相反他處於阻塞狀態,直到另一個執行緒呼叫了同一個條件上的signalAll方法時為止。當另一個執行緒準備轉賬給我們此前的轉賬方時,只要呼叫alAll();該呼叫會重新啟用因為這一條件而等待的所有執行緒。

當一個執行緒呼叫了await方法他沒法重新啟用自身,並寄希望於其他執行緒來呼叫signalAll方法來啟用自身,如果沒有其他執行緒來啟用等待的執行緒,那麼就會產生死鎖現象,如果所有的其他執行緒都被阻塞,最後一個活動執行緒在解除其他執行緒阻塞狀態前呼叫await,那麼它也被阻塞,就沒有任何執行緒可以解除其他執行緒的阻塞,程式就被掛起了。

那何時呼叫signalAll呢?正常來說應該是有利於等待執行緒的方向改變時來呼叫signalAll。在這個例子裡就是,當一個賬戶餘額發生變化時,等待的執行緒應該有機會檢查餘額。

public void transfer(int from,int to,int amount) throws InterruptedException { (); try{ while (accounts[from]<amount){ //阻塞當前執行緒,並放棄鎖 t(); } //轉賬的操作 ... alAll(); }finally { ck(); } }

當呼叫signalAll方法時並不是立即啟用一個等待執行緒,它僅僅解除了等待執行緒的阻塞,以便這些執行緒能夠在當前執行緒退出同步方法後,通過競爭實現對物件的訪問。還有一個方法是signal,它則是隨機解除某個執行緒的阻塞,如果該執行緒仍然不能執行,那麼則再次被阻塞,如果沒有其他執行緒再次呼叫signal,那麼系統就死鎖了。

3. Synchronized關鍵字

Lock和Condition介面為程式設計人員提供了高度的鎖定控制,然而大多數情況下,並不需要那樣的控制,並且可以使用一種嵌入到java語言內部的機制。從Java1.0版開始,Java中的每一個物件都有一個內部鎖。如果一個方法用synchronized關鍵字宣告,那麼物件的鎖將保護整個方法。也就是說,要呼叫該方法,執行緒必須獲得內部的物件鎖。

換句話說,public synchronized void method(){}等價於public void method(){();try{}finally{ck();}

上面銀行的例子,我們可以將Bank類的transfer方法宣告為synchronized,而不是使用一個顯示的鎖。

內部物件鎖只有一個相關條件,wait放大新增到一個執行緒到等待集中,notifyAll或者notify方法解除等待執行緒的阻塞狀態。也就是說wait相當於呼叫t(),notifyAll等價於alAll();

我們上面的例子transfer方法也可以這樣寫:

public synchronized void transfer(int from,int to,int amount)throws InterruptedException{ while (accounts[from]<amount) { wait(); } //轉賬的操作 ... notifyAll(); }

可以看到使用synchronized關鍵字來編寫程式碼要簡潔很多,當然要理解這一程式碼,你必須要了解每一個物件有一個內部鎖,並且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的執行緒,由條件來管理那些呼叫wait的執行緒。

4. 同步阻塞

上面我們說過,每一個Java物件都有一個鎖,執行緒可以呼叫同步方法來獲得鎖,還有另一種機制可以獲得鎖,通過進入一個同步阻塞,當執行緒進入如下形式的阻塞:

synchronized(obj){}

於是他獲得了obj的鎖。再來看看Bank類

public class Bank {private double[] accounts;private Object lock=new Object(); public Bank(int n,double initialBalance){ accounts=new double[n]; for (int i=0;i<th;i++){ accounts[i]=initialBalance; } } public void transfer(int from,int to,int amount){ synchronized(lock){ //轉賬的操作 ... } }}

在此,lock物件建立僅僅是用來使用每個Java物件持有的鎖。有時開發人員使用一個物件的鎖來實現額外的原子操作,稱為客戶端鎖定。例如Vector類,它的方法是同步的。現在假設在Vector中儲存銀行餘額

public void transfer(Vectoraccounts,int from,int to,int amount){ (from,(from)-amount); (to,(to)+amount;}

Vecror類的get和set方法是同步的,但是這並未對我們有所幫助。在第一次對get呼叫完成以後,一個執行緒完全可能在transfer方法中被被剝奪執行權,於是另一個執行緒可能在相同的儲存位置存入了不同的值,但是,我們可以截獲這個鎖

public void transfer(Vectoraccounts,int from,int to,int amount){ synchronized(accounts){ (from,(from)-amount); (to,(to)+amount; }}

客戶端鎖定(同步程式碼塊)是非常脆弱的,通常不推薦使用,一般實現同步最好用urrent包下提供的類,比如阻塞佇列。如果同步方法適合你的程式,那麼請儘量的使用同步方法,他可以減少編寫程式碼的數量,減少出錯的機率,如果特別需要使用Lock/Condition結構提供的獨有特性時,才使用Lock/Condition。

TAG標籤:JAVA 多執行緒 屬性 #