Java多線程面試題(面試官常問)

進程和線程

進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是從一個進程從創建、運行到消亡的過程。在Java中,當我們啟動main函數時其實就是啟動瞭一個JVM的進程,而mian函數所在的線程就是這個進程中的一個線程,稱為主線程。

線程是比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區資源,但每個線程都有自己的程序計數器、虛擬機和本地方法棧,所以系統在產生一個線程,或在各個線程之間切換工作是,負擔要比進程小很多,所以線程也稱輕量級進程。

並發和並行

  • 並發:同一時間段內,多個任務都在執行(單位時間內不一定同時執行)
  • 並行:單位時間內,多個任務同時執行。

上下文切換

多線程編程中一般線程的個數都大於CPU核心的個數,而一個CPU核心在任意時刻內隻能被一個線程使用,為瞭讓這些線程都能得到有效執行,CPU采取的策略時為每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程屬於一次上下文切換。

換句話說,當前任務在執行完CPU時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換會這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。

sleep()和wait()

  • 最主要的區別是sleep()方法沒有釋放鎖,而wait()方法釋放瞭鎖。
  • 兩者都可以暫停線程的執行。
  • wait()通常用於線程間交互/通信,sleep()通常用於暫停執行。
  • wait()方法被調用後,線程不會自動蘇醒(除非超時),需要別的線程調用同一個對象上的notify()notifyAll()方法。而sleep()方法執行完後,線程會自動蘇醒。

 start()和run()

為什麼調用start()方法時會執行run()方法,為什麼不能直接調用run()方法?

當我們new一個Thread時,線程進入瞭新建狀態,調用start()方法,會啟動一個線程並使線程進入就緒狀態,等分到時間片後就可以開始運行瞭。
start()會執行線程的相應準備工作,然後自動執行run()方法的內容,這是真正的多線程工作。
而直接執行run()方法會把run方法當作一個main線程下的普通方法去執行,並不是在某個線程中執行它,所以這不是多線程工作。

synchronized關鍵字

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

synchronized主要的三種使用方式:

1.修飾實例方法

作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖。

2.修飾靜態方法

給當前類加鎖,會作用於類的所有對象實例,因為靜態成員是類成員,不屬於任何一個實例對象,所以線程A調用一個實例對象的非靜態synchronized方法,而線程B調用該實例對象所屬類的靜態synchronized方法時是允許的,不會沖突互斥。因為訪問靜態synchronized方法占用的是當前類的鎖,而訪問非靜態synchronized方法占用的是當前實例對象鎖。

3.修飾代碼塊

指定加鎖對象,進入同步代碼庫前要獲得給定對象的鎖。

synchronized和ReentrantLock:

1.兩者都是可重入鎖
即自己可以再次獲取自己的內部鎖。比如一個線程獲得瞭某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖時還是可以獲取的。

2.前者依賴JVM而後者依賴API
synchronized是依賴於JVM實現的,ReentrantLock是依賴於JDK層面實現的。

3.ReentrantLock比synchronized功能多
ReentrantLock增加瞭一些高級功能,主要說有三點:①等待可中斷②可實現公平鎖③可實現選擇性通知。

volatile關鍵字

當前Java內存模型下,線程可以把變量保存到本地內存(如寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改瞭一個變量的值,而另外一個線程還在繼續使用它在寄存器中變量值的拷貝,造成數據的不一致。

volatile關鍵字就是解決這個問題,指示JVM這個變量不穩定,每次使用它都要到主存中進行讀取。除此之外還有一個重要的作用是防重排。

並發執行的三個重要特性:

1.原子性

要麼所有的操作都得到執行並且不會收到任何因素幹擾而中斷,要麼所有的操作都不執行。可使用synchronized來保證代碼原子性。

2.可見性
當對一個共享變量進行瞭修改後,那麼另外的線程都是立即可以看到修改後的最新值。volatile可以保證可見性。

3.有序性
代碼在執行過程中的先後順序,Java在編譯器以及運行期間的優化,代碼的執行順序未必就是編寫代碼時候的順序,即指令重排。volatile可以禁止指令重排優化。

ThreadLocal

通常情況下,我們創建的變量時可以被任何一個線程訪問並修改的。如果要實現每一個線程都有自己的專屬本地變量該如何解決?這就需要ThreadLocal類瞭。

ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。

當創建一個ThreadLocal變量後,訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal名稱的由來。可以使用get()和set()方法來獲取默認值或將其值更改為當前線程所存副本的值,從而避免瞭線程安全問題。

實際上,ThreadLocal類有一個靜態內部類ThreadLocalMap,可以把ThreadLocalMap看作是ThreadLocal類定制的HashMap,最終的變量是放在瞭當前線程的ThreadLocalMap中,而不是ThreadLocal類上,可以看作ThreadLocal類是ThreadLocalMap的封裝,傳遞瞭值。

如果再同一個線程中聲明瞭兩個ThradLocal對象的話,會使用Thread內部僅有的那個ThreadLocalMap存放數據的,TheadLocalMap的key就是ThreadLocal對象,value就是ThreadLocal對象調用set方法設置的值。

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

插播反爬信息 )博主CSDN地址:https://wzlodq.blog.csdn.net/

線程池

池化技術大傢應該很熟悉,線程池、數據庫連接池、Http連接池等等都是對這思想的應用。池化技術的思想主要是為瞭減少每次獲取資源的消耗,提高對資源的利用率。
使用線程池,可以降低資源消耗、提高響應速度、提高線程的可管理性。

Runnable和Callable

Runnable接口不會返回結果或拋出檢查異常,但Callable接口可以。
工具類Excutors可以實現Runnable對對象和Callable對象之間的相互轉換。

@FunctionalInterface
public interface Runnable{
 //沒有返回值也無法拋出異常
 public abstract void run();
}
@FunctionalInterface
public interface Callable<V>{
 //@return 計算得出結果
 //@throws 如果無法計算結果,則拋出異常
}

execute()和submit()

  1. execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否。
  2. submit()方法用於提交需要返回值的任務。線程池會返回一個Future類型對象,通過這個對象可以判斷任務是否執行成功。

 創建線程池

  1. 通過構造器ThreadpoolExecutor實現(下面介紹)。
  2. 通過工具類Executors實現(不推薦)

ThreadPoolExecutor: .

  • FixedThreadPool:該訪法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池中若有空閑線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閑時,便處理在任務隊列中的任務。
  • SingleThreadExecutor:方法返回- 個隻有一一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閑,按先入先出的順序執行隊列中的任務。
  • CachedThreadPool:該方法返回一個可 根據實際情況調整線程數量的線程池。線程池的線程數量不確定,但若有空閑線程可以復用,則會優先使用可復用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務執行完畢後,將返回線程池進行復用。

ThreadPoolExecutor

  • ThreadPoolExecutor構造函數重要參數分析:
  • corePoolSize:核⼼線程數線程數定義瞭最⼩可以同時運⾏的線程數量。
  • maximumPoolSize:當隊列中存放的任務達到隊列容量的時候,當前可以同時運⾏的線程數量變為最⼤線程數。
  • workQueue:當新任務來的時候會先判斷當前運⾏的線程數量是否達到核⼼線程數,如果達到的話,新任務就會被存放在隊列中。
  • keepAliveTime:當線程池中的線程數量⼤於 corePoolSize 的時候,如果這時沒有新的任務提交,核⼼線程外的線程不會⽴即銷毀,⽽是會等待,直到等待的時間超過瞭keepAliveTime 才會被回收銷毀;
  • unit:keepAliveTime 參數的時間單位。
  • threadFactory:executor 創建新線程的時候會⽤到。
  • handler:飽和策略

①ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException 來拒絕新任務的處理。
②ThreadPoolExecutor.CallerRunsPolicy:調⽤執⾏⾃⼰的線程運⾏任務。會降低對於新任務提交速度,影響程序的整體性能,另外會增加隊列容量。
③ThreadPoolExecutor.DiscardPolicy:不處理新任務,直接丟棄掉。
④ThreadPoolExecutor.DiscardOldestPolicy:此策略將丟棄最早的未處理的任務請求。

Demo

模擬瞭 10 個任務,我們配置的核⼼線程數為 5 、等待隊列容量為 100 ,所以每次隻能存在5個任務同時執⾏,剩下的5個任務會被放到等待隊列中去。當前的 5 個任務之⾏完成後,才會之⾏剩下的 5 個任務。

public class MyRunnable implements Runnable {
 private String command;
 public MyRunnable(String s) {
  this.command = s;
 }
 @Override
 public void run() {
  System.out.println(Thread.currentThread().getName() + "開始時間:" + new Date());
    processCommand();
  System.out.println(Thread.currentThread().getName() + "結束時間:" + new Date());
 }
 private void processCommand() {
  try {
   Thread.sleep(3000); //設花費3秒執行任務
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
 }
 @Override
 public String toString() {
  return this.command;
 }
}
public class Demo {
 private static final int CORE_POOL_SIZE = 5;//核⼼線程數為 5
 private static final int MAX_POOL_SIZE = 10;//最⼤線程數 10
 private static final int QUEUE_CAPACITY = 100;//容量100
 private static final Long KEEP_ALIVE_TIME = 1L;//等待時間為 1L
 public static void main(String[] args) {
  //通過ThreadPoolExecutor構造函數⾃定義參數創建
  ThreadPoolExecutor executor = new ThreadPoolExecutor(
    CORE_POOL_SIZE,
    MAX_POOL_SIZE,
    KEEP_ALIVE_TIME,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(QUEUE_CAPACITY),
    new ThreadPoolExecutor.CallerRunsPolicy());//飽和策略
  for (int i = 0; i < 10; i++) {
   //創建WorkerThread對象(WorkerThread類實現瞭Runnable接⼝)
   Runnable worker = new MyRunnable("" + i);
   executor.execute(worker);//執⾏Runnable
  }
  executor.shutdown();//終⽌線程池
  while (!executor.isTerminated()) {
  }
  System.out.println("結束");
 }
}
/*運行結果如下:
pool-1-thread-3開始時間:Mon Mar 29 22:46:02 CST 2021
pool-1-thread-2開始時間:Mon Mar 29 22:46:02 CST 2021
pool-1-thread-4開始時間:Mon Mar 29 22:46:02 CST 2021
pool-1-thread-5開始時間:Mon Mar 29 22:46:02 CST 2021
pool-1-thread-1開始時間:Mon Mar 29 22:46:02 CST 2021
pool-1-thread-2結束時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-2開始時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-3結束時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-3開始時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-4結束時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-4開始時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-5結束時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-5開始時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-1結束時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-1開始時間:Mon Mar 29 22:46:07 CST 2021
pool-1-thread-2結束時間:Mon Mar 29 22:46:12 CST 2021
pool-1-thread-3結束時間:Mon Mar 29 22:46:12 CST 2021
pool-1-thread-4結束時間:Mon Mar 29 22:46:12 CST 2021
pool-1-thread-5結束時間:Mon Mar 29 22:46:12 CST 2021
pool-1-thread-1結束時間:Mon Mar 29 22:46:12 CST 2021
結束
*/

到此這篇關於Java多線程面試題(面試官常問)的文章就介紹到這瞭,更多相關Java多線程面試題內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!