java線程
應(yīng)用多線程一來可以為主線程分擔(dān)耗時較多的任務(wù),提高主線程的響應(yīng)速度,二來隨著計算機多處理能力的增加,可以提高計算機的使用性能。
首先我們來看java是如何創(chuàng)建線程的。創(chuàng)建一個線程傳統(tǒng)上有兩種方式,一種是繼承線程Thread類,創(chuàng)建Thread類實例,調(diào)用start()方法;還有一種就是實現(xiàn)runnable接口,創(chuàng)建new Thread(runnable()).start().兩種方式本質(zhì)上還有創(chuàng)建一個線程類。還可以采用executer.execute()的方式提交一個線程,或者更高級的executorService.submit()方法來提交線程。
當(dāng)多個線程同時運行的時候,由于它們之間對于進程的資源是共享的,對于共享的資源占有會有先后,因而會產(chǎn)生競爭關(guān)系,同時還會產(chǎn)生資源的一致性問題。這些關(guān)系綜合在一起會產(chǎn)生如果處理不當(dāng)會產(chǎn)生不良的后果。當(dāng)線程多的時候,多個線程之間的關(guān)系會有多種,一種是主子線程關(guān)系,即子線程為主線程服務(wù),協(xié)助主線程完成一個任務(wù),還有是并列關(guān)系,即為了提高程序響應(yīng)速度,采用多個線程來并發(fā)處理同樣的任務(wù),還有一種是順序/互補關(guān)系
1.主子關(guān)系
主子關(guān)系更多的是主線程將一個耗時較多的任務(wù)分給子線程,子線程處理完成后通知給主線程。這種關(guān)系通常不會產(chǎn)生對資源的同步訪問,但是會存在子線程處理完任務(wù)后通知主線程的問題。那么如何解決這個問題呢?其實,java中一個線程在執(zhí)行完成任務(wù)后就會消亡,所以不會存在子線程主動通知主線程,那么子線程如何通知主線程呢?這里有兩種方式,一種是設(shè)置共享變量,子線程完成后,更改共享變量的值來達到通知主線程其任務(wù)完成的目的,另外一種就是主線程在空閑的時候主動探查子線程是否執(zhí)行完成任務(wù),一種方式是查看子線程是否還活動(alive)t.isAlive(),如果還活動,那么可以選擇等待(t.join()),也可以繼續(xù)執(zhí)行它自己的任務(wù)?;蛘呷绻捎胑xecutorService.submit()方法來提交線程的話,會得到一個furture對象,用于檢測子線程是否完成(furture.isDone)。
2. 并列關(guān)系
并列關(guān)系是比較復(fù)雜的多線程之間的關(guān)系,因為會設(shè)計到多個線程對同一個共享資源的訪問,為了保證多個線程對同一個共享資源訪問不出現(xiàn)沖突,java設(shè)計了一整套的方法來保證。
2.1 原子操作
首先,如果多個線程對于一個資源的訪問過程都是一次性操作,而不存在操作過程中資源的中間狀態(tài),那么這樣的操作稱為原子操作,如果線程對于資源的操作都是原子操作,那么多個線程之間就不需要同步,因為其本身并不存在沖突,那么對哪些資源的操作是原子操作呢?java中對于基本類型以及對象的引用類型,以及被聲明為volatile的變量
2.2 操作同步
那么如果對于一個資源的訪問不是原子操作,而是帶有中間狀態(tài)的操作會怎樣呢?單以簡單的c++為例,不考慮虛擬機的操作,c++可以分解為一下3步:
1)獲取c的值
2)將獲取的值加1;
3)將新的值寫回到c
這樣的3步操作如果有多個線程同時進行,那么后果就不是我們能夠預(yù)料的了。那么如何保證其操作的安全有序呢?java給出的解決方案是采用加鎖的方式來產(chǎn)生排他性操作,即我要訪問某個資源,如果已經(jīng)被我占有了,那么我就會給它加上一把鎖,這樣,在我占有使用的過程中,讓其他線程無法使用,只有我用完了,把鎖釋放了后其他線程才能使用。那么如何加鎖呢?最普遍常見的方法是在方法上使用synchronized關(guān)鍵字。加上synchronized關(guān)鍵字的方法是在一個線程執(zhí)行過程中會對其他線程產(chǎn)生排他性操作。那么synchronized關(guān)鍵字是否給方法上了鎖呢?答案是是的,這里涉及到內(nèi)在鎖的概念,在java中,每一個對象都有一個內(nèi)在鎖與它關(guān)聯(lián),一個線程要想排他性的訪問一個對象的字段必須首先獲得對象的內(nèi)部鎖,一旦獲得了該對象的內(nèi)部鎖,在其釋放之前,其他線程是無法獲取到該鎖的。那么這個內(nèi)部鎖到底是什么呢,我們可以理解為其就是對象本身,所以我們只要鎖定對象本身,我們就獲得了對象的訪問權(quán),所以我們還可以顯式的去鎖定對象synchronized(this),這樣我們就可以更靈活的不去鎖定這個方法本身,而是鎖定方法中需要同步的某個代碼塊。進而我們還可以顯式的定義與每個字段關(guān)聯(lián)的對象鎖,方便對每個字段的排他性訪問而互不影響。
public class MsLunch {
PRivate long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
2.3 鎖
鎖單獨講,其所應(yīng)有的含義應(yīng)該是能加鎖和釋放鎖。java.util.concurrent.locks中的lock接口就給出了這樣的解釋。它本身包含lock()和unlock()方法。這樣,我們就可以在需要加鎖的地方顯式的進行加鎖,lock.lock(),用完之后顯式的釋放掉鎖lock.unlock(),java.util.concurrent.locks中給我們提供了兩種常用的鎖的實現(xiàn)。
2.3.1 重入鎖ReentrantLock
重入鎖是在線程可以再次獲取到它已經(jīng)擁有的鎖,即對對象進行二次加鎖。對象的內(nèi)部鎖也是支持重入的。
2.3.2 重入讀寫鎖ReentrantReadWriteLock重入讀寫鎖是對一個資源既存在讀操作又存在寫操作的情況下定義的鎖,該鎖實際上包含兩把鎖,讀鎖和寫鎖。讀鎖對其他的讀操作沒有排他性,但是寫鎖對于其他操作有排他性,也就是說當(dāng)獲取讀鎖的時候只要該資源沒有寫鎖就可以,但是當(dāng)獲取寫鎖的時候必須要當(dāng)前資源沒有鎖,否則該線程將會處于等待過程中。很顯然,讀寫鎖對于資源處于大多數(shù)讀操作少量寫操作的時候有很大的優(yōu)勢,反之,會降低程序的性能。
3.互補關(guān)系
當(dāng)兩個線程之間的執(zhí)行是后一個線程需要前一個線程為其提供條件,而后一個線程的執(zhí)行又為前一個線程的執(zhí)行提供保障,我稱之為互補關(guān)系,典型的例子是生產(chǎn)-消費者模型。消費者需要生產(chǎn)者為其提供產(chǎn)品,消費者同樣需要消費產(chǎn)品為生產(chǎn)者提供空間。這樣的兩個線程之間,雖然也存在對共同資源的訪問-產(chǎn)品存放空間,這個通過前述各種同步就能夠很好地解決,但是還有一個新的問題,就是當(dāng)生產(chǎn)者有了產(chǎn)品的時候如何通知消費者,同樣消費者消耗掉產(chǎn)品如何通知生產(chǎn)者繼續(xù)生產(chǎn),如果通過前述的方式,二者設(shè)置共享變量,那么就會存在生產(chǎn)者和消費者不斷地對變量進行輪詢(Guarded Blocks),從而消耗大量cpu資源,又二者不屬于主從關(guān)系,因此無法使用join,那么解決這個問題就引入了新的機制,等待-通知機制(wait-notify/notifyAll)。當(dāng)生產(chǎn)者發(fā)現(xiàn)生產(chǎn)空間已經(jīng)占滿,就處于等待狀態(tài)wait,程序?qū)⑸a(chǎn)者線程掛起,當(dāng)消費者取走產(chǎn)品釋放出空間的時候,就通知notify生產(chǎn)者去生產(chǎn)產(chǎn)品,同樣當(dāng)消費者發(fā)現(xiàn)沒有產(chǎn)品的時候,也處于等待狀態(tài)(wait),生產(chǎn)者將產(chǎn)品生產(chǎn)好以后,就通知(notify)消費者。采用顯式加鎖的方案是對鎖對象產(chǎn)生條件性(condition)等待,當(dāng)對生產(chǎn)空間進行加鎖lock后,生產(chǎn)者對于生產(chǎn)空間添加產(chǎn)品,發(fā)出非空信號(notEmpty.signal()),同時產(chǎn)生非滿(notFull=lock.newCondition())等待(notFull.await()),消費者對于生產(chǎn)空間產(chǎn)生非空(notEmpty=lock.newCondition())等待(notEmpty.await()),當(dāng)被喚醒后取走產(chǎn)品發(fā)出(notFull.signal())喚醒生產(chǎn)者。
新聞熱點
疑難解答