1、公平鎖/非公平鎖
2、可重入鎖
3、獨享鎖/共享鎖
4、互斥鎖/讀寫鎖
5、樂觀鎖/悲觀鎖
6、分段鎖
7、偏向鎖/輕量級鎖/重量級鎖
8、自旋鎖(java.util.concurrent包下的幾乎都是利用鎖)
從底層角度看常見的鎖也就兩種:Synchronized和Lock接口以及ReadWriteLock接口(讀寫鎖)
從類關係看出Lock接口是jdk5後新添的來實現鎖的功能,其實現類:ReentrantLock、WriteLock、ReadLock。
其實還有一個接口ReadWriteLock,讀寫鎖(讀讀共享、讀寫獨享、寫讀獨享、寫寫獨享)。
Lock接口與synchronized關鍵字本質上都是實現同步功能。
區別:ReentrantLock:使用上需要顯示的獲取鎖和釋放鎖,提高可操作性、可中斷的獲取獲取鎖以及可超時的獲取鎖,默認是 非公平的但可以實現公平鎖,悲觀,獨享,互斥,可重入,重量級鎖。
ReentrantReadWriteLock:默認非公平但可實現公平的
,悲觀,寫獨享,讀共享,讀寫,可重入,重量級鎖。
synchronized:關鍵字,隱式的獲取鎖和釋放鎖,不具備可中斷、可超時,非公平、互斥、悲觀、獨享、可重入的重量級
Lock的使用也很簡單:
- Lock lock = new ReentrantLock();
- lock.lock();
- try{
-
- }finally{
- lock.unlock();
- }
- //注意:不要將lock方法寫在try塊中,因爲如果在獲取鎖的時候發生異常,異常拋出的同時也會導致鎖無故的釋
- //放 否則會程序會報監視狀態異常
- Exception in thread "線程一" java.lang.IllegalMonitorStateException
- at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
- at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
- //ReentrantLock必須要在finally中unlock(), 否則,如果在被加鎖的代碼中拋出了異常,那麼這個鎖將會永遠無法釋放.
-
- //synchronized就沒有這樣的問題, 遇到異常退出時,會釋放掉已經獲得的鎖.
Lock接口提供的 ,synchronized關鍵字所不具備的特性
特性 | 描述 |
嘗試性非阻塞地獲取鎖(tryLock方法) | 當前線程嘗試的獲取鎖,如果這一時段沒有被q其他線程獲取,則成功的獲取鎖,否則直接返回false |
能被中斷的獲取鎖(lockInterruptibly()throws InterruptedException) |
與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放。
|
- Lock lock = new ReentrantLock();
- final MMT m = new MMT(lock);
- Thread tt = new Thread(new Runnable() {
- @Override
- public void run() {
- System.out.println("線程一 開始執行。。。");
- try {
- m.update("張三");
- } catch (InterruptedException e) {
- System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
- }
- System.out.println("線程一 結束執行。。。");
- }
- },"線程一");
-
- Thread tt2 = new Thread(new Runnable() {
- @Override
- public void run() {
-
- System.out.println("線程二 開始執行。。。");
- try {
- m.update("李四");
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
- }
- System.out.println("線程二 結束執行。。。");
- }
- },"線程二");
-
- tt.start();
- tt2.start();
- //中斷線程
- tt.interrupt();
- try {
- tt.join();
- tt2.join();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
-
- }
-
- class MMT {
- String name;
- Lock lock=null;
- public MMT(Lock lock) {
- this.lock=lock;
- }
- public void update(String name) throws InterruptedException{
- // lock.lock();
- // boolean tryLock = lock.tryLock();//嘗試獲取鎖
- //中斷只是在當前線程獲取鎖之前,或者當前線程獲取鎖的時候被阻塞
- // lock.lockInterruptibly();
- lock.tryLock(3000, TimeUnit.SECONDS);
- try{
- setName(name);
- System.out.println(Thread.currentThread().getName()+" 變換後的姓名爲"+name);
- }finally{
- lock.unlock();
- }
- }
-
- public void setName(String name) {
- this.name = name;
- }
- public String getName() {
- return name;
- }
- }
可實現公平鎖
對於ReentrantLock而言,可實現公平鎖 ,通過構造函數指定是否需要公平,默認是非公平,區別在與非公平隨機性,並且高併發下吞吐量大,公平的話根據請求鎖等待的時間長短,等待的長了優先,類似FIFO,吞吐量降低了。
鎖綁定多個條件
指ReentrantLock對象可以同時綁定多個Condition條件對象,而在Synchroized中,鎖對象的wait方法、notify方法、和notifyall方法可以實現一個隱含條件,如果需要多個,得額外的添加一個鎖對象。在ReentrantLock中不需要,只需要創建多個條件對象即可(new Condition()),對應的await()、siganl()、signalAll()。
synchronized的優勢
synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中
應用場景:
在資源競爭不激烈的情況下,synchronized關鍵字的性能優與ReentrantLock,相反,ReentrantLock的性能保持常態,優於關鍵字。
按照其性質劃分:
公平鎖/非公平鎖
公平鎖指多個線程按照申請鎖的順序來依次獲取鎖。非公平鎖指多個線程獲取鎖的順序並不是按照申請鎖的順序來獲取,有可能後申請鎖的線程比先申請鎖的線程優先獲取到鎖,此極大的可能會造成線程飢餓現象,遲遲獲取不到鎖。由於ReentrantLock是通過AQS來實現線程調度,可以實現公平鎖,,但是synchroized是非公平的,無法實現公平鎖。
- /**
- * 公平鎖與非公平鎖測試
- */
- public class FairAndUnFairThreadT {
-
-
- public static void main(String[] args) throws InterruptedException {
- //默認非公平鎖
- final Lock lock = new ReentrantLock(true);
- final MM m = new MM(lock);
- for (int i=1;i<=20 ;i++){
- String name = "線程"+i;
- Thread tt = new Thread(new Runnable() {
- @Override
- public void run() {
- for(int i=0;i<2;i++){
- m.testReentrant();
- }
- }
- },name);
- tt.start();
- }
-
- }
- }
- class MM {
- Lock lock = null;
- MM(Lock lock){
- this.lock = lock;
- }
-
- public void testReentrant(){
- lock.lock();
- try{
- Thread.sleep(1);
- System.out.println(Thread.currentThread().getName() );
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
-
- public synchronized void testSync(){
- System.out.println(Thread.currentThread().getName());
- }
-
- }
但是未必絕對就是按照順序,可能因爲CPU準備原因,可能個別會不是公平的。
樂觀鎖與悲觀鎖
不是指什麼具體類型的鎖,而是指在併發同步的角度。悲觀鎖認爲對於共享資源的併發操作,一定是發生xi修改的,哪怕沒有發生修改,也會認爲是修改的,因此對於共享資源的操作,悲觀鎖採取加鎖的方式,認爲,不加鎖的併發操作一定會出現問題。樂觀鎖認爲對於共享資源的併發操作是不會發生修改的,在更新數據的時候,會採用嘗試更新,不斷重試的方式更新數據。樂觀的認爲,不加鎖的併發操作共享資源是沒問題的。從上面的描述看除,樂觀鎖不加鎖的併發操作會帶來性能上的提升,悲觀鎖的使用就是利用synchroized關鍵字或者lock接口的特性。樂觀鎖在java中的使用,是無鎖編程常常採用的是CAS自旋鎖,典型的例子就是併發原子類,通過CAS自旋(spinLock)來更新值。
獨享鎖與共享鎖
獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指可被多個線程所持有。在java中,對ReentrantLock對象以及synchroized關鍵字而言,是獨享鎖的。但是對於ReadWriteLock接口而言,其讀是共享鎖,其寫操作是獨享鎖。讀鎖的共享鎖是可保證併發讀的效率,讀寫、寫寫、寫讀的過程中都是互斥的,獨享的。獨享鎖與共享鎖在Lock的實現中是通過 AQS(抽象隊列同步器)來實現的。
互斥鎖與讀寫鎖
互斥鎖與讀寫鎖就是具體的實現,互斥鎖在java 中的體現就是synchronized關鍵字以及Lock接口實現類ReentrantLock,讀寫鎖在java中的具體實現就是ReentrantReadWriteLock。
可重入鎖
又名遞歸鎖,是指同一個線程在外層的方法獲取到了鎖,在進入內層方法會自動獲取到鎖。對於ReentrantLock和synchronized關鍵字都是可重入鎖的。最大的好處就是能夠避免一定程度的死鎖。
- public sychrnozied void test() {
- //執行邏輯,調用另一個加鎖的方法
- test2();
- }
-
- public sychronized void test2() {
- //執行業務邏輯
- }
在上面代碼中,sychronized關鍵字加在類方法上,執行test方法獲取當前對象作爲監視器的對象鎖,然後又調用test2同步方法。
一、如果鎖是可重入的話,那麼當前線程就在調用test2時並不需要再次獲取當前鎖對象,可以直接進入test2方法。
二、如果鎖是不具備可重入的話,那麼該線程在調用test2前會等待當前對象鎖的釋放,實際上該對象鎖已被當前線程所持有不可能再此獲得。那麼就會發生死鎖。
按照設計方案來分類(目的對鎖的進一步優化)
自旋鎖與自適應自旋鎖(或者說是自旋鎖的變種TicketLock、MCSLock、CLHLock)
底層採用CAS來保證原子性,自旋鎖獲取鎖的時候不會阻塞,而是通過不斷的while循環的方式嘗試獲取鎖。優點:減少線程上下文切換的消耗,缺點是會消耗CPU。如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。
偏向鎖、輕量級鎖、重量級鎖
這三種鎖是指鎖的狀態,並且是針對Synchronized,在java通過引入鎖升級的機制來實現高校的synchronized。鎖的狀態是通過對象監視器在對象頭中的字段來表明的。
偏向鎖:指一段同步代碼一直被同一個線程s所訪問,那麼該線程會自動的獲取鎖。降低獲取鎖的代價。
輕量級鎖:當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖, 不會阻塞,提高性能。
重量級鎖:當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒獲取到鎖
就會進入阻塞,該鎖膨脹爲重量級鎖。重量級會讓其他申請線程阻塞,性能降低。
偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它現在認爲只可能有一個線程來訪問它,所以當第一個
線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操作,並將
對象頭中的ThreadID改成自己的ID,之後再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。
一旦有第二個線程訪問這個對象,因爲偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態,這時表明在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變爲無鎖狀態,然後重新偏向新的線程,如果原來的線程依然存活,則馬上執行那個線程的操作棧,檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級爲輕量級鎖,(偏向鎖就是這個時候升級爲輕量級鎖的)。如果不存在使用了,則可以將對象回覆成無鎖狀態,然後重新偏向。
輕量級鎖認爲競爭存在,但是競爭的程度很輕,一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。
分段鎖
分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。