天天看點

校招面試之JAVA并發基礎面試總結問題彙總與答案整理(僅供參考)

目錄

  • 面試總結
  • 問題彙總與答案整理(僅供參考)
    • 1. 實作線程的方式及差別
    • 2. 線程的啟動
      • 2.1 為什麼要用start()方法啟動線程而不用run()啟動
      • 2.2 一個線程兩次調用start()方法會發生什麼
    • 3. 線程的停止
    • 4. 線程的狀态轉換
    • 5. 線程安全的定義
    • 6. wait()/notify()與sleep()
      • 6.1 wait()/notify()與sleep()的異同
      • 6.2 為什麼線程通信的方法wait()/notify()定義在Object類,而sleep()定義在Thread類
    • 7. 線程池
      • 7.1 為什麼要用線程池
      • 7.2 建立線程池的7個參數
    • 8. volatile關鍵字和synchronized關鍵字
      • 8.1 volatile的作用
      • 8.2 synchronized的作用
      • 8.3 volatile和synchronized的關系
    • 9. synchornized和Reentrantlock的差別
    • 10. JUC包下面的一些常見類
    • 11. Java并發相關代碼
      • 11.1 實作兩個線程輪流列印奇偶數
      • 11.2 實作生産者消費者模型(自己書寫阻塞隊列)
      • 11.3 單例模式的書寫及相關問題

面試總結

Java并發在面試過程中經常會問到,屬于必知必會的知識點,有的面試官甚至還會問的比較深入,是以有時間還是建議好好學習一下這方面的知識

問題彙總與答案整理(僅供參考)

1. 實作線程的方式及差別

這個問題從不同的角度來看有不同的答案,例如從有無傳回值可以劃分為繼承Thread類,實作Runnable接口的無傳回值類型以及實作Callable的有傳回值類型。(這樣類似的答案在網上搜有很多,比如通過線程池實作,定時器實作等)。

但是這個問題從Oracle的官方文檔上來看是分為兩類的,一種是實作Runnable接口,一種就是繼承Thread類,但其實一般實作Runnable接口用的比較多,因為這個相比繼承整個Thread類開銷小,而且Java是單繼承的,如果繼承了Thread類就不能繼承其他類了,但是Java是可以實作多個接口的,是以使用接口也更加的友善。

但通過源碼來看,其實他們兩個本質上都是一樣的,都是通過Thread類的run方法實作的,Thread類的run()方法代碼是這樣寫的:

public void run(){
    if(target!=null){
        target.run();
    }
}
           

這裡的target是屬于Runnable類的一個對象,如果是繼承Thread類,本質上是重寫這個run()方法,而如果實作Runnable接口則是調用這個接口寫的run()方法。

最後再總結一下,其實準備的講建立線程隻有一種方法就是構造Thread類,而實作線程的執行單元則是上述兩種方式。

PS:這個問題我相信這麼回答一定會比網上搜到的很多答案回答起來會加分不少

2. 線程的啟動

2.1 為什麼要用start()方法啟動線程而不用run()啟動

因為如果直接用run()方法來啟動,還是運作在main()線程中,如果用start()方法啟動,則會調用Java的native方法進而使得底層的線程排程器建立一個新的線程

2.2 一個線程兩次調用start()方法會發生什麼

會抛出一個非法的線程狀态異常,因為在start方法裡,首先會有對線程狀态的判斷,代碼為

if(threadStatus!=0){
    throw new IllegalThreadStateException();
}
           

這裡的threadStatus的變量初始值為0,如果一個線程調用start()方法後,其就不為0了,這時候在調用就會start()方法就會抛出異常了

3. 線程的停止

目前Java中停止線程的方法主要是通過Interrupt的中斷操作來進行停止的,但這需要請求方與被停止方的互相配合,在Java以前的版本中使用stop()方法或者suspend()方法來停止線程,但是這個方法已經被棄用了,因為這個方法是線程不安全的,使用該方法來停止線程會立刻停止run()方法中的剩餘全部工作,比如銀行存款的時候,要存好幾筆款,突然停了,這就會造成資料得不到同步,不一緻等問題。還有一個方法是通過volatile的boolean标志位結合while循環來停止線程,因為volatile可以使得這個标志位在多個線程之間是可見的,但是如果程式發生阻塞,一直在這個個循環中則無法判斷标志位,就會導緻無法停止線程

4. 線程的狀态轉換

線程總共分為六個狀态:

  • New 建立狀态
  • Runnable 可運作狀态
  • Blocked 被阻塞狀态
  • Waiting 等待狀态
  • Timed Waiting 逾時等待狀态
  • Terminatd 終止狀态

    狀态轉換關系如下,圖源Java線程狀态轉換圖:

    校招面試之JAVA并發基礎面試總結問題彙總與答案整理(僅供參考)

5. 線程安全的定義

《Java Concurrency In Practice》的作者對線程安全的定義為:當多個線程通路一個對象時,如果不用考慮這些線程運作時的排程,也不需要進行額外的同步操作,調用這個對象的行為都可以獲得一個正确的結果,那麼這個線程就是安全的

6. wait()/notify()與sleep()

6.1 wait()/notify()與sleep()的異同

相同點:

  • 都會使線程進入阻塞狀态
  • 都能夠響應中斷

不同點:

  • wait()/notify()要用在同步方法中,不然可能造成兩個線程互相等待的情況
  • wait()方法會釋放鎖,而sleep()方法則不會
  • wait()/notify()方法屬于Object類而sleep()方法屬于Thread類

6.2 為什麼線程通信的方法wait()/notify()定義在Object類,而sleep()定義在Thread類

因為在Java中鎖是對象級的而不是線程級的,每個對象都有個鎖,而線程可以通過獲得這些對象來獲得這些鎖,如果定義在Thread類就會導緻擷取鎖不明确,也無法同時擷取多個鎖,是以wait()方法是定義在Object類的,而sleep是指線程沉睡多長時間是線程級别的,并且其也不會釋放鎖,隻是讓線程在預期的時間内去執行

7. 線程池

7.1 為什麼要用線程池

其實類似相關的池化技術都可以用下面的句子來回答

  • 降低資源消耗:利用線程池可以重複利用已經建立的線程,進而避免了線程頻繁建立和銷毀帶來的開銷
  • 提高響應速度:利用線程池技術,線程不需要時間再進行建立,任務到來就能立即執行
  • 提高管理性:利用線程池可以進行統一的配置設定,調優和監控

7.2 建立線程池的7個參數

阿裡巴巴Java開發手冊建議大家使用ThreadPoolExecutor的方式來建立線程池,ThreadPoolExecutor有7個參數

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){

}

下面分别介紹一下括号中7個參數的含義:

  • corePoolSize: 線程池核心線程大小,線程池中維護的最小運作線程數量
  • maximumPoolSize: 線程池最大線程數量,一個任務被送出到線程池以後,首先會找有沒有空閑存活線程,如果有則直接執行,如果沒有則會緩存到工作隊列(後面會介紹)中,如果工作隊列滿了,才會建立一個新線程,然後從工作隊列的頭部取出一個任務交由新線程來處理,而将剛送出的任務放入工作隊列尾部。線程池不會無限制的去建立新線程,它會有一個最大線程數量(maximumPoolSize)的限制
  • keepAliveTime: 如果一個線程處于空閑狀态,且目前線程池中的線程數量大于corePoolSize,那麼在指定時間keepAliveTime之後會被銷毀
  • unit: keepAliveTime的時間機關
  • workQueue: 工作隊列,新任務被送出後,會先進入到此工作隊列中,任務排程時再從隊列中取出任務,jdk中提供了四種工作隊列:
  • ArrayBlockingQueue,LinkedBlockingQuene,SynchronousQuene,PriorityBlockingQueue
  • threadFactory: 線程工廠,建立新線程的時候會用到,可以用來設定線程名等
  • handler: 拒絕政策,當工作隊列中的任務已到達最大限制,并且線程池中的線程數量也達到最大限制,這時如果有新任務送出進來,就會發生拒絕政策,jdk提供了四種拒絕政策:CallerRunsPolicy,AbortPolicy,DiscardPolicy,DiscardOldestPolicy

    更為詳細的解答推薦大家參考部落格Java線程池七個參數詳解

8. volatile關鍵字和synchronized關鍵字

8.1 volatile的作用

volatile具有兩個作用:

  • 可見性:讀一個volatile變量之前,需要先使對應的本地緩存失效,這樣就必須到主記憶體讀取最新值,寫一個volatile屬性會立即刷入到主記憶體
  • 禁止指令重排序,例如解決單例模式雙重鎖亂序問題

8.2 synchronized的作用

synchornized關鍵字解決的是多個線程之間通路資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻隻能由一個線程執行,synchronized的使用方式有以下幾種:

  • 修飾執行個體方法:其作用的範圍是整個方法,作用的對象是調用這個方法的對象
  • 修飾靜态方法:其作用的範圍是整個方法,作用的對象是這個類的所有對象
  • 修飾代碼塊:其作用範圍是大括号{}括起來的代碼塊,作用的對象是調用這個代碼塊的對象

8.3 volatile和synchronized的關系

  • volatile本質是告訴JVM目前變量在寄存器中的值是不确定的,需要從主存中讀取。synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其它線程被阻塞
  • volatile僅能使用在變量級别,synchronized則可以使用在變量、方法
  • volatile僅能實作變量修改的可見性,而synchronized則可以保證變量修改的可見性和原子性
  • volatile不會造成線程阻塞,synchronized會造成線程阻塞
  • 使用volatile而不是synchronized的唯一安全的情況是類中隻有一個可變的域

    這個推薦大家一個部落格volatile和synchronized的差別

9. synchornized和Reentrantlock的差別

  • synchronized 競争鎖時會一直等待,ReentrantLock 可以嘗試擷取鎖,并得到擷取結果
  • synchronized 擷取鎖無法設定逾時,ReentrantLock 可以設定擷取鎖的逾時時間
  • synchronized 無法實作公平鎖,ReentrantLock 可以滿足公平鎖,即先等待先擷取到鎖
  • synchronized要麼随機喚醒一個線程要麼喚醒全部線程,ReenTrantLock提供了一個Condition類,用來實作分組喚醒需要喚醒的線程們
  • synchronized 是 JVM 層面實作的,ReentrantLock 是 JDK 代碼層面實作

10. JUC包下面的一些常見類

并發容器類:ConcurrentHashMap,CopyOnWriteArrayList (需要掌握其實作原理)

同步工具類:CountDownLatch,CyclicBarrier,Semaphore (需要掌握其實作原理)

阻塞隊列類:ArrayBlockingQueue, LinkedBlockingQueue

原子變量類:AtomicInteger, AutomicBoolean

11. Java并發相關代碼

11.1 實作兩個線程輪流列印奇偶數

方法一:使用wait/notify輪流喚醒

import java.util.*; 
public class PrintOddEven { 
    public final static Object lock = new Object(); 
    public static int count = 0; 
    public static void main(String[] args) { 
        MyRunnable1 r1 = new MyRunnable1(); 
        MyRunnable2 r2 = new MyRunnable2(); 
        Thread t1 = new Thread(r1); 
        Thread t2 = new Thread(r2); 
        t1.start(); 
        t2.start(); 
    } 
    static class MyRunnable1 implements Runnable{ 
        public void run() { 
            while(count<100) { 
                synchronized(lock) { 
                    lock.notify();                
                    System.out.println(Thread.currentThread().getName()
                    +':'+count++);                    
                    try { 
                        lock.wait(); 
                    } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
        } 
    } 
    static class MyRunnable2 implements Runnable{ 
        public void run() { 
            while(count<100) { 
                synchronized(lock) { 
                    lock.notify();                     
                    System.out.println(Thread.currentThread().getName()
                    +':'+count++);                     
                    try { 
                        lock.wait(); 
                    } catch (InterruptedException e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
        } 
    } 
} 
           

方法二:使用synchronized搶占鎖

public class RunnableStyle{ 
    public final static Object lock = new Object(); 
    public static int count = 0; 
    public static void main(String[] args) { 
        new Thread(new Runnable() { 
            @Override 
            public void run() { 
                while(count<100) { 
                    synchronized(lock){ 
                        if(count%2==0) 
                            System.out.println(Thread.currentThread().
                            getName() + count++); 
                    }                     
                }                 
            } 
        }).start(); 
        new Thread(new Runnable() { 
            @Override 
            public void run() { 
                while(count<100) { 
                    synchronized(lock){ 
                        if(count%2==1) 
                            System.out.println(Thread.currentThread().
                            getName() + count++); 
                    }                     
                }                 
            } 
        }).start();         
    }     
}
           

11.2 實作生産者消費者模型(自己書寫阻塞隊列)

import java.util.ArrayList; 
public class ProduceConsumer { 
    public static void main(String[] args) { 
        PCStore store = new PCStore(); 
        Producer producer = new Producer(store); 
        Consumer consumer = new Consumer(store); 
        Thread t1 = new Thread(producer); 
        Thread t2 = new Thread(consumer); 
        t1.start(); 
        t2.start(); 
    } 
} 
class PCStore{ 
    private ArrayList<Integer> store; 
    int maxsize = 10; 
    public PCStore(){ 
        store = new ArrayList<>(); 
    } 
    public synchronized void put(int i) { 
        if(store.size()==maxsize) { 
            try { 
                wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } 
        store.add(i); 
        System.out.println("倉庫放入了:"+ i); 
        notify(); 
    } 
    public synchronized void take(int i) { 
        if(store.size()==0) { 
            try { 
                wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
        } 
        store.remove(store.size()-1); 
        System.out.println("倉庫拿走了:"+ i); 
        notify(); 
    } 
} 
 
class Producer implements Runnable{ 
    private PCStore store; 
    public Producer(PCStore store){ 
        this.store = store; 
    } 
    @Override 
    public void run() { 
        for(int i=0;i<100;i++) { 
            store.put(i); 
        } 
    } 
} 
class Consumer implements Runnable{ 
    private PCStore store; 
    public Consumer(PCStore store){ 
        this.store = store; 
    } 
    @Override 
    public void run() { 
        for(int i=0;i<100;i++) { 
            store.take(i); 
        } 
    } 
}
           

11.3 單例模式的書寫及相關問題

單例模式的寫法有八種,推薦大家去看部落格都了解一下八種怎麼寫,一般面試官隻會要求寫一種線程安全的,在這裡我推薦兩種線程安全的寫法:

單例模式–雙重校驗法

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
           

單例模式–枚舉法

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {

    }
}
           

常見問題:

  1. 為什麼需要雙重校驗?

    因為如果隻進行上面的if進行校驗的話就會在多線程的情況下導緻線程不安全,而如果隻進行下面的if進行校驗的話就會導緻性能很差,每次有線程使用的時候都會使得其他線程發生阻塞

  2. 為什麼在建立對象的時候需要volatile?

    因為Java在建立對象的時候,其實是分為三步:

    1.為 uniqueInstance 配置設定記憶體空間

    2.初始化 uniqueInstance

    3.将 uniqueInstance 指向配置設定的記憶體位址

    但是由于 JVM 具有指令重排的特性,執行順序有可能第三步發生在第二步之前。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導緻一個線程獲得還沒有初始化的執行個體。例如,線程 1 執行了 第一步和 第三步,此時線程2調用 getInstance() 後發現 Instance 不為空,是以傳回Instance,但此時 Instance 還未被初始化。

  3. 枚舉法的優點?

    能夠實作線程安全,還能防止反序列化重新建立新的對象,寫法也很簡單