java多線程&併發面試108問(中)

接上一篇文章:上一篇

53、線程基本方法

線程相關的基本方法有 wait, notify, notifyAll, sleep, join, yield 等。

54、線程等待(wait)

調用該方法的線程進入 WAITING 狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意的是調用 wait()方法後, 會釋放對象的鎖。因此, wait 方法一般用在同步方法或同步代碼塊中。

55、線程睡眠(sleep)

sleep 導致當前線程休眠,與 wait 方法不同的是 sleep 不會釋放當前佔有的鎖,sleep(long)會導致線程進入 TIMED-WATING 狀態,而 wait()方法會導致當前線程進入 WATING 狀態

56、線程讓步(yield)

yield 會使當前線程讓出 CPU 執行時間片,與其他線程一起重新競爭 CPU 時間片。一般情況下, 優先級高的線程有更大的可能性成功競爭得到 CPU 時間片, 但這又不是絕對的,有的操作系統對線程優先級並不敏感。

57、線程中斷(interrupt)

中斷一個線程,其本意是給這個線程一個通知信號,會影響這個線程內部的一箇中斷標識位。 這個線程本身並不會因此而改變狀態(如阻塞,終止等)。

  1. 調用 interrupt()方法並不會中斷一個正在運行的線程。也就是說處於 Running 狀態的線程並不會因爲被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。
  2. 若調用 sleep()而使線程處於 TIMED-WATING 狀態,這時調用 interrupt()方法,會拋出
    InterruptedException,從而使線程提前結束 TIMED-WATING 狀態。
  3. 許多聲明拋出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),拋出異常前,都會清除中斷標識位,所以拋出異常後,調用 isInterrupted()方法將會返回 false。
  4. 中斷狀態是線程固有的一個標識位,可以通過此標識位安全的終止線程。比如,你想終止一個線程 thread 的時候,可以調用 thread.interrupt()方法,在線程的 run 方法內部可以根據 thread.isInterrupted()的值來優雅的終止線程。

58、Join 等待其他線程終止

join() 方法,等待其他線程終止,在當前線程中調用一個線程的 join() 方法,則當前線程轉爲阻塞狀態,回到另一個線程結束,當前線程再由阻塞狀態變爲就緒狀態,等待 cpu 的寵幸。

59、爲什麼要用 join()方法?

很多情況下,主線程生成並啓動了子線程,需要用到子線程返回的結果,也就是需要主線程需要在子線程結束後再結束,這時候就要用到 join() 方法 。

System.out.println(Thread.currentThread().getName() + "線程運行開始!"); 
Thread6 thread1 = new Thread6();
thread1.setName("線程 B"); thread1.join();
System.out.println("這時 thread1 執行完畢之後才能執行主線程");

60、線程喚醒(notify)

Object 類中的 notify() 方法, 喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,並在對實現做出決定時發生,線程通過調用其中一個 wait() 方法,在對象的監視器上等待, 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程。

61、線程其他方法

  1. sleep():強迫一個線程睡眠N毫秒。
  2. isAlive(): 判斷一個線程是否存活。
  3. join(): 等待線程終止。
  4. activeCount(): 程序中活躍的線程數。
  5. enumerate(): 枚舉程序中的線程。
  6. currentThread(): 得到當前線程。
  7. isDaemon(): 一個線程是否爲守護線程。
  8. setDaemon(): 設置一個線程爲守護線程。 (用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
  9. setName(): 爲線程設置一個名稱。
  10. wait(): 強迫一個線程等待。
  11. notify(): 通知一個線程繼續運行。
  12. setPriority(): 設置一個線程的優先級。
  13. getPriority()::獲得一個線程的優先級。

62、進程

(有時候也稱做任務)是指一個程序運行的實例。在 Linux 系統中,線程就是能並行運行並且與他們的父進程(創建他們的進程)共享同一地址空間(一段內存區域)和其他資源的輕量 級的進程。

63、上下文

是指某一時間點 CPU 寄存器和程序計數器的內容。

64、寄存器

是 CPU 內部的數量較少但是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度。

65、程序計數器

是一個專用的寄存器, 用於表明指令序列中 CPU 正在執行的位置,存的值爲正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。

66、PCB-“切換楨”

上下文切換可以認爲是內核(操作系統的核心)在 CPU 上對於進程(包括線程)進行切換,上下文切換過程中的信息是保存在進程控制塊(PCB, process control block)中的。 PCB 還經常被稱作“切換楨”(switchframe)。 信息會一直保存到 CPU 的內存中,直到他們被再次使用。

67、上下文切換的活動

  1. 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於內存中的某處。
  2. 在內存中檢索下一個進程的上下文並將其在 CPU 的寄存器中恢復。
  3. 跳轉到程序計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程在程序中。

68、引起線程上下文切換的原因

  1. 當前執行任務的時間片用完之後,系統 CPU 正常調度下一個任務;
  2. 當前執行任務碰到 IO 阻塞,調度器將此任務掛起,繼續下一任務;
  3. 多個任務搶佔鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
  4. 用戶代碼掛起當前任務,讓出 CPU 時間;
  5. 硬件中斷;

69、同步鎖

當多個線程同時訪問同一個數據時,很容易出現問題。爲了避免這種情況出現,我們要保證線程同步互斥,就是指併發執行的多個線程,在同一時間內只允許一個線程訪問共享數據。 Java 中可以使用 synchronized 關鍵字來取得一個對象的同步鎖。

70、死鎖

何爲死鎖,就是多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。

71、線程池原理

線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等候,等其它線程執行完畢, 再從隊列中取出任務來執行。 他的主要特點爲: 線程複用; 控制最大併發數; 管理線程。

72、線程復

每一個 Thread 的類都有一個 start 方法。 當調用 start 啓動線程時 Java 虛擬機會調用該類的 run
方法。 那麼該類的 run() 方法中就是調用了 Runnable 對象的 run() 方法。 我們可以繼承重寫
Thread 類,在其 start 方法中添加不斷循環調用傳遞過來的 Runnable 對象。 這就是線程池的實現原理。 循環方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以是阻塞的 。

73、線程池的組成

一般的線程池主要分爲以下 4 個組成部分:

  1. 線程池管理器:用於創建並管理線程池
  2. 工作線程:線程池中的線程
  3. 任務接口:每個任務必須實現的接口,用於工作線程調度其運行
  4. 任務隊列:用於存放待處理的任務,提供一種緩衝機制
    Java 中的線程池是通過 Executor 框架實現的,該框架中用到了 Executor, Executors, ExecutorService, ThreadPoolExecutor , Callable 和 Future、 FutureTask 這幾個類。
    在這裏插入圖片描述
    ThreadPoolExecutor 的構造方法如下:
public ThreadPoolExecutor(
	int corePoolSize,
	int maximumPoolSize, 
	long keepAliveTime, 
	TimeUnit unit, 
	BlockingQueue<Runnable> workQueue) {
	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, xecutors.defaultThreadFactory(), defaultHandler);
}
  1. corePoolSize:指定了線程池中的線程數量。
  2. maximumPoolSize:指定了線程池中的最大線程數量。
  3. keepAliveTime:當前線程池數量超過 corePoolSize 時,多餘的空閒線程的存活時間,即多次時間內會被銷燬。
  4. unit: keepAliveTime 的單位。
  5. workQueue:任務隊列,被提交但尚未被執行的任務。
  6. threadFactory:線程工廠,用於創建線程,一般用默認的即可。
  7. handler:拒絕策略,當任務太多來不及處理,如何拒絕任務。

74、拒絕策略

線程池中的線程已經用完了,無法繼續爲新任務服務,同時,等待隊列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。
JDK 內置的拒絕策略如下:

  1. AbortPolicy : 直接拋出異常,阻止系統正常運行。
  2. CallerRunsPolicy : 只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。
  3. DiscardOldestPolicy : 丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務。
  4. DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。
    以上內置拒絕策略均實現了 RejectedExecutionHandler 接口,若以上策略仍無法滿足實際需要,完全可以自己擴展 RejectedExecutionHandler 接口。

75、Java 線程池工作過程

  1. 線程池剛創建時,裏面沒有一個線程。任務隊列是作爲參數傳進來的。不過,就算隊列裏面有任務,線程池也不會馬上執行它們。
  2. 當調用 execute() 方法添加一個任務時,線程池會做如下判斷:
    a. 如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務;
    b. 如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列;
    c. 如果這時候隊列滿了,而且正在運行的線程數量小於 maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務;
    d. 如果隊列滿了,而且正在運行的線程數量大於或等於 maximumPoolSize,那麼線程池會拋出異常 RejectExecutionException。
  3. 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
  4. 當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運行的線程數大於 corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。

76、JAVA 阻塞隊列原理

阻塞隊列,關鍵字是阻塞,先理解阻塞的含義,在阻塞隊列中,線程阻塞有這樣的兩種情況:

  1. 當隊列中沒有數據的情況下,消費者端的所有線程都會被自動阻塞(掛起),直到有數據放入隊列。
  2. 當隊列中填滿數據的情況下,生產者端的所有線程都會被自動阻塞(掛起),直到隊列中有空的位置,線程被自動喚醒。
    阻塞隊列的主要方法 :
    在這裏插入圖片描述

拋出異常:拋出一個異常;
特殊值:返回一個特殊值(null 或 false,視情況而定) 則塞:在成功操作之前,一直阻塞線程
超時:放棄前只在最大的時間內阻塞
插入操作

  1. public abstract boolean add(E paramE): 將指定元素插入此隊列中(如果立即可行且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則拋出 IllegalStateException。如果該元素是 NULL,則會拋出 NullPointerException 異常。
  2. public abstract boolean offer(E paramE): 將指定元素插入此隊列中(如果立即可行且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則返回 false。
  3. public abstract void put(E paramE) throws InterruptedException: 將指定元素插入此隊列中,將等待可用的空間(如果有必要)
public void put(E paramE) throws InterruptedException { 
	checkNotNull(paramE);
	ReentrantLock localReentrantLock = this.lock; localReentrantLock.lockInterruptibly();
	try {
		while (this.count == this.items.length)
		this.notFull.await();//如果隊列滿了,則線程阻塞等待enqueue(paramE);
		localReentrantLock.unlock();
	} finally {
		localReentrantLock.unlock();
	}
}
  1. offer(E o, long timeout, TimeUnit unit): 可以設定等待的時間, 如果在指定的時間內, 還不能往隊列中加入 BlockingQueue, 則返回失敗。
    獲取數據操作:
  2. poll(time):取走 BlockingQueue 裏排在首位的對象,若不能立即取出,則可以等 time 參數規定的時間,取不到時返回 null;
  3. poll(long timeout, TimeUnit unit): 從 BlockingQueue 取出一個隊首的對象, 如果在指定時間內, 隊列一旦有數據可取, 則立即返回隊列中的數據。否則直到時間超時還沒有數據可取,返回失敗。
  4. take():取走 BlockingQueue 裏排在首位的對象,若 BlockingQueue 爲空,阻斷進入等待狀態直到 BlockingQueue 有新的數據被加入。
  5. drainTo():一次性從 BlockingQueue 獲取所有可用的數據對象(還可以指定獲取數據的個數),通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。

77、Java 中的阻塞隊列

  1. ArrayBlockingQueue :由數組結構組成的有界阻塞隊列。
  2. LinkedBlockingQueue :由鏈表結構組成的有界阻塞隊列。
  3. PriorityBlockingQueue :支持優先級排序的無界阻塞隊列。
  4. DelayQueue:使用優先級隊列實現的無界阻塞隊列。
  5. SynchronousQueue:不存儲元素的阻塞隊列。
  6. LinkedTransferQueue:由鏈表結構組成的無界阻塞隊列。
  7. LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列
    在這裏插入圖片描述

78、ArrayBlockingQueue(公平、非公平)

用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。 默認情況下不保證訪問者公平的訪問隊列,所謂公平訪問隊列是指阻塞的所有生產者線程或消費者線程,當隊列可用時,可以按照阻塞的先後順序訪問隊列,即先阻塞的生產者線程,可以先往隊列裏插入元素,先阻塞的消費者線程,可以先從隊列裏獲取元素。通常情況下爲了保證公平性會降低吞吐量。我們可以使用以下代碼創建一個公平的阻塞隊列


ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);

79、LinkedBlockingQueue(兩個獨立鎖提高併發)

基於鏈表的阻塞隊列,同 ArrayListBlockingQueue 類似,此隊列按照先進先出(FIFO)的原則對元素進行排序。而 LinkedBlockingQueue 之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。LinkedBlockingQueue 會默認一個類似無限大小的容量(Integer.MAX_VALUE)

80、PriorityBlockingQueue(compareTo 排序實現優先)

是一個支持優先級的無界隊列。默認情況下元素採取自然順序升序排列。 可以自定義實現
compareTo()方法來指定元素進行排序規則,或者初始化 PriorityBlockingQueue 時,指定構造參數 Comparator 來對元素進行排序。需要注意的是不能保證同優先級元素的順序。

擴展連接:更多請點擊這裏

博主公衆號程序員小羊 只發面試相關推文
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章