# 前言
這篇文章主要是對多執行緒的問題進行總結的,因此羅列了100個多執行緒的問題,
這些多執行緒的問題來源于各大網站,可能有些問題網上有、可能有些問題對應的答案也有、也可能有些各位網友也都看過,但是本文寫作的重心就是所有的問題都會按照自己的理解回答一遍,不會去看網上的答案,因此可能有些問題講的不對,能指正的希望大家不吝指教,整理了一份Java面試寶典完整版PDF
# 100個問題匯總
1、多執行緒有什么用?
一個可能在很多人看來很扯淡的一個問題:我會用多執行緒就好了,還管它有什么用?在我看來,這個回答更扯淡,所謂"知其然知其所以然","會用"只是"知其然","為什么用"才是"知其所以然",只有達到"知其然知其所以然"的程度才可以說是把一個知識點運用自如,OK,下面說說我對這個問題的看法:
(1)發揮多核CPU的優勢
隨著工業的進步,現在的筆記本、臺式機乃至商用的應用服務器至少也都是雙核的,4核、8核甚至16核的也都不少見,如果是單執行緒的程式,那么在雙核CPU上就浪費了50%,在4核CPU上就浪費了75%,單核CPU上所謂的"多執行緒"那是假的多執行緒,同一時間處理器只會處理一段邏輯,只不過執行緒之間切換得比較快,看著像多個執行緒"同時"運行罷了,多核CPU上的多執行緒才是真正的多執行緒,它能讓你的多段邏輯同時作業,多執行緒,可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的,
(2)防止阻塞
從程式運行效率的角度來看,單核CPU不但不會發揮出多執行緒的優勢,反而會因為在單核CPU上運行多執行緒導致執行緒背景關系的切換,而降低程式整體的效率,但是單核CPU我們還是要應用多執行緒,就是為了防止阻塞,試想,如果單核CPU使用單執行緒,那么只要這個執行緒阻塞了,比方說遠程讀取某個資料吧,對端遲遲未回傳又沒有設定超時時間,那么你的整個程式在資料回傳回來之前就停止運行了,多執行緒可以防止這個問題,多條執行緒同時運行,哪怕一條執行緒的代碼執行讀取資料阻塞,也不會影響其它任務的執行,
(3)便于建模
這是另外一個沒有這么明顯的優點了,假設有一個大的任務A,單執行緒編程,那么就要考慮很多,建立整個程式模型比較麻煩,但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程式模型,并通過多執行緒分別運行這幾個任務,那就簡單很多了,
2、創建執行緒的方式
比較常見的一個問題了,一般就是兩種:
(1)繼承Thread類
(2)實作Runnable介面
至于哪個好,不用說肯定是后者好,因為實作介面的方式比繼承類的方式更靈活,也能減少程式之間的耦合度,面向介面編程也是設計模式6大原則的核心,
3、start()方法和run()方法的區別
只有呼叫了start()方法,才會表現出多執行緒的特性,不同執行緒的run()方法里面的代碼交替執行,如果只是呼叫run()方法,那么代碼還是同步執行的,必須等待一個執行緒的run()方法里面的代碼全部執行完畢之后,另外一個執行緒才可以執行其run()方法里面的代碼,
4、Runnable介面和Callable介面的區別
有點深的問題了,也看出一個Java程式員學習知識的廣度,
Runnable介面中的run()方法的回傳值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;Callable介面中的call()方法是有回傳值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果,
這其實是很有用的一個特性,因為多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已,而Callable+Future/FutureTask卻可以獲取多執行緒運行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務,真的是非常有用,
5、CyclicBarrier和CountDownLatch的區別
兩個看上去有點像的類,都在java.util.concurrent下,都可以用來表示代碼運行到某個點上,二者的區別在于:
(1)CyclicBarrier的某個執行緒運行到某個點上之后,該執行緒即停止運行,直到所有的執行緒都到達了這個點,所有執行緒才重新運行;CountDownLatch則不是,某執行緒運行到某個點上之后,只是給某個數值-1而已,該執行緒繼續運行
(2)CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務
(3)CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了
6、volatile關鍵字的作用
一個非常重要的問題,是每個學習、應用多執行緒的Java程式員都必須掌握的,理解volatile關鍵字的作用的前提是要理解Java記憶體模型,這里就不講Java記憶體模型了,可以參見第31點,volatile關鍵字的作用主要有兩個:
(1)多執行緒主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變數,保證了其在多執行緒之間的可見性,即每次讀取到volatile變數,一定是最新的資料
(2)代碼底層執行不像我們看到的高級語言----Java程式這么簡單,它的執行是Java代碼-->位元組碼-->根據位元組碼執行對應的C/C++代碼-->C/C++代碼被編譯成匯編語言-->和硬體電路互動,現實中,為了獲取更好的性能JVM可能會對指令進行重排序,多執行緒下可能會出現一些意想不到的問題,使用volatile則會對禁止語意重排序,當然這也一定程度上降低了代碼執行效率
從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger,
7、什么是執行緒安全
又是一個理論的問題,各式各樣的答案有很多,我給出一個個人認為解釋地最好的:如果你的代碼在多執行緒下執行和在單執行緒下執行永遠都能獲得一樣的結果,那么你的代碼就是執行緒安全的,
這個問題有值得一提的地方,就是執行緒安全也是有幾個級別的:
(1)不可變
像String、Integer、Long這些,都是final型別的類,任何一個執行緒都改變不了它們的值,要改變除非新創建一個,因此這些不可變物件不需要任何同步手段就可以直接在多執行緒環境下使用
(2)絕對執行緒安全
不管運行時環境如何,呼叫者都不需要額外的同步措施,要做到這一點通常需要付出許多額外的代價,Java中標注自己是執行緒安全的類,實際上絕大多數都不是執行緒安全的,不過絕對執行緒安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相對執行緒安全
相對執行緒安全也就是我們通常意義上所說的執行緒安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限于此,如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制,
(4)執行緒非安全
這個就沒什么好說的了,ArrayList、LinkedList、HashMap等都是執行緒非安全的類
8、Java中如何獲取到執行緒dump檔案
死回圈、死鎖、阻塞、頁面打開慢等問題,打執行緒dump是最好的解決問題的途徑,所謂執行緒dump也就是執行緒堆疊,獲取到執行緒堆疊有兩步:
(1)獲取到執行緒的pid,可以通過使用jps命令,在Linux環境下還可以使用ps -ef | grep java
(2)列印執行緒堆疊,可以通過使用jstack pid命令,在Linux環境下還可以使用kill -3 pid
另外提一點,Thread類提供了一個getStackTrace()方法也可以用于獲取執行緒堆疊,這是一個實體方法,因此此方法是和具體執行緒實體系結的,每次獲取獲取到的是具體某個執行緒當前運行的堆疊,
9、一個執行緒如果出現了運行時例外會怎么樣
如果這個例外沒有被捕獲的話,這個執行緒就停止執行了,另外重要的一點是:如果這個執行緒持有某個某個物件的監視器,那么這個物件監視器會被立即釋放
10、如何在兩個執行緒之間共享資料
通過在執行緒之間共享物件就可以了,然后通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞佇列BlockingQueue就是為執行緒之間共享資料而設計的
11、sleep方法和wait方法有什么區別
這個問題常問,sleep方法和wait方法都可以用來放棄CPU一定的時間,不同點在于如果執行緒持有某個物件的監視器,sleep方法不會放棄這個物件的監視器,wait方法會放棄這個物件的監視器
12、生產者消費者模型的作用是什么
這個問題很理論,但是很重要:
(1)通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的運行效率,這是生產者消費者模型最重要的作用
(2)解耦,這是生產者消費者模型附帶的作用,解耦意味著生產者和消費者之間的聯系少,聯系越少越可以獨自發展而不需要收到相互的制約
13、ThreadLocal有什么用
簡單說ThreadLocal就是一種以空間換時間的做法,在每個Thread里面維護了一個以開地址法實作的ThreadLocal.ThreadLocalMap,把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了
14、為什么wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫
這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖
15、wait()方法和notify()/notifyAll()方法在放棄物件監視器時有什么區別
wait()方法和notify()/notifyAll()方法在放棄物件監視器的時候的區別在于:
wait()方法立即釋放物件監視器,notify()/notifyAll()方法則會等待執行緒剩余代碼執行完畢才會放棄物件監視器,
16、為什么要使用執行緒池
避免頻繁地創建和銷毀執行緒,達到執行緒物件的重用,另外,使用執行緒池還可以根據專案靈活地控制并發的數目,
17、怎么檢測一個執行緒是否持有物件監視器
我也是在網上看到一道多執行緒面試題才知道有方法可以判斷某個執行緒是否持有物件監視器:Thread類提供了一個holdsLock(Object obj)方法,當且僅當物件obj的監視器被某條執行緒持有的時候才會回傳true,注意這是一個static方法,這意味著"某條執行緒"指的是當前執行緒,
18、synchronized和ReentrantLock的區別
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別,既然ReentrantLock是類,那么它就提供了比
synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴展性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設定,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的資訊
(3)ReentrantLock可以靈活地實作多路通知
另外,二者的鎖機制其實也是不一樣的,ReentrantLock底層呼叫的是Unsafe的park方法加鎖,synchronized操作的應該是物件頭中mark word,這點我不能確定,
19、ConcurrentHashMap的并發度是什么
ConcurrentHashMap的并發度就是segment的大小,默認為16,這意味著最多同時可以有16條執行緒操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優勢,任何情況下,Hashtable能同時有兩條執行緒獲取Hashtable中的資料嗎?
20、ReadWriteLock是什么
首先明確一下,不是說ReentrantLock不好,只是ReentrantLock某些時候有局限,如果使用ReentrantLock,可能本身是為了防止執行緒A在寫資料、執行緒B在讀資料造成的資料不一致,但這樣,如果執行緒C在讀資料、執行緒D也在讀資料,讀資料是不會改變資料的,沒有必要加鎖,但是還是加鎖了,降低了程式的性能,
因為這個,才誕生了讀寫鎖ReadWriteLock,ReadWriteLock是一個讀寫鎖介面,ReentrantReadWriteLock是ReadWriteLock介面的一個具體實作,實作了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能,
21、FutureTask是什么
這個其實前面有提到過,FutureTask表示一個異步運算的任務,FutureTask里面可以傳入一個Callable的具體實作類,可以對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作,當然,由于FutureTask也是Runnable介面的實作類,所以FutureTask也可以放入執行緒池中,
22、Linux環境下如何查找哪個執行緒使用CPU最長
這是一個比較偏實踐的問題,這種問題我覺得挺有意義的,可以這么做:
(1)獲取專案的pid,jps或者ps -ef | grep java,這個前面有講過
(2)top -H -p pid,順序不能改變
這樣就可以列印出當前的專案,每條執行緒占用CPU時間的百分比,注意這里打出的是LWP,也就是作業系統原生執行緒的執行緒號,我筆記本山沒有部署Linux環境下的Java工程,因此沒有辦法截圖演示,網友朋友們如果公司是使用Linux環境部署專案的話,可以嘗試一下,
使用"top -H -p pid"+"jps pid"可以很容易地找到某條占用CPU高的執行緒的執行緒堆疊,從而定位占用CPU高的原因,一般是因為不當的代碼操作導致了死回圈,
最后提一點,"top -H -p pid"打出來的LWP是十進制的,"jps pid"打出來的本地執行緒號是十六進制的,轉換一下,就能定位到占用CPU高的執行緒的當前執行緒堆疊了,
23、Java編程寫一個會導致死鎖的程式
第一次看到這個題目,覺得這是一個非常好的問題,很多人都知道死鎖是怎么一回事兒:執行緒A和執行緒B相互等待對方持有的鎖導致程式無限死回圈下去,當然也僅限于此了,問一下怎么寫一個死鎖的程式就不知道了,這種情況說白了就是不懂什么是死鎖,懂一個理論就完事兒了,實踐中碰到死鎖的問題基本上是看不出來的,
真正理解什么是死鎖,這個問題其實不難,幾個步驟:
(1)兩個執行緒里面分別持有兩個Object物件:lock1和lock2,這兩個lock作為同步代碼塊的鎖;
(2)執行緒1的run()方法中同步代碼塊先獲取lock1的物件鎖,Thread.sleep(xxx),時間不需要太多,50毫秒差不多了,然后接著獲取lock2的物件鎖,這么做主要是為了防止執行緒1啟動一下子就連續獲得了lock1和lock2兩個物件的物件鎖
(3)執行緒2的run)(方法中同步代碼塊先獲取lock2的物件鎖,接著獲取lock1的物件鎖,當然這時lock1的物件鎖已經被執行緒1鎖持有,執行緒2肯定是要等待執行緒1釋放lock1的物件鎖的
這樣,執行緒1"睡覺"睡完,執行緒2已經獲取了lock2的物件鎖了,執行緒1此時嘗試獲取lock2的物件鎖,便被阻塞,此時一個死鎖就形成了,代碼就不寫了,占的篇幅有點多,Java多執行緒7:死鎖這篇文章里面有,就是上面步驟的代碼實作,
24、怎么喚醒一個阻塞的執行緒
如果執行緒是因為呼叫了wait()、sleep()或者join()方法而導致的阻塞,可以中斷執行緒,并且通過拋出InterruptedException來喚醒它;如果執行緒遇到了IO阻塞,無能為力,因為IO是作業系統實作的,Java代碼并沒有辦法直接接觸到作業系統,
25、不可變物件對多執行緒有什么幫助
前面有提到過的一個問題,不可變物件保證了物件的記憶體可見性,對不可變物件的讀取不需要進行額外的同步手段,提升了代碼執行效率,
26、什么是多執行緒的背景關系切換
多執行緒的背景關系切換是指CPU控制權由一個已經正在運行的執行緒切換到另外一個就緒并等待獲取CPU執行權的執行緒的程序,
27、如果你提交任務時,執行緒池佇列已滿,這時會發生什么
這里區分一下:
-
如果使用的是無界佇列LinkedBlockingQueue,也就是無界佇列的話,沒關系,繼續添加任務到阻塞佇列中等待執行,因為LinkedBlockingQueue可以近乎認為是一個無窮大的佇列,可以無限存放任務
-
如果使用的是有界佇列比如ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,會根據maximumPoolSize的值增加執行緒數量,如果增加了執行緒數量還是處理不過來,ArrayBlockingQueue繼續滿,那么則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy
28、Java中用到的執行緒調度演算法是什么
搶占式,一個執行緒用完CPU之后,作業系統會根據執行緒優先級、執行緒饑餓情況等資料算出一個總的優先級并分配下一個時間片給某個執行緒執行,
29、Thread.sleep(0)的作用是什么
這個問題和上面那個問題是相關的,我就連在一起了,由于Java采用搶占式的執行緒調度演算法,因此可能會出現某條執行緒常常獲取到CPU控制權的情況,為了讓某些優先級比較低的執行緒也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次作業系統分配時間片的操作,這也是平衡CPU控制權的一種操作,
30、什么是自旋
很多synchronized里面的代碼只是一些很簡單的代碼,執行時間非常快,此時等待的執行緒都加鎖可能是一種不太值得的操作,因為執行緒阻塞涉及到用戶態和內核態切換的問題,既然synchronized里面的代碼執行得非常快,不妨讓等待鎖的執行緒不要被阻塞,而是在synchronized的邊界做忙回圈,這就是自旋,如果做了多次忙回圈發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略,
31、什么是Java記憶體模型
Java記憶體模型定義了一種多執行緒訪問Java記憶體的規范,Java記憶體模型要完整講不是這里幾句話能說清楚的,我簡單總結一下Java記憶體模型的幾部分內容:
(1)Java記憶體模型將記憶體分為了主記憶體和作業記憶體,類的狀態,也就是類之間共享的變數,是存盤在主記憶體中的,每次Java執行緒用到這些主記憶體中的變數的時候,會讀一次主記憶體中的變數,并讓這些記憶體在自己的作業記憶體中有一份拷貝,運行自己執行緒代碼的時候,用到這些變數,操作的都是自己作業記憶體中的那一份,在執行緒代碼執行完畢之后,會將最新的值更新到主記憶體中去
(2)定義了幾個原子操作,用于操作主記憶體和作業記憶體中的變數
(3)定義了volatile變數的使用規則
(4)happens-before,即先行發生原則,定義了操作A必然先行發生于操作B的一些規則,比如在同一個執行緒內控制流前面的代碼一定先行發生于控制流后面的代碼、一個釋放鎖unlock的動作一定先行發生于后面對于同一個鎖進行鎖定lock的動作等等,只要符合這些規則,則不需要額外做同步措施,如果某段代碼不符合所有的happens-before規則,則這段代碼一定是執行緒非安全的
32、什么是CAS
CAS,全稱為Compare and Swap,即比較-替換,假設有三個運算元:記憶體值V、舊的預期值A、要修改的值B,當且僅當預期值A和記憶體值V相同時,才會將記憶體值修改為B并回傳true,否則什么都不做并回傳false,當然CAS一定要volatile變數配合,這樣才能保證每次拿到的變量是主記憶體中最新的那個值,否則舊的預期值A對某條執行緒來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功,
33、什么是樂觀鎖和悲觀鎖
(1)樂觀鎖:就像它的名字一樣,對于并發間操作產生的執行緒安全問題持樂觀狀態,樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生沖突,那么就應該有相應的重試邏輯,
(2)悲觀鎖:還是像它的名字一樣,對于并發間操作產生的執行緒安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了,
34、什么是AQS
簡單說一下AQS,AQS全稱為AbstractQueuedSychronizer,翻譯過來應該是抽象佇列同步器,
如果說java.util.concurrent的基礎是CAS的話,那么AQS就是整個Java并發包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它,AQS實際上以雙向佇列的形式連接所有的Entry,比方說ReentrantLock,所有等待的執行緒都被放在一個Entry中并連成雙向佇列,前面一個執行緒使用ReentrantLock好了,則雙向佇列實際上的第一個Entry開始運行,
AQS定義了對雙向佇列所有的操作,而只開放了tryLock和tryRelease方法給開發者使用,開發者可以根據自己的實作重寫tryLock和tryRelease方法,以實作自己的并發功能,
35、單例模式的執行緒安全性
老生常談的問題了,首先要說的是單例模式的執行緒安全意味著:某個類的實體在多執行緒環境下只會被創建一次出來,單例模式有很多種的寫法,我總結一下:
(1)餓漢式單例模式的寫法:執行緒安全
(2)懶漢式單例模式的寫法:非執行緒安全
(3)雙檢鎖單例模式的寫法:執行緒安全
36、Semaphore有什么作用
Semaphore就是一個信號量,它的作用是限制某段代碼塊的并發數,
Semaphore有一個建構式,可以傳入一個int型整數n,表示某段代碼最多只有n個執行緒可以訪問,如果超出了n,那么請等待,等到某個執行緒執行完畢這段代碼塊,下一個執行緒再進入,由此可以看出如果Semaphore建構式中傳入的int型整數n=1,相當于變成了一個synchronized了,
37、Hashtable的size()方法中明明只有一條陳述句"return count",為什么還要做同步?
這是我之前的一個困惑,不知道大家有沒有想過這個問題,某個方法中如果有多條陳述句,并且都在操作同一個類變數,那么在多執行緒環境下不加鎖,勢必會引發執行緒安全問題,這很好理解,但是size()方法明明只有一條陳述句,為什么還要加鎖?
關于這個問題,在慢慢地作業、學習中,有了理解,主要原因有兩點:
(1)同一時間只能有一條執行緒執行固定類的同步方法,但是對于類的非同步方法,可以多條執行緒同時訪問,所以,這樣就有問題了,可能執行緒A在執行Hashtable的put方法添加資料,執行緒B則可以正常呼叫size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能執行緒A添加了完了資料,但是沒有對size++,執行緒B就已經讀取size了,那么對于執行緒B來說讀取到的size一定是不準確的,而給size()方法加了同步之后,意味著執行緒B呼叫size()方法只有在執行緒A呼叫put方法完畢之后才可以呼叫,這樣就保證了執行緒安全性
(2)CPU執行代碼,執行的不是Java代碼,這點很關鍵,一定得記住,Java代碼最終是被翻譯成機器碼執行的,機器碼才是真正可以和硬體電路互動的代碼,即使你看到Java代碼只有一行,甚至你看到Java代碼編譯之后生成的位元組碼也只有一行,也不意味著對于底層來說這句陳述句的操作只有一個,一句"return count"假設被翻譯成了三句匯編陳述句執行,一句匯編陳述句和其機器碼做對應,完全可能執行完第一句,執行緒就切換了,
38、執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
這是一個非常刁鉆和狡猾的問題,請記住:執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法里面的代碼才是被執行緒自身所呼叫的,
如果說上面的說法讓你感到困惑,那么我舉個例子,假設Thread2中new了Thread1,main函式中new了Thread2,那么:
(1)Thread2的構造方法、靜態塊是main執行緒呼叫的,Thread2的run()方法是Thread2自己呼叫的
(2)Thread1的構造方法、靜態塊是Thread2呼叫的,Thread1的run()方法是Thread1自己呼叫的
39、同步方法和同步塊,哪個是更好的選擇
同步塊,這意味著同步塊之外的代碼是異步執行的,這比同步整個方法更提升代碼的效率,請知道一條原則:同步的范圍越小越好,
借著這一條,我額外提一點,雖說同步的范圍越少越好,但是在Java虛擬機中還是存在著一種叫做鎖粗化的優化方法,這種方法就是把同步范圍變大,這是有用的,比方說StringBuffer,它是一個執行緒安全的類,自然最常用的append()方法是一個同步方法,我們寫代碼的時候會反復append字串,這意味著要進行反復的加鎖->解鎖,這對性能不利,因為這意味著Java虛擬機在這條執行緒上要反復地在內核態和用戶態之間進行切換,因此Java虛擬機會將多次append方法呼叫的代碼進行一個鎖粗化的操作,將多次的append的操作擴展到append方法的頭尾,變成一個大的同步塊,這樣就減少了加鎖-->解鎖的次數,有效地提升了代碼執行的效率,
40、高并發、任務執行時間短的業務怎樣使用執行緒池?并發不高、任務執行時間長的業務怎樣使用執行緒池?并發高、業務執行時間長的業務怎樣使用執行緒池?
這是我在并發編程網上看到的一個問題,把這個問題放在最后一個,希望每個人都能看到并且思考一下,因為這個問題非常好、非常實際、非常專業,關于這個問題,個人看法是:
(1)高并發、任務執行時間短的業務,執行緒池執行緒數可以設定為CPU核數+1,減少執行緒背景關系的切換
(2)并發不高、任務執行時間長的業務要區分開看:
a)假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大執行緒池中的執行緒數目,讓CPU處理更多的業務
b)假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)一樣吧,執行緒池中的執行緒數設定得少一些,減少執行緒背景關系的切換
(3)并發高、業務執行時間長,解決這種型別任務的關鍵不在于執行緒池而在于整體架構的設計,看看這些業務里面某些資料是否能做快取是第一步,增加服務器是第二步,至于執行緒池的設定,設定參考(2),
最后,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦,
41、為什么使用Executor框架?
每次執行任務創建執行緒 new Thread()比較消耗性能,創建一個執行緒是比較耗時、耗資源的,
呼叫 new Thread()創建的執行緒缺乏管理,被稱為野執行緒,而且可以無限制的創建,執行緒之間的相互競爭會導致過多占用系統資源而導致系統癱瘓,還有執行緒之間的頻繁交替也會消耗很多系統資源,
接使用new Thread() 啟動的執行緒不利于擴展,比如定時執行、定期執行、定時定期執行、執行緒中斷等都不便實作,
42、在Java中Executor和Executors的區別?
Executors 工具類的不同方法按照我們的需求創建了不同的執行緒池,來滿足業務的需求,
Executor 介面物件能執行我們的執行緒任務,ExecutorService介面繼承了Executor介面并進行了擴展,提供了更多的方法我們能獲得任務執行的狀態并且可以獲取任務的回傳值,
使用ThreadPoolExecutor 可以創建自定義執行緒池,Future 表示異步計算的結果,他提供了檢查計算是否完成的方法,以等待計算的完成,并可以使用get()方法獲取計算的結果,
43、什么是原子操作?在Java Concurrency API中有哪些原子類(atomic classes)?
原子操作(atomic operation)意為”不可被中斷的一個或一系列操作” ,處理器使用基于對快取加鎖或總線加鎖的方式來實作多處理器之間的原子操作,
在Java中可以通過鎖和回圈CAS的方式來實作原子操作,CAS操作——Compare & Set,或是 Compare & Swap,現在幾乎所有的CPU指令都支持CAS的原子操作,
原子操作是指一個不受其他操作影響的操作任務單元,原子操作是在多執行緒環境下避免資料不一致必須的手段,
int++并不是一個原子操作,所以當一個執行緒讀取它的值并加1時,另外一個執行緒有可能會讀到之前的值,這就會引發錯誤,
為了解決這個問題,必須保證增加操作是原子的,在JDK1.5之前我們可以使用同步技術來做到這一點,到JDK1.5,java.util.concurrent.atomic包提供了int和long型別的原子包裝類,它們可以自動的保證對于他們的操作是原子的并且不需要使用同步,
java.util.concurrent這個包里面提供了一組原子類,其基本的特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的實體包含的方法時,具有排他性,
即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解,
-
原子類:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
-
原子陣列:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
-
原子屬性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解決ABA問題的原子類:AtomicMarkableReference(通過引入一個boolean來反映中間有沒有變過),AtomicStampedReference(通過引入一個int來累加來反映中間有沒有變過)
44、Java Concurrency API中的Lock介面(Lock interface)是什么?對比同步它有什么優勢?
Lock介面比同步方法和同步塊提供了更具擴展性的鎖操作,他們允許更靈活的結構,可以具有完全不同的性質,并且可以支持多個相關類的條件物件,
它的優勢有:
-
可以使鎖更公平
-
可以讓執行緒嘗試獲取鎖,并在無法獲取鎖的時候立即回傳或者等待一段時間
-
可以在不同的范圍,以不同的順序獲取和釋放鎖
整體上來說Lock是synchronized的擴展版,Lock提供了無條件的、可輪詢的(tryLock方法)、定時的(tryLock帶參方法)、可中斷的(lockInterruptibly)、可多條件佇列的(newCondition方法)鎖操作,
另外Lock的實作類基本都支持非公平鎖(默認)和公平鎖,synchronized只支持非公平鎖,當然,在大部分情況下,非公平鎖是高效的選擇,
45、什么是Executors框架?
Executor框架是一個根據一組執行策略呼叫,調度,執行和控制的異步任務的框架,
無限制的創建執行緒會引起應用程式記憶體溢位,所以創建一個執行緒池是個更好的的解決方案,因為可以限制執行緒的數量并且可以回收再利用這些執行緒,利用Executors框架可以非常方便的創建一個執行緒池,
46、什么是阻塞佇列?阻塞佇列的實作原理是什么?如何使用阻塞佇列來實作生產者-消費者模型?
阻塞佇列(BlockingQueue)是一個支持兩個附加操作的佇列,
這兩個附加的操作是:在佇列為空時,獲取元素的執行緒會等待佇列變為非空,當佇列滿時,存盤元素的執行緒會等待佇列可用,
阻塞佇列常用于生產者和消費者的場景,生產者是往佇列里添加元素的執行緒,消費者是從佇列里拿元素的執行緒,阻塞佇列就是生產者存放元素的容器,而消費者也只從容器里拿元素,
JDK7提供了7個阻塞佇列,分別是:
-
ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列,
-
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞佇列,
-
PriorityBlockingQueue :一個支持優先級排序的無界阻塞佇列,
-
DelayQueue:一個使用優先級佇列實作的無界阻塞佇列,
-
SynchronousQueue:一個不存盤元素的阻塞佇列,
-
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞佇列,
-
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞佇列,
Java 5之前實作同步存取時,可以使用普通的一個集合,然后在使用執行緒的協作和執行緒同步可以實作生產者,消費者模式,主要的技術就是用好,wait ,notify,notifyAll,sychronized這些關鍵字,而在java 5之后,可以使用阻塞佇列來實作,此方式大大簡少了代碼量,使得多執行緒編程更加容易,安全方面也有保障,
BlockingQueue介面是Queue的子介面,它的主要用途并不是作為容器,而是作為執行緒同步的的工具,因此他具有一個很明顯的特性,當生產者執行緒試圖向BlockingQueue放入元素時,如果佇列已滿,則執行緒被阻塞,當消費者執行緒試圖從中取出一個元素時,如果佇列為空,則該執行緒會被阻塞,正是因為它所具有這個特性,所以在程式中多個執行緒交替向BlockingQueue中放入元素,取出元素,它可以很好的控制執行緒之間的通信,
阻塞佇列使用最經典的場景就是socket客戶端資料的讀取和決議,讀取資料的執行緒不斷將資料放入佇列,然后決議執行緒不斷從佇列取資料決議,
47、什么是Callable和Future?
Callable介面類似于Runnable,從名字就可以看出來了,但是Runnable不會回傳結果,并且無法拋出回傳結果的例外,而Callable功能更強大一些,被執行緒執行后,可以回傳值,這個回傳值可以被Future拿到,也就是說,Future可以拿到異步執行任務的回傳值,可以認為是帶有回呼的Runnable,
Future介面表示異步任務,是還沒有完成的任務給出的未來結果,所以說Callable用于產生結果,Future用于獲取結果,
48、什么是FutureTask?使用ExecutorService啟動任務,
在Java并發程式中FutureTask表示一個可以取消的異步運算,它有啟動和取消運算、查詢運算是否完成和取回運算結果等方法,只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞,
一個FutureTask物件可以對呼叫了Callable和Runnable的物件進行包裝,由于FutureTask也是呼叫了Runnable介面所以它可以提交給Executor來執行,
49、什么是并發容器的實作?
何為同步容器:可以簡單地理解為通過synchronized來實作同步的容器,如果有多個執行緒呼叫同步容器的方法,它們將會串行執行,比如Vector,Hashtable,以及Collections.synchronizedSet,synchronizedList等方法回傳的容器,
可以通過查看Vector,Hashtable等這些同步容器的實作代碼,可以看到這些容器實作執行緒安全的方式就是將它們的狀態封裝起來,并在需要同步的方法上加上關鍵字synchronized,
并發容器使用了與同步容器完全不同的加鎖策略來提供更高的并發性和伸縮性,例如在ConcurrentHashMap中采用了一種粒度更細的加鎖機制,可以稱為分段鎖,在這種鎖機制下,允許任意數量的讀執行緒并發地訪問map,并且執行讀操作的執行緒和寫操作的執行緒也可以并發的訪問map,同時允許一定數量的寫操作執行緒并發地修改map,所以它可以在并發環境下實作更高的吞吐量,
50、多執行緒同步和互斥有幾種實作方法,都是什么?
執行緒同步是指執行緒之間所具有的一種制約關系,一個執行緒的執行依賴另一個執行緒的訊息,當它沒有得到另一個執行緒的訊息時應等待,直到訊息到達時才被喚醒,
執行緒互斥是指對于共享的行程系統資源,在各單個執行緒訪問時的排它性,當有若干個執行緒都要使用某一共享資源時,任何時刻最多只允許一個執行緒去使用,其它要使用該資源的執行緒必須等待,直到占用資源者釋放該資源,執行緒互斥可以看成是一種特殊的執行緒同步,
執行緒間的同步方法大體可分為兩類:用戶模式和內核模式,顧名思義,內核模式就是指利用系統內核物件的單一性來進行同步,使用時需要切換內核態與用戶態,而用戶模式就是不需要切換到內核態,只在用戶態完成操作,
用戶模式下的方法有:原子操作(例如一個單一的全域變數),臨界區,內核模式下的方法有:事件,信號量,互斥量,
51、什么是競爭條件?你怎樣發現和解決競爭?
當多個行程都企圖對共享資料進行某種處理,而最后的結果又取決于行程運行的順序時,則我們認為這發生了競爭條件(race condition),
52、為什么我們呼叫start()方法時會執行run()方法,為什么我們不能直接呼叫run()方法?
當你呼叫start()方法時你將創建新的執行緒,并且執行在run()方法里的代碼,
但是如果你直接呼叫run()方法,它不會創建新的執行緒也不會執行呼叫執行緒的代碼,只會把run方法當作普通方法去執行,
53、Java中你怎樣喚醒一個阻塞的執行緒?
在Java發展史上曾經使用suspend()、resume()方法對于執行緒進行阻塞喚醒,但隨之出現很多問題,比較典型的還是死鎖問題,
解決方案可以使用以物件為目標的阻塞,即利用Object類的wait()和notify()方法實作執行緒阻塞,
首先,wait、notify方法是針對物件的,呼叫任意物件的wait()方法都將導致執行緒阻塞,阻塞的同時也將釋放該物件的鎖,相應地,呼叫任意物件的notify()方法則將隨機解除該物件阻塞的執行緒,但它需要重新獲取改物件的鎖,直到獲取成功才能往下執行;
其次,wait、notify方法必須在synchronized塊或方法中被呼叫,并且要保證同步塊或方法的鎖物件與呼叫wait、notify方法的物件是同一個,如此一來在呼叫wait之前當前執行緒就已經成功獲取某物件的鎖,執行wait阻塞后當前執行緒就將之前獲取的物件鎖釋放,
54、在Java中CycliBarriar和CountdownLatch有什么區別?
CyclicBarrier可以重復使用,而CountdownLatch不能重復使用,
Java的concurrent包里面的CountDownLatch其實可以把它看作一個計數器,只不過這個計數器的操作是原子操作,同時只能有一個執行緒去操作這個計數器,也就是同時只能有一個執行緒去減這個計數器里面的值,
你可以向CountDownLatch物件設定一個初始的數字作為計數值,任何呼叫這個物件上的await()方法都會阻塞,直到這個計數器的計數值被其他的執行緒減為0為止,
所以在當前計數到達零之前,await 方法會一直受阻塞,之后,會釋放所有等待的執行緒,await的所有后續呼叫都將立即回傳,這種現象只出現一次——計數無法被重置,如果需要重置計數,請考慮使用 CyclicBarrier,
CountDownLatch的一個非常典型的應用場景是:有一個任務想要往下執行,但必須要等到其他的任務執行完畢后才可以繼續往下執行,假如我們這個想要繼續往下執行的任務呼叫一個CountDownLatch物件的await()方法,其他的任務執行完自己的任務后呼叫同一個CountDownLatch物件上的countDown()方法,這個呼叫await()方法的任務將一直阻塞等待,直到這個CountDownLatch物件的計數值減到0為止,
CyclicBarrier一個同步輔助類,它允許一組執行緒互相等待,直到到達某個公共屏障點 (common barrier point),在涉及一組固定大小的執行緒的程式中,這些執行緒必須不時地互相等待,此時 CyclicBarrier 很有用,因為該 barrier 在釋放等待執行緒后可以重用,所以稱它為回圈 的 barrier,
55、什么是不可變物件,它對寫并發應用有什么幫助?
不可變物件(Immutable Objects)即物件一旦被創建它的狀態(物件的資料,也即物件屬性值)就不能改變,反之即為可變物件(Mutable Objects),
不可變物件的類即為不可變類(Immutable Class),Java平臺類別庫中包含許多不可變類,如String、基本型別的包裝類、BigInteger和BigDecimal等,
不可變物件天生是執行緒安全的,它們的常量(域)是在建構式中創建的,既然它們的狀態無法修改,這些常量永遠不會變,
不可變物件永遠是執行緒安全的,只有滿足如下狀態,一個物件才是不可變的;它的狀態不能在創建后再被修改;所有域都是final型別;并且, 它被正確創建(創建期間沒有發生this參考的逸出),
56、什么是多執行緒中的背景關系切換?
在背景關系切換程序中,CPU會停止處理當前運行的程式,并保存當前程式運行的具體位置以便之后繼續運行,從這個角度來看,背景關系切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼,
在程式中,背景關系切換程序中的“頁碼”資訊是保存在行程控制塊(PCB)中的,PCB還經常被稱作“切換楨”(switchframe),“頁碼”資訊會一直保存到CPU的記憶體中,直到他們被再次使用,
背景關系切換是存盤和恢復CPU狀態的程序,它使得執行緒執行能夠從中斷點恢復執行,背景關系切換是多任務作業系統和多執行緒環境的基本特征,
57、Java中用到的執行緒調度演算法是什么?
計算機通常只有一個CPU,在任意時刻只能執行一潭訓器指令,每個執行緒只有獲得CPU的使用權才能執行指令.所謂多執行緒的并發運行,其實是指從宏觀上看,各個執行緒輪流獲得CPU的使用權,分別執行各自的任務,
在運行池中,會有多個處于就緒狀態的執行緒在等待CPU,JAVA虛擬機的一項任務就是負責執行緒的調度,執行緒調度是指按照特定機制為多個執行緒分配CPU的使用權.
有兩種調度模型:分時調度模型和搶占式調度模型,分時調度模型是指讓所有的執行緒輪流獲得cpu的使用權,并且平均分配每個執行緒占用的CPU的時間片這個也比較好理解,
java虛擬機采用搶占式調度模型,是指優先讓可運行池中優先級高的執行緒占用CPU,如果可運行池中的執行緒優先級相同,那么就隨機選擇一個執行緒,使其占用CPU,處于運行狀態的執行緒會一直運行,直至它不得不放棄CPU,
58、什么是執行緒組,為什么在Java中不推薦使用?
執行緒組和執行緒池是兩個不同的概念,他們的作用完全不同,前者是為了方便執行緒的管理,后者是為了管理執行緒的生命周期,復用執行緒,減少創建銷毀執行緒的開銷,
59、為什么使用Executor框架比使用應用創建和管理執行緒好?
為什么要使用Executor執行緒池框架 ?
1、每次執行任務創建執行緒 new Thread()比較消耗性能,創建一個執行緒是比較耗時、耗資源的,
2、呼叫 new Thread()創建的執行緒缺乏管理,被稱為野執行緒,而且可以無限制的創建,執行緒之間的相互競爭會導致過多占用系統資源而導致系統癱瘓,還有執行緒之間的頻繁交替也會消耗很多系統資源,
3、直接使用new Thread() 啟動的執行緒不利于擴展,比如定時執行、定期執行、定時定期執行、執行緒中斷等都不便實作,
使用Executor執行緒池框架的優點 :
1、能復用已存在并空閑的執行緒從而減少執行緒物件的創建從而減少了消亡執行緒的開銷,
2、可有效控制最大并發執行緒數,提高系統資源使用率,同時避免過多資源競爭,
3、框架中已經有定時、定期、單執行緒、并發數控制等功能,綜上所述使用執行緒池框架Executor能更好的管理執行緒、提供系統資源使用率,
60、java中有幾種方法可以實作一個執行緒?
-
繼承 Thread 類
-
實作 Runnable 介面
-
Callable介面和FutureTask類,需要實作的是 call() 方法
-
執行緒池創建執行緒,
61、如何停止一個正在運行的執行緒?
1. 使用共享變數的方式
在這種方式中,之所以引入共享變數,是因為該變數可以被多個執行相同任務的執行緒用來作為是否中斷的信號,通知中斷執行緒的執行,
2. 使用interrupt方法終止執行緒
如果一個執行緒由于等待某些事件的發生而被阻塞,又該怎樣停止該執行緒呢?這種情況經常會發生,比如當一個執行緒由于需要等候鍵盤輸入而被阻塞,或者呼叫Thread.join()方法,或者Thread.sleep()方法,在網路中呼叫ServerSocket.accept()方法,或者呼叫了DatagramSocket.receive()方法時,都有可能導致執行緒阻塞,使執行緒處于處于不可運行狀態時,即使主程式中將該執行緒的共享變數設定為true,但該執行緒此時根本無法檢查回圈標志,當然也就無法立即中斷,
這里我們給出的建議是,不要使用stop()方法,而是使用Thread提供的interrupt()方法,因為該方法雖然不會中斷一個正在運行的執行緒,但是它可以使一個被阻塞的執行緒拋出一個中斷例外,從而使執行緒提前結束阻塞狀態,退出堵塞代碼,
62、notify()和notifyAll()有什么區別?
當一個執行緒進入wait之后,就必須等其他執行緒notify/notifyall,使用notifyall,可以喚醒所有處于wait狀態的執行緒,使其重新進入鎖的爭奪佇列中,而notify只能喚醒一個,
如果沒把握,建議notifyAll,防止notigy因為信號丟失而造成程式例外,
63、什么是Daemon執行緒?它有什么意義?
所謂后臺(daemon)執行緒,是指在程式運行的時候在后臺提供一種通用服務的執行緒,并且這個執行緒并不屬于程式中不可或缺的部分,
因此,當所有的非后臺執行緒結束時,程式也就終止了,同時會殺死行程中的所有后臺執行緒,反過來說, 只要有任何非后臺執行緒還在運行,程式就不會終止,
必須在執行緒啟動之前呼叫setDaemon()方法,才能把它設定為后臺執行緒,注意:后臺行程在不執行finally子句的情況下就會終止其run()方法,
比如:JVM的垃圾回收執行緒就是Daemon執行緒,Finalizer也是守護執行緒,
64、java如何實作多執行緒之間的通訊和協作?
中斷 和 共享變數
65、什么是可重入鎖(ReentrantLock)?
舉例來說明鎖的可重入性
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
復制代碼
outer中呼叫了inner,outer先鎖住了lock,這樣inner就不能再獲取lock,其實呼叫outer的執行緒已經獲取了lock鎖,但是不能在inner中重復利用已經獲取的鎖資源,這種鎖即稱之為 不可重入可重入就意味著:執行緒可以進入任何一個它已經擁有的鎖所同步著的代碼塊,
synchronized、ReentrantLock都是可重入的鎖,可重入鎖相對來說簡化了并發編程的開發,
66、當一個執行緒進入某個物件的一個synchronized的實體方法后,其它執行緒是否可進入此物件的其它方法?
如果其他方法沒有synchronized的話,其他執行緒是可以進入的,
所以要開放一個執行緒安全的物件時,得保證每個方法都是執行緒安全的,
67、樂觀鎖和悲觀鎖的理解及如何實作,有哪些實作方式?
悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖,
傳統的關系型資料庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖,再比如Java里面的同步原語synchronized關鍵字的實作也是悲觀鎖,
樂觀鎖:顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制,
樂觀鎖適用于多讀的應用型別,這樣可以提高吞吐量,像資料庫提供的類似于write_condition機制,其實都是提供的樂觀鎖,
在Java中java.util.concurrent.atomic包下面的原子變數類就是使用了樂觀鎖的一種實作方式CAS實作的,
樂觀鎖的實作方式:
1、使用版本標識來確定讀到的資料與提交時的資料是否一致,提交后修改版本標識,不一致時可以采取丟棄和再次嘗試的策略,
2、java中的Compare and Swap即CAS ,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試, CAS 操作中包含三個運算元 —— 需要讀寫的記憶體位置(V)、進行比較的預期原值(A)和擬寫入的新值(B),如果記憶體位置V的值與預期原值A相匹配,那么處理器會自動將該位置值更新為新值B,否則處理器不做任何操作,
CAS缺點:
-
ABA問題:比如說一個執行緒one從記憶體位置V中取出A,這時候另一個執行緒two也從記憶體中取出A,并且two進行了一些操作變成了B,然后two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然后one操作成功,盡管執行緒one的CAS操作成功,但可能存在潛藏的問題,從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題,
-
回圈時間長開銷大:對于資源競爭嚴重(執行緒沖突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低于synchronized,
-
只能保證一個共享變數的原子操作:當對一個共享變數執行操作時,我們可以使用回圈CAS的方式來保證原子操作,但是對多個共享變數操作時,回圈CAS就無法保證操作的原子性,這個時候就可以用鎖,
68、SynchronizedMap和ConcurrentHashMap有什么區別?
SynchronizedMap一次鎖住整張表來保證執行緒安全,所以每次只能有一個執行緒來訪為map,ConcurrentHashMap使用分段鎖來保證在多執行緒下的性能,
ConcurrentHashMap中則是一次鎖住一個桶,ConcurrentHashMap默認將hash表分為16個桶,諸如get,put,remove等常用操作只鎖當前需要用到的桶,這樣,原來只能一個執行緒進入,現在卻能同時有16個寫執行緒執行,并發性能的提升是顯而易見的,
另外ConcurrentHashMap使用了一種不同的迭代方式,在這種迭代方式中,當iterator被創建后集合再發生改變就不再是拋出
ConcurrentModificationException,取而代之的是在改變時new新的資料從而不影響原有的資料 ,iterator完成后再將頭指標替換為新的資料 ,這樣iterator執行緒可以使用原來老的資料,而寫執行緒也可以并發的完成改變,
69、CopyOnWriteArrayList可以用于什么應用場景?
CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個串列時,不會拋出ConcurrentModificationException,在CopyOnWriteArrayList中,寫入將導致創建整個底層陣列的副本,而源陣列將保留在原地,使得復制的陣列在被修改時,讀取操作可以安全地執行,
1、由于寫操作的時候,需要拷貝陣列,會消耗記憶體,如果原陣列的內容比較多的情況下,可能導致young gc或者full gc;
2、不能用于實時讀的場景,像拷貝陣列、新增元素都需要時間,所以呼叫一個set操作后,讀取到資料可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實時性要求;
CopyOnWriteArrayList透露的思想
1、讀寫分離,讀和寫分開
2、最終一致性
3、使用另外開辟空間的思路,來解決并發沖突
70、什么叫執行緒安全?servlet是執行緒安全嗎?
執行緒安全是編程中的術語,指某個函式、函式庫在多執行緒環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變數,使程式功能正確完成,
Servlet不是執行緒安全的,servlet是單實體多執行緒的,當多個執行緒同時訪問同一個方法,是不能保證共享變數的執行緒安全性的,
Struts2的action是多實體多執行緒的,是執行緒安全的,每個請求過來都會new一個新的action分配給這個請求,請求完成后銷毀,
SpringMVC的Controller是執行緒安全的嗎?不是的,和Servlet類似的處理流程,
Struts2好處是不用考慮執行緒安全問題;Servlet和SpringMVC需要考慮執行緒安全問題,但是性能可以提升不用處理太多的gc,可以使用ThreadLocal來處理多執行緒的問題,
71、volatile有什么用?能否用一句話說明下volatile的應用場景?
volatile保證記憶體可見性和禁止指令重排,
volatile用于多執行緒環境下的單次操作(單次讀或者單次寫),
72、為什么代碼會重排序?
在執行程式時,為了提供性能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎么排序就怎么排序,它需要滿足以下兩個條件:
在單執行緒環境下不能改變程式運行的結果;存在資料依賴關系的不允許重排序需要注意的是:重排序不會影響單執行緒環境的執行結果,但是會破壞多執行緒的執行語意,
73、在java中wait和sleep方法的不同?
最大的不同是在等待時wait會釋放鎖,而sleep一直持有鎖,Wait通常被用于執行緒間互動,sleep通常被用于暫停執行,
直接了解的深入一點吧, 在Java中執行緒的狀態一共被分成6種:
(1)初始態:NEW
創建一個Thread物件,但還未呼叫start()啟動執行緒時,執行緒處于初始態,
(2)運行態:RUNNABLE
在Java中,運行態包括就緒態 和 運行態,就緒態 該狀態下的執行緒已經獲得執行所需的所有資源,只要CPU分配執行權就能運行,所有就緒態的執行緒存放在就緒佇列中,
運行態 獲得CPU執行權,正在執行的執行緒,由于一個CPU同一時刻只能執行一條執行緒,因此每個CPU每個時刻只有一條運行態的執行緒,
(3)阻塞態
當一條正在執行的執行緒請求某一資源失敗時,就會進入阻塞態,而在Java中,阻塞態專指請求鎖失敗時進入的狀態,由一個阻塞佇列存放所有阻塞態的執行緒,處于阻塞態的執行緒會不斷請求資源,一旦請求成功,就會進入就緒佇列,等待執行,PS:鎖、IO、Socket等都資源,
(4)等待態
當前執行緒中呼叫wait、join、park函式時,當前執行緒就會進入等待態,也有一個等待佇列存放所有等待態的執行緒,執行緒處于等待態表示它需要等待其他執行緒的指示才能繼續運行,進入等待態的執行緒會釋放CPU執行權,并釋放資源(如:鎖)
(5)超時等待態
當運行中的執行緒呼叫sleep(time)、wait、join、parkNanos、parkUntil時,就會進入該狀態;它和等待態一樣,并不是因為請求不到資源,而是主動進入,并且進入后需要其他執行緒喚醒;進入該狀態后釋放CPU執行權 和 占有的資源,與等待態的區別:到了超時時間后自動進入阻塞佇列,開始競爭鎖,
(6)終止態
執行緒執行結束后的狀態,
注意:
-
wait()方法會釋放CPU執行權 和 占有的鎖,
-
sleep(long)方法僅釋放CPU使用權,鎖仍然占用;執行緒被放入超時等待佇列,與yield相比,它會使執行緒較長時間得不到運行,
-
yield()方法僅釋放CPU執行權,鎖仍然占用,執行緒會被放入就緒佇列,會在短時間內再次執行,
-
wait和notify必須配套使用,即必須使用同一把鎖呼叫;
-
wait和notify必須放在一個同步塊中呼叫wait和notify的物件必須是他們所處同步塊的鎖物件,
74、為什么wait和notify方法要在同步塊中呼叫?
Java API強制要求這樣做,如果你不這么做,你的代碼會拋出IllegalMonitorStateException例外,還有一個原因是為了避免wait和notify之間產生競態條件,
75、為什么你應該在回圈中檢查等待條件?
處于等待狀態的執行緒可能會收到錯誤警報和偽喚醒,如果不在回圈中檢查等待條件,程式就會在沒有滿足結束條件的情況下退出,
76、Java中的同步集合與并發集合有什么區別?
同步集合與并發集合都為多執行緒和并發提供了合適的執行緒安全的集合,不過并發集合的可擴展性更高,在Java1.5之前程式員們只有同步集合來用且在多執行緒并發的時候會導致爭用,阻礙了系統的擴展性,Java5介紹了并發集合像ConcurrentHashMap,不僅提供執行緒安全還用鎖分離和內部磁區等現代技術提高了可擴展性,
77、什么是執行緒池?為什么要使用它?
創建執行緒要花費昂貴的資源和時間,如果任務來了才創建執行緒那么回應時間會變長,而且一個行程能創建的執行緒數有限,
為了避免這些問題,在程式啟動的時候就創建若干執行緒來回應處理,它們被稱為執行緒池,里面的執行緒叫作業執行緒,從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的執行緒池,
78、怎么檢測一個執行緒是否擁有鎖?
在java.lang.Thread中有一個方法叫holdsLock(),它回傳true如果當且僅當當前執行緒擁有某個具體物件的鎖,
79、你如何在Java中獲取執行緒堆疊?
kill -3 [java pid]不會在當前終端輸出,它會輸出到代碼執行的或指定的地方去,比如,kill -3 tomcat pid, 輸出堆疊到log目錄下,Jstack [java pid]這個比較簡單,在當前終端顯示,也可以重定向到指定檔案中,-JvisualVM:Thread Dump不做說明,打開JvisualVM后,都是界面操作,程序還是很簡單的,
80、JVM中哪個引數是用來控制執行緒的堆疊堆疊小的?
-Xss 每個執行緒的堆疊大小
81、Thread類中的yield方法有什么作用?
使當前執行緒從執行狀態(運行狀態)變為可執行態(就緒狀態),
當前執行緒到了就緒狀態,那么接下來哪個執行緒會從就緒狀態變成執行狀態呢?可能是當前執行緒,也可能是其他執行緒,看系統的分配了,
82、Java中ConcurrentHashMap的并發度是什么?
ConcurrentHashMap把實際map劃分成若干部分來實作它的可擴展性和執行緒安全,這種劃分是使用并發度獲得的,它是ConcurrentHashMap類建構式的一個可選引數,默認值為16,這樣在多執行緒情況下就能避免爭用,
在JDK8后,它摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實作,利用CAS演算法,同時加入了更多的輔助變數來提高并發度,具體內容還是查看原始碼吧,
83、Java中Semaphore是什么?
Java中的Semaphore是一種新的同步類,它是一個計數信號,從概念上講,從概念上講,信號量維護了一個許可集合,如有必要,在許可可用前會阻塞每一個 acquire(),然后再獲取該許可,
每個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者,但是,不使用實際的許可物件,Semaphore只對可用許可的號碼進行計數,并采取相應的行動,信號量常常用于多執行緒的代碼中,比如資料庫連接池,
84、Java執行緒池中submit() 和 execute()方法有什么區別?
兩個方法都可以向執行緒池提交任務,execute()方法的回傳型別是void,它定義在Executor介面中,
而submit()方法可以回傳持有計算結果的Future物件,它定義在ExecutorService介面中,它擴展了Executor介面,其它執行緒池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法,
85、什么是阻塞式方法?
阻塞式方法是指程式會一直等待該方法完成期間不做其他事情,ServerSocket的accept()方法就是一直等待客戶端連接,這里的阻塞是指呼叫結果回傳之前,當前執行緒會被掛起,直到得到結果之后才會回傳,此外,還有異步和非阻塞式方法在任務完成前就回傳,
86、Java中的ReadWriteLock是什么?
讀寫鎖是用來提升并發程式性能的鎖分離技術的成果,
87、volatile 變數和 atomic 變數有什么不同?
Volatile變數可以確保先行關系,即寫操作會發生在后續的讀操作之前, 但它并不能保證原子性,例如用volatile修飾count變數那么 count++ 操作就不是原子性的,
而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一,其它資料型別和參考變數也可以進行相似操作,
88、可以直接呼叫Thread類的run ()方法么?
當然可以,但是如果我們呼叫了Thread的run()方法,它的行為就會和普通的方法一樣,會在當前執行緒中執行,為了在新的執行緒中執行我們的代碼,必須使用Thread.start()方法,
89、如何讓正在運行的執行緒暫停一段時間?
我們可以使用Thread類的Sleep()方法讓執行緒暫停一段時間,需要注意的是,這并不會讓執行緒終止,一旦從休眠中喚醒執行緒,執行緒的狀態將會被改變為Runnable,并且根據執行緒調度,它將得到執行,
90、你對執行緒優先級的理解是什么?
每一個執行緒都是有優先級的,一般來說,高優先級的執行緒在運行時會具有優先權,但這依賴于執行緒調度的實作,這個實作是和作業系統相關的(OS dependent),
我們可以定義執行緒的優先級,但是這并不能保證高優先級的執行緒會在低優先級的執行緒前執行,執行緒優先級是一個int變數(從1-10),1代表最低優先級,10代表最高優先級,
java的執行緒優先級調度會委托給作業系統去處理,所以與具體的作業系統優先級有關,如非特別需要,一般無需設定執行緒優先級,
91、什么是執行緒調度器(Thread Scheduler)和時間分片(Time Slicing )?
執行緒調度器是一個作業系統服務,它負責為Runnable狀態的執行緒分配CPU時間,一旦我們創建一個執行緒并啟動它,它的執行便依賴于執行緒調度器的實作,同上一個問題,執行緒調度并不受到Java虛擬機控制,所以由應用程式來控制它是更好的選擇(也就是說不要讓你的程式依賴于執行緒的優先級),
時間分片是指將可用的CPU時間分配給可用的Runnable執行緒的程序,分配CPU時間可以基于執行緒優先級或者執行緒等待的時間,
92、你如何確保main()方法所在的執行緒是Java 程式最后結束的執行緒?
我們可以使用Thread類的join()方法來確保所有程式創建的執行緒在main()方法退出前結束,
93、執行緒之間是如何通信的?
當執行緒間是可以共享資源時,執行緒間通信是協調它們的重要的手段,Object類中wait()\notify()\notifyAll()方法可以用于執行緒間通信關于資源的鎖的狀態,
94、為什么執行緒通信的方法wait(), notify()和notifyAll()被定義在Object 類里?
Java的每個物件中都有一個鎖(monitor,也可以成為監視器) 并且wait(),notify()等方法用于等待物件的鎖或者通知其他執行緒物件的監視器可用,
在Java的執行緒中并沒有可供任何物件使用的鎖和同步器,這就是為什么這些方法是Object類的一部分,這樣Java的每一個類都有用于執行緒間通信的基本方法,
95、為什么wait(), notify()和notifyAll ()必須在同步方法或者同步塊中被呼叫?
當一個執行緒需要呼叫物件的wait()方法的時候,這個執行緒必須擁有該物件的鎖,接著它就會釋放這個物件鎖并進入等待狀態直到其他執行緒呼叫這個物件上的notify()方法,
同樣的,當一個執行緒需要呼叫物件的notify()方法時,它會釋放這個物件的鎖,以便其他在等待的執行緒就可以得到這個物件鎖,由于所有的這些方法都需要執行緒持有物件的鎖,這樣就只能通過同步來實作,所以他們只能在同步方法或者同步塊中被呼叫,
96、為什么Thread類的sleep()和yield ()方法是靜態的?
Thread類的sleep()和yield()方法將在當前正在執行的執行緒上運行,所以在其他處于等待狀態的執行緒上呼叫這些方法是沒有意義的,這就是為什么這些方法是靜態的,它們可以在當前正在執行的執行緒中作業,并避免程式員錯誤的認為可以在其他非運行執行緒呼叫這些方法,
97、如何確保執行緒安全?
在Java中可以有很多方法來保證執行緒安全——同步,使用原子類(atomic concurrent classes),實作并發鎖,使用volatile關鍵字,使用不變類和執行緒安全類,
98、同步方法和同步塊,哪個是更好的選擇?
同步塊是更好的選擇,因為它不會鎖住整個物件(當然你也可以讓它鎖住整個物件),同步方法會鎖住整個物件,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行并需要等待獲得這個物件上的鎖,
同步塊更要符合開放呼叫的原則,只在需要鎖住的代碼塊鎖住相應的物件,這樣從側面來說也可以避免死鎖,
99、如何創建守護執行緒?
使用Thread類的setDaemon(true)方法可以將執行緒設定為守護執行緒,需要注意的是,需要在呼叫start()方法前呼叫這個方法,否則會拋出IllegalThreadStateException例外,
100、什么是Java Timer 類?如何創建一個有特定時間間隔的任務?
java.util.Timer是一個工具類,可以用于安排一個執行緒在未來的某個特定時間執行,Timer類可以用安排一次性任務或者周期任務,
java.util.TimerTask是一個實作了Runnable介面的抽象類,我們需要去繼承這個類來創建我們自己的定時任務并使用Timer去安排它的執行,目前有開源的Qurtz可以用來創建定時任務,
總結:
所有的面試題目都不是一成不變的,上面的面試題只是給大家一個借鑒作用,最主要的是給自己增加知識的儲備,有備無患,整理了一份Java面試寶典完整版PDF
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/248852.html
標籤:Java
上一篇:Spring Boot Security 國際化 多語言 i18n 趟過巨坑
下一篇:JDK1.7-HashMap原理
