synchronized原理詳解

synchronized關鍵字

synchronized可以把任意一個非NULL的對象當做鎖,HotSpotVM中,這個鎖稱爲 對象監視器(Object Monitor):

  • 作用於靜態方法時,鎖住的是Class實例,相當於一個全局鎖,對所有調用這個靜態方法的線程有效。
  • 作用於非靜態方法時,鎖住的是對象的實例(this)。
  • 作用於一個代碼塊時,可以通過自定義任意一個對象obj當做鎖,鎖住的是所有以該對象爲鎖的代碼塊。

保證併發三大特性

synchronized能保證可見性、原子性和有序性

synchronized保證可見性原理
執行到synchronized代碼塊時,JVM會執行lock這個原子操作,將這個線程的工作內存中共享變量的副本值清空,之後這個線程再次需要用這些變量的時候,發現自己工作內存中的值已經失效了,就會重新從主存中獲取最新值。當執行完同步代碼塊時,JVM會執行unlock這個原子操作,工作內存的變量副本會立刻同步到主存。

synchronized保證原子性原理
只有獲取到鎖的線程才能去執行synchronized中的代碼塊,即使中途發生線程切換,這個線程持有的鎖不會釋放,所以這期間其他線程也無法獲取到鎖去執行這個代碼塊。但是發生異常的話,Synchronized鎖會自動釋放。

synchronized保證有序性原理
加上了synchronized關鍵詞的代碼塊,編譯器還是會進行代碼重排序的優化,只是synchronized保證了代碼塊只能同步訪問,下一個線程獲取鎖之後,上一個線程對共享變量做的改變對它是可見的,就是通過lock刷新工作內存的機制,這樣,編譯器優化的問題就不會出現。

synchronized的兩個特性

可重入特性

指的是同一線程的外層函數獲得鎖之後,內層函數還可以再次獲取該鎖,也就是說一個線程可以多次獲取同一把鎖。
好處

  • 避免死鎖(對於不可重入鎖,如果A方法中調用B方法,A和B方法都要獲取同一把鎖,A方法先獲取了鎖,去調用B方法的時候,B方法此時要等A釋放鎖才能執行,但是A方法要等B方法執行完了才能釋放鎖,這就造成了死鎖)
  • 可以讓我們更好的來封裝代碼,而不是將所有的邏輯都寫在一個同步方法中

示例

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}


// 自定義一個線程類
class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(getName() + "進入了同步代碼塊1");
            synchronized (MyThread.class) {
                System.out.println(getName() + "進入了同步代碼塊2");
            }
              // 這個代碼塊可以是調用本類或者其他類方法中的同步代碼塊
//            test0();
        }
    }

//    public void test0() {
//        synchronized (MyThread.class) {
//            System.out.println(getName() + "進入了同步代碼塊2");
//        }
//    }
}

結果:

Thread-0進入了同步代碼塊1
Thread-0進入了同步代碼塊2
Thread-1進入了同步代碼塊1
Thread-1進入了同步代碼塊2

實現原理:
同一個線程能多次獲取同一把鎖進入synchronized代碼塊中執行,實現原理是利用了一個變量記錄(假設微lockedThread)當前獲取到鎖的線程,還有一個變量(假設微lockedCount)記錄當前鎖被獲取的次數,當一個線程嘗試獲取鎖時,如果這個鎖已經被獲取過了,會去判斷獲取到鎖的線程是不是就是當前這個線程,如果是,則對lockedCount變量進行加1,獲取鎖成功。釋放鎖的時候,先對lockedCount變量進行減1,只有當lockedCount減爲0的時候,纔會真正釋放鎖。下面給出一個簡易模擬代碼:

public class Lock {
    boolean isLocked = false; // 標識鎖是否被線程獲得
    Thread lockedBy = null; // 記錄獲得鎖的線程
    int lockedCount = 0; // 記錄一個線程中,鎖被獲取的次數

    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread(); // 當前嘗試獲取鎖的線程

        // 鎖已經被線程獲得,並且獲取鎖的線程不是當前線程,則當前線程等待
        while(isLocked && lockedBy != thread) {
            wait();
        }

        isLocked = true;
        lockedBy = thread;
        lockedCount++;
    }

    public synchronized void unlock() {
        lockedCount--;

        // 該線程中,獲取鎖的程序都執行了釋放鎖操作,線程才真正釋放鎖
        if(0 == lockedCount) {
            isLocked = false;
            notify();
        }
    }
}

不可中斷特性

一個線程獲得鎖後,其他嘗試獲取這個鎖的線程只能等待或者阻塞,不能被中斷,只能一直等着擁有鎖的線程釋放鎖。

public class Test {
    private static  Object obj = new Object();

    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = () -> {
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "進入同步代碼塊");

                // 保證不退出同步代碼塊
                try {
                    Thread.sleep(88888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println(name + "執行結束");
                }
            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();

        System.out.println("停止第二個線程之前");
        t2.interrupt();
        System.out.println("停止第二個線程之後");

        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}
Thread-0進入同步代碼塊
停止第二個線程之前
停止第二個線程之後
TIMED_WAITING #第一個線程進入等待狀態
BLOCKED       #第二個線程進入阻塞狀態
Thread-0執行結束
Thread-1進入同步代碼塊
Thread-1執行結束
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.exapmle.service.test9.Test.lambda$main$0(Test.java:14)
	at java.lang.Thread.run(Thread.java:748)

Lock鎖可以設置爲可中斷的,也可以設置爲不可中斷的

public class LockTest {
    private static Lock lock = new ReentrantLock();

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

    // 不可中斷鎖 lock()
    public static void test01() throws InterruptedException {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            try{
                lock.lock();
                System.out.println(name+"獲取鎖");
                // 讓獲取鎖的線程暫時不退出
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name+"釋放鎖");
            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();

        System.out.println("停止第二個線程之前");
        t2.interrupt();
        System.out.println("停止第二個線程之後");

        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }

    // 可中斷鎖 tryLock()
    public static void test02() throws InterruptedException {
        Runnable runnable = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try{
                // 3s之內沒有獲取到鎖,就停止阻塞,去執行其他任務
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if(b) {
                    System.out.println(name+"獲取鎖");
                    // 讓獲取鎖的線程暫時不退出
                    Thread.sleep(88888);
                }else{
                    System.out.println(name+"沒有獲取到鎖");
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(b) {
                    lock.unlock();
                    System.out.println("name"+"釋放鎖");
                }

            }
        };

        Thread t1 = new Thread(runnable);
        t1.start();
        Thread.sleep(1000);

        Thread t2 = new Thread(runnable);
        t2.start();
    }
}

test01執行結果:

Thread-0獲取鎖
停止第二個線程之前
停止第二個線程之後
TIMED_WAITING
WAITING            # 雖然執行了t2.interrupt(); #但是線程2並沒有被中斷,而是進入等待狀態
Thread-0釋放鎖
Thread-1獲取鎖      # 等線程1釋放鎖之後,第二個線程獲取鎖
Thread-1釋放鎖
java.lang.InterruptedException: sleep interrupted  # 中斷異常,因爲不可中斷
	at java.lang.Thread.sleep(Native Method)
	at com.exapmle.service.test9.LockTest.lambda$test01$0(LockTest.java:22)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

test02執行結果:

Thread-0獲取鎖
Thread-1沒有獲取到鎖         #3s之內沒有獲取到鎖,就停止阻塞,去執行其他任務
name釋放鎖

Java對象

在JVM中,對象在內存中的佈局分爲三塊區域:

  • 對象頭:Java對象頭在32位虛擬機上佔64bit、在64位虛擬機上佔96bit
  • 實例數據:存放類的屬性數據信息,包括父類的屬性信息;
  • 對齊填充:由於虛擬機要求 對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊;

在這裏插入圖片描述

對象頭

Hotspot虛擬機的對象頭包括兩部分,Mark Word(標記字段,64位虛擬機上佔64bit)、Klass Pointer(類型指針,Hotspot虛擬機默認開啓指針壓縮,所以64位虛擬機上佔32bit。沒有開啓指針壓縮時一個指針佔8字節。開啓指針壓縮是因爲對象太大,會減小緩存命中率,GC開銷增大。使用參數-XX:-UseCompressedOops可以關閉指針壓縮):

  • Klass Pointer是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個Class
  • Mark Word用於存儲對象自身的運行時數據,如:HashCode、GC分代年齡、Synchronized鎖狀態標誌
    是實現Synchronized鎖的關鍵,Synchronized加鎖實際是對對象頭狀態的改變

Synchronized鎖狀態:無鎖不可偏向、無鎖可偏向(但還沒偏向)、偏向鎖、輕量級鎖、重量級鎖

Mark Word:
在這裏插入圖片描述
Lock Record
Lock Record是線程私有的數據結構,每一個被鎖住的對象Mark Word都會和一個Lock Record關聯,用於存儲鎖對象的Mark Word的拷貝。

synchronized原理

public class Test {
    private static Object obj = new Object();

    // synchronized作用在代碼塊上
    public static void main(String[] args) {
        synchronized (obj) {
            System.out.println("1");
        }
    }

    // synchronized作用在方法上
    public synchronized void fun() {
        System.out.println("2");
    }
}

現在通過反彙編字節碼文件看看synchronized彙編源碼:

javap -p -v 字節碼文件xx.class #-p是顯示所有方法 -v是顯示所有細節

在這裏插入圖片描述
synchronized是通過monitor監視器鎖來實現,monitor對象中有兩個主要屬性:owner記錄當前擁有鎖的線程,recursion記錄當前鎖被獲取的次數。對於synchronized修飾的代碼塊,在源碼編譯成字節碼的時候,會在同步代碼塊的入口和出口分別插入monitorenter和monitorexit這兩個字節碼指令。對於synchronized修飾的方法,會在該方法上添加ACC_SYNCHRONIZED的標識,表示它是一個同步方法。

monitorenter字節碼指令
當程序執行到monitorenter指令時會嘗試去獲取當前鎖對象對應的monitor權限:

  • 如果monitor的recursion爲0,則該線程進入monitor,然後將recursion設置爲1、owner設置爲當前線程,該線程成爲monitor的所有者;
  • 如果線程已經佔用了該monitor(即判斷到recursion不爲0,然後看owner是否爲當前線程),說明這時候是持有鎖的線程再次獲取這個鎖,則可以獲取成功,將recursion加1
  • 如果其他線程佔用了monitor,則該線程通過自旋操作再次嘗試幾次去獲取鎖,如果還沒有獲取到就被阻塞

monitorexit字節碼指令
當執行到monitorexit指令時會將recursion的值減1,當這個值減到0的時候,當前線程就不再擁有這個monitor對象的所有權,就會釋放鎖,然後其他被這個鎖阻塞的線程就可以嘗試去獲取這個monitor對象。

從上面反彙編結果圖中看到還存在一個monitorexit指令,下面有一個Exception table,記錄的是有可能出現異常的指令。這就說明,同步代碼塊中如果出現了異常,那麼會執行到monitorexit指令將recursion減1。所以,synchronized出現異常時會釋放鎖。

在這裏插入圖片描述
當synchronized作用於方法上時,會給這個方法設置一個叫做ACC_SYNCHRONIZED的標識。當一個方法被調用時,會檢測方法是否設置了ACC_SYNCHRONIZED標識,如果設置了,線程會去獲取monitor,獲取成功之後才能執行同步方法體,執行完後釋放monitor所有權。

兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。

monitor監視器鎖

monitor監視器鎖也就是通常說的synchronized對象鎖。任何一個Java對象都有一個Monitor與之關聯,Monitor對象存在於每個Java對象的對象頭Mark Word中(存儲的是Monitor對象的指針),這就是爲什麼Java中任意對象可以作爲鎖的原因。
一個Monitor被持有後,它將處於鎖定狀態。Monitor是由ObjectMonitor實現的,位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的。

//ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,
//用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象 ),
//_owner指向持有ObjectMonitor對象的線程

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 重入次數
    _waiters      = 0, // 等待線程數
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 當前持有鎖的線程
    _WaitSet      = NULL; // 處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表,有資格成爲候選資源的線程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

monitor競爭

當有多個線程競爭鎖時,流程如下:
在這裏插入圖片描述

monitor等待

當一個線程嘗試獲取鎖,如果鎖已經被其他線程獲取到了,它會再次自旋嘗試獲取鎖,如果還是獲取不到,則進行下面的流程:
在這裏插入圖片描述
第2步要通過CAS把node節點push到_cxq列表中,因爲一次push操作可能失敗

monitor釋放

  1. 執行完同步代碼快時會讓_recursions減1,當_recursions減爲0時,釋放該鎖
  2. 根據不同的策略喚醒等待該鎖的其他線程

synchronized是重量級鎖

Synchronized實現線程互斥會導致用戶態和內核態的切換,這種切換會消耗大量的系統資源,因爲用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工作。所以說synchronized是重量級鎖,JDK6開始對Synchronized進行了鎖升級優化

CAS

CAS(Compare And Swap)比較相同再交換,是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令。
CAS的作用:CAS可以將比較和交換轉換爲原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變量賦值時的原子操作。

在concurrent併發包下提供的AtomicInteger類就使用了CAS保證併發操作下對共享變量自增操作的正確性:

import java.util.concurrent.atomic.AtomicInteger;

public class Test {
    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];

        for(int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 1000; j++) {
                        // 自增操作
                        atomicInteger.incrementAndGet();
                    }
                }
            });
            threads[i].start();
        }

        // join()的意思是等待所有的子線程都執行完了,主線程才繼續往後執行
        for(Thread t : threads) {
            t.join();
        }

        System.out.println(atomicInteger.get());//10000
    }
}

CAS原理

CAS操作依賴3個值:內存中的值V,舊的估計值X,要修改的新值B,如果舊的預估值X等於內存中的值V,就將新的值B保存到內存中
AtomicInteger和Unsafe部分源碼:

package java.util.concurrent.atomic;

public class AtomicInteger extends Number implements java.io.Serializable {
	// ...
	private static final long valueOffset; // 根據AtomicInteger對象的內存地址和偏移量valueOffset就能找到value的內存地址
	private volatile int value; // 保存實際的值,用volatile修飾,保證可見性
	
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
    	// 調用了sun.misc.getAndAddInt()
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
	// ...
}
package sun.misc;
public final class Unsafe {
	// ...
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5; // 舊的預估值,就是此時內存中的值
        do {
        	// var1:AtomicInteger對象 
        	// var2:AtomicInteger對象中的偏移量valueOffset
            var5 = this.getIntVolatile(var1, var2);
            //CAS操作,比較相同則交換一個int值
            // var5+var4:要修改的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 

        return var5;
    }
    // ...
}
  • 現在線程1和線程2都在對atomicInteger.value執行自增操作,假設線程2線先執行
  • 假設現在atomicInteger.value是0,線程2執行到getAndAddInt()時,取到舊的預估值就是0
  • 此時CPU切換執行線程1,線程1執行到getAndAddInt()時,取到舊的預估值也是0
  • 線程1執行`compareAndSwapInt(var1, var2, var5, var5 + var4),var1和var2結合找到當前內存中的最新值【現在是0】,var5就是舊的預估值(之前取的內存中的值)【現在是0】,var4就是自增操作加1的這個【數值1】,現在比較內存最新值【0】和預估值【0】相等,則將var5+var4的值【1】賦給內存中的這個值。
  • 線程1賦值成功,compareAndSwapInt()返回true,線程1結束
  • 此時切換回線程2,找到當前內存最新值【1】,線程1舊的預估值還是【0】,比較兩者不相等,compareAndSwapInt()返回false,繼續執行do while循環,此時重新取內存中的值【1】給var5,然後再執行compareAndSwapInt()。此時找到當前內存最新值【1】,就的預估值【2】,兩者相等,則將var5+var4【2】賦給內存中的這個值
  • 線程1賦值成功,compareAndSwapInt()返回true,線程1結束
  • 最終,內存中的值是2
  • 樂觀鎖:認爲讀多寫少,遇到併發寫的可能性很小,因此每次去拿數據的時候都認爲別的線程不會修改,就不會上鎖,但是在更新數據的時候會判斷當前數據是否被修改過了。
    Java中的樂觀鎖基本都是通過CAS實現的
  • 悲觀鎖:認爲寫的情況很多,遇到併發寫的可能性高,因此每次去拿數據的時候都會認爲別的線程會修改這個數據,因此線程一上來執行就加鎖,直到執行完才釋放鎖
    synchronized和ReentrantLock屬於悲觀鎖

CAS適用場景

  • CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾,結合CAS和volatile可以實現無鎖併發,適用於多核CPU下線程間競爭不激烈的場景
  • 因爲沒有使用synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一
  • 但如果競爭激烈,那線程在getAndAddInt()中的do while循環處肯定會發生多次重試,反而會影響效率

synchronized鎖升級過程

HotSpot虛擬機中,JDK1.6之前,synchronized是重量級鎖,即使是線程交替執行無競爭併發的情況下,一個線程也要執行Synchronized加鎖,進行用戶態和內核態的切換。

JDK6開始對鎖進行了改進和優化,使得線程之間更高效地操作共享數據,以及解決競爭問題,從而提高程序運行效率。在JDK6中,synchronized鎖粒度是一個升級的過程:無鎖->偏向鎖->輕量級鎖-> 重量級鎖

鎖存在四種狀態依次是:無鎖狀態(可偏向和不可偏向)、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。

偏向鎖

HotSpot研究者發現,大多數情況下鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得與釋放,爲了讓線程獲得鎖的代價更低,引入了偏向鎖。偏向鎖就是這個鎖會偏向第一個獲得鎖的線程,在整個運行過程中只有一個線程的時候是這種鎖。

無鎖—>偏向鎖

  • A線程訪問同步代碼塊,使用CAS操作將Thread ID放到鎖對象的Mark Word中
  • 如果CAS操作成功,此時線程A獲取到鎖
  • 如果CAS操作失敗,證明還有別的線程持有鎖,則啓動偏向鎖撤銷

偏向鎖—>撤銷

  • 讓A線程在全局安全點阻塞
  • 遍歷線程棧,查看是否有被鎖對象的鎖記錄( Lock Record),如果有Lock Record,需要修復鎖記錄和Markword,使其變成無鎖狀態
  • 恢復A線程,將是否爲偏向鎖狀態置爲 0 ,開始進行輕量級加鎖流程

優缺點

  • 在單線程重複執行同步代碼塊時提升了性能,因爲如果只有一個線程執行同步代碼塊,就沒必要調用操作系統內核加鎖。
  • 如果有很多線程競爭鎖,偏向鎖是無效的,還因爲撤銷偏向鎖的動作必須等待全局安全點纔行,反而降低了性能。

適用場景
適用於單線程反覆進入同步代碼塊的情況。

JVM開啓/關閉偏向鎖

  • 開啓偏向鎖(JDK1.6之前):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 開啓偏向鎖(JDK1.6及其之後):-XX:BiasedLockingStartupDelay=0
    JDK6之後默認開啓,JDK1.8中默認會延遲4s後纔開啓偏向鎖,這是爲了提高JVM啓動速度,使用參數-XX:BiasedLockingStartupDelay=0可以關閉延遲。聽說在JDK11中沒有延遲,可以自己驗證下哦!
  • 關閉偏向鎖:-XX:-UseBiasedLocking

參數-XX:+PrintFlagsInitial打印出的信息中顯示了偏向鎖默認延遲時間(JDK1.8):
在這裏插入圖片描述

偏向鎖提升單線程反覆執行同步代碼快性能的原理之一
按照HotSpot的設計,每次加鎖/解鎖都會涉及一些CAS操作,CAS操作會延遲本地調用。偏向鎖的做法是一旦線程獲得了這個鎖,這個線程之後再次執行執行獲取這個鎖是不用走加鎖/解鎖操作的,即只需要判斷當前是偏向鎖並且鎖的擁有者是它自己就行。
CAS爲什麼會延遲本地調用?
多核cpu、併發情況下,用volatile修飾的共享變量要保證可見性,假如此時core1和core2同時把一個共享變量拷貝到了自己的cpu緩存中,當core1修改了這個共享變量的值,通過總線寫回到主存的時候,通過總線嗅探機制,會使core2中對應的這個值失效,也就是將他的緩存清空,當core使用這個變量發現數據失效了,就會重新取主存中的這個變量,這種通過總線監聽來回通信稱爲“Cache 一致性流量”。core1和core2的值再一次相等時,稱爲“Cache一致性”。
而CAS剛好導致了Cache一致性流量情況加重,偏向鎖通過消除不必要的CAS降低了Cache一致性流。

輕量級鎖

在多線程交替執行同步代碼快的情況下(就是線程A執行完了線程B纔來執行,線程B執行完了線程C纔來執行…,多個線程之間不會有鎖競爭的情況),會使用輕量級鎖。如果多個線程在同一時刻進入臨界區,會導致輕量級鎖膨脹升級爲重量級鎖。

輕量級鎖加鎖過程

  • 在A線程棧幀中建立一個鎖記錄(Lock Record),將Mark Word拷貝到自己棧幀中的Lock Record中,這個位置叫 displayced hdr
    在這裏插入圖片描述
  • 將Lock Record中的owner指向鎖對象
  • 使用CAS操作將Lock Record的地址記錄到Mark Word中,如果操作成功則進行下一步,否則進行最後一步 在這裏插入圖片描述
  • CAS操作成功,那麼這個線程就獲取到了這個鎖,然後將鎖標誌位設爲輕量級鎖模式(00)
  • CAS操作失敗,JVM首先會檢查鎖對象Mark Word中是否已經指向了當前棧幀,如果是則說明是鎖重入;否則說明多個線程競爭鎖,輕量級鎖升級爲重量級鎖,不過在這之前還有自旋操作

這裏爲什麼要使用CAS操作?
假如A、B兩個線程都將MarkWord拷貝到自己棧幀中的LockRecord中,A線程先將MarkWord更新爲指向自己LockRecord的指針,A線程就算獲取鎖成功了;B線程在執行CAS操作將MarkWord更新爲指向自己LockRecord的指針,發現MarkWord變了,CAS操作就會失敗,說明存在鎖競爭,則鎖開始膨脹。

輕量級鎖釋放過程

  • 取出Lock Record中保存的Mark Word信息,用CAS操作將取出的數據重新賦值到Mark Word中,操作成功,則釋放鎖成功
  • 否則,說明其他線程嘗試獲取鎖,需要升級爲重量級鎖

輕量級鎖加鎖過程中爲什麼要把對象頭裏的Mark Word複製到線程棧的鎖記錄中?
因爲升級爲輕量級鎖是在多線程的情況下,這些線程可能會競爭鎖,那麼獲取到鎖的線程將自己棧幀中的LockRecord地址記錄到MarkWord中時要進行CAS操作,如果發現MarkWord中的值發生了變化,那CAS操作失敗,說明存在鎖競爭。

優點
在多線程交替執行同步代碼快的情況下,可以避免重量級鎖引起的性能消耗。

自旋

重量級鎖的開銷很大,要儘量避免輕量級鎖轉爲重量級鎖。因此,當鎖升級爲輕量級鎖之後,如果依然有新線程過來競爭鎖,首先新線程會自旋嘗試獲取鎖,嘗試到一定次數依然沒有拿到,鎖就會升級爲重量級鎖。自旋鎖是JDK4中引入的,在JDK6中才默認開啓。

JVM開發團隊發現在很多應用中,共享數據的鎖定狀態只會持續很短的一段時間,如果爲了這麼短的一段時間使線程阻塞和喚醒導致的開銷不值得。先進行自旋,這個線程就不會放棄處理器執行時間而掛起。
自旋次數默認是10次,可以使用參數-XX:PreBlockSpin來更改。這個自旋次數不好確定,在JDK6中引入了適應性自旋鎖。

適應性自旋鎖:
自適應意味着自旋次數不再固定,而是由前一個嘗試獲取這個鎖的線程自旋時間來決定。假如前一個線程自旋10次就獲得了鎖,JVM會認爲這個鎖很容易獲取,那麼當前這個線程也可以自旋10次或者再多幾次就能獲取到鎖。假如前面的線程自旋了很多次還沒有獲取到鎖,JVM會認爲這個鎖很難獲取,那以後要獲取這個鎖時就不再進行自旋過程,以免浪費資源。

monitor鎖的競爭過程就用到了自適應自旋鎖。

適用場景
線程持有鎖的時間短,否則自旋時間長對CPU也會造成壓力。

重量級鎖

synchronized鎖是通過對象關聯的一個叫做監視器鎖(monitor)的對象來實現的,監視器鎖本質又是依賴於底層的操作系統Mutex Lock來實現,而操作系統實現線程之間的切換就需要用戶態和內核態的轉換,這個成本很高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲 “重量級鎖”。

鎖消除

鎖消除是JDK6對鎖的優化。下面這段代碼,StringBuffer是線程安全的,append方法上加了synchronized關鍵字,但是對於new StringBuffer().append(s1).append(s2).append(s3).toString()這行代碼,鎖對象是this,也就是StringBuffer的一個實例,每個線程執行到這句代碼時,都會實例化一個StringBuffer對象,它們的鎖和要鎖住的資源是不同的,因此也就沒必要在append方法上加鎖,因此JVM會自動將這個鎖消除。

鎖消除的依據是逃逸分析的數據支持

public class Test {
    public static void main(String[] args) {
        contactString("aa", "bb", "cc");
    }

    public static String contactString(String s1, String s2, String s3) {
        return new StringBuffer().append(s1).append(s2).append(s3).toString();
    }
}

鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的情況下,對鎖進行消除。

鎖粗化

下面這段代碼,for循環中,會進出100次append同步方法,JVM就會將append方法是的鎖消除,將鎖加到for循環上,這要很多鎖就變成了一個鎖。

public class Test {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        
        for(int i = 0; i < 100; i++) {
            sb.append("aa");
        }
    }
}

鎖粗化是指JVM會探測到一連串細小的操作都使用同一個對象加鎖,將同步代碼塊的範圍放大,放到這串操作的外面,這樣只需加鎖一次即可。

示例

JDK8
參數:-XX:BiasedLockingStartupDelay=0

public class Test {
     final static Object LOCK = new Object();
     public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        
         Thread t1 = new Thread() {
             @Override
             public void run() {
                 getLock();
             }
         };
         t1.setName("t1");
         t1.start();


        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

         Thread t2 = new Thread() {
             @Override
             public void run() {
                 getLock();
             }
         };
         t2.setName("t2");
         t2.start();

    }
    public static void getLock() {
        synchronized(LOCK) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        }
    }
}

在這裏插入圖片描述
將線程1、2中間的sleep()註釋掉,兩線程就會競爭鎖,此時是重量級鎖,並且可以看到MarkWord中有,除了鎖標記(10)外,其餘52個bit也是相等的,其中記錄的就是鎖對象關聯的ObjectMonitor的地址:
在這裏插入圖片描述

public class Test {
     final static Object LOCK = new Object();
     public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        System.out.println(Integer.toHexString(LOCK.hashCode()));
        System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        getLock();

    }
    public static void getLock() {
        synchronized(LOCK) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
        }
    }
}

在這裏插入圖片描述

synchronized小結

synchronized底層使用了monitorenter和monitorexit指令,每個鎖對象都會關聯一個monitor監視器,它有兩個主要屬性:owner記錄當前擁有鎖的線程,recursion記錄當前鎖被獲取的次數。當執行到monitorexit時,recursion會減1,當它的值減到0時,這個線程就會釋放鎖。

synchronized和Lock的區別

synchronized和Lock都可以用來解決多線程安全問題,保證線程同步。區別是:

  • synchronized是關鍵字,Lock是接口,必須通過實例化一個實現了Lock鎖的接口才能得到Lock鎖對象,比如ReentrantLock
  • synchronized發生異常時,會自動釋放鎖;Lock鎖必須通過調用unLock()去釋放,因此可能造成死鎖現象
  • synchronized不能讓等待鎖的線程中斷;而Lock可以讓等待鎖的線程中斷,就是通過調用它的tryLock()方法,如果調用的是lock()方法,則是不可中斷的
  • synchronized無法知道線程是否成功獲取鎖,而Lock可以,當設置爲可中斷鎖時,tryLock()方法返回布爾值代表是否獲得鎖
  • synchronized能鎖住方法和代碼塊,加鎖和釋放鎖是由JVM自動完成的,Lock只能鎖住代碼塊,加鎖和釋放鎖的時機由程序員自己決定
  • Lock可以使用讀鎖提高多線程的讀效率,讀鎖:Lock的一個實現類ReentrantReadWriteLock,允許多個線程讀,但只允許一個線程寫
  • synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。公平鎖就是喚醒線程的時候,哪個線程先來就先喚醒那個,非公平鎖是隨機喚醒一個線程。可以給ReentrantLock的構造器傳一個布爾值設定是否是公平鎖。

平時寫代碼如何對synchronized優化

  • 減小synchronized的範圍,同步代碼塊中代碼執行時間儘量短
  • 降低synchronized鎖的粒度
  • 讀寫分離,讀取時不加鎖,寫入和刪除時加鎖
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章