1. 程式人生 > 程式設計 >Java 併發進階(一)

Java 併發進階(一)

說一說自己對於 synchronized 關鍵字的瞭解說說自己是怎麼使用 synchronized 關鍵字,在專案中用到了嗎講一下 synchronized 關鍵字的底層原理說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎談談 synchronized和ReentrantLock 的區別說說 synchronized 關鍵字和 volatile 關鍵字的區別ThreadLocal 是什麼?有哪些使用場景?ThreadLocal示例ThreadLocal原理ThreadLocal 記憶體洩露問題

說一說自己對於 synchronized 關鍵字的瞭解

synchronized 關鍵字解決的是多個執行緒之間訪問資源的同步性,synchronized 關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。

另外,在 Java 早期版本中,synchronized 屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的,Java 的執行緒是對映到作業系統的原生執行緒之上的。如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對 synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6 對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

說說自己是怎麼使用 synchronized 關鍵字,在專案中用到了嗎

synchronized 關鍵字最主要有以下 3 種應用方式,下面分別介紹

  • 修飾例項方法,作用於當前例項加鎖,進入同步程式碼前要獲得當前例項的鎖
  • 修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前類物件的鎖
  • 修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。

總結: synchronized 關鍵字加到靜態方法和 synchronized(class)程式碼塊上都是給 Class 類上鎖。synchronized 關鍵字加到例項方法上是給物件例項上鎖。儘量不要使用 synchronized(String a) 因為 JVM 中,字串常量池具有快取功能!

面試中面試官經常會說:“單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單例模式的原理唄!”

雙重校驗鎖實現物件單例(執行緒安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判斷物件是否已經例項過,沒有例項化過才進入加鎖程式碼
        if (uniqueInstance == null) {
            //類物件加鎖
            synchronized (Singleton.class) {
                null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
複製程式碼

另外,需要注意 uniqueInstance 採用 volatile 關鍵字修飾也是很有必要。

uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段程式碼其實是分為三步執行:

  1. 為 uniqueInstance 分配記憶體空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的記憶體地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單執行緒環境下不會出先問題,但是在多執行緒環境下會導致一個執行緒獲得還沒有初始化的例項。例如,執行緒 T1 執行了 1 和 3,此時 T2 呼叫 getUniqueInstance() 後發現 uniqueInstance 不為空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多執行緒環境下也能正常執行

講一下 synchronized 關鍵字的底層原理

synchronized 關鍵字底層原理屬於 JVM 層面。

Java 虛擬機器器中的同步(Synchronization)基於進入和退出管程(Monitor)物件實現, 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步程式碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 並不是由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法呼叫指令讀取執行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的

推薦閱讀:深入理解Java併發之synchronized實現原理

1、synchronized 同步語句塊的情況

SynchronizedDemo {

    void method(){
        synchronized (this){
            System.out.println("synchronized code");
        }
    }
}
複製程式碼

通過 JDK 自帶的 javap 命令檢視 SynchronizedDemo 類的相關位元組碼資訊:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯後的 .class 檔案,然後執行 javap -verbose SynchronizedDemo.class

在這裡插入圖片描述

從上面我們可以看出:

synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。 當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 monitor(monitor物件存在於每個Java物件的物件頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什麼Java中任意物件可以作為鎖的原因) 的持有權。當計數器為0則可以成功獲取,獲取後將鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設為0,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。

2、synchronized 修飾方法的的情況

public synchronized foo(){
        System."synchronized method");
    }
}
複製程式碼
在這裡插入圖片描述

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。

說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎

JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。

鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。

談談 synchronized和ReentrantLock 的區別

1、兩者都是可重入鎖

兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。

2、synchronized 依賴於 JVM 而 ReentrantLock 依賴於 API

synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機器器團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機器器層面實現的,並沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過檢視它的原始碼,來看它是如何實現的。

3、ReentrantLock 比 synchronized 增加了一些高階功能

相比synchronized,ReentrantLock增加了一些高階功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以繫結多個條件)

  • ReentrantLock提供了一種能夠中斷等待鎖的執行緒的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
  • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。 ReentrantLock預設情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
  • synchronized關鍵字與wait()和notify()/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock物件中可以建立多個Condition例項(即物件監視器),執行緒物件可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在排程執行緒上更加靈活。 在使用notify()/notifyAll()方法進行通知時,被通知的執行緒是由 JVM 選擇的,用ReentrantLock類結合Condition例項可以實現“選擇性通知” ,這個功能非常重要,而且是Condition介面預設提供的。而synchronized關鍵字就相當於整個Lock物件中只有一個Condition例項,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition例項的signalAll()方法 只會喚醒註冊在該Condition例項中的所有等待執行緒。

如果你想使用上述功能,那麼選擇ReentrantLock是一個不錯的選擇。

4、效能已不是選擇標準


講一下Java記憶體模型

在 JDK1.2 之前,Java 的記憶體模型實現總是從主存(即共享記憶體)讀取變數,是不需要進行特別的注意的。而在當前的 Java 記憶體模型下,執行緒可以把變數儲存本地記憶體比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。

在這裡插入圖片描述

要解決這個問題,就需要把變數宣告為volatile,這就指示 JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。

說白了, volatile 關鍵字的主要作用就是保證變數的可見性,然後還有一個作用是防止指令重排序

在這裡插入圖片描述

說說 synchronized 關鍵字和 volatile 關鍵字的區別

  • volatile 關鍵字是執行緒同步的輕量級實現,所以 volatile 效能肯定比 synchronized 關鍵字要好。但是 volatile 關鍵字只能用於變數而 synchronized 關鍵字可以修飾方法以及程式碼塊。synchronized 關鍵字在 JavaSE1.6 之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些。
  • 多執行緒訪問 volatile 關鍵字不會發生阻塞,而 synchronized 關鍵字可能會發生阻塞
  • volatile 關鍵字能保證資料的可見性,但不能保證資料的原子性。synchronized 關鍵字兩者都能保證。
  • volatile 關鍵字主要用於解決變數在多個執行緒之間的可見性,而 synchronized 關鍵字解決的是多個執行緒之間訪問資源的同步性。

ThreadLocal 是什麼?有哪些使用場景?

它是執行緒的區域性變數,屬於執行緒自身所有,不在多個執行緒間共享。ThreadLocal 定義的通常是與執行緒關聯的私有靜態欄位(例如,使用者ID或事務ID)。

  1. 使用 ThreadLocal 可以代替一些引數的顯式傳遞。
  2. 比如用來儲存使用者 Session。Session 的特性很適合 ThreadLocal ,因為 Session 只在當前會話週期內有效,會話結束便銷燬。
  3. 在一些多執行緒的情況下,如果用執行緒同步的方式,當併發比較高的時候會影響效能,可以改為 ThreadLocal 的方式,例如高效能序列化框架 Kyro 就要用 ThreadLocal 來保證高效能和執行緒安全;
  4. 執行緒內上下文管理器、資料庫連線等可以用到 ThreadLocal;

ThreadLocal示例

import java.text.SimpleDateFormat;
import java.util.Random;

ThreadLocalDemo implements Runnable {

    // SimpleDateFormat 不是執行緒安全的,所以每個執行緒都要有自己獨立的副本
    static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMDD HHmm"));

    @Override
    run() {
        System.out.println("Thread Name = "+Thread.currentThread().getName()+" default form atter = "+formatter.get().toPattern());

        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        formatter.set(new SimpleDateFormat());

        System.out.println(" formatter = "+formatter.get().toPattern());
    }

    main(String[] args) throws InterruptedException {
        ThreadLocalDemo obj = new ThreadLocalDemo();
        for(int i=0;i<10;i++){
            Thread thread = new Thread(obj,""+i);
            Thread.sleep(new Random().nextInt(1000));
            thread.start();
        }
    }
}
複製程式碼

執行結果為:

Thread Name = 0 default form atter = yyyyMMDD HHmm
Thread Name = 1 Name = 0 formatter = yy-M-d ah:mm
Thread Name = 1 formatter = yy-M-d ah:mm
Thread Name = 2 Name = 3 Name = 2 formatter = yy-M-d ah:mm
Thread Name = 4 Name = 5 Name = 3 formatter = yy-M-d ah:mm
Thread Name = 5 formatter = yy-M-d ah:mm
Thread Name = 6 Name = 4 formatter = yy-M-d ah:mm
Thread Name = 7 Name = 8 Name = 7 formatter = yy-M-d ah:mm
Thread Name = 6 formatter = yy-M-d ah:mm
Thread Name = 8 formatter = yy-M-d ah:mm
Thread Name = 9 Name = 9 formatter = yy-M-d ah:mm
複製程式碼

從輸出中可以看出,Thread-0 已經改變了 formatter 的值,但仍然是 Thread-2 預設格式化程式與初始化值相同,其他執行緒也一樣。

ThreadLocal原理

從 Thread類原始碼入手。

Thread Runnable {
 ......
//與此執行緒有關的ThreadLocal值。由ThreadLocal類維護
ThreadLocal.ThreadLocalMap threadLocals = null;

//與此執行緒有關的InheritableThreadLocal值。由InheritableThreadLocal類維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}
複製程式碼

從上面 Thread 類原始碼可以看出 Thread 類中有一個 threadLocals 和 一個 inheritableThreadLocals 變數,它們都是 ThreadLocalMap 型別的變數,我們可以把 ThreadLocalMap 理解為ThreadLocal 類實現的定製化的 HashMap。預設情況下這兩個變數都是null,只有當前執行緒呼叫 ThreadLocal 類的 set或get方法時才建立它們,實際上呼叫這兩個方法的時候,我們呼叫的是ThreadLocalMap類對應的 get()、set()方法。

ThreadLocal類的set()方法

    set(value{
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(thisvalue);
        else
            createMap(t,153); font-weight: bold; word-wrap: inherit !important; word-break: inherit !important;" class="hljs-keyword">value);
    }
    ThreadLocalMap getMap(Thread t{
        return t.threadLocals;
    }
複製程式碼

通過上面這些內容,我們足以通過猜測得出結論:最終的變數是放在了當前執行緒的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變數值。

每個 Thread 中都具備一個 ThreadLocalMap,而 ThreadLocalMap 可以儲存以 ThreadLocal 為 key 的鍵值對。這裡解釋了為什麼每個執行緒訪問同一個 ThreadLocal,得到的確是不同的數值。另外,ThreadLocal 是 map 結構是為了讓每個執行緒可以關聯多個 ThreadLocal 變數。

ThreadLocalMap 是 ThreadLocal 的靜態內部類。

在這裡插入圖片描述

先佔個坑,後期回頭來專門學習 ThreadLocal 類。

ThreadLocal 記憶體洩露問題

實際上 ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強引用。弱引用的特點是,如果這個物件只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap 中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。

ThreadLocalMap 實現中已經考慮了這種情況,在呼叫 set()、get()、remove() 方法的時候,會清理掉 key 為 null 的記錄。如果說會出現記憶體洩漏,那只有在出現了 key 為 null 的記錄後,沒有手動呼叫 remove() 方法,並且之後也不再呼叫 get()、set()、remove() 方法的情況下。