synchronized鎖由淺入深解析

一:幾種鎖的概念

1.1 自旋鎖

       自旋鎖,當一個線程去獲取鎖時,如果發現鎖已經被其他線程獲取,就一直循環等待,然後不斷的判斷是否能夠獲取到鎖,一直到獲取到鎖後纔會退出循環。

1.2 樂觀鎖

       樂觀鎖,是假設不會發生衝突,當去修改值的時候才判斷是否和自己獲得的值是一樣的(CAS的實現,值也可以是版本號),如果一樣就更新值,否則就再次去讀取值,然後比較再更新。就是說每次去讀數據的時候不會加鎖,只有在更新數據的時候纔去判斷這個值或者版本號有沒有被其他線程更新,所以說樂觀鎖適用於讀操作比較多的場景。

1.3 悲觀鎖

       悲觀鎖,是假設會有衝突發生,每次去讀數據的時候,就會加鎖,這樣別的線程就獲取不到鎖,會一直阻塞直到鎖被釋放。synchronized就是悲觀鎖。

1.4 可重入鎖/不可重入鎖

       顧名思義,可重入鎖就是當線程拿到鎖後,再沒有釋放鎖之前,可以再次拿到鎖進行操作,而不會出現死鎖;不可重入鎖,就是鎖只能被拿一次,想要再次獲得鎖,只能在釋放鎖後再去獲取。

         可重入鎖栗子如下:

        //可重入鎖
        ReentrantLock lock = new ReentrantLock();

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                i++;
                lock.lock();
                j++;
                lock.lock();
                i++;
                System.out.println("i=== " + i + ";j==== " + j);
            }
        });

        thread.start();

運行結果如下:

1.5 獨享鎖(寫鎖)/共享鎖(讀鎖) 

       獨享鎖,也就是同時只能被一個線程拿到;共享鎖,就是可以有多個線程同時獲得鎖,比如Semaphore就是共享鎖。

1.6 公平鎖/非公平鎖

       公平鎖,當多個線程去拿鎖的時候,如果是按照拿鎖的順序去獲得鎖的,那麼就是公平鎖;如果可以出現插隊的情況,就是非公平鎖。

二:synchronized解讀

2.1 synchronized的使用

 1:synchronized可以用在實例方法和靜態方法上,是隱式使用。

 2:synchronized可以用在代碼塊上,是顯式使用。

 3:synchronized鎖是可重入鎖、獨享鎖、悲觀鎖。

下面是具體實例:

public class SynchronizedDemo {
    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
//                counter1.add();
                Counter.staticAdd();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
//                counter2.add();
                Counter.staticAdd();
            }
        }).start();

    }
}
class Counter {
    public static volatile int a;
    //用在實例方法上,是synchronized(this)
    public synchronized void add() {
        System.out.println("線程:"+ Thread.currentThread().getName());
        a++;
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //用在靜態方法上,是synchronized(Counter.class)
    public synchronized static void staticAdd() {
        System.out.println("線程:"+ Thread.currentThread().getName());
        a++;
        LockSupport.parkNanos(1000 * 1000 * 1000 * 2);
    }
    public void demo() {
        //用在代碼塊上
        synchronized (this) {
            a++;
        }
    }
}

      上面的例子,當synchronized用在實例方法上,其實就是對this加鎖,也就是實例化的對象,當實例化多個對象時,其實就是加了多個鎖,當在多個線程多個實例調用的時候,不會出現阻塞;synchronized用在靜態方法上,其實就是對類對象進行加鎖。

2.2 鎖消除

       鎖消除是JIT在編譯的時候做的優化,當在單線程情況下,加鎖解鎖會造成CPU性能的消耗,而且單線程中,也不需要加鎖,所以JIT編譯優化做了鎖消除,即就是沒有鎖。

    //鎖消除,在單線程情況下,JIT編譯會對此做優化,避免加鎖解鎖造成的CPU性能消耗
    public static void main(String[] args) {
//        StringBuilder builder = new StringBuilder(); //線程不安全
        StringBuffer buffer = new StringBuffer(); //線程安全
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        System.out.println(buffer);
    }
    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

        上述代碼可以看到,stringBuffer的append源碼中加了synchronized,當在單線程中,這個synchronized就會被消除掉,這是JIT編譯時做的事情。

2.3 鎖粗化

鎖粗化是JIT編譯時做的優化,我們平時在編碼中也可以做些優化。

 //鎖粗化,JIT編譯時優化
    public static void main(String[] args) {

        for (int i=0; i<10; i++) {
            synchronized (LockCoarse.class) {
                a++;
            }
        }
        //進行了優化
        synchronized (LockCoarse.class) {
            for (int i=0; i<10; i++) {
                a++;
            }
        }

        //-------------------------------

        synchronized (LockCoarse.class) {
            a++;
        }

        synchronized (LockCoarse.class) {
            a++;
        }

        synchronized (LockCoarse.class) {
            a++;
        }

        //進行了優化
        synchronized (LockCoarse.class) {
            a++;
            a++;
            a++;
        }
    }

2.4 synchronized深度解析

思考:

1:synchronized加鎖後,狀態是如何記錄的呢?

2:synchronized加鎖的狀態記錄在什麼地方呢?

3:synchronized加鎖讓線程掛起,解鎖後喚醒其他線程,是如何做的?

JVM中有線程共享的區域:java方法區和堆內存,堆內存中存的是實例化的對象,對象內存中除了存字段的信息外,還會有一個對象頭:

根據上面的圖片可以看到,對象頭中的信息有:

1:class meta address,就是指向方法區內的,類的元信息。

2:array length,是當對象是數組對象時,記錄數組的長度的。

3:mark word,記錄的是鎖的信息,即鎖的狀態、鎖的類型等。

mark word詳解:

當一開始沒有線程拿鎖時,mark word中記錄的是無鎖信息,如下圖:

偏向鎖:

       當在單線程中,一個線程去拿鎖後,這個時候就是偏向鎖,內存中會記錄當前線程的id,這個時候就相當於無鎖了,因爲單線程中,加鎖解鎖會造成CPU性能的消耗,JIT會做優化;只有當另外的線程過來拿鎖,發現線程ID和自己的不一樣時,這個時候鎖就會升級爲輕量級鎖。(JDK1.6之後默認偏向鎖是開啓的,可以在JVM優化裏去關閉)

輕量級鎖:

       輕量級鎖,當多個線程去拿鎖(用CAS的方式去拿),若有線程成功拿到鎖,另外的線程就會自旋,不停地嘗試去獲取鎖,而且自旋的次數有限制,當達到最大的自旋次數後,鎖就會升級爲重量級鎖。

      上述圖,假設線程1和線程2都去獲取鎖,這個時候假設線程1拿到了鎖,那麼線程2就會一直自旋,循環的去嘗試獲取鎖;當自旋到一定的次數後,鎖就會升級爲重量級鎖。 

local thread address記錄的就是線程的地址,00指的是這是一個輕量級鎖。

重量級鎖:

        在每個對象中,都會有一個monitor監視器,假設T1線程和T2線程去拿重量級鎖,如果T1拿到了鎖,那麼在monitor中會記錄T1的地址,T2沒有拿到鎖,那麼它會進入一個entryList集合,差不多就是等待隊列,這個時候沒有拿到鎖的T2就不會一直自旋了。

        上圖中,可以看到,owner就是獲得鎖的線程的地址,它指向線程,EntryList存放的就是沒有拿到鎖的線程;當一個線程使用了wait方法使得自己掛起,因爲wait只能在synchronized關鍵字中使用,那麼當調用wait之後,會自動釋放鎖,這個時候調用了wait方法的線程會進入waitSet中,那麼EntryList中的線程就有機會去拿鎖,當有線程調用了notify或者notifyAll時,在waitSet中的線程會被喚醒,喚醒之後的線程會嘗試去拿鎖,拿不到會再次進入EntryList中;如果拿到鎖的線程直接釋放鎖,那麼它會離開monitor的監視。

鎖升級過程:

無鎖 ——》偏向鎖 ——》輕量級鎖 ——》重量級鎖

     當鎖爲重量級鎖時,鎖全部釋放了,沒有線程拿鎖,會直接到無鎖,偏向鎖關閉狀態,再次有線程拿鎖時,會直接拿到重量級鎖。

      到此,整個鎖的過程結束了,如有不足,萬望諒解!

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