Java中Synchronized原理詳解以及鎖的升級


Java為了解決並發的原子性,提供了以下兩個解決方案:
1、Synchronized關鍵字
2、Lock

這篇文章我們先說一下Synchronized關鍵字,Lock等着下篇文章再說。

Synchronized是隱式鎖,當編譯的時候,會自動在同步代碼的前后分別加入monitorenter和monitorexit語句。

1、Synchronized的三種用法

package juc;

public class TestSyn {

    static int x = 0;
    int y = 0;
    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public static  void add(){
        x++;
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

上述的代碼,我們實現了兩個線程對變量分別加1000次的操作。

我們執行發現
在這里插入圖片描述
這就是我們之前說的,x++不是一個原子操作,當出現多線程並發的時候,會出現線程不安全。

Synchronized是一個關鍵字修飾詞,可以修飾靜態方法、非靜態方法、代碼塊。

Synchronized是一個鎖,鎖是加載對象上的。當加上靜態方法上的時候,對應的對象是class對象。每個類只有一個class對象,是一個互斥鎖。

1.1、靜態方法

public static synchronized void add(){
        x++;
}

在add方法里加上synchronized關鍵字,程序就線程安全了
在這里插入圖片描述
當在方法頭上加了synchronized關鍵字后,同時只能有一個線程進入方法內執行。當一個線程獲取鎖后,如果其他線程進入該方法后,會被阻塞。當其他線程執行完畢后,會釋放鎖,然后喚醒對應的線程。

1.2、非靜態方法。

package juc;

import java.util.concurrent.locks.Lock;

public class TestSyn {

    static int x = 0;
    int y = 0;
    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public  synchronized void add(){
        x++;
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

當Synchronized加在非靜態方法的時候,是加載this對象上的。

1.3、代碼塊

package juc;

import java.util.concurrent.locks.Lock;

public class TestSyn {

    static int x = 0;
    int y = 0;
    final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public void add(){
        synchronized (object){
            x++;
        }
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

如上述代碼所示,Synchronized修飾代碼塊的時候,我們用final Object object = new Object()來鎖的對象。
鎖的對象最好用final修飾,因為鎖的對象最好不要變,否則如果鎖的對象發生變化的話,兩個線程會同時進入代碼塊內,造成了線程不安全。

2、Synchronized原理以及鎖升級

在JDK1.6之前,Synchronized鎖的原理是通過操作系統的mutex互斥鎖實現的,需要線程從用戶態切換到內核態,這個十分消耗資源的。在JDK1.6之后,Synchronized引入了三種狀態的鎖來提高了鎖的性能!

從資源消耗級別從低到高分別為:偏向鎖、輕量級鎖、重量級鎖。

Synchronized是在對象上加鎖,我們首先說下Synchronized鎖對象的內存布局。

我們都知道對象存在堆上,對象分為對象頭和實例數據。對象頭中有一個Mark Word,Mark Word中存儲了鎖相關的信息,如下圖所示。

在這里插入圖片描述
由上圖所示,Mark Word中的最后兩位代表鎖的類別。
10代表重量級鎖、00代表輕量級鎖、01代表無鎖或者偏向鎖,這個時候需要看倒數第三位,如果倒數第三位位1代表為偏向鎖,如果為0代表無鎖。

1、偏向鎖
Synchronized的默認鎖是偏向鎖。Mark Word中與偏向鎖有關的屬性有三個:鎖標志(01)、偏向鎖標志(1)、線程ID、Epoch(偏向鎖占用的次數)
偏向鎖對象初始化的時候,線程ID為null、Epoch為0。

偏向鎖的獲取過程:
當線程獲取偏向鎖的時候,首先查看偏向鎖標志是否為0。
1、如果偏向鎖標志位0,則進行CAS操作,CAS(偏向鎖標志,鎖標志,線程ID,Epoch)。
如果CAS成功的話,就是將線程ID設置為當前線程的ID,將鎖標志設置為01,偏向鎖設置為1,Epoch設置為Epoch+1,並在當前線程的棧幀空間開辟鎖記錄lock record寫入當前線程ID。
如果CAS失敗的話,說明有其他線程同時競爭,會將偏向鎖升級為輕量鎖。

如果偏向鎖標志位1,則查看線程ID是否與自己的線程ID相同,如果相同,則直接執行同步區代碼
如果線程ID不與自己相同,則判斷擁有偏向鎖的線程是否還存活,
如果死亡的話,就將鎖標志設置為0,偏向鎖標志設置為0,線程ID設置為null,當前線程開始CAS請求。
如果存活的話,從上到下遍歷線程中的棧幀是否存在lock record,如果存在的話,就說明當前線程還擁有着偏向鎖對象,就等到安全點的時候,將擁有偏向鎖的線程暫停,將偏向鎖升級為輕量鎖。
如果不存在的話,就將鎖標志設置為0,偏向鎖標志設置為0,線程ID設置為null,這就是叫做偏向鎖撤銷

當線程使用完偏向鎖的時候,是不會將對象頭中的線程ID撤銷的,只有其他線程來獲取偏向鎖的時候,才會撤銷。

偏向鎖適用於單線程 多次獲取偏向鎖的情況,減少了線程切換的開銷。
如果存在高並發的情況下,不要設置偏向鎖,將其關閉。因為要立馬要升級鎖,白白浪費了偏向鎖創建和銷毀的資源。

當epoch大於40的時候,也會自動升級為輕量鎖。

2、輕量級鎖
輕量級鎖獲取:
假設當前輕量級鎖還未被獲取,線程將會在棧幀中創建一個鎖記錄空間lock record,然后將鎖對象的mark word拷貝到lock record中,接着
執行cas將lock record的地址寫入到鎖對象的mark word,如果鎖對象中的mark word和lock record中之前拷貝的mark record相等,cas才會成功。
然后將lock record中的owner指針指向鎖對象中的mark record。

如果cas失敗的話,說明有其他線程捷足先登了,已經將其他線程的lock record地址寫入鎖對象的mark word了,就會將鎖升級為重量級鎖,對鎖對象中的mark word改寫為重量級鎖對應的格局,線程會被阻塞。

如果當前輕量級鎖已經被獲取的話,直接就將鎖升級為重量級鎖。

輕量級鎖的釋放
當線程釋放輕量級鎖的時候,會進行cas將lock record中的mark word 寫入到鎖對象中的mark word中,如果鎖對象中的mark word和lock record的地址一樣,cas才會成功。

如果cas失敗的話,說明鎖對象中的mark word已經在升級為重量級鎖的時候被改變了。這個時候會喚醒之前等待的線程。

3、重量級鎖
重量級鎖的時候,對象markword對應着一個C++對象,稱之為Monitor(監視器),對應的類文件如下

ObjectMonitor() {
    _count        = 0; //用來記錄該對象被線程獲取鎖的次數
    _waiters      = 0;
    _recursions   = 0; //鎖的重入次數
    _owner        = NULL; //指向持有ObjectMonitor對象的線程 
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
  }

重量級鎖,就是利用操作系統中的mutex互斥鎖,當申請鎖的時候,首先owner和當前線程一樣或者owner為null,如果一致就將recursions+1,然后執行同步代碼段。

如果owner不和當前線程一樣,將會執行一定次數的自旋鎖來請求鎖,如果請求不成功則會阻塞,將其加入到EntryList中。

當線程調用了moniterenter時,將count+1,如果調用了monitorexit,將count-1,當count=0時,線程就要釋放鎖了,當有線程釋放鎖的時候,會喚醒waitset和entrylist中的線程。

如果獲取鎖對象的線程調用了wait方法,則將線程放入waitSet中。

3、Synchronized可重入鎖的原理

Synchronized是一個可重入鎖,看下面的例子我們理解下什么是可重入鎖

package juc;

public class TestLoad {

    final static Object object = new Object();
    public static void main(String[] args) {

        test1();
    }

    public static void test1(){
        synchronized (object){
            System.out.println("test1");
            test2();
        }
    }

    public static void test2(){
        synchronized (object){
            System.out.println("test2");
        }
    }
}

如上述代碼所示,test1方法中調用了object鎖對象,在沒有釋放鎖對象之前我們又調用了test2方法,test2方法中也需要申請object鎖對象。

可重入鎖的意思就是線程在還沒釋放鎖對象的時候,又重新申請調用同一個鎖,如果是可重入鎖的話就可以申請成功,如上圖所示。,如果是不可重入的話就會被阻塞,然后陷入死鎖,因為當前對象在重新申請鎖對象的前並沒有釋放鎖對象,在重新申請的時候會被阻塞,等待鎖對象釋放,所以會陷入死循環。

在這里插入圖片描述

Synchronized是怎么實現可重入的呢?

1、偏向鎖
存儲的有線程ID,如果線程ID一樣就直接重入
2、輕量級鎖
如果鎖對象頭記錄的地址是在當前線程棧幀內,就直接重入。
3、重量級鎖
如果mutex互斥鎖中的owner和當前線程一樣,則直接重入。

4、總結

名字 適用場景 原因 是否可以關閉
偏向鎖 單線程申請多次鎖 偏向鎖的做法簡單,就只在鎖對象中的mark word中標明線程ID,只通過一次CAS來獲取鎖,通過比較線程ID來判斷鎖的沖突 可以關閉
輕量級鎖 多個線程不同時申請鎖 通過多次的CAS來申請鎖,因為CAS消耗的cpu資源肯定是小於線程阻塞,然后被喚醒的切換消耗的cpu資源的。因為線程的阻塞和喚醒需要從用戶態轉化到內核態 可以關閉
重量級鎖 多個線程鎖沖突劇烈 如果線程之間的鎖劇烈沖突,CAS消耗的cpu資源會遠遠大於線程阻塞然后喚醒的消耗的cpu資源。在阻塞前會進行一定次數的自旋操作 不可關閉

偏向鎖做法:
1、當未加鎖時,在鎖對象mark word中cas寫入線程ID。
2、當mark word中的線程ID與當前線程相等時,直接執行同步代碼
3、當線程ID不等於當前線程ID時,查看mark word中對應的線程是否存活,並且是否引用當前鎖對象,如果存活並引用,就在安全點時,將擁有鎖的線程暫停,並將其升級為輕量級鎖。
4、如果不存活,就將鎖重置,變為無鎖狀態,轉到1

2、輕量級鎖
1、當未加鎖時,當前線程將鎖對象中的mark word復制到當前線程棧幀中的lock record中,然后通過cas將lock record的地址寫入到鎖對象的mark word中,舊值是lock record中的拷貝的mark word。如果更新成功,將lock record中的owner指針指向鎖對象中的mark word。
2、如果cas不成功的話,說明有其他線程同時申請了鎖對象,並且已經捷足先登的將其lock record的地址寫入到了鎖對象中的mark word了,就將鎖升級到重量級鎖,重寫鎖對象對應的mark word。
3、如果鎖對象加鎖了,就直接升級為重量級鎖。

4、當線程釋放輕量級鎖時,將lock record中之前復制的mark word 通過cas寫入到鎖對象中的mark word,舊值為lock record的地址。
如果cas失敗,就說明發生了重量級鎖的升級。這個時候就會喚醒其他線程。

3、重量級鎖
重量級鎖利用的操作系統的mutex互斥鎖,當出現鎖沖突時,將會執行一定次數的自旋鎖來請求鎖,如果請求不成功則會阻塞,並將線程掛到阻塞隊列中。

如果線程釋放鎖的時候,就喚醒阻塞隊列中的其他線程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM