鎖機(jī)制:synchronized、Lock、Condition

1、synchronized

把代碼塊聲明為?synchronized,有兩個(gè)重要后果,通常是指該代碼具有?原子性(atomicity)和?可見性(visibility)。

1.1 原子性

原子性意味著個(gè)時(shí)刻,只有一個(gè)線程能夠執(zhí)行一段代碼,這段代碼通過一個(gè)monitor object保護(hù)。從而防止多個(gè)線程在更新共享狀態(tài)時(shí)相互沖突。

1.2 可見性

可見性則更為微妙,它要對付內(nèi)存緩存和編譯器優(yōu)化的各種反常行為。它必須確保釋放鎖之前對共享數(shù)據(jù)做出的更改對于隨后獲得該鎖的另一個(gè)線程是可見的 。

作用:如果沒有同步機(jī)制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發(fā)許多嚴(yán)重問題。

原理:當(dāng)對象獲取鎖時(shí),它首先使自己的高速緩存無效,這樣就可以保證直接從主內(nèi)存中裝入變量。 同樣,在對象釋放鎖之前,它會刷新其高速緩存,強(qiáng)制使已做的任何更改都出現(xiàn)在主內(nèi)存中。 這樣,會保證在同一個(gè)鎖上同步的兩個(gè)線程看到在 synchronized 塊內(nèi)修改的變量的相同值。

一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優(yōu)化),不受緩存變量值的約束,但是如果開發(fā)人員使用了同步,那么運(yùn)行庫將確保某一線程對變量所做的更新先于對現(xiàn)有synchronized?塊所進(jìn)行的更新,當(dāng)進(jìn)入由同一監(jiān)控器(lock)保護(hù)的另一個(gè)synchronized塊時(shí),將立刻可以看到這些對變量所做的更新。類似的規(guī)則也存在于volatile變量上。

——volatile只保證可見性,不保證原子性!

1.3 何時(shí)要同步?

可見性同步的基本規(guī)則是在以下情況中必須同步:?

讀取上一次可能是由另一個(gè)線程寫入的變量?

寫入下一次可能由另一個(gè)線程讀取的變量

一致性同步:當(dāng)修改多個(gè)相關(guān)值時(shí),您想要其它線程原子地看到這組更改—— 要么看到全部更改,要么什么也看不到。

這適用于相關(guān)數(shù)據(jù)項(xiàng)(如粒子的位置和速率)和元數(shù)據(jù)項(xiàng)(如鏈表中包含的數(shù)據(jù)值和列表自身中的數(shù)據(jù)項(xiàng)的鏈)。

在某些情況中,您不必用同步來將數(shù)據(jù)從一個(gè)線程傳遞到另一個(gè),因?yàn)?JVM 已經(jīng)隱含地為您執(zhí)行同步。這些情況包括:

由靜態(tài)初始化器(在靜態(tài)字段上或 static{} 塊中的初始化器)

初始化數(shù)據(jù)時(shí)?

訪問 final 字段時(shí) ——final對象呢?

在創(chuàng)建線程之前創(chuàng)建對象時(shí)?

線程可以看見它將要處理的對象時(shí)

1.4 synchronize的限制

synchronized是不錯(cuò),但它并不完美。它有一些功能性的限制:

它無法中斷一個(gè)正在等候獲得鎖的線程;

也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖;

同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進(jìn)行,多數(shù)情況下,這沒問題(而且與異常處理交互得很好),但是,確實(shí)存在一些非塊結(jié)構(gòu)的鎖定更合適的情況。

2、ReentrantLock

java.util.concurrent.lock?中的Lock?框架是鎖定的一個(gè)抽象,它允許把鎖定的實(shí)現(xiàn)作為 Java 類,而不是作為語言的特性來實(shí)現(xiàn)。這就為Lock?的多種實(shí)現(xiàn)留下了空間,各種實(shí)現(xiàn)可能有不同的調(diào)度算法、性能特性或者鎖定語義。

ReentrantLock?類實(shí)現(xiàn)了Lock?,它擁有與synchronized?相同的并發(fā)性和內(nèi)存語義,但是添加了類似鎖投票定時(shí)鎖等候可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當(dāng)許多線程都想訪問共享資源時(shí),JVM 可以花更少的時(shí)候來調(diào)度線程,把更多時(shí)間用在執(zhí)行線程上。)

class?Outputter1?{????

private?Lock?lock?=?new?ReentrantLock();//?鎖對象? ??

public?void?output(String?name)?{???????????

lock.lock();//?得到鎖? ??

try?{????

for(int?i?=?0;?i?<?name.length();?i++)?{????

????????????????System.out.print(name.charAt(i));????

????????????}????

}finally?{????

lock.unlock();//?釋放鎖????

????????}????

????}????

}????

區(qū)別:

需要注意的是,用sychronized修飾的方法或者語句塊在代碼執(zhí)行完之后鎖自動釋放,而是用Lock需要我們手動釋放鎖,所以為了保證鎖最終被釋放(發(fā)生異常情況),要把互斥區(qū)放在try內(nèi),釋放鎖放在finally內(nèi)?。?/p>

3、讀寫鎖ReadWriteLock

上例中展示的是和synchronized相同的功能,那Lock的優(yōu)勢在哪里?

例如一個(gè)類對其內(nèi)部共享數(shù)據(jù)data提供了get()和set()方法,如果用synchronized,則代碼如下:

class?syncData?{????????

private?int?data;//?共享數(shù)據(jù)????????

public?synchronized?void?set(int?data)?{????

System.out.println(Thread.currentThread().getName()?+"準(zhǔn)備寫入數(shù)據(jù)");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????e.printStackTrace();????

????????}????

this.data?=?data;????

System.out.println(Thread.currentThread().getName()?+"寫入"?+?this.data);????

????}???????

public?synchronized??void?get()?{????

System.out.println(Thread.currentThread().getName()?+"準(zhǔn)備讀取數(shù)據(jù)");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????e.printStackTrace();????

????????}????

System.out.println(Thread.currentThread().getName()?+"讀取"?+?this.data);????

????}????

}????

然后寫個(gè)測試類來用多個(gè)線程分別讀寫這個(gè)共享數(shù)據(jù):

public?static?void?main(String[]?args)?{????

//????????final?Data?data?=?new?Data();????

final?syncData?data?=?new?syncData();????

//????????final?RwLockData?data?=?new?RwLockData();????


//寫入??

for?(int?i?=?0;?i?<?3;?i++)?{????

Thread?t?=new?Thread(new?Runnable()?{????

@Override??

public?void?run()?{????

for?(int?j?=?0;?j?<?5;?j++)?{????

data.set(new?Random().nextInt(30));????

????????????????????}????

????????????????}????

????????????});??

t.setName("Thread-W"?+?i);??

????????????t.start();??

????????}????

//讀取??

for?(int?i?=?0;?i?<?3;?i++)?{????

Thread?t?=new?Thread(new?Runnable()?{????

@Override??

public?void?run()?{????

for?(int?j?=?0;?j?<?5;?j++)?{????

????????????????????????data.get();????

????????????????????}????

????????????????}????

????????????});????

t.setName("Thread-R"?+?i);??

????????????t.start();??

????????}????

????}????

運(yùn)行結(jié)果:

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入0??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)?//R0和R2可以同時(shí)讀取,不應(yīng)該互斥!??

Thread-R0讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入18??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入16??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入19??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入21??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入4??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入10??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入4??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入1??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入14??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入2??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入4??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入20??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入29??

現(xiàn)在一切都看起來很好!各個(gè)線程互不干擾!等等。。讀取線程和寫入線程互不干擾是正常的,但是兩個(gè)讀取線程是否需要互不干擾??

對!讀取線程不應(yīng)該互斥!

我們可以用讀寫鎖ReadWriteLock實(shí)現(xiàn):

import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

class?Data?{????????

private?int?data;//?共享數(shù)據(jù)????

private?ReadWriteLock?rwl?=?new?ReentrantReadWriteLock();???????

public?void?set(int?data)?{????

rwl.writeLock().lock();//?取到寫鎖????

try?{????

System.out.println(Thread.currentThread().getName()?+"準(zhǔn)備寫入數(shù)據(jù)");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????????e.printStackTrace();????

????????????}????

this.data?=?data;????

System.out.println(Thread.currentThread().getName()?+"寫入"?+?this.data);????

}finally?{????

rwl.writeLock().unlock();//?釋放寫鎖????

????????}????

????}???????


public?void?get()?{????

rwl.readLock().lock();//?取到讀鎖????

try?{????

System.out.println(Thread.currentThread().getName()?+"準(zhǔn)備讀取數(shù)據(jù)");????

try?{????

Thread.sleep(20);????

}catch?(InterruptedException?e)?{????

????????????????e.printStackTrace();????

????????????}????

System.out.println(Thread.currentThread().getName()?+"讀取"?+?this.data);????

}finally?{????

rwl.readLock().unlock();//?釋放讀鎖????

????????}????

????}????

}????

測試結(jié)果:

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入9??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入24??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入12??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入22??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入15??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入6??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入13??

Thread-W0準(zhǔn)備寫入數(shù)據(jù)??

Thread-W0寫入0??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入23??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入24??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入24??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入17??

Thread-W2準(zhǔn)備寫入數(shù)據(jù)??

Thread-W2寫入11??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取11??

Thread-R1讀取11??

Thread-R2讀取11??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入18??

Thread-W1準(zhǔn)備寫入數(shù)據(jù)??

Thread-W1寫入1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R0讀取1??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-R2讀取1??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R0讀取1??

Thread-R2讀取1??

Thread-R1讀取1??

Thread-R0準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1準(zhǔn)備讀取數(shù)據(jù)??

Thread-R2準(zhǔn)備讀取數(shù)據(jù)??

Thread-R1讀取1??

Thread-R2讀取1??

Thread-R0讀取1??

與互斥鎖定相比,讀-寫鎖定允許對共享數(shù)據(jù)進(jìn)行更高級別的并發(fā)訪問。雖然一次只有一個(gè)線程(writer?線程)可以修改共享數(shù)據(jù),但在許多情況下,任何數(shù)量的線程可以同時(shí)讀取共享數(shù)據(jù)(reader?線程)

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所允許的并發(fā)性增強(qiáng)將帶來更大的性能提高。

在實(shí)踐中,只有在多處理器上并且只在訪問模式適用于共享數(shù)據(jù)時(shí),才能完全實(shí)現(xiàn)并發(fā)性增強(qiáng)。——例如,某個(gè)最初用數(shù)據(jù)填充并且之后不經(jīng)常對其進(jìn)行修改的 collection,因?yàn)榻?jīng)常對其進(jìn)行搜索(比如搜索某種目錄),所以這樣的 collection 是使用讀-寫鎖定的理想候選者。

4、線程間通信Condition

Condition可以替代傳統(tǒng)的線程間通信,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——為什么方法名不直接叫wait()/notify()/nofityAll()?因?yàn)镺bject的這幾個(gè)方法是final的,不可重寫!

傳統(tǒng)線程的通信方式,Condition都可以實(shí)現(xiàn)。

注意,Condition是被綁定到Lock上的,要創(chuàng)建一個(gè)Lock的Condition必須用newCondition()方法。

Condition的強(qiáng)大之處在于它可以為多個(gè)線程間建立不同的Condition

看JDK文檔中的一個(gè)例子:假定有一個(gè)綁定的緩沖區(qū),它支持?put?和?take?方法。如果試圖在空的緩沖區(qū)上執(zhí)行?take操作,則在某一個(gè)項(xiàng)變得可用之前,線程將一直阻塞;如果試圖在滿的緩沖區(qū)上執(zhí)行?put?操作,則在有空間變得可用之前,線程將一直阻塞。我們喜歡在單獨(dú)的等待 set 中保存put?線程和take?線程,這樣就可以在緩沖區(qū)中的項(xiàng)或空間變得可用時(shí)利用最佳規(guī)劃,一次只通知一個(gè)線程??梢允褂脙蓚€(gè)Condition?實(shí)例來做到這一點(diǎn)。

——其實(shí)就是java.util.concurrent.ArrayBlockingQueue的功能

class?BoundedBuffer?{??

final?Lock?lock?=?new?ReentrantLock();??????????//鎖對象??

final?Condition?notFull??=?lock.newCondition();?//寫線程鎖??

final?Condition?notEmpty?=?lock.newCondition();?//讀線程鎖??


final?Object[]?items?=?new?Object[100];//緩存隊(duì)列??

int?putptr;??//寫索引??

int?takeptr;?//讀索引??

int?count;???//隊(duì)列中數(shù)據(jù)數(shù)目??


//寫??

public?void?put(Object?x)?throws?InterruptedException?{??

lock.lock();//鎖定??

try?{??

//?如果隊(duì)列滿,則阻塞<寫線程>??

while?(count?==?items.length)?{??

????????notFull.await();???

??????}??

//?寫入隊(duì)列,并更新寫索引??

??????items[putptr]?=?x;???

if?(++putptr?==?items.length)?putptr?=?0;???

??????++count;??


//?喚醒<讀線程>??

??????notEmpty.signal();???

}finally?{???

lock.unlock();//解除鎖定???

????}???

??}??


//讀???

public?Object?take()?throws?InterruptedException?{???

lock.lock();//鎖定???

try?{??

//?如果隊(duì)列空,則阻塞<讀線程>??

while?(count?==?0)?{??

?????????notEmpty.await();??

??????}??


//讀取隊(duì)列,并更新讀索引??

??????Object?x?=?items[takeptr];???

if?(++takeptr?==?items.length)?takeptr?=?0;??

??????--count;??


//?喚醒<寫線程>??

??????notFull.signal();???

return?x;???

}finally?{???

lock.unlock();//解除鎖定???

????}???

??}???

優(yōu)點(diǎn):

假設(shè)緩存隊(duì)列中已經(jīng)存滿,那么阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。

那么假設(shè)只有一個(gè)Condition會有什么效果呢?緩存隊(duì)列中已經(jīng)存滿,這個(gè)Lock不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那么線程剛被喚醒,又被阻塞了,這時(shí)又去喚醒,這樣就浪費(fèi)了很多時(shí)間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容