同步代碼塊,同步方法,或者是用java提供的鎖機(jī)制,我們可以實(shí)現(xiàn)對共享資源變量的同步控制。
技術(shù)點(diǎn):
1、線程與進(jìn)程:
在開始之前先把進(jìn)程與線程進(jìn)行區(qū)分一下,一個(gè)程序最少需要一個(gè)進(jìn)程,而一個(gè)進(jìn)程最少需要一個(gè)線程。關(guān)系是線程–>進(jìn)程–>程序的大致組成結(jié)構(gòu)。所以線程是程序執(zhí)行流的最小單位,而進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。以下我們所有討論的都是建立在線程基礎(chǔ)之上。
2、Thread的幾個(gè)重要方法:
我們先了解一下Thread的幾個(gè)重要方法。
a、start()方法,開始執(zhí)行該線程;
b、stop()方法,強(qiáng)制結(jié)束該線程執(zhí)行;
c、join方法,等待該線程結(jié)束。
d、sleep()方法,線程進(jìn)入等待。
e、run()方法,直接執(zhí)行線程的run()方法,但是線程調(diào)用start()方法時(shí)也會運(yùn)行run()方法,區(qū)別就是一個(gè)是由線程調(diào)度運(yùn)行run()方法,一個(gè)是直接調(diào)用了線程中的run()方法??!
看到這里,可能有些人就會問啦,那wait()和notify()呢?
要注意,其實(shí)wait()與notify()方法是Object的方法,不是Thread的方法??!
同時(shí),<b>wait()與notify()會配合使用,分別表示線程掛起和線程恢復(fù)。</b>
這里還有一個(gè)很常見的問題,順帶提一下:<b>wait()與sleep()的區(qū)別,簡單來說wait()會釋放對象鎖而sleep()不會釋放對象鎖。</b>
3、線程狀態(tài):
線程總共有<b>5大狀態(tài)</b>,通過上面第二個(gè)知識點(diǎn)的介紹,理解起來就簡單了。
<b>新建狀態(tài)</b>:新建線程對象,并沒有調(diào)用start()方法之前
<b>就緒狀態(tài)</b>:調(diào)用start()方法之后線程就進(jìn)入就緒狀態(tài),但是并不是說只要調(diào)用start()方法線程就馬上變?yōu)楫?dāng)前線程,在變?yōu)楫?dāng)前線程之前都是為就緒狀態(tài)。值得一提的是,線程在睡眠和掛起中恢復(fù)的時(shí)候也會進(jìn)入就緒狀態(tài)哦。
<b>運(yùn)行狀態(tài)</b>:線程被設(shè)置為當(dāng)前線程,開始執(zhí)行run()方法。就是線程進(jìn)入運(yùn)行狀態(tài)
<b>阻塞狀態(tài)</b>:線程被暫停,比如說調(diào)用sleep()方法后線程就進(jìn)入阻塞狀態(tài)
<b>死亡狀態(tài)</b>:線程執(zhí)行結(jié)束
4、鎖類型
<b>可重入鎖</b>(synchronized和ReentrantLock):在執(zhí)行對象中所有同步方法不用再次獲得鎖
<b>可中斷鎖</b>(synchronized就不是可中斷鎖,而Lock是可中斷鎖):在等待獲取鎖過程中可中斷
<b>公平鎖</b>(ReentrantLock和ReentrantReadWriteLock): 按等待獲取鎖的線程的等待時(shí)間進(jìn)行獲取,等待時(shí)間長的具有優(yōu)先獲取鎖權(quán)利
<b>讀寫鎖</b>(ReadWriteLock和ReentrantReadWriteLock):對資源讀取和寫入的時(shí)候拆分為2部分處理,讀的時(shí)候可以多線程一起讀,寫的時(shí)候必須同步地寫
Synchronized與Lock的區(qū)別
| 類別 | synchronized | Lock |
|---|---|---|
| 存在層次 | Java的關(guān)鍵字,在jvm層面上 | 是一個(gè)接口 |
| 鎖的釋放 | 1、以獲取鎖的線程執(zhí)行完同步代碼,釋放鎖 2、線程執(zhí)行發(fā)生異常,jvm會讓線程釋放鎖 |
在finally中必須釋放鎖,不然容易造成線程死鎖 |
| 鎖的獲取 | 假設(shè)A線程獲得鎖,B線程等待。 如果A線程阻塞,B線程會一直等待 |
分情況而定,Lock有多個(gè)鎖獲取的方式, 大致就是可以嘗試獲得鎖,線程可以不用一直等待 (可以通過tryLock判斷有沒有鎖) |
| 鎖狀態(tài) | 無法判斷 | 可以判斷 |
| 鎖類型 | 可重入 不可中斷 非公平 |
可重入 可判斷 可公平(兩者皆可) |
| 性能 | 少量同步 | 大量同步 <b>1.</b>Lock可以提高多個(gè)線程進(jìn)行讀操作的效率。 (可以通過readwritelock實(shí)現(xiàn)讀寫分離) <b>2.</b>在資源競爭不是很激烈的情況下, Synchronized的性能要優(yōu)于ReetrantLock, 但是在資源競爭很激烈的情況下, Synchronized的性能會下降幾十倍, 但是ReetrantLock的性能能維持常態(tài); <b>3.</b>ReentrantLock提供了多樣化的同步, 比如有時(shí)間限制的同步,可以被Interrupt的同步 (synchronized的同步是不能Interrupt的)等。 在資源競爭不激烈的情形下, 性能稍微比synchronized差點(diǎn)點(diǎn)。 但是當(dāng)同步非常激烈的時(shí)候, synchronized的性能一下子能下降好幾十倍。而ReentrantLock確還能維持常態(tài)。 |
Synchronized與Static Synchronized
每個(gè)類有一個(gè)鎖,它可以用來控制對static數(shù)據(jù)成員的并發(fā)訪問。
訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖。
static synchronized控制類的所有實(shí)例(對象)的訪問(相應(yīng)代碼塊)。
synchronized相當(dāng)于 this.synchronized,static synchronized相當(dāng)于Something.synchronized
Lock接口
//Lock是一個(gè)接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。
unLock()方法是用來釋放鎖的。
在Lock中聲明了四個(gè)方法來獲取鎖,那么這四個(gè)方法有何區(qū)別呢?
首先lock()方法是平常使用得最多的一個(gè)方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進(jìn)行等待。
由于在前面講到如果采用Lock,必須主動去釋放鎖,并且在發(fā)生異常時(shí),不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進(jìn)行,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被被釋放,防止死鎖的發(fā)生。通常使用Lock來進(jìn)行同步的話,是以下面這種形式去使用的:
Lock lock = ...;
lock.lock();
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
//釋放鎖
lock.unlock();
}
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失?。存i已被其他線程獲取),則返回false,也就說這個(gè)方法無論如何都會立即返回。在拿不到鎖時(shí)不會一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。
所以,一般情況下通過tryLock來獲取鎖時(shí)是這樣使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務(wù)
}catch(Exception ex){
//
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
lockInterruptibly()方法比較特殊,當(dāng)通過這個(gè)方法去獲取鎖時(shí),如果線程正在等待獲取鎖,則這個(gè)線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)。也就使說,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖,而線程B只有在等待,那么對線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,當(dāng)一個(gè)線程獲取了鎖之后,是不會被interrupt()方法中斷的。單獨(dú)調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程。
因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí),如果不能獲取到,只有進(jìn)行等待的情況下,是可以響應(yīng)中斷的。
而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài),是無法被中斷的,只有一直等待下去。
Lock類型
一、公平鎖/非公平鎖
公平鎖是指多個(gè)線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個(gè)線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優(yōu)先獲取鎖。有可能,會造成優(yōu)先級反轉(zhuǎn)或者饑餓現(xiàn)象。
對于ReentrantLock而言,通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。非公平鎖的優(yōu)點(diǎn)在于吞吐量比公平鎖大。
對于Synchronized而言,也是一種非公平鎖。由于其并不像ReentrantLock是通過AQS的來實(shí)現(xiàn)線程調(diào)度,所以并沒有任何辦法使其變成公平鎖。
二、可重入鎖
可重入鎖又名遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,在進(jìn)入內(nèi)層方法會自動獲取鎖。
說的有點(diǎn)抽象,下面會有一個(gè)代碼的示例。
對于Java ReentrantLock而言, 他的名字就可以看出是一個(gè)可重入鎖,其名字是ReentrantLock重新進(jìn)入鎖。
對于Synchronized而言,也是一個(gè)可重入鎖??芍厝腈i的一個(gè)好處是可一定程度避免死鎖。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
三、獨(dú)享鎖/共享鎖
獨(dú)享鎖是指該鎖一次只能被一個(gè)線程所持有。
共享鎖是指該鎖可被多個(gè)線程所持有。
對于Java ReentrantLock而言,其是獨(dú)享鎖。但是對于Lock的另一個(gè)實(shí)現(xiàn)類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨(dú)享鎖。
讀鎖的共享鎖可保證并發(fā)讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
獨(dú)享鎖與共享鎖也是通過AQS來實(shí)現(xiàn)的,通過實(shí)現(xiàn)不同的方法,來實(shí)現(xiàn)獨(dú)享或者共享。
對于Synchronized而言,當(dāng)然是獨(dú)享鎖。
四、互斥鎖/讀寫鎖
上面講的獨(dú)享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是<b>具體的實(shí)現(xiàn)</b>。
互斥鎖在Java中的具體實(shí)現(xiàn)就是ReentrantLock
讀寫鎖在Java中的具體實(shí)現(xiàn)就是ReadWriteLock
五、樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖不是指具體的什么類型的鎖,而是指看待并發(fā)同步的<b>角度</b>。
悲觀鎖認(rèn)為對于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定是會發(fā)生修改的,哪怕沒有修改,也會認(rèn)為修改。因此對于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖的并發(fā)操作一定會出問題。
樂觀鎖則認(rèn)為對于同一個(gè)數(shù)據(jù)的并發(fā)操作,是不會發(fā)生修改的。在更新數(shù)據(jù)的時(shí)候,會采用嘗試更新,不斷重新的方式更新數(shù)據(jù)。樂觀的認(rèn)為,不加鎖的并發(fā)操作是沒有事情的。
從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
悲觀鎖在Java中的使用,就是利用各種鎖。
樂觀鎖在Java中的使用,是無鎖編程,常常采用的是CAS算法,典型的例子就是原子類,通過CAS自旋實(shí)現(xiàn)原子操作的更新。
六、分段鎖
分段鎖其實(shí)是一種鎖的<b>設(shè)計(jì)</b>,并不是具體的一種鎖,對于ConcurrentHashMap而言,其并發(fā)的實(shí)現(xiàn)就是通過分段鎖的形式來實(shí)現(xiàn)高效的并發(fā)操作。
我們以ConcurrentHashMap來說一下分段鎖的含義以及設(shè)計(jì)思想,ConcurrentHashMap中的分段鎖稱為Segment,它即類似于HashMap(JDK7與JDK8中HashMap的實(shí)現(xiàn))的結(jié)構(gòu),即內(nèi)部擁有一個(gè)Entry數(shù)組,數(shù)組中的每個(gè)元素又是一個(gè)鏈表;同時(shí)又是一個(gè)ReentrantLock(Segment繼承了ReentrantLock)。
當(dāng)需要put元素的時(shí)候,并不是對整個(gè)hashmap進(jìn)行加鎖,而是先通過hashcode來知道他要放在那一個(gè)分段中,然后對這個(gè)分段進(jìn)行加鎖,所以當(dāng)多線程put的時(shí)候,只要不是放在一個(gè)分段中,就實(shí)現(xiàn)了真正的并行的插入。
但是,在統(tǒng)計(jì)size的時(shí)候,可就是獲取hashmap全局信息的時(shí)候,就需要獲取所有的分段鎖才能統(tǒng)計(jì)。
分段鎖的設(shè)計(jì)目的是細(xì)化鎖的粒度,當(dāng)操作不需要更新整個(gè)數(shù)組的時(shí)候,就僅僅針對數(shù)組中的一項(xiàng)進(jìn)行加鎖操作。
七、偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的<b>狀態(tài)</b>,并且是針對Synchronized。在Java 5通過引入鎖升級的機(jī)制來實(shí)現(xiàn)高效Synchronized。這三種鎖的狀態(tài)是通過對象監(jiān)視器在對象頭中的字段來表明的。
<b>偏向鎖</b>是指一段同步代碼一直被一個(gè)線程所訪問,那么該線程會自動獲取鎖。降低獲取鎖的代價(jià)。
<b>輕量級鎖</b>是指當(dāng)鎖是偏向鎖的時(shí)候,被另一個(gè)線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
<b>重量級鎖</b>是指當(dāng)鎖為輕量級鎖的時(shí)候,另一個(gè)線程雖然是自旋,但自旋不會一直持續(xù)下去,當(dāng)自旋一定次數(shù)的時(shí)候,還沒有獲取到鎖,就會進(jìn)入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進(jìn)入阻塞,性能降低。
八、自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會消耗CPU。
線程自旋和適應(yīng)性自旋
我們知道,java線程其實(shí)是映射在內(nèi)核之上的,線程的掛起和恢復(fù)會極大的影響開銷。
并且jdk官方人員發(fā)現(xiàn),很多線程在等待鎖的時(shí)候,在很短的一段時(shí)間就獲得了鎖,所以它們在線程等待的時(shí)候,并不需要把線程掛起,而是讓他無目的的循環(huán),一般設(shè)置10次。
這樣就避免了線程切換的開銷,極大的提升了性能。
而<b>適應(yīng)性自旋</b>,是賦予了自旋一種學(xué)習(xí)能力,它并不固定自旋10次一下。
他可以根據(jù)它前面線程的自旋情況,從而調(diào)整它的自旋,甚至是不經(jīng)過自旋而直接掛起。