Java併發進階面試題總結

今天繼續來看看有關Java多線程的高階面試題。java

  • synchronized關鍵字
  • volatile關鍵字
  • ThreadLocal
  • 線程池
  • 阻塞隊列
  • Atomic 原子類
  • AQS

synchronized關鍵字

說一說本身對於synchronized關鍵字的瞭解

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

另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的。操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對synchronized 較大優化,因此如今的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。github

怎麼使用synchronized關鍵字

synchronized關鍵字最主要的三種使用方式:面試

  • 修飾實例方法:做用於當前對象實例加鎖,進入同步代碼前要得到當前對象實例的鎖
  • 修飾靜態方法:也就是給當前類加鎖,會做用於類的全部對象實例,由於靜態成員不屬於任何一個實例對象,是類成員( static 代表這是該類的一個靜態資源,無論new了多少個對象,只有一份)。因此若是一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B須要調用這個實例對象所屬類的靜態 synchronized 方法,是容許的,不會發生互斥現象,由於訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖
  • 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

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

一、synchronized 同步語句塊的狀況segmentfault

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代碼塊");
        }
    }
}

反編譯後緩存

1.png

從上面咱們能夠看出:多線程

synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在於每一個Java對象的對象頭中,synchronized 鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由) 的持有權。當計數器爲0則能夠成功獲取,獲取後將鎖計數器設爲1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設爲0,代表鎖被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另一個線程釋放爲止。併發

二、synchronized 修飾方法的的狀況框架

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

反編譯後dom

2.png

synchronized 修飾的方法並無 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 經過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。

說說JDK1.6以後的synchronized關鍵字底層作了哪些優化?

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

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

有關這些鎖的詳細知識能夠參考:死磕synchronized底層原理

談談synchronized和ReentrantLock的區別

一、二者都是可重入鎖
二、synchronized依賴於JVM 而ReentrantLock依賴於 API
三、相比synchronized,ReentrantLock增長了一些高級功能。
主要來講主要有三點: ①等待可中斷;②可實現公平鎖;③可實現選擇性通知

volatile關鍵字

說說你對volatile關鍵字的理解

就我理解的而言,被volatile修飾的共享變量,就具備瞭如下兩點特性:

  1. 保證了不一樣線程對該變量操做的內存可見性;
  2. 禁止指令重排序

談談volatile底層的實現機制

下面這段話摘自《深刻理解Java虛擬機》:
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。
lock前綴指令實際上至關於一個內存屏障,內存屏障會提供3個功能:

  • 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
  • 它會強制將對緩存的修改操做當即寫入主存;
  • 若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

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

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

有關volatile的更多知識請移步:淺談volatile關鍵字

ThreadLocal

ThreadLocal是怎麼爲每一個線程建立副本的?

首先,在每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,key值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。
初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找。

想看源碼解析的請移步:初始ThreadLocal

ThreadLocal 內存泄露問題

ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,而 value 是強引用。因此,若是 ThreadLocal 沒有被外部強引用的狀況下,在垃圾回收的時候,key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如咱們不作任何措施的話,value 永遠沒法被GC 回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種狀況,在調用set()、get()、remove()方法的時候,會清理掉key爲null的記錄。使用完 ThreadLocal方法後 最好手動調用remove()方法。

static class Entry extends WeakReference<ThreadLocal<?>> {          
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
       }

弱引用介紹:

若是一個對象只具備弱引用,那就相似於無關緊要的生活用品。弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程當中, 一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。

線程池

爲何要使用線程池?

  • 下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
  • 提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。
  • 提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。

執行execute()方法和submit()方法的區別是什麼呢?

  1. execute()方法用於提交不須要返回值的任務,因此沒法判斷任務是否被線程池執行成功與否;
  2. submit()方法用於提交須要返回值的任務。線程池會返回一個Future類型的對象,經過這個Future對象能夠判斷任務是否執行成功,而且能夠經過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後當即返回,這時候有可能任務沒有執行完。

阻塞隊列

寫一個生產者-消費者隊列

可使用阻塞隊列或wait/notify,這裏使用阻塞隊列來實現

//消費者
public class Producer implements Runnable{
   private final BlockingQueue<Integer> queue;

   public Producer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       try {
           while (true){
               Thread.sleep(1000);//模擬耗時
               queue.put(produce());
           }
       }catch (InterruptedException e){

       }
   }

   private int produce() {
       int n=new Random().nextInt(10000);
       System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
       return n;
   }
}
//消費者
public class Consumer implements Runnable {
   private final BlockingQueue<Integer> queue;

   public Consumer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       while (true){
           try {
               Thread.sleep(2000);//模擬耗時
               consume(queue.take());
           }catch (InterruptedException e){

           }

       }
   }

   private void consume(Integer n) {
       System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);

   }
}
//測試
public class Main {

   public static void main(String[] args) {
       BlockingQueue<Integer> queue=new ArrayBlockingQueue<Integer>(100);
       Producer p=new Producer(queue);
       Consumer c1=new Consumer(queue);
       Consumer c2=new Consumer(queue);

       new Thread(p).start();
       new Thread(c1).start();
       new Thread(c2).start();
   }
}

Atomic 原子類

介紹一下Atomic 原子類

Atomic翻譯成中文是原子的意思。在這裏Atomic是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾。

因此,所謂原子類說簡單點就是具備原子/原子操做特徵的類。

併發包java.util.concurrent的原子類都存放在java.util.concurrent.atomic下,以下圖所示。

1.png

講講AtomicInteger的使用

AtomicInteger 類經常使用方法

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

簡單介紹一下AtomicInteger類的原理

AtomicInteger 類主要利用 CAS + volatilenative 方法來保證原子操做,從而避免 synchronized 的高開銷,執行效率大爲提高。

AQS

談談AQS

AQS的全稱爲(AbstractQueuedSynchronizer),AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用普遍的大量的同步器,好比咱們提到的ReentrantLock,Semaphore,其餘的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。

AQS原理分析

AQS核心思想是,若是被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工做線程,而且將共享資源設置爲鎖定狀態。若是被請求的共享資源被佔用,那麼就須要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

1.png

AQS使用一個int成員變量state來表示同步狀態,經過內置的FIFO隊列來完成獲取資源線程的排隊工做。AQS使用CAS對該同步狀態進行原子操做實現對其值的修改。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性

狀態信息經過protected類型的getState,setState,compareAndSetState進行操做

//返回同步狀態的當前值
protected final int getState() {  
        return state;
}
 // 設置同步狀態的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操做)將同步狀態值設置爲給定值update若是當前同步狀態的值等於expect(指望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS定義兩種資源共享方式

  • Exclusive(獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:

    • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore等。

ReentrantReadWriteLock 能夠當作是組合式,由於ReentrantReadWriteLock也就是讀寫鎖容許多個線程同時對某一資源進行讀。

不一樣的自定義同步器爭用共享資源的方式也不一樣。自定義同步器在實現時只須要實現共享資源 state 的獲取與釋放方式便可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。

參考

Java併發進階常見面試題總結

相關文章
相關標籤/搜索