一 開篇
客觀的講,sychronized 和 lock都屬於悲觀鎖(共享的資源每次只能給一個線程使用,其他線程處於阻塞狀態,用完之後才釋放資源給其他線程使用)。都能夠實現數據的同步訪問,sychronized是java中的一個關鍵字,屬於java內置的語言特性,在Java1.5之後,在java.util.concurrent.locks包下提供了另外一種方式來實現同步訪問,那就是Lock。
那麼疑問來了,既然sychronized可以實現數據的同步訪問,那麼爲什麼還要增加lock呢?接下來我們來講解下增加lock的原因。
二 sychronized 和 Lock的區別
1.來源:sychronized是java的關鍵字,是java的內置特性。Lock不是java關鍵字,是java.util.concurrent.locks包下的一個類,通過這個類可以實現數據的同步訪問。
2.釋放鎖:sychronized方法(代碼塊)執行完畢之後或者執行期間出現異常,系統會自動釋放鎖,不會導致死鎖現象。Lock必須要用戶去手動釋放鎖,如果沒有主動釋放,那麼很可能出現死鎖現象。
3.響應中斷:Lock可以讓等待的線程響應中斷,而sychronized不行,會一直等待下去
4.獲取鎖狀態:Lock可以知道線程是否成功獲取鎖,sychronized不行
5.高併發效率:Lock可以提高多線程作業效率
在性能上來說,如果資源競爭不是很激烈的情況下,兩者性能相差不多,當有大量的線程競爭資源時,而Lock的性能遠遠高於sychronized,所以根據情況選擇合適的鎖是關鍵
三 爲什麼引入Lock類?
1.爲了能夠靈活設置線程等待時間和響應中斷,提高程序執行效率
從上面瞭解到,sychronized釋放鎖的情況有兩種:1.sychronized方法(代碼塊)執行完畢。2.線程執行過程中發生異常,JVM自動釋放鎖。那麼如果中途遇到等待IO或者sleep等原因被阻塞了,其他線程只能乾等,這種情況下,程序的執行效率將會非常低下。但Lock可以設置線程等待的時間和相應中斷!!!,其他線程可放棄等待,去做其他事情。
2.有效識別線程讀操作,提高資源可利用範圍
sychronized加鎖時,多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。
但Lock可以使得多個線程都是讀操作時,線程之間不會發生衝突。
3.sychronized無法知道線程有沒有獲取到鎖,但Lock可以知道
tryLock()和tryLock(long time,Unit unit)方法是有返回值的,獲取到鎖返回true,否則返回false
四 java.util.concurrent.locks包下的類和接口介紹
java.util.concurrent.locks包下,我們需要關注的也就:兩個接口(Lock接口,ReadWriteLock接口),兩個實現類(ReentrantLock類,ReentrantReadWriteLock類),其中,ReentrantLock類是Lock接口的唯一實現,ReentrantReadWriteLock類是ReadWriteLock接口的實現。
1.Lock接口
通過查看源碼可知,Lock是一個接口,方法如下:
public interface Lock {
//獲取鎖,無返回值
void lock();
//獲取鎖,獲取不到中斷(發音:因特rub特-波類)
void lockInterruptibly() throws InterruptedException;
//獲取鎖,成功返回true,否則false
boolean tryLock();
//嘗試一段時間內獲取鎖,成功返回true,否則false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//手動釋放鎖
void unlock();
Condition newCondition();
}
Lock接口中的方法:lock(),lockInterruptibly(),tryLock(),tryLock(long time,TimeUnit unit)都是用來獲取鎖的。unLock()是用來釋放鎖的,newCondition()方法暫不做贅述。
1. lock()方法
lock()方法是比較常用的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
前面講到,如果使用Lock,必須要手動釋放鎖,即便發生異常,也不會釋放鎖。因此,使用Lock時,建議必須要用try{}catch(){} 捕獲一下,然後將釋放鎖的操作放在finally()代碼塊中,防止死鎖發生,使用方式如下。
Lock lock=...;
lock.lock();
try{
//事務處理
}catch(Exception e){
}finally{
//釋放鎖
lock.unlock();
}
2. tryLock()方法
tryLock()方法是有返回值的,嘗試獲取鎖,如果成功返回true,否則返回false,如果拿不到鎖可以嘗試去做其他事情,不用一直等待。
tryLock(long time, TimeUnit unit)方法可tryLock()類似,區別在於獲取不到鎖時會等待一段時間,在時間期限內拿到鎖返回ture,否則返回false,使用方式如下。
Lock lock=...;
if(lock.tryLock()){
try{
//處理事務
}catch(Exception e){
}finally{
//釋放鎖
lock.unLock();
}
}else{
//獲取不到鎖,做其他操作
}
3.lockInterruptibly()方法
lockInterruptibly()方法比較特殊,能夠響應中斷,即中斷等待狀態,也就是或如果兩個線程A,B分別獲取鎖,A獲取到鎖是,B一直處於等待狀態,可以通過B.interrupt()方法能夠中斷線程B的等待過程。
由於lockInterruptibly()的聲明中拋出了異常,所以lock.lockInterruptibly()必須放在try{}catch{}中或者拋出InterruptedException 異常,使用代碼如下。
public class LockTest{
private Lock lock=new ReentrantLock();
public void method(){
try{
lock.lockInterruptibly();
//事務處理
System.out.println(Thread.currentThread().getName()+"獲取到鎖!!!");
System.out.println(Thread.currentThread().getName()+"先睡它個30s!!!");
Thread.sleep(30000);
}catch(Exception e){
}finally{
//釋放鎖
lock.unlock();
}
}
@Test
public void testInterruptibly() throws InterruptedException{
Runnable t1=new Runnable(){
@Override
public void run(){
method();
}
};
Runnable t2=new Runnable(){
@Override
public void run(){
method();
}
};
String tName=Thread.currentThread().getName();
System.out.println(tName+"-啓動t1!");
Thread t11=new Thread(t1);
t11.start();
System.out.println(tName+"-我等個5秒,再啓動t2");
Thread.sleep(5000);
System.out.println(tName+"-啓動t2");
Thread t22=new Thread(t2);
t22.start();
System.out.println(tName+"-t2獲取不到鎖,t1睡覺了,沒釋放,我等個5秒!");
Thread.sleep(5000);
System.out.println(tName+"-等了5秒了,不等了,把t1中斷了!");
t22.interrupt();
Thread.sleep(Long.MAX_VALUE);
}
}
注意:已經獲取到鎖的線程是無法用interrupt()方法中斷的,interrupt()方法只能中斷被阻塞的線程。因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
2. ReentrantLock實現類
ReentrantLock,(發音:瑞恩垂特Lock)意思爲“可重入鎖”,ReentrantLock具有公平和非公平兩種模式,也各有優缺點:
公平鎖是嚴格的以FIFO的方式進行鎖的競爭,但是非公平鎖是無序的鎖競爭,剛釋放鎖的線程很大程度上能比較快的獲取到鎖,隊列中的線程只能等待,所以非公平鎖可能會有“飢餓”的問題。但是重複的鎖獲取能減小線程之間的切換,而公平鎖則是嚴格的線程切換,這樣對操作系統的影響是比較大的,所以非公平鎖的吞吐量是大於公平鎖的,這也是爲什麼JDK將非公平鎖作爲默認的實現。
ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法.
lock(),tryLock(),tryLock(long time,TimeUnit unit),lockInterruptibly()方法使用方式即我們上面介紹的那樣,唯一注意的地方是聲明Lock的時候
private
Lock lock =
new
ReentrantLock();
//注意這個地方
提示:如果希望線程能夠正確的獲取鎖,要注意:同一個實例對象,多個線程纔有競爭關係,加鎖纔有意義,不同的實例對象要有競爭關係,必須在對象類級別加鎖。
3. ReadWriteLock接口
ReadWriteLock也是一個接口,它只定義了兩個抽象方法
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
readLock()用來獲取讀鎖,wirteLock()用來獲取寫鎖。也就是說將資源的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。下面的ReentrantReadWriteLock實現了ReadWriteLock接口。
4.ReentrantReadWriteLock實現類
ReentrantReadWriteLock實現了接口ReadWriteLock,ReentrantReadWriteLock裏面提供了豐富的方法,最主要的兩個方法爲:readLock()和wirteLock()用來獲取讀鎖和寫鎖。
我們知道,sychronized加鎖是無法識別線程是讀操作還是寫操作的,一律是一個線程獲取到鎖後,其他線程等待狀態,但ReentrantReadWriteLock的讀鎖和寫鎖實現了讀寫分離,讀操作可以允許多個線程同時進行,使用方式如下。
private ReadWriteLock rwl=new ReentrantReadWriteLock();
public void m1(Thread thread){
rwl.readLock().lock();//讀鎖
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
} finally {
rwl.readLock().unlock();
}
}
不同的線程可同時申請讀鎖,大大提高程序執行效率。
但要注意的是,如果有一個線程已經佔用了讀鎖,其他線程想要申請寫鎖,那麼申請寫鎖的線程必須要等待讀鎖釋放。
如果一個線程已經佔用了寫鎖,其他線程想申請讀鎖或者寫鎖,則申請線程必須要等待寫鎖釋放。
五 鎖概念介紹
1.可重入鎖
定義:如果鎖具備可重入性,那麼我們稱之爲可重入鎖。像sychronized和ReentrantLock都是可重入鎖。可重入性在我看來實際是表明了鎖的分配機制:鎖是基於線程分配的,而不是基於方法調用的分配。
舉個簡單的例子,有兩個sychronized修飾的方法,method1和method2,其中method1調用了method2,那麼如果一個線程獲取了method1的鎖,那麼該不需要再去申請method2的鎖,可直接執行方法method2,代碼如下:
public class MyClass{
public sychronized void method1(){
method2();
}
public sychronized void method2(){
}
}
上述method1和method2都被sychronized修飾了,如果sychronized不具備可重入性,假如某一時刻,線程A獲取到了method1的鎖,那麼還需申請該對象鎖,問題是A已經佔用了該對象鎖,這樣A就會一直等待永遠獲取不到的鎖。
然而,sychronized和ReentrantLock都具有可重入性,所以不會發生上述這種情況。
2.可中斷鎖
可中斷鎖:顧名思義,可以響應中斷的鎖。sychronized是不可中斷鎖,Lock是可以中斷的。
如果某個線程執行鎖代碼期間,其他線程不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。就是前面講述的Lock中的lockInterruptiblly()方法。通過threadX.interrupt()可實現線程的中斷。
3.公平鎖和非公平鎖
遵循FIFO的原理,即儘量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
sychronized是非公平鎖,無法保證獲取鎖的順序和請求鎖的順序一致,所以就可能出現某個線程永遠獲取不到鎖的情況。但重複的獲取鎖,可以減少線程間的切換消耗的資源,所以非公平鎖的吞吐量會比公平鎖的要大。
ReentrantLock和ReentrantReadWriteLock默認都是非公平鎖,但是可以設置成公平鎖,看下這兩個類的源代碼:
ReentrantLock內部設置了2個靜態內部類,一個NonfairSync,一個FairSync,分別用來實現公平鎖和非公平鎖。
看下ReentrantLock的構造函數,如下圖,根據構造函數可以看到,默認無參情況下是非公平鎖。設置鎖的公平性可以
ReentrantLock lock=new ReentrantLock(true);
傳true參數代表公平鎖,不傳或者傳false,代表非公平鎖。
此外,ReentrantLock類中還定義了很多其他的方法,比如:
- isFair() //判斷鎖是否是公平鎖
- isLocked() //判斷鎖是否被任何線程獲取了
- isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了
- hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置爲公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。
4.讀寫鎖
讀寫鎖其實就是將對資源的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因爲有了讀寫鎖,才使得多個線程之間的讀操作不會發生衝突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock().lock()獲取讀鎖,通過writeLock().lock()獲取寫鎖。
六 拓展 volatile
在java線程併發處理中,有一個關鍵字volatile的使用目前存在很大的混淆,以爲使用這個關鍵字,在進行多線程併發處理的時候就可以萬事大吉。
Java語言是支持多線程的,爲了解決線程併發的問題,在語言內部引入了 同步塊 和 volatile 關鍵字機制。
用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最的值。volatile很容易被誤用,用來進行原子性操作。
舉個例子,實際操作,我們實現一個計數器,每次線程啓動的時候,會調用計數器inc方法,對計數器進行加一,同時啓動1000個線程,去進行i++計算,看看實際結果,運行結果:Counter.count=995
實際運算結果每次可能都不一樣,本機的結果爲:運行結果:Counter.count=995,可以看出,在多線程的環境下,Counter.count並沒有期望結果是1000
很多人以爲,這個是多線程併發問題,只需要在變量count之前加上volatile就可以避免這個問題,那我們在修改代碼看看,看看結果是不是符合我們的期望
運行結果:Counter.count=992
運行結果還是沒有我們期望的1000,下面我們分析一下原因
- 在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,
- 線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然後把堆內存
- 變量的具體值load到線程本地內存中,建立一個變量副本,之後線程就不再和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,
- 在修改完之後的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。
- read and load 從主存複製變量到當前工作內存use and assign 執行代碼,改變共享變量值 store and write 用工作內存數據刷新主存相關內容
- 其中use and assign 可以多次出現
- 但是這一些操作並不是原子性,也就是 在read load之後,如果主內存count變量發生修改之後,線程工作內存中的值由於已經加載,不會產生對應的變化,所以計算出來的結果會和預期不一樣
對於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的
例如假如線程1,線程2 在進行read,load 操作中,發現主內存中count的值都是5,那麼都會加載這個最新的值
在線程1堆count進行修改之後,會write到主內存中,主內存中的count變量就會變爲6
線程2由於已經進行read,load操作,在進行運算之後,也會更新主內存count的變量值爲6
導致兩個線程及時用volatile關鍵字修改之後,還是會存在併發的情況。