這裡是參考B站上的大佬做的面試題筆記。大家也可以去看視訊講解!!!
文章目錄
- 21、對線程安全的了解
- 22、Thread和Runnable的差別
- 23、說說你對守護線程的了解
- 24、ThreadLocal的原理和使用場景
- 25、ThreadLocal記憶體洩漏問題,如何避免
- 26、并發、并行、串行
- 27、并發三大特性
- 28、為什麼使用線程池?解釋下線程池解釋?
- 29、線程池處理流程
- 30、線程池中阻塞隊列的作用?為什麼是先添加隊列而不是先建立最大線程?
21、對線程安全的了解
不是線程安全、應該是記憶體安全,堆是共享記憶體,可以被所有線程通路
當多個線程通路一個對象時,如果不用進行額外的同步控制或其他的協調操作,調用這個對象的行為都可以獲得正确的結果,我們就說這個對象是線程安全的。
- 堆是程序和線程共有的空間,分全局堆和局部堆。全局堆就是所有沒有配置設定的空間,局部堆就是使用者配置設定的空間。堆在作業系統對程序初始化的時候配置設定,運作過程中也可以向系統要額外的堆,但是用完了要還給作業系統,要不然就是記憶體洩漏
在java中,堆是java虛拟機所管理的記憶體中最大的一塊,是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。堆所存在的記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體以及數組都在這裡配置設定記憶體。
- 棧是每個線程獨有的,儲存其運作狀态和局部自動變量的。棧線上程開始的時候初始化,每個線程的棧互相獨立,是以,棧是線程安全的。作業系統在切換線程的時候會自動切換棧。棧空間不需要在進階語言裡面顯示的配置設定和釋放。
目前主流作業系統都是多任務的,即多個程序同時運作。為了保證安全,每個程序隻能通路配置設定給自己的記憶體空間,而不能通路别的程序的,這是由作業系統保障的。
在每個程序的記憶體空間中都會有一塊特殊的公共區域,通常稱為堆(記憶體)。程序的所有線程都可以通路到該區域,這就是造成問題的潛在原因。
- =============
22、Thread和Runnable的差別
Thread和Runnable的實質是繼承關系,沒有可比性。無論使用Runnable還是Thread,都會new Thread,然後執行run()方法。用法上,如果有複雜的線程操作需求,那就選擇繼承Thread,如果知識簡單的執行一個任務,那就實作Runnable
- =============
23、說說你對守護線程的了解
守護線程:為所有非守護線程提供服務的線程;任何一個守護線程都是整個JVM中所有非守護線程的保姆;
守護線程類似于整個程序的一個默默無聞的小喽啰;它的生死無關重要,它卻依賴整個程序而運作;哪天其他線程結束了,沒有要執行的了,程式就結束了,理都沒理守護線程,就把它中斷了;
注意:由于守護線程的終止是自身無法控制的,是以千萬不要把IO、File等重要操作邏輯配置設定給它;因為它不靠譜;
守護線程的作用是什麼?
舉例,GC垃圾回收線程:就是一個經典的守護線程,當我們的程式中不再有任何運作的Thread,程式就不會再産生垃圾,垃圾回收器也就無事可做,是以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它開始終再低級别的狀态中運作,用于實時監控和管理系統中的可回收資源。
應用場景:
- 1、來為其它線程提供服務支援的情況;
- 2、或者在任何情況下,程式結束時,這個線程必須正常且立刻關閉,就可以作為守護線程來使用;反之,如果一個正在執行某個操作的線程必須要正确地關閉掉否則就會出現不好的後果的話,那麼這個線程就不能是守護線程,而是使用者線程。通常都是些關鍵的事務,比方說,資料庫錄入或者更新,這些操作都是不能中斷的。
thread.setDaemon(true)
必須在
thread.start()
之前設定,否則會抛出一個
IllegalThreadException
異常。你不能把正在運作的正常線程設定為守護線程。
在Daemon線程中産生的新線程也是Daemon的。
守護線程不能用于去通路固有資源,比如讀寫操作或者計算邏輯。因為它會在任何時候甚至在一個操作的中間發生中斷
java自帶的多線程架構,比如ExecutorService,會将守護線程轉換為使用者線程,是以如果要使用背景線程就不能用java的線程池。
- =============
24、ThreadLocal的原理和使用場景
每一個Thread對象均含有一個ThreadLocalMap類型的成員變量threadLocals,它存儲本線程中所有ThreadLocal對象及其對應的值
ThreadLocalMap由一個個Entry對象構成。
Entry
繼承自
WeakReference<ThreadLocal<?>>
,一個
Entry
由
ThreadLocal
對象和
object
構成。由此可見,
Entry
的key是
ThreadLocal
對象,并且是一個弱引用,當沒指向key的強引用後,該key就會被垃圾收集器回收。
當執行set方法時,ThreadLocal首先會擷取目前線程對象,然後擷取目前線程的ThreadLocalMap對象。再以目前ThreadLocal對象為key,将值存儲進ThreadLocalMap對象中。
get方法執行過程類似,ThreadLocal首先會擷取目前線程對象,然後擷取目前線程的ThreadLocalMap對象。再以目前ThreadLocal對象為key,擷取對應的value。
由于每一條線程均含有各自私有的ThreadLocalMap容器,這些容器互相獨立互不影響,是以不會存線上程安全性問題,進而也無需使用同步機制來保證多條線程通路容器的互斥性。
使用場景:
- 1、在進行對象跨層傳遞的時候,使用ThreadLocal可以避免多次傳遞,打破層次間的限制
- 2、線程間資料隔離
- 3、進行事務操作,用于存儲線程事務資訊。
25、ThreadLocal記憶體洩漏問題,如何避免
記憶體洩漏為程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩漏危害可以忽略,但記憶體洩漏堆積後果很嚴重,無論多少記憶體,遲早會被占光。
不再會被使用的對象或者變量占用的記憶體不能被回收,就是記憶體洩漏。
強引用:使用最普遍的引用(new),一個對象具有強引用,不會被垃圾回收器回收。當記憶體空間不足,java虛拟機甯願抛出
OutOfMemoryError
錯誤,使程式異常終止,也不回收這種對象。
如果想取消強引用和某個對象之間的關聯,可以顯式地将引用指派為null,這樣就可以使JVM在合适的時間就會回收該對象。
弱引用:JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的對象。在java中,用
java.lang.ref.WeakReference
類來表示。可以在緩存中使用弱引用。
ThreadLocal的實作原理,每一個Thread維護一個ThreadLocalMap,key為使用弱引用的ThreadLocal執行個體,value為線程變量的副本
hreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal不存在外部強引用時,key(ThreadLocal)勢必會被GC回收,這樣就會導緻ThreadLocalMap中key為null,而value還存在這強引用,隻有thead線程退出以後value的強引用鍊條才會斷掉,但如果目前現線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鍊(紅色鍊條)
key使用強引用
當threadLocalMap的key為強引用回收ThreadLocal時,因為ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動删除,ThreadLocal不會被回收,導緻Entry記憶體洩漏。
key的弱引用
當ThreadLocalMap的key為弱引用回收ThreadLocal時,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動删除,ThreadLocal也會被回收。當key為null,在下一次ThreadLocalMap調用set(),get(),remove()方法的時候會被清除value值
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都調用它的remove()方法清除資料
- 将ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用通路到Entry的value值,進而清除掉。
- =============
26、并發、并行、串行
- 串行在時間上不可能發生重疊,前一個任務沒搞定,下一個任務就隻能等着
- 并行在時間上是重疊的,兩個任務在同一個時刻互不幹擾的同時執行
- 并發允許兩個任務彼此幹擾。統一時間點,隻有一個任務運作,交替執行。
- =============
27、并發三大特性
原子性
- 原子性是指在一個操作中cpu不可以在中途暫停然後再排程,即不被中斷操作,要不全部執行完成,要不都不執行。就好比轉賬,從賬戶A向賬戶B轉1000元,那麼必然包括兩個操作:從賬戶A減去1000元,往賬戶B加上1000元。2個操作必須全部完成。
可見性
-
當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
若兩個線程在不同的cpu,那麼線程A改變了i的值還沒重新整理到主存,線程2又使用了i,那麼這個i值肯定還是之氣的,線程1對變量的修改線程2沒有看到這就是可見性問題
有序性
- 虛拟機在進行代碼編譯時,對于那些改變順序之後不會對最終結果造成影響的代碼,虛拟機不一定會按照我們寫的代碼的順序來執行,有可能将他們重新排序。實際上,對于有些代碼進行排序之後,雖然對變量的值沒有造成影響,但由肯能會出現線程安全問題
28、為什麼使用線程池?解釋下線程池解釋?
1、降低資源消耗;提高線程使用率,降低建立和銷毀線程的消耗
2、提高響應速度;任務來了,直接有線程可用可執行,而不是先建立線程,再執行。
3、提高線程的可管理性;線程是稀缺資源,使用線程池可以統一配置設定調優監控
-
代表核心線程數,也就是正常情況下建立工作線程的線程數,這些線程建立後并不會消除,而是一種常駐線程。corepoolsize
-
代表的是最大的線程數,它與核心線程數相對應,表示最大允許被建立的線程數,比如目前任務較多,将核心線程數都用完了,還無法滿足需求時,此時就會建立新的線程,但是線程池内線程總數不會超過最大線程數maxinumpoolsize
-
表示超出核心線程數之外的線程的空閑存活時間,也就是核心線程不會消除,但是超出核心線程的部分線程如果空閑一定的時間會被消除,我們可以通過setKeepAliveTime來設定空閑時間keepAliveTime、unit
-
用來存放待執行的任務,假設我們現在核心線程都已被停用,還有任務進來則全部放入隊列,直到整個隊列被放滿但任務還再持續進入則會開始建立新的線程。workQueue
-
實際上是一個線程工廠,用來生産線程執行任務。我們可以選擇使用預設的建立工廠,産生的線程都會在同一個組内,擁有相同的優先級,且都不是守護線程。當然我們也可以選擇自定義線程工廠,一般我們會根據業務來指定不同的線程工廠ThreadFactory
- handler任務拒絕政策,有兩種情況,第一種是當我們調用shutdown等方法關閉線程池後,這時候即使線程池内部還有執行完的任務正在執行,但是由于線程池已經關閉,我們在繼續向線程池送出任務就會遭到拒絕。另一種情況就是當達到最大線程數,線程池已經沒有能力繼續處理新送出的任務時,這也是拒絕
29、線程池處理流程
30、線程池中阻塞隊列的作用?為什麼是先添加隊列而不是先建立最大線程?
1、一般的隊列隻能保證作為一個有限長度的緩沖區,如果超出了緩沖長度,就無法保留目前的任務了,阻塞隊列通過阻塞可以保留住目前想要繼續入隊的任務。
阻塞隊列可以保證任務隊列中沒有任務時阻塞擷取任務的線程,使得線程進入wait狀态,釋放cpu資源。
阻塞隊列自帶阻塞和喚醒功能,不需要額外處理,無任務執行時,線程池利用阻塞隊列的take方法挂起,進而維持核心線程的存活,不至于一直占用cpu資源