Java面試題基礎篇(三)

LNhome發表於2018-03-28

執行緒

22、建立執行緒的方式及實現

繼承Thread類:
定義Thread的子類,重寫run()方法,通過start()進行啟動執行緒。
實現Runnable介面:
建立Runnable介面的實現類的例項,並用這個例項作為Thread的target來建立Thread物件,通用通過start()啟動執行緒。
實現Callable介面
Callable介面提供了一個call()方法作為執行緒執行體,call()方法可以有返回值,可以宣告丟擲異常。實現方式:建立Callable介面的實現類,並實現call()方法,然後建立該實現類的例項。使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了Callable物件的call()方法的返回值。使用FutureTask物件作為Thread物件的target建立並啟動執行緒。呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

23、sleep(),wait(),yield()和join()方法的區別

sleep():
sleep()方法需要指定等待的時間,它可以讓當前正在執行的執行緒在指定的時間內暫停執行,進入阻塞狀態,該方法既可以讓其他同優先順序或者高優先順序的執行緒得到執行的機會,也可以讓低優先順序的執行緒得到執行機會。但是sleep()方法不會釋放“鎖標誌”,也就是說如果有synchronized同步塊,其他執行緒仍然不能訪問共享資料。
wait():
wait()方法與sleep()方法的不同之處在於,wait()方法會釋放物件的“鎖標誌”。當呼叫某一物件的wait()方法後,會使當前執行緒暫停執行,並將當前執行緒放入物件等待池中,直到呼叫了notify()方法後,將從物件等待池中移出任意一個執行緒並放入鎖標誌等待池中,只有鎖標誌等待池中的執行緒可以獲取鎖標誌,它們隨時準備爭奪鎖的擁有權。當呼叫了某個物件的notifyAll()方法,會將物件等待池中的所有執行緒都移動到該物件的鎖標誌等待池。 除了使用notify()和notifyAll()方法,還可以使用帶毫秒引數的wait(long timeout)方法,效果是在延遲timeout毫秒後,被暫停的執行緒將被恢復到鎖標誌等待池。 此外,wait(),notify()及notifyAll()只能在synchronized語句中使用,但是如果使用的是ReenTrantLock實現同步,該如何達到這三個方法的效果呢?解決方法是使用ReenTrantLock.newCondition()獲取一個Condition類物件,然後Condition的await(),signal()以及signalAll()分別對應上面的三個方法。
yield():
yield()方法和sleep()方法類似,也不會釋放“鎖標誌”,區別在於,它沒有引數,即yield()方法只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行,另外yield()方法只能使同優先順序或者高優先順序的執行緒得到執行機會,這也和sleep()方法不同。
join():
join()方法會使當前執行緒等待呼叫join()方法的執行緒結束後才能繼續執行。

24、說說 CountDownLatch 原理


CountDownLatch概述:
CountDownLatch是一個用來控制併發的常見的工具(CountDownLatch countDownLatch = new CountDownLatch(2)),它允許一個執行緒等待其他執行緒執行到某一操作後不再阻塞。CountDownLatch的建構函式中使用的int型變數的意思是需要等待多少個操作的完成。這裡是2所以需要等到呼叫了兩次countDown()方法之後主執行緒的await()方法才會返回。這意味著如果我們錯誤的估計了需要等待的操作的個數或者在某個應該呼叫countDown()方法的地方忘記了呼叫那麼將意味著await()方法將永遠的阻塞下去。
CountDownLatch實現原理:
CountDownLatch實際上是使用計數器的方式去控制的,初始化CountDownLatch時傳入一個int變數,每當呼叫countDownt()方法的時候就使得這個變數減1,而對於await()這個方法則取判斷這個int變數的值是否為0,是則表示所有的操作均已經完成,否則繼續等待。

25、說說 CyclicBarrier 原理

CyclicBarrier概述:
CyclicBarrier讓一組執行緒到達一個屏障(同步點)時被阻塞,直到最後一個執行緒到達屏障,屏障才會開啟,所有被攔截的執行緒繼續執行,執行緒通過await()方法進入屏障,然後當前執行緒被阻塞。
CyclicBarrier實現原理:
CyclicBarrier在內部定義了一個Lock物件,每當一個執行緒呼叫CyclicBarrier的await()方法時,將攔截的執行緒數加1,然後判斷攔截的執行緒數是否等於初始化的執行緒數,如果不是,進入Lock物件的條件佇列等待,如果是,執行barrierAction物件的Runnable方法,然後將鎖的條件佇列中所有執行緒放入鎖等待佇列中,這些執行緒會依次的獲取鎖、釋放鎖,接著先從await方法返回,再從CyclicBarrier的await方法返回。

CyclicBarrier主要用於一組執行緒之間的相互等待,而CountDownLatch一般用於一組執行緒等待另一組些執行緒。


26、說說 Semaphore 原理

Semaphore(訊號量)是用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,保證合理的使用公共資源。執行緒通過acquire方法獲取訊號量的許可,當訊號量中沒有可用的許可時,執行緒阻塞,直到有可用的執行緒許可為止。執行緒可以通過release方法釋放它持有的訊號量許可。

27、說說 Exchanger 原理

Exchanger概述:
Exchanger一般用於兩個工作執行緒之間交換資料,它對外提供的方法是同步的,用於成對出現的執行緒之間交換資料。一個執行緒達到Exchanger的呼叫點時,如果它的夥伴執行緒在此前已經呼叫了此方法,那麼它的夥伴執行緒會被喚醒並與之進行物件交換然後各自返回。如果它的夥伴還沒有到達交換點,那麼當前執行緒會被掛起,直到夥伴執行緒到達完成資料的交換或者當前執行緒被中斷丟擲中斷異常又或者等候超時丟擲超時異常。
Exchanger實現原理:
我們假定一個空的棧(Stack),棧頂(Top)當然是沒有元素的。同時我們假定一個資料結構Node,包含一個要交換的元素E和一個要填充的“洞”Node。這時執行緒T1攜帶節點node1進入棧(cas_push),當然這是CAS操作,這樣棧頂就不為空了。執行緒T2攜帶節點node2進入棧,發現棧裡面已經有元素了node1,同時發現node1的hold(Node)為空,於是將自己(node2)填充到node1的hold中(cas_fill)。然後將元素node1從棧中彈出(cas_take)。這樣執行緒T1就得到了node1.hold.item也就是node2的元素e2,執行緒T2就得到了node1.item也就是e1,從而達到了交換的目的。


28、說說 CountDownLatch 與 CyclicBarrier 區別

CyclicBarrier主要用於一組執行緒之間的相互等待,而CountDownLatch一般用於一組執行緒等待另一組些執行緒。


29、ThreadLocal 原理分析

ThreadLocal概述:
ThreadLocal類用來提供執行緒內部的區域性變數,這些變數在多執行緒環境下訪問時能保證各個執行緒裡變數相對獨立於其他執行緒。ThreadLocal為每一個執行緒建立一個單獨的變數副本,提供保持物件的方法和避免引數傳遞的複雜性。
ThreadLocal實現原理:
ThreadLocal可以看作一個容器,容器中存放著屬於當前執行緒的變數,ThreadLocal提供方法對變數進行操作。在ThraadLocal類中有一個靜態的內部類ThreadLocalMap,用鍵值對的形式儲存每一個執行緒的變數副本,ThreadLocalMap中元素的key就是當前的ThreadLocal物件,而value對應執行緒的變數副本,每個執行緒可能存在多個ThreadLocal。
記憶體洩漏問題:
每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收. 
  所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。


30、講講執行緒池的實現原理

執行緒池的優點:
執行緒是稀缺資源,使用執行緒池可以減少建立和銷燬執行緒的次數,每個工作執行緒都可以重複使用。可以根據系統的承受能力,調整執行緒池中工作執行緒的數量,防止因為消耗過多的記憶體導伺服器崩潰。
執行緒池的實現原理:
(1)判斷執行緒池中的核心執行緒是否都在執行任務,如果不是(還有核心執行緒沒有被建立或者核心執行緒空閒)則建立一個新的工作執行緒來執行任務,如果核心執行緒都在執行任務,則進入第二個流程。
(2)執行緒池判斷工作佇列是否已滿,如果工作佇列沒有滿,則將新提交的任務儲存在這個工作佇列中,如果工作佇列已滿進入下個流程。
(3)判斷執行緒池中的執行緒是否都處於工作狀態,如果沒有,則建立一個新的工作執行緒來執行任務,直到執行緒池達到最大執行緒數,則交給飽和策略來處理這個任務。
飽和策略 RejectedExecutionHandler:
當執行緒池中的執行緒達到最大執行緒數,說明執行緒處於飽和狀態,那麼必須對新提交的任務採用特殊的策略進行處理。執行緒池預設的策略是AbortPolicy,表示無法處理新的任務而丟擲異常。java中提供四種策略,AbortPolicy:直接丟擲異常;CallerRunsPolicy:只用呼叫所在的執行緒執行任務;DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務;DiscardPolicy:不處理,丟棄掉。


31、執行緒池的幾種方式

newFixedThreadPool(int nThreads):
建立一個指定工作執行緒數量的執行緒池。每當提交一個任務就建立一個工作執行緒,如果工作執行緒數量達到執行緒池初始的最大數,則將提交的任務存入到池佇列中。它具有執行緒池提高程式效率和節省建立執行緒時所耗的開銷的優點。但是,線上程池空閒時,即執行緒池中沒有可執行任務時,它不會釋放工作執行緒,還會佔用一定的系統資源。
newSingleThreadExecutor()
建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。執行緒的最大數量為Interger. MAX_VALUE,如果長時間沒有往執行緒池中提交任務,即如果工作執行緒空閒了指定的時間(預設為1分鐘),則該工作執行緒將自動終止。終止後,如果你又提交了新的任務,則執行緒池重新建立一個工作執行緒。在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量執行緒同時執行,很有會造成系統癱瘓。
newSingleThreadExecutor()
建立一個單執行緒化的Executor,即只建立唯一的工作者執行緒來執行任務,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO,LIFO,優先順序)執行。如果這個執行緒異常結束,會有另一個取代它,保證順序執行。單工作執行緒最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個執行緒是活動的。
newScheduleThreadPool()
建立一個定長的執行緒池,而且支援定時的以及週期性的任務執行,支援定時及週期性任務執行。


32、執行緒的宣告週期

新建:
當程式使用new關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時僅由JVM為其分配記憶體,並初始化其成員變數的值。
就緒:
當執行緒物件呼叫了start()方法之後,該執行緒處於就緒狀態。Java虛擬機器會為其建立方法呼叫棧和程式計數器,等待排程執行。
執行:
如果處於就緒狀態的執行緒獲得了CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態。
阻塞:
當處於執行狀態的執行緒失去所佔用資源之後,便進入阻塞狀態。
死亡:
執行緒run()、main() 方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。


鎖機制

33、說說執行緒安全問題

執行緒安全概念:
類或者物件在多執行緒併發的場景下,能夠儲存程式的邏輯是可以被接受的而且不是被擾亂的,能夠保證業務邏輯處理不出問題。
判斷執行緒是否安全的方法:
(1)程式是否執行在多執行緒環境下。
(2)多執行緒是否會共享一個資源並且對這個共享資源有讀和寫操作。
如何解決執行緒安全問題:
(1)將物件設定為無狀態的。
(2)使用區域性變數
(3)物件不得不使用屬性時,考慮用ThreadLocal類包裝,包裝後的屬性就是執行緒安全的,但是各執行緒修改的屬性不被共享。
(4)使用執行緒同步技術(synchronized和lock)將讀寫的共享資原始碼塊鎖上,讓多執行緒呼叫這段程式碼的時候按順序來訪問。


34、volatile 實現原理

volatile概述:
如果一個變數被volatile修飾,則java可以確保所有的執行緒看到這個變數是一致的,如果某個執行緒對volatile修飾的共享變數進行更新,那麼其他執行緒可以立馬看到這個更新。對volatile變數的單次讀/寫操作可以保證原子型,如long和double型別變數,但是並不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。
volatile使用:
(1)防止重排序,例項化一個物件可以分為三個步驟:分配記憶體空間,初始化物件,將記憶體空間的地址賦值給對應的引用。但由於作業系統可以對指令進行重排,所以上面的過程也有可能變成如下過程:分配記憶體空間,將記憶體空間的地址賦值給對應的引用,初始化物件。如果是上述這個流程的話就有可能將未初始化的物件暴漏出來,從而導致不可預測的結果。因此為了防止這個過程的重排序,需要將變數使用v
(2)實現可見性,可見性問題主要指一個執行緒修改了共享變數值,而另一個執行緒卻看不到。引起可見性問題的主要原因是每個執行緒擁有自己的一個快取記憶體區——執行緒工作記憶體。volatile關鍵字能有效的解決這個問題。
(3)volatile能保證對單次讀/寫的原子性,因為long和double兩種資料型別的操作可分為高32位和低32位兩部分,因此普通的long或double型別讀/寫可能不是原子的。因此,鼓勵大家將共享的long和double變數設定為volatile型別,這樣能保證任何情況下對long和double的單次讀/寫操作都具有原子性。
volatile的原理:
(1)可見性實現,執行緒本身並不直接與主記憶體進行互動,而是通過執行緒的工作記憶體來完成相應的操作,這也是導致執行緒間資料不可見的本質原因。對volatile變數的寫操作與普通變數的主要區別有兩點:修改volatile變數時會強制將修改後的值重新整理到主記憶體中;修改volatile變數後會導致其他執行緒工作記憶體中對應的變數失效,因此,再讀取該變數的時候就需要重新讀取主記憶體中的值。
(2)有序性實現,a happen-before b,表示a所做的任何操作對b是可見的,在java中對volatile變數的寫操作 happen-before後續的讀操作。(volatile規則)。java的重排序分為編譯器重排序和處理器重排序,為保證volatile的有序性,JMM會對volatile變數限制這兩種型別的重排序。
(3)為了實現volatile可見性和happen-befor的語義。JVM底層是通過一個叫做“記憶體屏障”的東西來完成。記憶體屏障,也叫做記憶體柵欄,是一組處理器指令,用於實現對記憶體操作的順序限制。

35、synchronize 實現原理

synchronized可以保證方法或者程式碼塊在執行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變數的記憶體可見性。Java中每一個物件都可以作為鎖,這是synchronized實現同步的基礎:
(1)普通同步方法,鎖是當前例項物件
(2)普通同步方法,鎖是當前例項物件
(3)同步方法塊,鎖是括號裡面的物件


36、synchronized 與 lock 的區別

(1)存在層次,synchronized是java關鍵字,在jvm層面,Lock是一個類。
(2)鎖的釋放,synchronized獲取鎖的執行緒執行完同步程式碼釋放鎖,如果在執行過程中出現異常,jvm會讓執行緒釋放鎖。Lock需要手動釋放鎖,不然容易造成執行緒死鎖。
(3)鎖的獲取,synchronized的執行緒獲取鎖,其他執行緒會一直等待,直到獲取鎖的執行緒釋放鎖。Lock有多個獲取鎖的方式,可以嘗試獲取鎖,不用一直等待。
(4)鎖狀態,synchronized無法判斷鎖狀態,Lock可以判斷鎖狀態。
(5)鎖型別,synchronized可重入 不可中斷 非公平,Lock可重入 可判斷 可公平(兩者皆可)。
(6)效能,synchronized少量同步,Lock大量同步。


37、CAS 樂觀鎖

樂觀鎖的核心演算法是CAS(比較並交換),它涉及到三個運算元,記憶體值,預期值,新值,當且僅當預期值和記憶體值相等時才將記憶體值修改為新值。


38、ABA 問題

CAS的核心思想是通過比對記憶體值與預期值是否一樣而判斷記憶體值是否被修改過,但是這個判斷邏輯並不嚴謹,假如記憶體值原本是A,後來被一條執行緒更改為B,最後又被另一條執行緒更改為A,則CAS認為此記憶體值並沒有發生改變,但實際上是有被其他執行緒改過的,這種情況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變數更新都把版本號加一。

相關文章