Java鎖原理與應用

 一、鎖

  鎖是一種互斥的機制,在多線程環境中實現對資源的協調與控制,凡是有資源被多線程共享,涉及到修改的情況就要考慮鎖的加持。

 (0)Java鎖原理

  0)引申:Java對象結構

    Java對象結構分爲3部分:

    ①對象頭(包括:Mark Word(存儲了當前對象運行時的狀態信息,如HashCode、指向鎖記錄的指針等)、Class Pointer(指針,指向當前對象類型所在方法區中的Class信息));

     如圖,MarkWord結構(jdk1.8)

    

     HotSpot 64位操作系統(一個對象的markWord在內存佔用8字節)

     

   鎖標誌位,分別代表無鎖、偏向鎖、輕量級鎖、重量級鎖4種狀態;

   ②實例數據;

   ③對齊填充字節(在內存中佔8字節)。

  1)在Java中,每個對象都擁有一把鎖,存放在對象頭中,記錄了當前對象被哪個線程佔用。

  2)操作系統用戶態和內核態

     由於需要限制不同的程序之間的訪問能力,防止他們獲取別的程序的內存數據,或者獲取外圍設備的數據,併發送到網絡,CPU劃分出兩個權限等級,用戶態和內核態。所有用戶程序都是運行在用戶態的,當程序需要做一些內核態的事情,,例如從硬盤讀取數據,,或者從鍵盤獲取輸入等。而唯一可以做這些事情的就是操作系統,此時程序就需要操作系統請求以程序的名義來執行這些操作,即將用戶態程序切換到內核態。

      內核態: CPU可以訪問內存所有數據,,包括外圍設備, 例如硬盤,、網卡, CPU也可以將自己從一個程序切換到另一個程序。

    用戶態: 只能受限的訪問內存,,且不允許訪問外圍設備.,佔用CPU的能力被剝奪。

(1)鎖的實現方式

  0)引申:

    在java中,鎖的實現主要採用兩種方式:1、基於Object的悲觀鎖;2、基於CAS的樂觀鎖,Lock接口是基於CAS原理實現。java5之前的版本只有synchronized鎖,基於操作系統提供的指令,在內核態實現多線程之間訪問資源的同步性;之後發現基於內核態的synchronize的鎖開銷很大,提出了Lock鎖機制,在java5版本中被官方採納;隨後java官方對synchronized進行了優化,提出了對象鎖的4種狀態概念。在java的後續版本中,兩者在性能上差別需要根據實際情況進行選擇使用。

  1)synchronized

    j.u.c.Lock中說明synchronized是在硬件層面依賴特殊的CPU指令。synchronized別編譯後會生成monitorenter和monitorexit兩個字節碼指令,依賴這兩個字節碼指令進行線程同步。monitor,監視器(管程),一旦線程進入了monitor,那麼其他線程只能等待,只有當這個線程退出,其他線程纔有機會進入。monitor依賴於操作系統的Mutex Lock實現,所以每當掛起或喚醒線程,都要切換到操作系統的內核態,這個操作比較重量級。在某些情況下,甚至於切換時間本身就會超出線程執行任務的時間。java6開始,對synchronized進行了優化,引入了對象鎖的4種狀態,分別是無鎖、偏向鎖、輕量級鎖、重量級鎖。

eg:

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

 對以上示例代碼執行javap  -p -c 指令(說明,javap <---> java class文件分解器, -p <-----> 展示所有的類和成員, -c <-------> 對代碼進行反編譯, 具體指令說明可通過javap -help 展示)

 

  synchronized特點:

  ①支持線程可重入;

  ②等待狀態(前一個線程並未釋放鎖,當前線程處於不斷嘗試獲取鎖的狀態(對應java中定義的RUNNABLE狀態))不可中斷;

  ③synchronized會自動釋放鎖;

  ④synchronized是非公平鎖;

  ⑤synchronized既可以鎖住代碼塊,也可以鎖住方法;

  注:java中定義了線程執行的的6種狀態

    1.創建 2. 執行 3.銷燬 4.時間限制的等待 5.無線等待 6.阻塞

  

 

    操作系統中定義的線程狀態有3種:運行態、阻塞態、就緒態;線程的生命週期在此基礎上添加了創建和銷燬;

    

  2)Lock接口

     Lock接口提供了區別於synchronized的另一種具有方法操作的同步方式,支持更多靈活的結構,可以關聯多個Condition(java提供的用戶線程通信的接口)對象。

package lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class RetreenLockTest {

private static Lock rl = new ReentrantLock();

private static void testReentrantLock(){
Runnable run = ()->{
String name = Thread.currentThread().getName();
//保證lock和unlock不論中間代碼是否拋出異常,都正常執行
rl.lock();
try {
System.out.println(name + "獲得鎖");
}finally {
rl.unlock();
System.out.println(name+"釋放鎖");
}
};

Thread t1 = new Thread(run);
t1.setName("001");
t1.start();

Thread t2 = new Thread(run);
t2.setName("002");
t2.start();
}
}

    特點:

  ①Lock的lock()方法在等待鎖釋放過程中是不可中斷的,而tryLock()方法是可中斷的;

  ②Lock需要手動釋放鎖;

  ③Lock使用讀鎖可以提高多線程讀的效率;

  ④RetreenLock可以控制是否使用公平鎖;

  ⑤Lock只能鎖住代碼塊;

(2)鎖的分類

  1)樂觀鎖/悲觀鎖

  樂觀鎖認爲每次讀取數據的時候總是認爲沒有其他線程進行更新操作,所以不去加鎖。但是在更新的時候回去對比一下原來的值,看有沒有被更改過。適用於讀多寫少的場景。樂觀鎖的本質是CAS。

   eg:

  (1)mysql中類比version號更新 update xxx set a=aaa where id=xx and version=1

  (2)java中的atomic包屬於樂觀鎖實現,即CAS。

  悲觀鎖在每次讀取數據的時候都認爲其他線程會修改數據,所以讀取數據的時候也加鎖,這樣別人想拿的時候就會阻塞,直到這個線程釋放鎖,這就影響了併發性能。適合寫操作比較多的場景。

   eg:

     (1) mysql中類比for select xxx for update; update update xx set a = aaa 案例中synchronized實現就是悲觀鎖(1.6之後優化爲鎖升級機制),悲觀鎖書寫不當很容易影響性能。

  樂觀鎖和悲觀鎖往往依靠數據庫提供的鎖機制實現,數據庫鎖才能真正保證數據訪問的排他性,應用層鎖無法保證外部系統不會修改數據。

  2)獨享鎖/共享鎖

  獨享鎖是指該鎖一次只能被一個線程所持有,而共享鎖是指該鎖可被多個線程所持有。

  案例一:ReentrantLock,獨享鎖,基於AQS(AbstractQueuedSynchronizer),實現了公平鎖和非公平鎖,ReentrantLock支持可重入(單個線程執行時重新進入同一個子程序仍然是線程安全的,即一個線程可以不用釋放鎖而重複獲取一個鎖多次,只是在釋放的時候也需要響應釋放多次)。

  

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PrivateLock {
    Lock lock = new ReentrantLock();
    long start = System.currentTimeMillis();
    void read() {
        lock.lock();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        System.out.println("read time = "+(System.currentTimeMillis() ‐ start));
    }
    public static void main(String[] args) {
        final PrivateLock lock = new PrivateLock();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    lock.read();
                }
            }).start();
        }
    }
}

結果分析:每個線程結束的時間點逐個上升,鎖被獨享,一個用完下一個,依次獲取鎖

  案例二:ReadWriteLock,read共享,write獨享

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedLock {
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock lock = readWriteLock.readLock();
    long start = System.currentTimeMillis();
    void read() {
        lock.lock();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        System.out.println("end time = "+(System.currentTimeMillis() ‐ start));
    }
    public static void main(String[] args) {
        final SharedLock lock = new SharedLock();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    lock.read();
                }
            }).start();
        }
    }
}

結果分析:每個線程獨自跑,各在100ms左右,證明是共享的

  案例三:同樣是上例,換成writeLock

Lock lock = readWriteLock.writeLock();

  小節:

  • 讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
  • 獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

 3)分段鎖

  ConcurrentHashMap線程安全的主要原理,CHM中維護了一個segment數組,數組中的每個元素是HashEntry數組;segment繼承ReentrantLock,每個segment對象就是一把鎖,一個segment對象內部存在一個HashEntry數組,即HashEntry數組中的數據同步依賴同一把鎖,不同的HashEntry數組的讀寫互不干擾,就形成了分段鎖。

 4)可重入鎖  

  可重入鎖指的獲取到鎖後,如果同步塊內需要再次獲取同一把鎖的時候,直接放行,而不是等待。其意義在於防止死鎖。前面使用的synchronized和ReentrantLock都是可重入鎖。 實現原理實現是通過爲每個鎖關聯一個請求計數器和一個佔有它的線程。如果同一個線程再次請求這個鎖,計數器 將遞增,線程退出同步塊,計數器值將遞減。直到計數器爲0鎖被釋放。 場景見於父類和子類的鎖的重入(調super方法),以及多個加鎖方法的嵌套調用。

  案例一:父子可重入

public class ParentLock {
    byte[] lock = new byte[0];
    public void f1(){
        synchronized (lock){
            System.out.println("f1 from parent");
        }
    }
}

public class SonLock extends ParentLock {
    public void f1() {
        synchronized (super.lock){
            super.f1();
            System.out.println("f1 from son");
        }
    }
    public static void main(String[] args) {
        SonLock lock = new SonLock();
        lock.f1();
    }
}

案例二:內嵌方法可重入

public class NestedLock {
    public synchronized void f1(){
        System.out.println("f1");
    }
    public synchronized void f2(){
        f1();
        System.out.println("f2");
    }
    public static void main(String[] args) {
        NestedLock lock = new NestedLock();
        //可以正常打印 f1,f2
        lock.f2();
    }
}

 5)公平鎖/非公平鎖

  基本概念:常見於AQS,公平鎖就是在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個,就佔有鎖,否則就會加入到等待隊列中,直到按照FIFO的規則從隊列中取到自己。 非公平鎖與公平鎖基本類似,只是在放入隊列前先判斷當前鎖是否被線程持有。如果鎖空閒,那麼他可以直接搶佔,而不需要判斷當前隊列中是否有等待線程。只有鎖被佔用的話,纔會進入排隊。

  優缺點:公平鎖的優點是等待鎖的線程不會餓死,進入隊列規規矩矩的排隊,遲早會輪到。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。 非公平鎖的性能要高於公平鎖,因爲線程有機率不阻塞直接獲得鎖。ReentrantLock默認使用非公平鎖就是基於性能考量。但是非公平鎖的缺點是可能引發隊列中的線程始終拿不到鎖,一直排隊被餓死。

  編碼方式:ReentrantLock支持創建公平鎖和非公平鎖(默認),想要實現公平鎖,使用new ReentrantLock(true)。AQS中有一個state標識鎖的佔用情況,一個隊列存儲等待線程。 state=0表示鎖空閒。如果是公平鎖,那就看看隊列有沒有線程在等,有的話不參與競爭,追加到尾部。如果是非公平鎖,那就直接參與競爭,不管隊列有沒有等待者。 state>0表示有線程佔着鎖,這時候無論公平與非公平,都直接去排隊。

  備註: 因爲ReentrantLock是可以定義公平、非公平鎖次數。所以state>0而不是簡單的0和1,而synchronized只能是非公平鎖

   6)CountDownLatch

    允許一條或多條線程等待其他線程中的一組操作完成後,再繼續執行;

  如圖所示:CountDownLatch調用次序

  

 7)鎖升級

  java中每個對象都可作爲鎖,鎖有四種級別,按照量級從輕到重分爲:無鎖、偏向鎖、輕量級鎖、重量級鎖。設計4種鎖的目的是線程儘量在操作系統的用戶空間完成鎖的的獲取與釋放,一旦進入重量級鎖狀態,將會調用內核空間,產生較大的開銷。

  • 偏向鎖(一個對象被加鎖,但是在實際運行過程中,只有一個線程會獲取這個對象鎖,那麼,此時最好的方式就是不經過系統狀態切換,在用戶態就完成任務,即對象的mark word標記中需要記錄線程id);
  • 輕量鎖(兩個線程需要獲取對象鎖的情況,線程通過CAS機制嘗試獲取鎖,一旦獲得,線程和對象鎖綁定,並且互相知道對方的存在);
  • 重量級鎖(自旋等待的線程超過一個,輕量級鎖就升級爲重量級鎖,對象鎖的狀態被標記爲重量級鎖,需要通過monitor來對線程進行控制,此時使用同步原語來鎖定資源,對線程的控制最爲嚴格)就是圍繞如何使得cpu的佔用更划算而展開的。

  在操作系統中,阻塞就要存儲當前線程狀態,喚醒就要再恢復,這個過程是要消耗時間的。如果A使用鎖的時間遠遠小於B被阻塞和掛起的執行時間,那麼我們將B掛起阻塞就相當的不合算,於是出現自旋,自旋指的是鎖已經被其他線程佔用時,當前線程不會被掛起,而是在不停的試圖獲取鎖(可以理解爲不停的循環),每循環一次表示一次自旋過程。顯然這種操作會消耗CPU時間,但是相比線程下文切換時間要少的時候,自旋划算。 如果自旋的線程過多,再上重量級鎖阻塞和掛起。

     舉個例子,假設公司只有一個會議室(共享資源)

  • 偏向鎖: 前期公司只有1個團隊,那麼什麼時候開會都能滿足,就不需要預約,OA裏直接默認設定爲使用者A。A在會議室門口掛了 個牌子,寫着A專用。
  • 輕量級鎖: 隨着業務發展,擴充爲2個團隊,於是當AB同時需要開會時,兩者在OA搶佔。偏向鎖升級爲輕量級鎖,但是未搶到者在門口會不停敲門詢問(自旋,循環)。
  • 重量級鎖: 後來隨着團隊規模繼續擴充,發現這種不停敲門的方式很煩,BCDEF……都在門口站着一直問。於是鎖再次升級。 如果會議室被A佔用,那麼其他團隊直接等着(wait進入阻塞),直到A用完。

注意點:

  • 上面幾種鎖都是JVM自己內部實現,我們不需要干預,但是可以配置jvm參數開啓/關閉自旋鎖、偏向鎖。
  • 鎖可以升級,但是不能反向降級:偏向鎖→輕量級鎖→重量級鎖
  • 無鎖爭用的時候使用偏向鎖,第二個線程到了升級爲輕量級鎖進行競爭,更多線程時,進入重量級鎖阻塞

  

  8)互斥鎖/讀寫鎖

  • 典型的互斥鎖:synchronized,ReentrantLock,讀寫鎖:ReadWriteLock 前面都用過了;
  • 互斥鎖屬於獨享鎖,讀寫鎖裏的寫鎖屬於獨享鎖,而讀鎖屬於共享鎖。

    

 二、AQS

    1)概念

     AbstractQuenedSynchronizer 抽象的隊列式同步器,是一個抽象類,這個類在 java.util.concurrent.locks包。AQS是對CAS的進一步封裝和豐富,引入了獨佔鎖、共享鎖等性質,是除了java自帶的synchronized關鍵字之外的鎖機制。它是實現同步器的基礎組件,併發包中鎖的底層使用AQS實現。

     AQS通過state記錄上鎖狀態,所有線程共享該資源,誰先修改成功誰就持有鎖。修改失敗的就被交給AQS進行丟到FIFo同步隊列排隊等候,直到線程釋放鎖,喚醒自己,重新去修改state。

 

  2)AQS上鎖、解鎖過程 

    上鎖:state的值是否爲0,爲0的話可以搶鎖,此時就調用CAS的方法(AQS中的compareAndSetSate方法)去原子性修改其值,返回true,上鎖成功。

     排隊獲取鎖:假如此時A線程上鎖成功了,A還沒有釋放鎖。B線程來的時候,判斷state是否爲0,若不是,AQS就開始將其包裝成一個Node節點,如果是第一個來排隊的線程(這時會有一個自旋),判斷隊列爲空,new 一個空Node作爲頭節點和尾節點(head=tail=new Node())。然後在第二次循環的時候,將新的節點的前繼節點node.pre = tail, tail.next = node,cas修改尾節點爲新節點compareAndSetTail(tail, node),成功加入尾部節點。將進來排隊包裝成節點並且放進隊尾之後,下一步就是自旋循環狀態,再一次判斷自己是否符合搶鎖條件(node.pre==head),如果不符合就進入park,將線程waiting,等待前面持有鎖的線程釋放鎖。

    解鎖:tryRelease() 方法返回true的時候,AQS開始拿到當前持鎖的node=head節點,將其head.next =null方便gc。判斷當前節點不爲空,狀態不爲0時(釋放鎖的時候cas將其狀態改爲0),開始喚醒下一個節點。找到正常狀態的節點之後,執行LockSupport.unpark(s.thread);,喚醒下一個要拿鎖的節點,喚醒之後繼續執行acquireQueued方法中的自旋。                                                  

三、原子操作(atomic)

 1)概念

  原子操作(atomic operation)意爲"不可被中斷的一個或一系列操作" 。類似於數據庫事務,redis的multi。

 2)CAS

  Compare And Swap,即比較並替換,CAS操作包含三個操作數—內存位置(addr)、預期原值(oldValue)、新值(newValue)。通過源碼理解cas的原理,對象接收到線程進行修改的操作,判斷對象值是否和線程提供的oldValue相同,若一致,則替換爲newValue;若不一致,則代表已經修改過,返回狀態0。

      計數器問題發生歸根結底是取值和運算後的賦值中間,發生了插隊現象。

  eg,cas應用:

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private static AtomicInteger i = new AtomicInteger(0);
    public int get(){
        return i.get();
    }
    public void inc(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        i.incrementAndGet();
    }
    public static void main(String[] args) throws InterruptedException {
        final AtomicCounter counter = new AtomicCounter();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    counter.inc();
                }
            }).start();
        }
        Thread.sleep(3000);
        //同樣可以正確輸出10
        System.out.println(counter.i.get());
    }
}

注:AtomicInteger是基於unsafe類cas思想實現。

3)cas原理分析

  cas函數原理:

 

  上圖所示代碼存在線程安全問題,能保證cas正確執行的前提是cas的操作必須是原子性的,cpu對cas原子操作提供了指令,如x86架構下的cpu,通過cmpxchg指令支持cas,上層只需要調用即可。

4)atomic

  上面展示了AtomicInteger,關於atomic包,還有很多其他類型:

  基本類型

    AtomicBoolean:以原子更新的方式更新boolean;

    AtomicInteger:以原子更新的方式更新Integer;

    AtomicLong:以原子更新的方式更新Long;

  引用類型

    AtomicReference : 原子更新引用類型

    AtomicReferenceFieldUpdater :原子更新引用類型的字段

    AtomicMarkableReference : 原子更新帶有標誌位的引用類型;

  數組

    AtomicIntegerArray:原子更新整型數組裏的元素。

    AtomicLongArray:原子更新長整型數組裏的元素。

    AtomicReferenceArray:原子更新引用類型數組裏的元素。

  字段

    AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。

    AtomicLongFieldUpdater:原子更新長整型字段的更新器。

    AtomicStampedReference:原子更新帶有版本號的引用類型。

  注意:使用atomic要注意原子性的邊界,把握不好會起不到應有的效果,原子性被破壞。

  案例:隔離失敗了!

import java.util.HashMap;
import java.util.Map;
public class BadLocal{
    public static void main(String[] args) {
        ThreadLocal<Map> local = new ThreadLocal();
        Map map = new HashMap();
        new Thread(()‐>{
            //在線程設置後,過段時間取name
            //猜一猜結果?
            map.put("name","i am "+Thread.currentThread().getName());
            local.set(map);
            System.out.println(Thread.currentThread().getName()+":"
                               +local.get().get("name"));
            //do something...
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"
                               +local.get().get("name"));
        }).start();
        new Thread(()‐>{
            //在線程中賦值name
            map.put("name","i am "+Thread.currentThread().getName());
            local.set(map);
        }).start();
    }
}

 

 

 感謝閱讀,借鑑了不少大佬資料,如需轉載,請註明出處,謝謝!https://www.cnblogs.com/huyangshu-fs/p/14296132.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章