如果需要查看具體的synchronized和lock的實現原理,請參考:解決多線程安全問題-無非兩個方法synchronized和lock 具體原理(百度)
在並發編程中,經常遇到多個線程訪問同一個 共享資源 ,這時候作為開發者必須考慮如何維護數據一致性,在java中synchronized關鍵字被常用於維護數據一致性。synchronized機制是給共享資源上鎖,只有拿到鎖的線程才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的,因為對於共享資源屬性訪問是必要也是必須的,下文會有具體示例演示。
一.java中的鎖
一般在java中所說的鎖就是指的內置鎖,每個java對象都可以作為一個實現同步的鎖,雖然說在java中一切皆對象, 但是鎖必須是引用類型的,基本數據類型則不可以 。每一個引用類型的對象都可以隱式的扮演一個用於同步的鎖的角色,執行線程進入synchronized塊之前會自動獲得鎖,無論是通過正常語句退出還是執行過程中拋出了異常,線程都會在放棄對synchronized塊的控制時自動釋放鎖。 獲得鎖的唯一途徑就是進入這個內部鎖保護的同步塊或方法 。
正如引言中所說,對共享資源的訪問必須是順序的,也就是說當多個線程對共享資源訪問的時候,只能有一個線程可以獲得該共享資源的鎖,當線程A嘗試獲取線程B的鎖時,線程A必須等待或者阻塞,直到線程B釋放該鎖為止,否則線程A將一直等待下去,因此java內置鎖也稱作互斥鎖,也即是說鎖實際上是一種互斥機制。
根據使用方式的不同一般我們會將鎖分為對象鎖和類鎖,兩個鎖是有很大差別的,對象鎖是作用在實例方法或者一個對象實例上面的,而類鎖是作用在靜態方法或者Class對象上面的。一個類可以有多個實例對象,因此一個類的對象鎖可能會有多個,但是每個類只有一個Class對象,所以類鎖只有一個。 類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定的是實例方法還是靜態方法區別的 。
在java中實現鎖機制不僅僅限於使用synchronized關鍵字,還有JDK1.5之后提供的Lock,Lock不在本文討論范圍之內。一個synchronized塊包含兩個部分:鎖對象的引用,以及這個鎖保護的代碼塊。如果作用在實例方法上面,鎖就是該方法所在的當前對象,靜態synchronized方法會從Class對象上獲得鎖。
鎖的相關概念介紹
1.可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,比如說method1,而在method1中會調用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執行方法method2。
看下面這段代碼就明白了:
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } }
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由於等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。
3.公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對於ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置為公平鎖。
看一下這2個類的源代碼就清楚了:
在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。
我們可以在創建ReentrantLock對象時,通過以下方式來設置鎖的公平性:
ReentrantLock lock = new ReentrantLock(true);
如果參數為true表示為公平鎖,為fasle為非公平鎖。默認情況下,如果使用無參構造器,則是非公平鎖。
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖 isLocked() //判斷鎖是否被任何線程獲取了 isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了 hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設置為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。
4.讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發生沖突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
5、自旋鎖
首先是一種鎖,與互斥鎖相似,基本作用是用於線程(進程)之間的同步。與普通鎖不同的是,一個線程A在獲得普通鎖后,如果再有線程B試圖獲取鎖,那么這個線程B將會掛起(阻塞);試想下,如果兩個線程資源競爭不是特別激烈,而處理器阻塞一個線程引起的線程上下文的切換的代價高於等待資源的代價的時候(鎖的已保持者保持鎖時間比較短),那么線程B可以不放棄CPU時間片,而是在“原地”忙等,直到鎖的持有者釋放了該鎖,這就是自旋鎖的原理,可見自旋鎖是一種非阻塞鎖。
二、自旋鎖可能引起的問題:
1.過多占據CPU時間:如果鎖的當前持有者長時間不釋放該鎖,那么等待者將長時間的占據cpu時間片,導致CPU資源的浪費,因此可以設定一個時間,當鎖持有者超過這個時間不釋放鎖時,等待者會放棄CPU時間片阻塞;
2.死鎖問題:試想一下,有一個線程連續兩次試圖獲得自旋鎖(比如在遞歸程序中),第一次這個線程獲得了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被占用(其實是被自己占用),那么這時,線程會一直等待自己釋放該鎖,而不能繼續執行,這樣就引起了死鎖。因此遞歸程序使用自旋鎖應該遵循以下原則:遞歸程序決不能在持有自旋鎖時調用它自己,也決不能在遞歸調用時試圖獲得相同的自旋鎖。
JAVA中一種自旋鎖的實現: CAS是Compare And Set的縮寫
import java.util.concurrent.atomic.AtomicReference; class SpinLock { //java中原子(CAS)操作 AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的線程對象 private int count; public void lock() { Thread cur = Thread.currentThread(); //lock函數將owner設置為當前線程,並且預測原來的值為空。unlock函數將owner設置為null,並且預測值為當前線程。當有第二個線程調用lock操作時由於owner值不為空,導致循環 //一直被執行,直至第一個線程調用unlock函數將owner設置為null,第二個線程才能進入臨界區。 while (!owner.compareAndSet(null, cur)){ } } public void unLock() { Thread cur = Thread.currentThread(); owner.compareAndSet(cur, null); } } } public class Test implements Runnable { static int sum; private SpinLock lock; public Test(SpinLock lock) { this.lock = lock; } public static void main(String[] args) throws InterruptedException { SpinLock lock = new SpinLock(); for (int i = 0; i < 100; i++) { Test test = new Test(lock); Thread t = new Thread(test); t.start(); } Thread.currentThread().sleep(1000); System.out.println(sum); } @Override public void run() { this.lock.lock(); sum++; this.lock.unLock(); } }
一、公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。
對於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次一下。
他可以根據它前面線程的自旋情況,從而調整它的自旋,甚至是不經過自旋而直接掛起。
二.synchronized使用示例
1.多窗口售票
假設一個火車票售票系統,有若干個窗口同時售票,很顯然在這里票是作為多個窗口的共享資源存在的,由於座位號是確定的,因此票上面的號碼也是確定的,我們用多個線程來模擬多個窗口同時售票,首先在不使用synchronized關鍵字的情況下測試一下售票情況。
先將票本身作為一個共享資源放在單獨的線程中,這種作為共享資源存在的線程很顯然應該是實現Runnable接口,我們將票的總數num作為一個入參傳入,每次生成一個票之后將num做減法運算,直至num為0即停止,說明票已經售完了,然后開啟多個線程將票資源傳入。
public class Ticket implements Runnable{ private int num;//票數量 private boolean flag=true;//若為false則售票停止 public Ticket(int num){ this.num=num; } @Override public void run() { while(flag){ ticket(); } } private void ticket(){ if(num<=0){ flag=false; return; } try { Thread.sleep(20);//模擬延時操作 } catch (InterruptedException e) { e.printStackTrace(); } //輸出當前窗口號以及出票序列號 System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--); } } public class MainTest { public static void main(String[] args) { Ticketticket = new Ticket(5); Threadwindow01 = new Thread(ticket, "窗口01"); Threadwindow02 = new Thread(ticket, "窗口02"); Threadwindow03 = new Thread(ticket, "窗口03"); window01.start(); window02.start(); window03.start(); } }
程序的輸出結果如下:
窗口02售出票序列號:5 窗口03售出票序列號:4 窗口01售出票序列號:5 窗口02售出票序列號:3 窗口01售出票序列號:2 窗口03售出票序列號:2 窗口02售出票序列號:1 窗口03售出票序列號:0 窗口01售出票序列號:-1
從上面程序運行結果可以看出不但票的序號有重號而且出票數量也不對,這種售票系統比12306可要爛多了,人家在繁忙的時候只是刷不到票而已,而這里的售票系統倒好了,出票比預計的多了而且會出現多個人爭搶做同一個座位的風險。如果是單個售票窗口是不會出現這種問題,多窗口同時售票就會出現爭搶共享資源因此紊亂的現象,解決該現象也很簡單,就是在ticket()方法前面加上synchronized關鍵字或者將ticket()方法的方法體完全用synchronized塊包括起來。
//方式一 private synchronized void ticket(){ if(num<=0){ flag=false; return; } try { Thread.sleep(20);//模擬延時操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--); } //方式二 private void ticket(){ synchronized (this) { if (num <= 0) { flag = false; return; } try { Thread.sleep(20);//模擬延時操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售出票序列號:" + num--); } }
再看一下加入synchronized關鍵字的程序運行結果:
窗口01售出票序列號:5 窗口03售出票序列號:4 窗口03售出票序列號:3 窗口02售出票序列號:2 窗口02售出票序列號:1
從這里可以看出在實例方法上面加上synchronized關鍵字的實現效果跟對整個方法體加上synchronized效果是一樣的。 另外一點需要注意加鎖的時機也非常重要 ,本示例中ticket()方法中有兩處操作容易出現紊亂,一個是在if語句模塊,一處是在num–,這兩處操作本身都不是原子類型的操作,但是在使用運行的時候需要這兩處當成一個整體操作,所以synchronized將整個方法體都包裹在了一起。如若不然,假設num當前值是1,但是窗口01執行到了num–,整個操作還沒執行完成,只進行了賦值運算還沒進行自減運算,但是窗口02已經進入到了if語句模塊,此時num還是等於1,等到窗口02執行到了輸出語句的時候,窗口01的num–也已經將自減運算執行完成,這時候窗口02就會輸出序列號0的票。再者如果將synchronized關鍵字加在了run方法上面,這時候的操作不會出現紊亂或者錯誤,但是這種加鎖方式無異於單窗口操作,當窗口01拿到鎖進入run()方法之后,必須等到flag為false才會將語句執行完成跳出循環,這時候的num就已經為0了,也就是說票已經被售賣完了,這種方式摒棄了多線程操作,違背了最初的設計原則-多窗口售票。
2.懶漢式單例模式
創建單例模式有很多中實現方式,本文只討論懶漢式創建。在Android開發過程中單例模式可以說是最常使用的一種設計模式,因為它操作簡單還可以有效減少內存溢出。下面是懶漢式創建單例模式一個示例:
(懶漢式與餓漢式的區別:Singleton 單例模式(懶漢方式和餓漢方式))
public class Singleton { private static Singletoninstance; private Singleton() { } public static SingletongetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
如果對於多窗口售票邏輯已經完全明白了的話就可以看出這里的實現方式是有問題的,我們可以簡單的創建幾個線程來獲取單例輸出對象的hascode值。
com.sunny.singleton.Singleton@15c330aa
com.sunny.singleton.Singleton@15c330aa
com.sunny.singleton.Singleton@41aff40f
在多線程模式下發現會出現不同的對象,這種單例模式很顯然不是我們想要的,那么根據上面多窗口售票的邏輯我們在getInstance()方法上面加上一個synchronized關鍵字,給該方法加上鎖,加上鎖之后可以避免多線程模式下生成多個不同對象,但是同樣會帶來一個效率問題,因為不管哪個線性進入getInstance()方法都會先獲得鎖,然后再次釋放鎖,這是一個方面,另一個方面就是只有在第一次調用getInstance()方法的時候,也就是在if語句塊內才會出現多線程並發問題,而我們卻索性將整個方法都上鎖了。討論到這里就引出了另外一個問題,究竟是synchronized方法好還是synchronized代碼塊好呢? 有一個原則就是鎖的范圍越小越好 ,加鎖的目的就是將鎖進去的代碼作為原子性操作,因為非原子操作都不是線程安全的,因此synchronized代碼塊應該是在開發過程中優先考慮使用的加鎖方式。
public static SingletongetInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }
這里也會遇到類似上面的問題,多線程並發下回生成多個實例,如線程A和線程B都進入if語句塊,假設線程A先獲得鎖,線程B則等待,當new一個實例后,線程A釋放鎖,線程B獲得鎖后會再次執行new語句,同樣不能保證單例要求,那么下面代碼再來一個null判斷,進行雙重檢查上鎖呢?
public static SingletongetInstance() { if (instance == null) { synchronized (Singleton.class) { if(instance==null){ instance = new Singleton(); } } } return instance; }
該模式就是雙重檢查上鎖實現的單例模式,這里在代碼層面我們已經 基本 保證了線程安全了,但是還是有問題的, 雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現bug,而是歸咎於java平台內存模型。內存模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。 更為詳細的介紹可以參考 Java單例模式中雙重檢查鎖的問題 。所以單例模式創建比較建議使用惡漢式創建或者靜態內部類方式創建。
3.synchronized不具有繼承性
我們可以通過一個簡單的demo驗證這個問題,在一個方法中順序的輸出一系列數字,並且輸出該數字所在的線程名稱,在父類中加上synchronized關鍵字,子類重寫父類方法測試一下加上synchronized關鍵字和不加關鍵字的區別即可。
public class Parent { public synchronized void test() { for (int i = 0; i < 5; i++) { System.out.println("Parent " + Thread.currentThread().getName() + ":" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
子類繼承父類Parent,重寫test()方法.
public class Child extends Parent { @Override public void test() { for (int i = 0; i < 5; i++) { System.out.println("Child " + Thread.currentThread().getName() + ":" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
測試代碼如下:
final Child c = new Child(); new Thread() { public void run() { c.test(); }; }.start(); new Thread() { public void run() { c.test(); }; }.start();
輸出結果如下:
Parent Thread-0:0 Child Thread-0:0 Parent Thread-0:1 Child Thread-1:0 Parent Thread-0:2 Child Thread-0:1 Parent Thread-0:3 Child Thread-1:1 Parent Thread-0:4 Child Thread-0:2 Parent Thread-1:0 Child Thread-1:2 Parent Thread-1:1 Child Thread-0:3 Parent Thread-1:2 Child Thread-1:3 Parent Thread-1:3 Child Thread-0:4 Parent Thread-1:4 Child Thread-1:4
通過輸出信息可以知道,父類Parent中會將單個線程中序列號輸出完成才會執行另一個線程中代碼,但是子類Child中確是兩個線程交替輸出數字,所以synchronized不具有繼承性。
4.死鎖示例
死鎖是多線程開發中比較常見的一個問題。若有多個線程訪問多個資源時,相互之間存在競爭,就容易出現死鎖。下面就是一個死鎖的示例,當一個線程等待另一個線程持有的鎖時,而另一個線程也在等待該線程鎖持有的鎖,這時候兩個線程都會處於阻塞狀態,程序便出現死鎖。
package com.lock; class Thread01 extends Thread{ private Object resource01; private Object resource02; public Thread01(Object resource01, Object resource02) { this.resource01 = resource01; this.resource02 = resource02; } @Override public void run() { synchronized(resource01){ System.out.println("Thread01 locked resource01"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource02) { System.out.println("Thread01 locked resource02"); } } } } class Thread02 extends Thread{ private Object resource01; private Object resource02; public Thread02(Object resource01, Object resource02) { this.resource01 = resource01; this.resource02 = resource02; } @Override public void run() { synchronized(resource02){ System.out.println("Thread02 locked resource02"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resource01) { System.out.println("Thread02 locked resource01"); } } } } public class deadlock { public static void main(String[] args) { final Object resource01="resource01"; final Object resource02="resource02"; Thread01 thread01=new Thread01(resource01, resource02); Thread02 thread02=new Thread02(resource01, resource02); thread01.start(); thread02.start(); } }
結果為:
Thread02 locked resource02
Thread01 locked resource01
執行上面的程序就會一直等待下去,出現死鎖。當線程Thread01獲得resource01的鎖后,等待500ms,然后嘗試獲取resource02的鎖,但是此時resouce02鎖已經被Thread02持有,同樣Thread02也等待了500ms嘗試獲取resouce01鎖,但是該所已經被Thread01持有,這樣兩個線程都在等待對方所有的資源,造成了死鎖。
三.其它
關鍵字synchronized具有鎖重入功能,當一個線程已經持有一個對象鎖后,再次請求該對象鎖時是可以得到該對象的鎖的,這種方式是必須的,否則在一個synchronized方法內部就沒有辦法調用該對象的另外一個synchronized方法了。鎖重入是通過為每個所關聯一個計數器和一個占有它的線程,當計數器為0時,認為鎖是未被占有的。線程請求一個未被占有的鎖時,JVM會記錄鎖的占有者,並將計數器設置為1。如果同一個線程再次請求該鎖,計數器會遞增,每次占有的線程退出同步代碼塊時計數器會遞減,直至減為0時鎖才會被釋放。
在聲明一個對象作為鎖的時候要注意字符串類型鎖對象,因為字符串有一個常量池,如果不同的線程持有的鎖是具有相同字符的字符串鎖時,兩個鎖實際上同一個鎖。
ReentrantLock特性
輪詢鎖的和定時鎖
可輪詢和可定時的鎖請求是通過tryLock()方法實現的,和無條件獲取鎖不一樣. ReentrantLock可以有靈活的容錯機制.死鎖的很多情況是由於順序鎖引起的, 不同線程在試圖獲得鎖的時候阻塞,並且不釋放自己已經持有的鎖, 最后造成死鎖. tryLock()方法在試圖獲得鎖的時候,如果該鎖已經被其它線程持有,則按照設置方式立刻返回,而不是一直阻塞等下去,同時在返回后釋放自己持有的鎖.可以根據返回的結果進行重試或者取消,進而避免死鎖的發生.
公平性
ReentrantLock構造函數中提供公平性鎖和非公平鎖(默認)兩種選擇。所謂公平鎖,線程將按照他們發出請求的順序來獲取鎖,不允許插隊;但在非公平鎖上,則允許插隊:當一個線程發生獲取鎖的請求的時刻,如果這個鎖是可用的,那這個線程將跳過所在隊列里等待線程並獲得鎖。我們一般希望所有鎖是非公平的。因為當執行加鎖操作時,公平性將講由於線程掛起和恢復線程時開銷而極大的降低性能。考慮這么一種情況:A線程持有鎖,B線程請求這個鎖,因此B線程被掛起;A線程釋放這個鎖時,B線程將被喚醒,因此再次嘗試獲取鎖;與此同時,C線程也請求獲取這個鎖,那么C線程很可能在B線程被完全喚醒之前獲得、使用以及釋放這個鎖。這是種雙贏的局面,B獲取鎖的時刻(B被喚醒后才能獲取鎖)並沒有推遲,C更早地獲取了鎖,並且吞吐量也獲得了提高。在大多數情況下,非公平鎖的性能要高於公平鎖的性能。
可中斷獲鎖獲取操作
lockInterruptibly方法能夠在獲取鎖的同時保持對中斷的響應,因此無需創建其它類型的不可中斷阻塞操作。
讀寫鎖ReadWriteLock
ReentrantLock是一種標准的互斥鎖,每次最多只有一個線程能持有鎖。讀寫鎖不一樣,暴露了兩個Lock對象,其中一個用於讀操作,而另外一個用於寫操作。
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(); }
可選擇實現:
1.釋放優先
2.讀線程插隊
3.重入性
4.降級
5.升級
ReentrantReadWriteLock實現了ReadWriteLock接口,構造器提供了公平鎖和非公平鎖兩種創建方式。讀寫鎖適用於讀多寫少的情況,可以實現更好的並發性。