鎖的分類和synchronized底層原理

一、JAVA中鎖的概念

1、自旋鎖

是指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。

自旋鎖有兩種表現形式:
1、AtomicIneger 中的使用場景,自旋是爲了修改一個變量,直接用CAS去進行操作
2、如下形式,通過自旋實現了一把鎖

public class Demo2_SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<Thread>();
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!owner.compareAndSet(null, current)) {
            // DO nothing
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        owner.compareAndSet(current, null);
    }


    public static void main(String args[]){
        Demo2_SpinLock lock = new Demo2_SpinLock();
        lock.lock();
        //do something
        lock.unlock();
    }
}

2、樂觀鎖

假定沒有衝突,在修改數據時如果發現數據和之前獲取的不一致,則讀最新數據,修改後重試修改。

3、悲觀鎖

假定會發生併發衝突,同步所有對數據的相關操作,從讀數據就開始上鎖。synchronized就是典型的悲觀鎖。

4、獨享鎖(寫)

給資源加上寫鎖,線程可以修改資源,其他線程不能再加鎖; (只能單個線程寫)

5、共享鎖(讀)

給資源加上讀鎖後只能讀不能改,其他線程也只能加讀鎖,不能加寫鎖; (可多個線程讀)

應用場景:給數據庫連接加說,限制數據庫連接數量,同一時間只能有指定數量的連接,這個鎖就是共享鎖。

ReadWriteLock

概念: ReadWriteLock維護了一對關聯鎖,一個只用於讀操作,一個只用於寫操作;其中讀鎖可以由多個線程同時持有,而寫鎖是拍他的。另外同一時間,兩把鎖不能被不同線程持有
使用場景: 適合讀取操作多於寫入操作的場景,改進互斥鎖的性能。比如:集合的併發線程安全性改造、緩存組件。
鎖降級: 指的是寫鎖降級成爲讀鎖。持有寫鎖的同時,再次獲取讀鎖,隨後釋放寫鎖的過程。寫鎖是線程獨佔,讀鎖是共享,所以寫->讀是降級。(讀->寫,是不能實現的)

6、可重入鎖、不可重入鎖

線程拿到一把鎖之後,該線程可以多次自由進入同一把鎖所同步的其他代碼就稱可重入鎖;獲取鎖之後,即使是該線程也不能進入同一把鎖的其他代碼就稱爲不可重入鎖。

可重入鎖和補可重入鎖都是針對同一線程獲取到鎖的情況下討論的,即使是可重入鎖也不能讓多個線程同時獲取到鎖,如果多個線程可獲取到那麼就是共享鎖了。

使用可重入鎖時,鎖了多少次鎖,就要釋放多少次才能把鎖真正的釋放掉。

package com.dongnao.concurrent.period5;

import com.dongnao.concurrent.period4.KodyLock;

import java.util.concurrent.locks.ReentrantLock;

public class Demo1_ReentrantTest {

    private static  int i = 0;

    //可重入鎖
    private final static ReentrantLock lc = new ReentrantLock();
    //    private final static KodyLock lc = new KodyLock();
    public static void add() throws InterruptedException {
        lc.lock();
        i++;
        System.out.println("here i am...");
        Thread.sleep(1000L);
        add();
        lc.unlock();
    }
    public static void main(String args[]) throws InterruptedException {
        add();
    }
}

上面代碼中ReentrantLock就是一個可重入鎖,程序會一直輸出here i am... ,如果ReentrantLock是不可重入鎖,那麼只會輸出一次here i am...,第二次調用add方法的時候就會阻塞住。1
輸出結果:

here i am...
here i am...
here i am...
here i am...
here i am...
here i am...
here i am...
。。。

補充:synchronized是可重入鎖。

可重入鎖特性的實現需要解決以下兩個問題。
1)線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。
2)鎖的最終釋放。
nonfairTryAcquire方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。同步狀態表示鎖被一個線程重複獲取的次數。
如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同
步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

7、公平鎖、非公平鎖

爭搶鎖的順序,如果是按先來後到,則爲公平,否則就是非公平鎖。

二、synchronized的底層原理

1、堆內存中的Java對象

每個對象在內存中存儲的除了基本的字段屬性值(如果字段是複雜對象的話,那麼存儲的就是對象引用),還有一個對象頭。
在這裏插入圖片描述

對象頭裏面的Mark Word記錄這對象鎖的狀態,有如下四種狀態:

在這裏插入圖片描述

2、輕量級鎖

在未鎖定的狀態下,可以通過CAS來搶鎖,搶到的是輕量級鎖
在這裏插入圖片描述

3、重量級鎖

輕量級鎖中的自旋有一定的次數限制,超過了次數限制,輕量級鎖升級爲重量級鎖。
在這裏插入圖片描述

4、偏向鎖

在JDK6 以後,默認已經開啓了偏向鎖這個優化,通過JVM 參數 -XX:-UseBiasedLocking 來禁用偏向鎖,若偏向鎖開啓,只有一個線程搶鎖,可獲取到偏向鎖。
在這裏插入圖片描述

5、鎖的升級過程

在這裏插入圖片描述
1、處於Runnable狀態而還沒運行的線程1,會去搶owner,搶到之後開啓偏向鎖,線程運行。
2、此時如果有線程2來搶鎖,那麼鎖會升級成輕量級鎖
3、如果線程2搶不到鎖(線程1還沒有釋放鎖),那麼線程2會自旋繼續搶鎖,自旋有一定的限制,自旋超過一定的次數,鎖再次升級爲重量級鎖,搶不到鎖的線程會進入entyList集合中等待執行(在重量級配圖中所示)。
4、如果線程2,搶到了鎖(在線程2開始之前,線程1已經釋放了鎖),那麼鎖還是偏向鎖的狀態。
5、如果運行中的線程調用了wait等方法,那麼線程就會進入WaitSet,處於Waiting狀態。
6、如果有線程調用了notify方法,那麼WaitSet(WaitSet中有線程的情況下)中的一個線程就會進入到EntryList中,參與或等待搶鎖。如果調用的是notifyAll,那麼WaitSet(WaitSet中有線程的情況下)中的所有線程都會進入到EntryList中,參與或等待搶鎖。
注意:
1、wait、notify、notifyAll只能在synchronized關鍵字中使用,且調用wait、notify、notifyAll的對象與鎖對象相同,否則會拋出IllegalMonitorStateException異常。
2、wait() 方法調用後,會破壞原子性。
補充:
1、偏向標記第一次有用,出現過爭用後就沒用了。 -XX:-UseBiasedLocking 禁用使用偏置鎖定;
2、偏向鎖,本質就是無鎖,如果沒有發生過任何多線程爭搶鎖的情況,JVM認爲就是單線程,無需做同步(jvm爲了少幹活:同步在JVM底層是有很多操作來實現的,如果是沒有爭用,就不需要去做同步操作)

6、重量級鎖 - 監視器(monitor)

修改mark word如果失敗,會自旋CAS一定次數,該次數可以通過參數配置:超過次數,仍未搶到鎖,則鎖升級爲重量級鎖,進入阻塞。

monitor也叫做管程,計算機操作系統原理中有提及類似概念。一個對象會有一個對應的monitor。
在這裏插入圖片描述

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