Java并發(fā)控制機(jī)制詳解

在一般性開發(fā)中氓辣,筆者經(jīng)常看到很多同學(xué)在對待java并發(fā)開發(fā)模型中只會(huì)使用一些基礎(chǔ)的方法棺亭。比如Volatile徽龟,synchronized。像Lock和atomic這類高級(jí)并發(fā)包很多人并不經(jīng)常使用广料。我想大部分原因都是來之于對原理的不屬性導(dǎo)致的砾脑。在繁忙的開發(fā)工作中,又有誰會(huì)很準(zhǔn)確的把握和使用正確的并發(fā)模型呢艾杏?

所以最近基于這個(gè)思想韧衣,本人打算把并發(fā)控制機(jī)制這部分整理成一篇文章。既是對自己掌握知識(shí)的一個(gè)回憶,也是希望這篇講到的類容能幫助到大部分開發(fā)者畅铭。

并行程序開發(fā)不可避免地要涉及多線程萧求、多任務(wù)的協(xié)作和數(shù)據(jù)共享等問題。在JDK中顶瞒,提供了多種途徑實(shí)現(xiàn)多線程間的并發(fā)控制夸政。比如常用的:內(nèi)部鎖、重入鎖榴徐、讀寫鎖和信號(hào)量守问。

Java內(nèi)存模型

在java中,每一個(gè)線程有一塊工作內(nèi)存區(qū)坑资,其中存放著被所有線程共享的主內(nèi)存中的變量的值的拷貝耗帕。當(dāng)線程執(zhí)行時(shí),它在自己的工作內(nèi)存中操作這些變量袱贮。

為了存取一個(gè)共享的變量仿便,一個(gè)線程通常先獲取鎖定并且清除它的工作內(nèi)存區(qū),這保證該共享變量從所有線程的共享內(nèi)存區(qū)正確地裝入到線程的工作內(nèi)存區(qū)攒巍,當(dāng)線程解鎖時(shí)保證該工作內(nèi)存區(qū)中變量的值協(xié)會(huì)到共享內(nèi)存中嗽仪。

當(dāng)一個(gè)線程使用某一個(gè)變量時(shí),不論程序是否正確地使用線程同步操作柒莉,它獲取的值一定是由它本身或者其他線程存儲(chǔ)到變量中的值闻坚。例如,如果兩個(gè)線程把不同的值或者對象引用存儲(chǔ)到同一個(gè)共享變量中兢孝,那么該變量的值要么是這個(gè)線程的窿凤,要么是那個(gè)線程的,共享變量的值不會(huì)是由兩個(gè)線程的引用值組合而成跨蟹。

一個(gè)變量時(shí)Java程序可以存取的一個(gè)地址雳殊,它不僅包括基本類型變量、引用類型變量窗轩,而且還包括數(shù)組類型變量夯秃。保存在主內(nèi)存區(qū)的變量可以被所有線程共享,但是一個(gè)線程存取另一個(gè)線程的參數(shù)或者局部變量時(shí)不可能的品姓,所以開發(fā)人員不必?fù)?dān)心局部變量的線程安全問題寝并。

volatile變量–多線程間可見

由于每個(gè)線程都有自己的工作內(nèi)存區(qū)箫措,因此當(dāng)一個(gè)線程改變自己的工作內(nèi)存中的數(shù)據(jù)時(shí)腹备,對其他線程來說,可能是不可見的斤蔓。為此植酥,可以使用volatile關(guān)鍵字破事所有線程軍讀寫內(nèi)存中的變量,從而使得volatile變量在多線程間可見。

聲明為volatile的變量可以做到如下保證:

1友驮、其他線程對變量的修改漂羊,可以及時(shí)反應(yīng)在當(dāng)前線程中;

2卸留、確保當(dāng)前線程對volatile變量的修改走越,能及時(shí)寫回到共享內(nèi)存中,并被其他線程所見耻瑟;

3旨指、使用volatile聲明的變量,編譯器會(huì)保證其有序性喳整。

同步關(guān)鍵字synchronized

同步關(guān)鍵字synchronized是Java語言中最為常用的同步方法之一谆构。在JDK早期版本中,synchronized的性能并不是太好框都,值適合于鎖競爭不是特別激烈的場合搬素。在JDK6中,synchronized和非公平鎖的差距已經(jīng)縮小魏保。更為重要的是熬尺,synchronized更為簡潔明了,代碼可讀性和維護(hù)性比較好谓罗。

鎖定一個(gè)對象的方法:

public synchronized void method(){}

當(dāng)method()方法被調(diào)用時(shí)猪杭,調(diào)用線程首先必須獲得當(dāng)前對象所,若當(dāng)前對象鎖被其他線程持有妥衣,這調(diào)用線程會(huì)等待皂吮,犯法結(jié)束后,對象鎖會(huì)被釋放税手,以上方法等價(jià)于下面的寫法:

publicvoidmethod(){

synchronized(this){

// do something …

}

}

其次蜂筹,使用synchronized還可以構(gòu)造同步塊,與同步方法相比芦倒,同步塊可以更為精確控制同步代碼范圍艺挪。一個(gè)小的同步代碼非常有離與鎖的快進(jìn)快出,從而使系統(tǒng)擁有更高的吞吐量兵扬。

publicvoidmethod(Object o){

// before

synchronized(o){

// do something ...

}

// after

}

synchronized也可以用于static函數(shù):

public synchronized static void method(){}

這個(gè)地方一定要注意麻裳,synchronized的鎖是加在當(dāng)前Class對象上,因此器钟,所有對該方法的調(diào)用津坑,都必須獲得Class對象的鎖。

雖然synchronized可以保證對象或者代碼段的線程安全傲霸,但是僅使用synchronized還是不足以控制擁有復(fù)雜邏輯的線程交互疆瑰。為了實(shí)現(xiàn)多線程間的交互眉反,還需要使用Object對象的wait()和notify()方法。

典型用法:

synchronized(obj){

while(){

obj.wait();

// 收到通知后穆役,繼續(xù)執(zhí)行寸五。

}

}

在使用wait()方法前,需要獲得對象鎖耿币。在wait()方法執(zhí)行時(shí)梳杏,當(dāng)前線程或釋放obj的獨(dú)占鎖,供其他線程使用淹接。

當(dāng)?shù)却趏bj上線程收到obj.notify()時(shí)秘狞,它就能重新獲得obj的獨(dú)占鎖,并繼續(xù)運(yùn)行蹈集。注意了烁试,notify()方法是隨機(jī)喚起等待在當(dāng)前對象的某一個(gè)線程。

下面是一個(gè)阻塞隊(duì)列的實(shí)現(xiàn):

publicclassBlockQueue{

privateList list =newArrayList();

publicsynchronizedObject pop()throwsInterruptedException{

while(list.size()==0){

this.wait();

}

if(list.size()>0){

returnlist.remove(0);

}else{

returnnull;

}

}

publicsynchronizedObject put(Object obj){

list.add(obj);

this.notify();

}

}

synchronized配合wait()拢肆、notify()應(yīng)該是Java開發(fā)者必須掌握的基本技能减响。

Reentrantlock重入鎖

Reentrantlock稱為重入鎖。它比synchronized擁有更加強(qiáng)大的功能郭怪,它可以中斷支示、可定時(shí)。在高并發(fā)的情況下鄙才,它比synchronized有明顯的性能優(yōu)勢颂鸿。

Reentrantlock提供了公平和非公平兩種鎖。公平鎖是對鎖的獲取是先進(jìn)先出攒庵,而非公平鎖是可以插隊(duì)的嘴纺。當(dāng)然從性能上分析,非公平鎖的性能要好得多浓冒。因此栽渴,在無特殊需要,應(yīng)該優(yōu)選非公平鎖稳懒,但是synchronized提供鎖業(yè)不是絕對公平的闲擦。Reentrantlock在構(gòu)造的時(shí)候可以指定鎖是否公平。

在使用重入鎖時(shí)场梆,一定要在程序最后釋放鎖墅冷。一般釋放鎖的代碼要寫在finally里。否則或油,如果程序出現(xiàn)異常寞忿,Loack就永遠(yuǎn)無法釋放了。synchronized的鎖是JVM最后自動(dòng)釋放的装哆。

經(jīng)典使用方式如下:

try{

if(lock.tryLock(5, TimeUnit.SECONDS)) {//如果已經(jīng)被lock罐脊,嘗試等待5s定嗓,看是否可以獲得鎖蜕琴,如果5s后仍然無法獲得鎖則返回false繼續(xù)執(zhí)行

// lock.lockInterruptibly();可以響應(yīng)中斷事件

try{

//操作

}finally{

lock.unlock();

}

}

}catch(InterruptedException e) {

e.printStackTrace();//當(dāng)前線程被中斷時(shí)(interrupt)萍桌,會(huì)拋InterruptedException

}

Reentrantlock提供了非常豐富的鎖控制功能,靈活應(yīng)用這些控制方法凌简,可以提高應(yīng)用程序的性能上炎。不過這里并非是極力推薦使用Reentrantlock。重入鎖算是JDK中提供的高級(jí)開發(fā)工具雏搂。

ReadWriteLock讀寫鎖

讀寫分離是一種非常常見的數(shù)據(jù)處理思想藕施。在sql中應(yīng)該算是必須用到的技術(shù)。ReadWriteLock是在JDK5中提供的讀寫分離鎖凸郑。讀寫分離鎖可以有效地幫助減少鎖競爭裳食,以提升系統(tǒng)性能。讀寫分離使用場景主要是如果在系統(tǒng)中芙沥,讀操作次數(shù)遠(yuǎn)遠(yuǎn)大于寫操作诲祸。使用方式如下:

privateReentrantReadWriteLock readWriteLock =newReentrantReadWriteLock();

privateLock readLock = readWriteLock.readLock();

privateLock writeLock = readWriteLock.writeLock();

publicObject handleRead()throwsInterruptedException {

try{

readLock.lock();

Thread.sleep(1000);

returnvalue;

}finally{

readLock.unlock();

}

}

publicObject handleRead()throwsInterruptedException {

try{

writeLock.lock();

Thread.sleep(1000);

returnvalue;

}finally{

writeLock.unlock();

}

}

Condition對象

Conditiond對象用于協(xié)調(diào)多線程間的復(fù)雜協(xié)作。主要與鎖相關(guān)聯(lián)而昨。通過Lock接口中的newCondition()方法可以生成一個(gè)與Lock綁定的Condition實(shí)例救氯。Condition對象和鎖的關(guān)系就如用Object.wait()、Object.notify()兩個(gè)函數(shù)以及synchronized關(guān)鍵字一樣歌憨。

這里可以把ArrayBlockingQueue的源碼摘出來看一下:

publicclassArrayBlockingQueueextendsAbstractQueue

implementsBlockingQueue, java.io.Serializable {

/** Main lock guarding all access */

finalReentrantLock lock;

/** Condition for waiting takes */

privatefinalCondition notEmpty;

/** Condition for waiting puts */

privatefinalCondition notFull;

publicArrayBlockingQueue(intcapacity,booleanfair) {

if(capacity <=0)

thrownewIllegalArgumentException();

this.items =newObject[capacity];

lock =newReentrantLock(fair);

notEmpty = lock.newCondition();// 生成與Lock綁定的Condition

notFull = lock.newCondition();

}

publicvoidput(E e)throwsInterruptedException {

checkNotNull(e);

finalReentrantLock lock =this.lock;

lock.lockInterruptibly();

try{

while(count == items.length)

notFull.await();

insert(e);

}finally{

lock.unlock();

}

}

privatevoidinsert(E x) {

items[putIndex] = x;

putIndex = inc(putIndex);

++count;

notEmpty.signal();// 通知

}

publicE take()throwsInterruptedException {

finalReentrantLock lock =this.lock;

lock.lockInterruptibly();

try{

while(count ==0)// 如果隊(duì)列為空

notEmpty.await();// 則消費(fèi)者隊(duì)列要等待一個(gè)非空的信號(hào)

returnextract();

}finally{

lock.unlock();

}

}

privateE extract() {

finalObject[] items =this.items;

E x =this.cast(items[takeIndex]);

items[takeIndex] =null;

takeIndex = inc(takeIndex);

--count;

notFull.signal();// 通知put() 線程隊(duì)列已有空閑空間

returnx;

}

// other code

}

Semaphore信號(hào)量

信號(hào)量為多線程協(xié)作提供了更為強(qiáng)大的控制方法着憨。信號(hào)量是對鎖的擴(kuò)展。無論是內(nèi)部鎖synchronized還是重入鎖ReentrantLock务嫡,一次都允許一個(gè)線程訪問一個(gè)資源甲抖,而信號(hào)量卻可以指定多個(gè)線程同時(shí)訪問某一個(gè)資源。從構(gòu)造函數(shù)可以看出:

public Semaphore(int permits) {}

public Semaphore(int permits, boolean fair){} // 可以指定是否公平

permits指定了信號(hào)量的準(zhǔn)入書心铃,也就是同時(shí)能申請多少個(gè)許可惧眠。當(dāng)每個(gè)線程每次只申請一個(gè)許可時(shí),這就相當(dāng)于指定了同時(shí)有多少個(gè)線程可以訪問某一個(gè)資源于个。這里羅列一下主要方法的使用:

public void acquire() throws InterruptedException {} //嘗試獲得一個(gè)準(zhǔn)入的許可氛魁。若無法獲得,則線程會(huì)等待厅篓,知道有線程釋放一個(gè)許可或者當(dāng)前線程被中斷秀存。

public void acquireUninterruptibly(){} // 類似于acquire(),但是不會(huì)響應(yīng)中斷羽氮。

public boolean tryAcquire(){} // 嘗試獲取或链,如果成功則為true,否則false档押。這個(gè)方法不會(huì)等待澳盐,立即返回祈纯。

public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {} // 嘗試等待多長時(shí)間

public void release() //用于在現(xiàn)場訪問資源結(jié)束后,釋放一個(gè)許可叼耙,以使其他等待許可的線程可以進(jìn)行資源訪問腕窥。

下面來看一下JDK文檔中提供使用信號(hào)量的實(shí)例。這個(gè)實(shí)例很好的解釋了如何通過信號(hào)量控制資源訪問筛婉。

publicclassPool {

privatestaticfinalintMAX_AVAILABLE =100;

privatefinalSemaphore available =newSemaphore(MAX_AVAILABLE,true);

publicObject getItem()throwsInterruptedException {

available.acquire();

// 申請一個(gè)許可

// 同時(shí)只能有100個(gè)線程進(jìn)入取得可用項(xiàng)簇爆,

// 超過100個(gè)則需要等待

returngetNextAvailableItem();

}

publicvoidputItem(Object x) {

// 將給定項(xiàng)放回池內(nèi),標(biāo)記為未被使用

if(markAsUnused(x)) {

available.release();

// 新增了一個(gè)可用項(xiàng)爽撒,釋放一個(gè)許可入蛆,請求資源的線程被激活一個(gè)

}

}

// 僅作示例參考,非真實(shí)數(shù)據(jù)

protectedObject[] items =newObject[MAX_AVAILABLE];// 用于對象池復(fù)用對象

protectedboolean[] used =newboolean[MAX_AVAILABLE];// 標(biāo)記作用

protectedsynchronizedObject getNextAvailableItem() {

for(inti =0; i < MAX_AVAILABLE; ++i) {

if(!used[i]) {

used[i] =true;

returnitems[i];

}

}

returnnull;

}

protectedsynchronizedbooleanmarkAsUnused(Object item) {

for(inti =0; i < MAX_AVAILABLE; ++i) {

if(item == items[i]) {

if(used[i]) {

used[i] =false;

returntrue;

}else{

returnfalse;

}

}

}

returnfalse;

}

}

此實(shí)例簡單實(shí)現(xiàn)了一個(gè)對象池硕勿,對象池最大容量為100哨毁。因此,當(dāng)同時(shí)有100個(gè)對象請求時(shí)源武,對象池就會(huì)出現(xiàn)資源短缺扼褪,未能獲得資源的線程就需要等待。當(dāng)某個(gè)線程使用對象完畢后软能,就需要將對象返回給對象池迎捺。此時(shí),由于可用資源增加查排,因此凳枝,可以激活一個(gè)等待該資源的線程。

ThreadLocal線程局部變量

在剛開始接觸ThreadLocal跋核,筆者很難理解這個(gè)線程局部變量的使用場景岖瑰。當(dāng)現(xiàn)在回過頭去看,ThreadLocal是一種多線程間并發(fā)訪問變量的解決方案砂代。與synchronized等加鎖的方式不同蹋订,ThreadLocal完全不提供鎖,而使用了以空間換時(shí)間的手段刻伊,為每個(gè)線程提供變量的獨(dú)立副本露戒,以保障線程安全,因此它不是一種數(shù)據(jù)共享的解決方案捶箱。

ThreadLocal是解決線程安全問題一個(gè)很好的思路智什,ThreadLocal類中有一個(gè)Map,用于存儲(chǔ)每一個(gè)線程的變量副本丁屎,Map中元素的鍵為線程對象荠锭,而值對應(yīng)線程的變量副本,由于Key值不可重復(fù)晨川,每一個(gè)“線程對象”對應(yīng)線程的“變量副本”证九,而到達(dá)了線程安全删豺。

特別值得注意的地方,從性能上說愧怜,ThreadLocal并不具有絕對的又是呀页,在并發(fā)量不是很高時(shí),也行加鎖的性能會(huì)更好叫搁。但作為一套與鎖完全無關(guān)的線程安全解決方案赔桌,在高并發(fā)量或者所競爭激烈的場合供炎,使用ThreadLocal可以在一定程度上減少鎖競爭渴逻。

下面是一個(gè)ThreadLocal的簡單使用:

publicclassTestNum {

// 通過匿名內(nèi)部類覆蓋ThreadLocal的initialValue()方法,指定初始值

privatestaticThreadLocal seqNum =newThreadLocal() {

publicInteger initialValue() {

return0;

}

};

// 獲取下一個(gè)序列值

publicintgetNextNum() {

seqNum.set(seqNum.get() +1);

returnseqNum.get();

}publicstaticvoidmain(String[] args) {

TestNum sn =newTestNum();

//3個(gè)線程共享sn音诫,各自產(chǎn)生序列號(hào)

TestClient t1 =newTestClient(sn);

TestClient t2 =newTestClient(sn);

TestClient t3 =newTestClient(sn);

t1.start();

t2.start();

t3.start();

}

privatestaticclassTestClientextendsThread {

privateTestNum sn;

publicTestClient(TestNum sn) {

this.sn = sn;

}

publicvoidrun() {

for(inti =0; i <3; i++) {

// 每個(gè)線程打出3個(gè)序列值

System.out.println("thread["+ Thread.currentThread().getName() +"] --> sn["

+ sn.getNextNum() +"]");

}

}

}

}

輸出結(jié)果:

thread[Thread-0] –> sn[1]

thread[Thread-1] –> sn[1]

thread[Thread-2] –> sn[1]

thread[Thread-1] –> sn[2]

thread[Thread-0] –> sn[2]

thread[Thread-1] –> sn[3]

thread[Thread-2] –> sn[2]

thread[Thread-0] –> sn[3]

thread[Thread-2] –> sn[3]

輸出的結(jié)果信息可以發(fā)現(xiàn)每個(gè)線程所產(chǎn)生的序號(hào)雖然都共享同一個(gè)TestNum實(shí)例惨奕,但它們并沒有發(fā)生相互干擾的情況,而是各自產(chǎn)生獨(dú)立的序列號(hào)竭钝,這是因?yàn)門hreadLocal為每一個(gè)線程提供了單獨(dú)的副本梨撞。

鎖的性能和優(yōu)化

“鎖”是最常用的同步方法之一。在平常開發(fā)中香罐,經(jīng)常能看到很多同學(xué)直接把鎖加很大一段代碼上卧波。還有的同學(xué)只會(huì)用一種鎖方式解決所有共享問題。顯然這樣的編碼是讓人無法接受的庇茫。特別的在高并發(fā)的環(huán)境下港粱,激烈的鎖競爭會(huì)導(dǎo)致程序的性能下降德更加明顯。因此合理使用鎖對程序的性能直接相關(guān)旦签。

1查坪、線程的開銷

在多核情況下,使用多線程可以明顯提高系統(tǒng)的性能宁炫。但是在實(shí)際情況中偿曙,使用多線程的方式會(huì)額外增加系統(tǒng)的開銷。相對于單核系統(tǒng)任務(wù)本身的資源消耗外羔巢,多線程應(yīng)用還需要維護(hù)額外多線程特有的信息望忆。比如,線程本身的元數(shù)據(jù)竿秆,線程調(diào)度启摄,線程上下文的切換等。

2袍辞、減小鎖持有時(shí)間在使用鎖進(jìn)行并發(fā)控制的程序中鞋仍,當(dāng)鎖發(fā)生競爭時(shí),單個(gè)線程對鎖的持有時(shí)間與系統(tǒng)性能有著直接的關(guān)系搅吁。如果線程持有鎖的時(shí)間很長威创,那么相對地落午,鎖的競爭程度也就越激烈。因此肚豺,在程序開發(fā)過程中溃斋,應(yīng)該盡可能地減少對某個(gè)鎖的占有時(shí)間,以減少線程間互斥的可能吸申。比如下面這一段代碼:

publicsynchronizedvoidsyncMehod(){

beforeMethod();

mutexMethod();

afterMethod();

}

此實(shí)例如果只有mutexMethod()方法是有同步需要的梗劫,而在beforeMethod(),和afterMethod()并不需要做同步控制。如果beforeMethod(),和afterMethod()分別是重量級(jí)的方法截碴,則會(huì)花費(fèi)較長的CPU時(shí)間梳侨。在這個(gè)時(shí)候,如果并發(fā)量較大時(shí)日丹,使用這種同步方案會(huì)導(dǎo)致等待線程大量增加走哺。因?yàn)楫?dāng)前執(zhí)行的線程只有在執(zhí)行完所有任務(wù)后,才會(huì)釋放鎖哲虾。

下面是優(yōu)化后的方案丙躏,只在必要的時(shí)候進(jìn)行同步,這樣就能明顯減少線程持有鎖的時(shí)間束凑,提高系統(tǒng)的吞吐量晒旅。代碼如下:

publicvoidsyncMehod(){

beforeMethod();

synchronized(this){

mutexMethod();

}

afterMethod();

}

3、減少鎖粒度

減小鎖粒度也是一種削弱多線程鎖競爭的一種有效手段汪诉,這種技術(shù)典型的使用場景就是ConcurrentHashMap這個(gè)類废恋。在普通的HashMap中每當(dāng)對集合進(jìn)行add()操作或者get()操作時(shí),總是獲得集合對象的鎖摩瞎。這種操作完全是一種同步行為拴签,因?yàn)殒i是在整個(gè)集合對象上的,因此旗们,在高并發(fā)時(shí)蚓哩,激烈的鎖競爭會(huì)影響到系統(tǒng)的吞吐量。

如果看過源碼的同學(xué)應(yīng)該知道HashMap是數(shù)組+鏈表的方式做實(shí)現(xiàn)的上渴。ConcurrentHashMap在HashMap的基礎(chǔ)上將整個(gè)HashMap分成若干個(gè)段(Segment)岸梨,每個(gè)段都是一個(gè)子HashMap。如果需要在增加一個(gè)新的表項(xiàng)稠氮,并不是將這個(gè)HashMap加鎖曹阔,二十搜線根據(jù)hashcode得到該表項(xiàng)應(yīng)該被存放在哪個(gè)段中,然后對該段加鎖隔披,并完成put()操作赃份。這樣,在多線程環(huán)境中,如果多個(gè)線程同時(shí)進(jìn)行寫入操作抓韩,只要被寫入的項(xiàng)不存在同一個(gè)段中纠永,那么線程間便可以做到真正的并行。具體的實(shí)現(xiàn)希望讀者自己花點(diǎn)時(shí)間讀一讀ConcurrentHashMap這個(gè)類的源碼谒拴,這里就不再做過多描述了尝江。

4、鎖分離

在前面提起過ReadWriteLock讀寫鎖英上,那么讀寫分離的延伸就是鎖的分離炭序。同樣可以在JDK中找到鎖分離的源碼LinkedBlockingQueue。

publicclassLinkedBlockingQueueextendsAbstractQueue

implementsBlockingQueue, java.io.Serializable {

/* Lock held by take, poll, etc /

private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */

private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */

private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */

private final Condition notFull = putLock.newCondition();

public E take() throws InterruptedException {

E x;

int c = -1;

final AtomicInteger count = this.count;

final ReentrantLock takeLock = this.takeLock;

takeLock.lockInterruptibly(); // 不能有兩個(gè)線程同時(shí)讀取數(shù)據(jù)

try {

while (count.get() == 0) { // 如果當(dāng)前沒有可用數(shù)據(jù)苍日,一直等待put()的通知

notEmpty.await();

}

x = dequeue(); // 從頭部移除一項(xiàng)

c = count.getAndDecrement(); // size減1

if (c > 1)

notEmpty.signal(); // 通知其他take()操作

} finally {

takeLock.unlock(); // 釋放鎖

}

if (c == capacity)

signalNotFull(); // 通知put()操作惭聂,已有空余空間

return x;

}

public void put(E e) throws InterruptedException {

if (e == null) throw new NullPointerException();

// Note: convention in all put/take/etc is to preset local var

// holding count negative to indicate failure unless set.

int c = -1;

Node node = new Node(e);

final ReentrantLock putLock = this.putLock;

final AtomicInteger count = this.count;

putLock.lockInterruptibly(); // 不能有兩個(gè)線程同時(shí)put數(shù)據(jù)

try {

/*

* Note that count is used in wait guard even though it is

* not protected by lock. This works because count can

* only decrease at this point (all other puts are shut

* out by lock), and we (or some other waiting put) are

* signalled if it ever changes from capacity. Similarly

* for all other uses of count in other wait guards.

*/

while(count.get() == capacity) {// 隊(duì)列滿了 則等待

notFull.await();

}

enqueue(node);// 加入隊(duì)列

c = count.getAndIncrement();// size加1

if(c +1< capacity)

notFull.signal();// 如果有足夠空間,通知其他線程

}finally{

putLock.unlock();// 釋放鎖

}

if(c ==0)

signalNotEmpty();// 插入成功后易遣,通知take()操作讀取數(shù)據(jù)

}

// other code

}

這里需要說明一下的就是彼妻,take()和put()函數(shù)是相互獨(dú)立的嫌佑,它們之間不存在鎖競爭關(guān)系豆茫。只需要在take()和put()各自方法內(nèi)部分別對takeLock和putLock發(fā)生競爭。從而屋摇,削弱了鎖競爭的可能性揩魂。

5、鎖粗化

上面說到的減小鎖時(shí)間和粒度炮温,這樣做就是為了滿足每個(gè)線程持有鎖的時(shí)間盡量短火脉。但是,在粒度上應(yīng)該把握一個(gè)度柒啤,如果對用一個(gè)鎖不停地進(jìn)行請求倦挂、同步和釋放,其本身也會(huì)消耗系統(tǒng)寶貴的資源担巩,反而加大了系統(tǒng)開銷方援。

我們需要知道的是,虛擬機(jī)在遇到一連串連續(xù)的對同一鎖不斷進(jìn)行請求和釋放的操作時(shí)涛癌,便會(huì)把所有的鎖操作整合成對鎖的一次請求犯戏,從而減少對鎖的請求同步次數(shù),這樣的操作叫做鎖的粗化拳话。下面是一段整合實(shí)例演示:

publicvoidsyncMehod(){

synchronized(lock){

method1();

}

synchronized(lock){

method2();

}

}

JVM整合后的形式:

publicvoidsyncMehod(){

synchronized(lock){

method1();

method2();

}

}

因此先匪,這樣的整合給我們開發(fā)人員對鎖粒度的把握給出了很好的演示作用。

無鎖的并行計(jì)算

上面花了很大篇幅在說鎖的事情弃衍,同時(shí)也提到過鎖是會(huì)帶來一定的上下文切換的額外資源開銷呀非,在高并發(fā)時(shí),”鎖“的激烈競爭可能會(huì)成為系統(tǒng)瓶頸镜盯。因此岸裙,這里可以使用一種非阻塞同步方法坦冠。這種無鎖方式依然能保證數(shù)據(jù)和程序在高并發(fā)環(huán)境下保持多線程間的一致性。

1哥桥、非阻塞同步/無鎖

非阻塞同步方式其實(shí)在前面的ThreadLocal中已經(jīng)有所體現(xiàn)辙浑,每個(gè)線程擁有各自獨(dú)立的變量副本,因此在并行計(jì)算時(shí)拟糕,無需相互等待判呕。這里筆者主要推薦一種更為重要的、基于比較并交換(Compare And Swap)CAS算法的無鎖并發(fā)控制方法送滞。

CAS算法的過程:它包含3個(gè)參數(shù)CAS(V,E,N)侠草。V表示要更新的變量,E表示預(yù)期值犁嗅,N表示新值边涕。僅當(dāng)V值等于E值時(shí),才會(huì)將V的值設(shè)為N褂微,如果V值和E值不同功蜓,則說明已經(jīng)有其他線程做了更新,則當(dāng)前線程什么都不做宠蚂。最后CAS返回當(dāng)前V的真實(shí)值式撼。CAS操作時(shí)抱著樂觀的態(tài)度進(jìn)行的,它總是認(rèn)為自己可以成功完成操作求厕。當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí)著隆,只有一個(gè)會(huì)勝出,并成功更新呀癣,其余俊輝失敗美浦。失敗的線程不會(huì)被掛起,僅是被告知失敗项栏,并且允許再次嘗試浦辨,當(dāng)然也允許失敗的線程放棄操作⊥担基于這樣的原理荤牍,CAS操作及時(shí)沒有鎖,也可以發(fā)現(xiàn)其他線程對當(dāng)前線程的干擾庆冕,并且進(jìn)行恰當(dāng)?shù)奶幚怼?/p>

2康吵、原子量操作

JDK的java.util.concurrent.atomic包提供了使用無鎖算法實(shí)現(xiàn)的原子操作類,代碼內(nèi)部主要使用了底層native代碼的實(shí)現(xiàn)访递。有興趣的同學(xué)可以繼續(xù)跟蹤一下native層面的代碼晦嵌。這里就不貼表層的代碼實(shí)現(xiàn)了。

下面主要以一個(gè)例子來展示普通同步方法和無鎖同步的性能差距:

publicclassTestAtomic {

privatestaticfinalintMAX_THREADS =3;

privatestaticfinalintTASK_COUNT =3;

privatestaticfinalintTARGET_COUNT =100*10000;

privateAtomicInteger acount =newAtomicInteger(0);

privateintcount =0;

synchronizedintinc() {

return++count;

}

synchronizedintgetCount() {

returncount;

}

publicclassSyncThreadimplementsRunnable {

String name;

longstartTime;

TestAtomic out;

publicSyncThread(TestAtomic o,longstartTime) {

this.out = o;

this.startTime = startTime;

}

@Override

publicvoidrun() {

intv = out.inc();

while(v < TARGET_COUNT) {

v = out.inc();

}

longendTime = System.currentTimeMillis();

System.out.println("SyncThread spend:"+ (endTime - startTime) +"ms"+", v="+ v);

}

}

publicclassAtomicThreadimplementsRunnable {

String name;

longstartTime;

publicAtomicThread(longstartTime) {

this.startTime = startTime;

}

@Override

publicvoidrun() {

intv = acount.incrementAndGet();

while(v < TARGET_COUNT) {

v = acount.incrementAndGet();

}

longendTime = System.currentTimeMillis();

System.out.println("AtomicThread spend:"+ (endTime - startTime) +"ms"+", v="+ v);

}

}

@Test

publicvoidtestSync()throwsInterruptedException {

ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);

longstartTime = System.currentTimeMillis();

SyncThread sync =newSyncThread(this, startTime);

for(inti =0; i < TASK_COUNT; i++) {

exe.submit(sync);

}

Thread.sleep(10000);

}

@Test

publicvoidtestAtomic()throwsInterruptedException {

ExecutorService exe = Executors.newFixedThreadPool(MAX_THREADS);

longstartTime = System.currentTimeMillis();

AtomicThread atomic =newAtomicThread(startTime);

for(inti =0; i < TASK_COUNT; i++) {

exe.submit(atomic);

}

Thread.sleep(10000);

}

}

測試結(jié)果如下:

testSync():

SyncThread spend:201ms, v=1000002

SyncThread spend:201ms, v=1000000

SyncThread spend:201ms, v=1000001

testAtomic():

AtomicThread spend:43ms, v=1000000

AtomicThread spend:44ms, v=1000001

AtomicThread spend:46ms, v=1000002

相信這樣的測試結(jié)果將內(nèi)部鎖和非阻塞同步算法的性能差異體現(xiàn)的非常明顯。因此筆者更推薦直接視同atomic下的這個(gè)原子類惭载。

結(jié)束語

終于把想表達(dá)的這些東西整理完成了旱函,其實(shí)還有一些想CountDownLatch這樣的類沒有講到。不過上面的所講到的絕對是并發(fā)編程中的核心描滔。也許有些讀者朋友能在網(wǎng)上看到很多這樣的知識(shí)點(diǎn)棒妨,但是個(gè)人還是覺得知識(shí)只有在對比的基礎(chǔ)上才能找到它合適的使用場景。因此含长,這也是小編整理這篇文章的原因券腔,也希望這篇文章能幫到更多的同學(xué)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拘泞,一起剝皮案震驚了整個(gè)濱河市纷纫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌陪腌,老刑警劉巖辱魁,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诗鸭,居然都是意外死亡染簇,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門只泼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來剖笙,“玉大人,你說我怎么就攤上這事请唱。” “怎么了过蹂?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵十绑,是天一觀的道長。 經(jīng)常有香客問我酷勺,道長本橙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任脆诉,我火速辦了婚禮甚亭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘击胜。我一直安慰自己亏狰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布偶摔。 她就那樣靜靜地躺著暇唾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上策州,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天瘸味,我揣著相機(jī)與錄音,去河邊找鬼够挂。 笑死旁仿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孽糖。 我是一名探鬼主播丁逝,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼梭姓!你這毒婦竟也來了霜幼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤誉尖,失蹤者是張志新(化名)和其女友劉穎罪既,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铡恕,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琢感,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了探熔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驹针。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖诀艰,靈堂內(nèi)的尸體忽然破棺而出柬甥,到底是詐尸還是另有隱情,我是刑警寧澤其垄,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布苛蒲,位于F島的核電站,受9級(jí)特大地震影響绿满,放射性物質(zhì)發(fā)生泄漏臂外。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一喇颁、第九天 我趴在偏房一處隱蔽的房頂上張望漏健。 院中可真熱鬧,春花似錦橘霎、人聲如沸蔫浆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽克懊。三九已至忱辅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谭溉,已是汗流浹背墙懂。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扮念,地道東北人损搬。 一個(gè)月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像柜与,于是被迫代替她去往敵國和親巧勤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容