基礎概念
線程的生命周期有哪些狀態?它們是如何轉換的?
答案:線程的生命周期有以下六種狀態:
新建(New):線程被創建但尚未啟動,此時線程對象已被分配內存空間,相關屬性已被初始化。
就緒(Runnable):線程調用start()方法后進入此狀態,表明線程已準備好運行,等待系統調度獲取 CPU 資源。
運行(Running):線程獲取到 CPU 資源,正在執行run()方法中的代碼邏輯。
阻塞(Blocked):線程因某些原因暫停執行,放棄 CPU 使用權。如等待獲取鎖、執行wait()方法進入等待狀態、執行 I/O 操作等。
超時等待(Timed Waiting):線程進入等待狀態,但有指定的等待時間。例如通過Thread.sleep(long millis)、wait(long timeout)等方法進入此狀態,時間到期后會自動返回就緒狀態。
終止(Terminated):線程執行完run()方法中的代碼,或者因異常等原因提前結束,線程進入終止狀態,此時線程的生命周期結束。
狀態轉換:新建狀態的線程調用start()方法進入就緒狀態;就緒狀態的線程被 CPU 調度選中進入運行狀態;運行狀態的線程執行wait()方法或等待獲取鎖等情況會進入阻塞狀態,執行Thread.sleep(long millis)等方法會進入超時等待狀態;阻塞狀態和超時等待狀態的線程在滿足相應條件(如被notify()喚醒、獲取到鎖、等待時間結束等)后會回到就緒狀態;運行狀態的線程執行完run()方法或出現異常等會進入終止狀態。
同步與鎖
synchronized關鍵字的作用是什么?它是如何實現同步的?
答案:synchronized關鍵字用于實現線程之間的同步,確保在同一時刻只有一個線程能夠訪問被 synchronized修飾的代碼塊或方法,從而保證數據的一致性和完整性。
當一個線程訪問被synchronized修飾的代碼塊或方法時,它會先獲取對象的鎖(如果是靜態方法,則獲取類的鎖)。如果鎖已經被其他線程持有,那么當前線程會進入阻塞狀態,直到獲取到鎖。在獲取到鎖后,線程才能執行相應的代碼。當線程執行完同步代碼塊或方法后,會釋放鎖,以便其他線程可以獲取鎖并執行同步代碼。
說說ReentrantLock和synchronized的區別。
答案:
實現機制:synchronized是 Java 語言的關鍵字,由 JVM 底層實現;ReentrantLock是 Java.util.concurrent 包中的類,通過代碼實現。
鎖的獲取與釋放:synchronized在代碼塊或方法執行完后自動釋放鎖;ReentrantLock需要手動調用unlock()方法釋放鎖,通常在finally塊中進行,以確保鎖一定會被釋放,否則可能導致死鎖。
可重入性:兩者都具有可重入性,即同一個線程可以多次獲取同一個鎖。
公平性:synchronized是非公平鎖,線程獲取鎖的順序不確定;ReentrantLock可以通過構造函數參數指定是否為公平鎖,公平鎖會按照線程請求鎖的順序分配鎖,減少線程饑餓現象,但會降低一定的性能。
功能特性:ReentrantLock提供了更多的功能特性,如可以嘗試獲取鎖(tryLock()方法)、可中斷地獲取鎖(lockInterruptibly()方法)等,而synchronized不具備這些功能。
什么是死鎖?如何避免死鎖?
答案:死鎖是指兩個或多個線程在執行過程中,因爭奪資源而造成的一種互相等待的狀態,若無外力作用,這些線程將永遠無法繼續執行。例如,線程 A 持有資源 1 并等待資源 2,而線程 B 持有資源 2 并等待資源 1,此時兩個線程相互等待,形成死鎖。
避免死鎖的方法有:
按順序獲取鎖:確保所有線程按照相同的順序獲取鎖,避免鎖的交叉獲取。
避免鎖嵌套:盡量減少在一個同步塊中獲取另一個鎖的情況,降低死鎖發生的可能性。
設置鎖超時:使用具有超時機制的鎖獲取方法,如ReentrantLock的tryLock(long timeout, TimeUnit unit)方法,當獲取鎖超時后,線程可以放棄等待,避免無限期等待。
使用資源分配圖檢測:在程序運行過程中,通過資源分配圖來檢測是否存在死鎖,如果發現死鎖,可以采取相應的措施進行處理,如終止某些線程或釋放某些資源。
線程間通信
線程間如何進行通信?請列舉幾種方式。
答案:
使用wait()、notify()和notifyAll()方法:這三個方法是Object類的方法,在同步代碼塊或同步方法中使用。一個線程調用wait()方法后會釋放鎖并進入等待狀態,其他線程可以調用notify()或notifyAll()方法喚醒等待的線程。notify()方法隨機喚醒一個等待的線程,notifyAll()方法喚醒所有等待的線程。
使用BlockingQueue:BlockingQueue是一個阻塞隊列,當隊列滿時,向隊列中添加元素的線程會被阻塞;當隊列為空時,從隊列中獲取元素的線程會被阻塞。通過這種方式實現線程間的通信和同步,例如ArrayBlockingQueue、LinkedBlockingQueue等。
使用CountDownLatch:CountDownLatch可以讓一個或多個線程等待其他線程完成一組操作后再繼續執行。通過countDown()方法減少計數器的值,當計數器的值為 0 時,等待的線程被喚醒繼續執行。
使用CyclicBarrier:CyclicBarrier用于讓一組線程互相等待,直到所有線程都到達某個屏障點,然后再一起繼續執行。它可以重復使用,當所有線程都到達屏障后,屏障會被重置,可以再次使用。
解釋一下wait()和sleep()方法的區別。
答案:
所屬類:wait()方法是Object類的方法,而sleep()方法是Thread類的方法。
釋放鎖的行為:wait()方法會釋放當前線程持有的對象鎖,使得其他線程可以獲取該鎖并訪問同步代碼塊或方法;sleep()方法不會釋放鎖,線程在睡眠期間仍然持有鎖,其他線程無法訪問被該線程鎖住的資源。
使用場景:wait()方法通常用于線程間的通信和協作,例如一個線程等待另一個線程完成某個操作后再繼續執行;sleep()方法主要用于讓線程暫停一段時間,例如在循環中控制執行頻率,或者在某些操作之間添加延遲。
喚醒方式:wait()方法需要被其他線程調用notify()或notifyAll()方法喚醒,或者等待指定的時間后自動喚醒;sleep()方法在指定的睡眠時間到達后自動喚醒。
并發容器與框架
請介紹一下ConcurrentHashMap的實現原理。
答案:ConcurrentHashMap是 Java 中用于在多線程環境下高效存儲和訪問數據的哈希表實現。它采用了分段鎖(Segment)的技術,將整個哈希表分成多個段,每個段都有自己的鎖。在 JDK 8 及以后的版本中,ConcurrentHashMap摒棄了分段鎖的概念,采用了 CAS(Compare and Swap)操作和synchronized關鍵字來實現并發安全。
當進行插入、刪除或查詢操作時,ConcurrentHashMap首先根據鍵的哈希值確定要操作的桶(bucket)。對于插入操作,會使用 CAS 操作嘗試將新元素插入到桶中,如果桶為空,則直接插入;如果桶不為空,則可能需要對桶中的元素進行遍歷和更新。在遍歷和更新過程中,會使用synchronized關鍵字對桶進行加鎖,以確保同一時刻只有一個線程能夠訪問該桶。對于查詢操作,由于ConcurrentHashMap的桶中的元素是通過鏈表或紅黑樹來存儲的,所以查詢操作可以在不加鎖的情況下進行,通過 volatile 關鍵字保證了桶中元素的可見性,從而實現了高并發下的高效查詢。
Java 中的BlockingQueue有哪些實現類?它們的特點是什么?
答案:
ArrayBlockingQueue:基于數組實現的有界阻塞隊列,在創建時需要指定隊列的容量。它按照先進先出(FIFO)的原則對元素進行排序,插入和刪除操作在隊列的兩端進行,使用一把鎖來保證并發安全。
LinkedBlockingQueue:基于鏈表實現的阻塞隊列,可以指定隊列的容量,也可以不指定,默認容量為Integer.MAX_VALUE。它同樣按照 FIFO 原則對元素進行排序,插入和刪除操作分別在鏈表的頭和尾進行,使用兩把鎖(一把用于讀操作,一把用于寫操作)來提高并發性能。
PriorityBlockingQueue:基于優先級堆實現的無界阻塞隊列,元素按照優先級進行排序。在插入元素時,會根據元素的優先級將其插入到合適的位置,取出元素時,會取出優先級最高的元素。它使用一把鎖來保證并發安全。
DelayQueue:基于優先級隊列實現的無界阻塞隊列,隊列中的元素必須實現Delayed接口,該接口定義了getDelay(TimeUnit unit)方法用于獲取元素的延遲時間。只有當元素的延遲時間到期后,才能從隊列中取出元素。它使用一把鎖和一個條件變量來實現延遲隊列的功能。
談談你對Executor框架的理解。它有哪些主要的組件?
答案:Executor框架是 Java 中用于管理和執行線程任務的框架,它提供了一種高效的方式來創建、執行和管理線程,將任務的提交與執行解耦,使得代碼更加易于維護和擴展。
主要組件包括:
Executor接口:定義了一個execute(Runnable command)方法,用于執行給定的Runnable任務。
ExecutorService接口:繼承自Executor接口,提供了更豐富的方法,如提交任務、關閉線程池等。它可以管理一組線程,并且可以通過不同的策略來分配任務給線程執行。
ThreadPoolExecutor類:ExecutorService接口的主要實現類,用于創建線程池。它可以根據不同的參數配置創建不同類型的線程池,如固定大小的線程池、可緩存的線程池等。通過線程池可以有效地復用線程,減少線程創建和銷毀的開銷,提高系統的性能和響應性。
ScheduledExecutorService接口:用于定時執行任務或周期性執行任務的接口,繼承自ExecutorService接口。
ScheduledThreadPoolExecutor類:ScheduledExecutorService接口的實現類,用于創建定時線程池,可以按照指定的延遲時間或周期執行任務。
性能優化與實踐
在多線程編程中,如何提高程序的性能?
答案:
合理使用線程池:線程池可以復用線程,減少線程創建和銷毀的開銷。根據任務的特點選擇合適的線程池類型,如固定大小的線程池適用于任務數量相對穩定的情況,可緩存的線程池適用于任務數量波動較大的情況。
減少鎖競爭:鎖的競爭會導致線程阻塞和上下文切換,降低程序性能。可以通過優化鎖的粒度,盡量縮小同步代碼塊的范圍,或者使用無鎖的數據結構和算法來避免鎖競爭。
避免線程上下文切換:線程上下文切換會消耗一定的時間和資源。可以通過減少線程的數量、合理安排任務的執行順序、避免不必要的阻塞等方式來減少線程上下文切換的發生。
使用無鎖數據結構:在一些場景下,無鎖數據結構可以提供更高的并發性能,如ConcurrentLinkedQueue、AtomicInteger等。這些數據結構通過使用 CAS 操作等技術來實現無鎖并發訪問,避免了鎖的開銷。
優化線程間通信:合理使用線程間通信機制,如BlockingQueue、CountDownLatch等,避免不必要的等待和喚醒操作,提高線程間的協作效率。
充分利用多核處理器:根據系統的處理器核心數量,合理分配線程數量,使得每個核心都能充分發揮作用,提高系統的并行度。
請描述一個你在實際項目中遇到的多線程問題,以及你是如何解決的。
答案:(以下是一個示例,你可以根據實際情況進行修改和補充)在一個電商項目中,有多個線程同時對商品庫存進行更新操作。由于并發訪問,出現了庫存數據不一致的問題,有些訂單扣除了庫存但沒有更新到數據庫,而有些訂單則更新了多次庫存,導致庫存數量不準確。
解決方法如下:
首先,分析問題的原因是多個線程對庫存的并發更新沒有進行有效的同步控制。
然后,使用ReentrantLock對庫存更新操作進行加鎖,確保在同一時刻只有一個線程能夠更新庫存。在更新庫存的方法中,先獲取鎖,然后進行庫存更新操作,最后釋放鎖。
同時,為了提高性能,對庫存更新的邏輯進行了優化,將一些不必要的操作放在鎖外面執行,只在鎖內部執行關鍵的庫存更新代碼,減少鎖的持有時間。
另外,添加了日志記錄功能,對每次庫存更新操作進行詳細的日志記錄,以便在出現問題時能夠快速定位和排查。
通過以上措施,解決了庫存數據不一致的問題,保證了多線程環境下庫存更新的準確性和穩定性。