Android鎖的實現(xiàn)

鎖的對比

java中的鎖一共有4種狀態(tài),級別從低到高分別是:

  • 無鎖狀態(tài)
  • 偏向鎖
  • 輕量級鎖
  • 重量級鎖

鎖只能升級,不能降級

偏向鎖

顧名思義,為了讓線程獲得鎖的代價更低,引入了偏向鎖。

加鎖

當一個線程訪問同步塊并且獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程id,這樣,這個線程便獲取了這個對象的偏向鎖,之后這個線程進入和退出就不需要通過CAS操作,也就是原子操作,來進行加鎖和解鎖,只需要簡單的測試下對象頭存儲的偏向鎖的線程id是否和自身的id一致,如果一致,那么已經獲取鎖,直接進入。否則,判斷對象中是否已經存儲了偏向鎖,如果沒有鎖,那么使用CAS競爭鎖,如果設置了,那么嘗試使用CAS將對象頭的偏向鎖指向當前線程。

解鎖

偏向鎖的解鎖時機是在競爭時才會釋放鎖,撤銷時需要等待全局安全點,這個時間點沒有正在執(zhí)行的字節(jié)碼,首先會暫停擁有偏向鎖的線程,然后檢查偏向鎖的線程是否存活,如果不活動,那么直接設置為無鎖狀態(tài)。否則要么偏向其他鎖,要么恢復到無鎖或者標記對象不適合偏向鎖。

輕量鎖

會自旋嘗試獲取鎖,消耗cpu資源

加鎖

一旦多線程發(fā)起了鎖競爭,并且釋放了偏向鎖之后,線程通過CAS修改Mark Word,如果當前沒有對象持有同步體的鎖,那么直接將同步體的鎖修改的輕量鎖,否則,該線程將自旋獲取鎖,直到膨脹為重量級鎖,修改同步體的Mark Word為重量級鎖,然后阻塞

解鎖

一旦有其他線程因想獲取當前鎖而膨脹為重量級鎖,那么這個線程將會通過CAS替換Mark Word,然后失敗,解鎖,并且喚醒其他等待線程。

重量級鎖

會阻塞,不消耗cpu資源,但是響應時間較慢

原子操作

處理器實現(xiàn)原子操作

總線鎖

通過總線鎖保證只有單一處理器占有共享內存

緩存鎖

只對某個內存地址進行加鎖,保證其他的內存地址不受影響。

java實現(xiàn)原子操作

循環(huán)使用CAS操作

需要解決以下三個問題

  • ABA問題
    如果一個值最開始為1,中間修改為2,最后再修改為1,其實這個值是修改過的,所以只通過基本的CAS操作是有問題的,這個時候就需要通過引用來處理。
  • 耗時
    通過pause指令解決
  • 只能保證一個共享變量
    將多個共享變量合成一個共享變量

鎖機制也是通過循環(huán)CAS操作

volatile原理

在jvm內存模型上,即每一個線程都持有一份對于內存的拷貝,volatile保證了對于一個volatile變量的讀,總能看到任意線程對這個volatile變量最后的寫入。
我們可以這么理解,對一個volatile變量,它的set和get都將加鎖,只要是一個volatile變量,對該變量的讀寫就具有原子性,但是復合操作,比如說volatile操作整體上不具有原子性。

  • 可見性:對于java內存模型,每一個線程都擁有一個主存的副本,在一個共享變量被某個線程修改之后,將會將其他線程中內存的副本標志為無效,強制從主存中取值。
  • 原子性:對于被volatile修飾的變量,其get和set操作都加了鎖,
  • 不可重排:對于被volatile修飾的變量,保證編譯器不會對一些代碼時序做重排

synchronized

內部也是利用了鎖。
每一個對象都有一個自己的monitor,必須先獲取這個monitor對象才能夠進入同步塊或同步方法,而這個monitor對象的獲取是排他的,也就是同一時刻只能有一個線程獲取到這個monitor

修飾同步塊

主要通過monitorenter和monitorexit指令來進行獲取

修飾方法

主要是通過修飾符ACC_SYMCHRONIZED來修飾

如果獲取對象的monitor失敗,線程將會切換為BLOCK狀態(tài),

synchronized作為java中用得最廣泛的同步關鍵字。實際上內部實現(xiàn)的依靠著兩個關鍵的命令,monitorenter和monitorexit。synchronized同樣是通過互斥量和臨界區(qū)來實現(xiàn)。monitorenter指令表示進入臨界區(qū),互斥量+1,monitorexit表示退出臨界區(qū),互斥量減一。在jdk1.5之前,synchroize操作都是重操作,所加的鎖都是自旋鎖。即,線程在執(zhí)行了monitorenter時,發(fā)現(xiàn)互斥量已經為0了,則必須在外面循環(huán)等待,而不會進行阻塞,加入等待隊列。而在jdk1.6之后,對synchronized做了很多優(yōu)化操作,鎖粗化,鎖消除,輕量級鎖,偏向鎖,適應性自旋鎖等來減少操作的開銷。下面我們分別來介紹幾個優(yōu)化操作。

  • 鎖膨脹(Lock Coarsening):如果系統(tǒng)檢測到有對同一個對象反復加鎖或者解鎖,那么系統(tǒng)在保證加鎖的正確性之后,會將鎖擴張,保證不必要的加鎖操作和解鎖操作。
  • 鎖削除(Lock Elimination):在虛擬機運行時,通過JIT編譯器的逃逸分析檢測出部分上鎖的代碼不可能出現(xiàn)數據多線程數據共享的狀態(tài),那么自然會將這個鎖給去除。
  • 輕量級鎖(Lightweight Locking):輕量級鎖是相對于重量級鎖來說的,它的本意是在沒有多線程競爭的情況下,減少重量級鎖使用的互斥量產生的性能消耗,輕量級鎖不用使用操作系統(tǒng)的互斥量,僅僅只是在hotspot對象頭記錄,在這里,我們必須先介紹一下Hotspot的對象頭,其中又個markWork,用來記錄當前的鎖狀態(tài):
    01: 表示未鎖定
    00: 表示輕量級鎖定
    10: 表示重量級鎖定
  • 偏向鎖:偏向第一個持有鎖的對象,一旦持有,則不會再進行同步。直到有下一個線程嘗試獲取這個鎖。
  • 適應性自旋鎖:在線程首次嘗試輕量級鎖失敗時,會進入忙等待一段時間,然后再次重試,在重試了一定次數失敗之后,就會進入阻塞態(tài)。

和lock的區(qū)別

synchronized將會隱式的獲取鎖,但是它把鎖給固化了,沒有辦法比較靈活的利用鎖,比如說非阻塞式的獲取鎖,超時獲取鎖等等。

synchronized和lock方式兩種加鎖的方式的最底層實現(xiàn)是一致的,都是通過臨界區(qū)的互斥量來判斷的。只是synchronized加鎖及解鎖操作都是由jvm為我們實現(xiàn)好了。而lock加鎖方式是基于我們代碼的實現(xiàn),我們必須要手動的解鎖。這就讓lock方式能夠有一些相對于synchronized有一些優(yōu)點了。

  • 提供了tryLock方法,可以讓我們提前預知能否加鎖而不會讓該線程進入阻塞狀態(tài)。synchronized加鎖之后,一旦不能加鎖便會進入阻塞狀態(tài)。

  • lockInterruptibly方法:提供了立即響應線程中斷的方法,通常來說,線程并不會是在任何狀態(tài)下都會被立即中斷的,只有在線程處于阻塞狀態(tài)才會被立即中斷。線程在獲取鎖的時候,會處于一種不可中斷的狀態(tài),比如synchonized或者單純的lock操作就是這樣的。

  • 提供了獲取鎖超時的返回選項,對于競爭激烈的臨界區(qū),這種機制顯得格外重要。這個機制也是通過tryLock來實現(xiàn),多加了一個超時參數判斷。

在我們代碼的實現(xiàn)要求中,假如沒有明確的要求,我們可以利用synchronized關鍵字,sunchronized關鍵字在競爭不是很激烈的情況下,性能要優(yōu)于lock操作,但是在競爭激烈的情況下,性能將會下降特別多。

隊列同步器 AbstractQueuedSynchronizer

提供了3類模版方法:

  • 獨占式獲取與釋放同步狀態(tài)
  • 共享式獲取與釋放同步狀態(tài)
  • 查詢同步隊列的等待線程

通過下面3個方法來訪問或修改同步狀態(tài):

  • getState
  • setState
  • compareAndSetState

同步器內部依賴于同步隊列來完成同步狀態(tài)的管理,當前線程獲取同步狀態(tài)失敗時,同步器將會把這個線程構造成一個node添加到同步隊列,并且阻塞線程,當同步狀態(tài)釋放時,將會把首節(jié)點的線程喚醒,讓其再次嘗試獲取同步狀態(tài)。 對于入隊操作,同樣也需要CAS操作進行入隊

  • 可重入鎖: 線程可以進入它已經加上鎖的同步代碼塊。即假如這個線程在某個位置加上可重入鎖,在后面這個線程仍然可以再次加鎖。進入同樣的同步代碼塊,也就是臨界區(qū)。
  • 不可重入鎖: 一旦線程在某個地方加上鎖了,在沒解鎖前,這個鎖就是該線程唯一一次進入該臨界區(qū)的通道。而且,在這個臨界區(qū)中,有且僅能有一個這樣的鎖。

重入鎖

ReetrantLock,顧名思義,它表示該鎖能夠支持一個線程對資源的重復加鎖。
symchronized默認支持的是可重入鎖。

java中鎖主要是在util.concurrent.locks包下,最常用的就是ReentrantLock類,這個鎖被成為可重入鎖。既然有可重入鎖,也就會有不可重入鎖,這種鎖的區(qū)別主要是針對于同一個線程來說的,下面簡單聊聊這兩種鎖的區(qū)別:

在java中,ReentrantLock和synchronized所加上的鎖都是可以重入.

在介紹ReentrantLock之前,我們必須先了解其內部實現(xiàn)的兩種鎖,公平鎖和非公平鎖。

  • 公平鎖: 所有的嘗試加鎖的對象,都會添加到一個隊列中,先來的排在隊列前頭,到時候可以加鎖時,從隊首取第一個加鎖對象。
  • 非公平鎖: 所有嘗試加鎖的對象能夠加上鎖的順序不能保證,隨機。
    我們先來看看ReentrantLock對象的創(chuàng)建:
Lock fairLock = new ReentrantLock(true); //創(chuàng)建公平鎖
Lock noFairLock = new Reentrantlock();  //創(chuàng)建非公平鎖

=====>兩種構造函數的實現(xiàn)
 public ReentrantLock() {
        sync = new NonfairSync();
    }
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }    
{%endcodeblock%}
我們先只針對于非公平鎖來分析,我們平時調用這個鎖并不會特意的指定鎖類型。對于加鎖,我們只需要調用lock方法即可。
{%codeblock%}
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
{%endcodeblock%}
compareAndSetState中,0時我們期待的值,1是我們現(xiàn)在想要更新的值,這個值表示的是當前臨界區(qū)的互斥量。加入返回true的話,表示當前沒有線程給這個臨界區(qū)上鎖,則直接上鎖,設置持有線程,否則,現(xiàn)在已經有鎖了,調用acquire(1).
{%codeblock%}
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
========>添加隊列節(jié)點
private Node addWaiter(Node mode) {
        //新建隊列節(jié)點,節(jié)點是需要記錄線程的
        Node node = new Node(Thread.currentThread(), mode);
        //獲取尾部元素
        Node pred = tail;
        if (pred != null) {
            //插入在尾部后面
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //加入沒有尾部元素,執(zhí)行enq操作,
        enq(node);
        return node;
    }
==========>
  private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { 
                //沒有元素,插入頭部
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //有元素,插入尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
=========> 利用斷言,判斷是否能夠加鎖
 public boolean tryAcquire(long acquires) {
            assertEquals(LOCKED, acquires);
            return compareAndSetState(UNLOCKED, LOCKED);
        }
=======> 在嘗試獲取鎖失敗之后,會添加到等待隊列中
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //找到前一個節(jié)點
                final Node p = node.predecessor();
                //如果這個節(jié)點是頭節(jié)點的話,那么在再次嘗試進入臨界區(qū)
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //shouldParkAfterFailedAcquire返回這個線程是否需要阻塞等待,
                //parkAndCheckInterrupt()用于阻塞這個線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

通過上面這個加鎖操作,我們可以了解當一個線程嘗試加鎖失敗時,會判斷是否需要阻塞,如果需要阻塞,則直接阻塞該線程.
上面操作,是直接執(zhí)行l(wèi)ock方法,我們可以先嘗試執(zhí)行tryLock方法:

 public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    =======>嘗試獲取非公平鎖,嘗試加鎖,加鎖是以線程的基礎
  final boolean nonfairTryAcquire(int acquires) {
            //獲取當前線程
            final Thread current = Thread.currentThread();
            //state表示獲取的當前狀態(tài)的互斥量值
            int c = getState();
            if (c == 0) {
                //為0表示沒上鎖,重新嘗試進入臨界區(qū)
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                //當前的線程與以上鎖的線程是同一個,可重入,計算同一個計算上鎖個數
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }    

下面我們來看看解鎖操作:

public void unlock() {
        sync.release(1);
    }
    
=======>
 public final boolean release(long arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
======>在tryRelease中利用斷言解鎖,假如現(xiàn)在沒有鎖,那么則會拋異常
 public boolean tryRelease(long releases) {
            if (getState() != LOCKED) throw new IllegalMonitorStateException();
            setState(UNLOCKED);
            return true;
        }
======>unparkSuccessor(h)讓隊列頭進入臨界區(qū)

 private void unparkSuccessor(Node node) {
    
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        //尋找第一個合法的等待節(jié)點
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != null && p != node; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        
        if (s != null)
            //打斷阻塞,進入臨界區(qū)
            LockSupport.unpark(s.thread);
    }

加鎖

如果當前對象沒有加鎖,那么直接加鎖,如果已經加鎖,判斷之前加鎖的線程與當前要加鎖線程是否是同一個,如果是,那么再次獲取。

解鎖

如果一個線程獲取了n次鎖,那么就需要解鎖n次。

讀寫鎖

在同一時刻允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程都被阻塞。其實內部是維護了一對鎖

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容