面試題004-Java-Java多線程(下)
這里寫目錄標題
- 面試題004-Java-Java多線程(下)
- 題目自測
- 題目答案
- 1. synchronized 關鍵字的作用?
- 2. volatile 關鍵字的作用?
- 3. synchronized 和 volatile 的區別?
- 4. synchronized 和 ReentrantLock 的區別?
- 5. ThreadLocal有什么用?
- 6. 線程池有什么用?為什么不推薦使用內置線程池?
- 7. 如何自定義線程池?
- 8. Java線程池有哪些參數?阻塞隊列有幾種?拒絕策略有幾種?
- 9. 線程池處理任務的流程了解嗎?
- 10. 如何給線程池命名?為什么建議給線程池命名?
- 參考資料
題目自測
- 1. synchronized 關鍵字的作用?
- 2. volatile 關鍵字的作用?
- 3. synchronized 和 volatile 的區別?
- 4. synchronized 和 ReentrantLock 的區別?
- 5. ThreadLocal有什么用?
- 6. 線程池有什么用?為什么不推薦使用內置線程池?
- 7. 如何自定義線程池?
- 8. Java線程池有哪些參數?阻塞隊列有幾種?拒絕策略有幾種?
- 9. 線程池處理任務的流程了解嗎?
- 10. 如何給線程池命名?為什么建議給線程池命名?
題目答案
1. synchronized 關鍵字的作用?
答:在Java中synchronized關鍵字用于實現線程同步,主要解決是多個線程之間訪問相同資源不會發生數據不一致的問題。它可以作用于方法或代碼塊上,以保證同一時間只有一個線程可以執行被同步的方法或代碼塊。
-
同步實例方法: 表示該方法在同一時間只能由一個線程訪問同一個實例。
public class SynchronizedExample {public synchronized void synchronizedMethod() {// 同步代碼} }
-
同步靜態方法:表示該方法在同一時間只能由一個線程訪問同一個類的所有實例。
public class SynchronizedExample {public static synchronized void synchronizedStaticMethod() {// 同步代碼} }
-
同步代碼塊:表示該代碼塊在同一時間只能由一個線程訪問該對象。
public class SynchronizedExample {private final Object lock = new Object();public void synchronizedBlock() {synchronized (this) {// 同步代碼}} }
2. volatile 關鍵字的作用?
答:在Java中volatile關鍵字用于聲明變量的可見性和防止指令重排,從而提供一種輕量級的同步機制。
在可見性方面,用volatile修飾的變量,來確保一個線程對該變量的修改對其他線程立即可見。
在防止指令重排方面,volatile會禁止JVM對變量操作的指令進行重新排序。
3. synchronized 和 volatile 的區別?
答:synchronized和volatile兩個關鍵字都用于實現線程同步的機制。
- volatile是線程同步的輕量級實現,volatile性能比synchronized要好。volatile關鍵字只能用于變量,而synchronized關鍵字可以修飾方法和代碼塊。
- volatile能保證數據的可見性,但不能保證數據的原子性。synchronized兩者都能保證。
- volatile主要解決變量在多個線程之間的可見性,而synchronize解決的是多個線程之間訪問資源的同步性。
4. synchronized 和 ReentrantLock 的區別?
答:synchronized和ReentrantLock都是Java中用于實現線程同步的機制,并且都是可重入鎖。
- synchronized:
- 是Java語言的內置關鍵字,用于對代碼塊或方法進行同步。
- 簡單易用,直接在方法或代碼塊上使用即可。
- 由JVM實現,使用方便,但功能較為有限。
- ReentrantLock:
- ReentrantLock是java.util.concurrent.locks包中的類,提供了更靈活和豐富的鎖機制(響應中斷、嘗試獲取鎖、使用條件變量等)。
- 需要顯示的加鎖和解鎖,通過代碼來控制鎖的獲取和釋放。
- 由Java庫實現,功能強大,靈活性高。
5. ThreadLocal有什么用?
答:ThreadLocal在Java中用于創建線程局部變量,確保每個變量都有自己的獨立副本變量,從而避免多線程共享一個變量帶來的線程安全問題,同時提高了并發性能。然而在使用ThreadLocal時需要注意內存泄露問題,由于ThreadLocal變量的生命周期與線程線相同,如果線程池中線程長時間不被銷毀,而ThreadLocal變量沒有被正確移除,可能會導致內存泄露問題,因此建議在不再使用ThreadLocal變量時顯示調用remove()方法。
它的使用場景有用戶會話管理、數據庫連接管理、事物管理等。
6. 線程池有什么用?為什么不推薦使用內置線程池?
答:Java中的線程池是一種基于池化技術設計用于執行異步任務的框架,它維護了一定數量的線程,避免頻繁地創建和銷毀線程帶來的性能開銷和資源浪費。他的主要作用是提高資源復用、提高系統穩定性、便于管理和提供靈活的并發策略。
不推薦使用內置線程池的主要原因是內置線程池等配置選項有限、不能滿足所有的應用場景的需求。Java庫中提供了幾種預定義線程的實現,如Executors類中的newFixedThreadPool、newCacheThreadPool、newSingleThreadExecutor等。
- FixedThreadPool 和 SingleThreadExecutor 使用無界隊列(LinkedBlockingQueue),在任務提交速度超過執行速度時,任務隊列可能無限增長,導致內存耗盡。
- CacheThreadPool 使用無界線程池,在高并發下可能會創建大量線程、導致系統資源耗盡。未被回收的線程可能會長時間占用資源,造成線程泄漏問題。
7. 如何自定義線程池?
答:在Java中,自定義線程池可以通過ThreadPoolExecutor類來實現。ThreadPoolExecutor提供了豐富的配置選項,如核心線程數(corePoolSize)、最大線程數(maximumPoolSize)、任務隊列(workQueue)和拒絕策略(rejectedExecutionHandler)等,可以根據具體需求進行靈活配置。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; public class CustomThreadPoolExample { public static void main(String[] args) { // 核心線程數 :線程池在空閑時保留的線程數,即使沒有任務需要處理。int corePoolSize = 5; // 最大線程數 :線程池允許創建的最大線程數。int maximumPoolSize = 10; // 非核心線程空閑存活時間 :當線程數超過核心線程數時,多余的空閑線程的存活時間。long keepAliveTime = 1L; // 時間單位 : 空閑線程存活時間的時間單位。TimeUnit unit = TimeUnit.SECONDS; // 任務隊列 :用于保存等待執行任務的隊列。ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 線程工廠 : 用于創建新線程。ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 拒絕策略 : 當線程池和隊列都滿時,如何處理新任務RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 創建ThreadPoolExecutor ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler ); // 提交任務 for (int i = 0; i < 15; i++) { int taskId = i; executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " is processing " + taskId); try { // 模擬任務執行時間 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 關閉線程池(不再接受新任務,但已提交的任務會繼續執行) executor.shutdown(); // 等待所有任務完成 while (!executor.isTerminated()) { // 等待一段時間 } System.out.println("All tasks completed."); }
}
8. Java線程池有哪些參數?阻塞隊列有幾種?拒絕策略有幾種?
答:
線程池的參數:
- corePoolSize(核心線程數):線程池中始終保留的線程數量,即使這些線程處于空閑狀態。
- maximumPoolSize(最大線程數):線程池中允許創建的最大線程數量。當任務隊列已滿且當前線程數小于最大線程數時,會創建新線程來處理任務。
- keepAliveTime(空閑線程存活時間):當線程池中的線程數超過核心線程數時,多余的空閑線程在終止前等待新任務的最長時間。
- timeUnit(時間單位):keepAliveTime 參數的時間單位。常見值包括 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
- workQueue(任務隊列):用于保存等待執行任務的隊列l。
- threadFactory(線程工廠):用于創建新線程。
- rejectedExecutionHandler(拒絕策略):當線程池和隊列都滿時,如何處理新任務。
阻塞隊列:
- ArrayBlockingQueue:一個基于數組的有界阻塞隊列。按FIFO(先進先出)順序保存任務。
- LinkedBlockingQueue:一個基于鏈表的可選有界阻塞隊列。通常用于無限制的任務隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等待一個相應的移除操作。
- PriorityBlockingQueue:一個基于優先級的無限阻塞隊列。任務按照優先級順序執行。
- DelayQueue:一個基于優先級隊列的無界阻塞隊列,只有在延遲期滿時才能從隊列中取走元素。
拒絕策略:
- AbortPolicy(默認策略):拋出 RejectedExecutionException 異常,阻止系統正常工作。
- CallerRunsPolicy:由調用線程(提交任務的線程)處理該任務。這種策略會降低新任務的提交速度,從而減輕線程池的負載。
- DiscardPolicy:直接丟棄任務,不拋出異常。如果允許任務丟失,這種策略可以用于避免系統過載。
- DiscardOldestPolicy:丟棄隊列中最舊的任務(即即將執行的任務),然后重新嘗試提交新任務。
9. 線程池處理任務的流程了解嗎?
答:線程池處理任務的流程可以總結為:提交任務、檢查核心線程數、任務隊列處理、非核心線程創建、執行任務以及線程回收。
- 通過submit()或execute()方法提交任務
- 檢查核心線程數,如果當前線程數少于核心線程數,那么就創建新的核心線程來執行任務。如果當前線程數已達到核心線程數,將任務放入任務隊列。
- 如果任務隊列沒有滿,將任務隊列繼續放入隊列。如果任務隊列已滿,檢查當前線程數是否小于最大線程數。
- 如果當前線程數小于最大線程數,創建新的非核心線程執行任務,如果當前線程已經達到了最大線程數,執行拒絕策略。
- 線程從任務隊列取出任務執行。
- 線程池中的非核心線程在完成任務后不會立即銷毀,進入保持存活狀態,只有當這些線程在空閑時間超過keepAliveTime后被回收。
10. 如何給線程池命名?為什么建議給線程池命名?
答:要給線程池中的線程命名,可以自定義一個 ThreadFactory,在創建線程時設置線程名稱。
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;/*** 線程工廠,它設置線程名稱,有利于我們定位問題。*/
public final class NamingThreadFactory implements ThreadFactory {private final AtomicInteger threadNum = new AtomicInteger();private final String name;/*** 創建一個帶名字的線程池生產工廠*/public NamingThreadFactory(String name) {this.name = name;}@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setName(name + " [#" + threadNum.incrementAndGet() + "]");return t;}
}
在 Java 中,為線程池中的線程命名是一種良好的實踐。命名線程有助于調試和監控,使得可以輕松識別和跟蹤線程的行為和狀態。
參考資料
- JavaGuide
- 牛客網-Java面試寶典