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