Java多線程問題(上)

一、多線程有什麼用?

一個可能在不少人看來很扯淡的一個問題:我會用多線程就行了,還管它有什麼用?在我看來,這個回答更扯淡。所謂」知其然知其因此然」,」會用」只是」知其然」,」爲何用」纔是」知其因此然」,只有達到」知其然知其因此然」的程度才能夠說是把一個知識點運用自如。OK,下面說說我對這個問題的見解:java

(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,分別創建程序模型,並經過多線程分別運行這幾個任務,那就簡單不少了。服務器

二、建立線程的方式

比較常見的一個問題了,通常就是兩種:多線程

(1)繼承Thread類併發

(2)實現Runnable接口異步

可是二者究竟孰優孰劣呢-》咱們都知道在Java中是不支持多繼承的,這就意味着一個類只能右一個「爸爸」,可是Java的類卻能夠實現N個接口,只要實現這些接口裏面的方法便可。面向對象的編程的一個設計模式就是面向接口編程,因此咱們通常更提倡第二種方法。可是若是要說第三種方法,Java也是有的。Java中有線程池的概念,也能夠藉助線程池來建立相關的線程,相關內容不作太多論述。

三、start()方法和run()方法的區別

只有調用了start()方法,纔會表現出多線程的特性,不一樣線程的run()方法裏面的代碼交替執行。若是隻是調用run()方法,那麼代碼仍是同步執行的,必須等待一個線程的run()方法裏面的代碼所有執行完畢以後,另一個線程才能夠執行其run()方法裏面的代碼,也就是說,若是單獨調用run()方法的話,和類的其餘方法並無太大區別,並不會建立一個線程用來專門執行。

四、Runnable接口和Callable接口的區別

有點深的問題了,也看出一個Java程序員學習知識的廣度。

Runnable接口中的run()方法的返回值是void,它作的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果。

這實際上是頗有用的一個特性,由於多線程相比單線程更難、更復雜的一個重要緣由就是由於多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候咱們指望的數據是否已經賦值完畢?沒法得知,咱們能作的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻能夠獲取多線程運行的結果,能夠在等待時間太長沒獲取到須要的數據的狀況下取消該線程的任務,真的是很是有用。

五、CyclicBarrier和CountDownLatch的區別

兩個看上去有點像的類,都在java.util.concurrent下,均可以用來表示代碼運行到某個點上,兩者的區別在於:

(1)CyclicBarrier的某個線程運行到某個點上以後,該線程即中止運行,直到全部的線程都到達了這個點,全部線程才從新運行;CountDownLatch則不是,某線程運行到某個點上以後,只是給某個數值-1而已,該線程繼續運行

(2)CyclicBarrier只能喚起一個任務,CountDownLatch能夠喚起多個任務

(3)CyclicBarrier可重用,CountDownLatch不可重用,計數值爲0該CountDownLatch就不可再用了

六、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。

七、什麼是線程安全

又是一個理論的問題,各式各樣的答案有不少,我給出一個我的認爲解釋地最好的:若是你的代碼在多線程下執行和在單線程下執行永遠都能得到同樣的結果,那麼你的代碼就是線程安全的

這個問題有值得一提的地方,就是線程安全也是有幾個級別的:

(1)不可變

像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新建立一個,所以這些不可變對象不須要任何同步手段就能夠直接在多線程環境下使用

(2)絕對線程安全

無論運行時環境如何,調用者都不須要額外的同步措施。要作到這一點一般須要付出許多額外的代價,Java中標註本身是線程安全的類,實際上絕大多數都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet

(3)相對線程安全

相對線程安全也就是咱們一般意義上所說的線程安全,像Vector這種,add、remove方法都是原子操做,不會被打斷,但也僅限於此,若是有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的狀況下都會出現ConcurrentModificationException,也就是fail-fast機制,這種狀況也會出如今當你使用Iterator遍歷時候出現。

(4)線程非安全

這個就沒什麼好說的了,ArrayList、LinkedList、HashMap等都是線程非安全的類

八、Java中如何獲取到線程dump文件

死循環、死鎖、阻塞、頁面打開慢等問題,打線程dump是最好的解決問題的途徑。所謂線程dump也就是線程堆棧,獲取到線程堆棧有兩步:

(1)獲取到線程的pid,能夠經過使用jps命令,在Linux環境下還可使用ps -ef | grep java

(2)打印線程堆棧,能夠經過使用jstack pid命令,在Linux環境下還可使用kill -3 pid

另外提一點,Thread類提供了一個getStackTrace()方法也能夠用於獲取線程堆棧。這是一個實例方法,所以此方法是和具體線程實例綁定的,每次獲取獲取到的是具體某個線程當前運行的堆棧,

九、一個線程若是出現了運行時異常會怎麼樣

若是這個異常沒有被捕獲的話,這個線程就中止執行了。另外重要的一點是:若是這個線程持有某個某個對象的監視器,那麼這個對象監視器會被當即釋放

十、如何在兩個線程之間共享數據

經過在線程之間共享對象就能夠了,而後經過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞隊列BlockingQueue就是爲線程之間共享數據而設計的。這裏也就引出了生產者消費者模型的兩種編寫方法,一個是wait/nodify,另一個是BlockingQueue。

十一、sleep方法和wait方法有什麼區別

1.sleep方法和wait方法均可以用來放棄CPU必定的時間,不一樣點在於若是線程持有某個對象的監視器,sleep方法不會放棄這個對象的監視器,wait方法會放棄這個對象的監視器

2.sleep方法是在Thread上的,而wait方法是在Object上的,再調用wait方法時,線程必定要持有相關的鎖,也就是說必定要在synchronized塊內執行。

十二、生產者消費者模型的做用是什麼

這個問題很理論,可是很重要:

(1)經過平衡生產者的生產能力和消費者的消費能力來提高整個系統的運行效率,這是生產者消費者模型最重要的做用

(2)解耦,這是生產者消費者模型附帶的做用,解耦意味着生產者和消費者之間的聯繫少,聯繫越少越能夠獨自發展而不須要收到相互的制約,讓我想到了消息隊列,可是不清楚具體應該不該該這麼講。

1三、ThreadLocal有什麼用

簡單說ThreadLocal就是一種以空間換時間的作法,在每一個Thread裏面維護了一個以開地址法實現的ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,天然就沒有線程安全方面的問題了。其實內部的Map是一個以Thread爲鍵的一個特殊map,由於每一個線程都有獨特的實例,因此天然後不會有數據衝突問題,也就間接實現了數據隔離。

1四、爲何wait()方法和notify()/notifyAll()方法要在同步塊中被調用

這是JDK強制的,wait()方法和notify()/notifyAll()方法在調用前都必須先得到對象的鎖。咱們能夠逆思惟來考慮這個問題,假設不要求這些方法都方法同步塊的話,也就是說再調用的時候線程並無持有鎖對象,那麼問題來了,既然沒有持有鎖對象,調用wait()方法的話線程要如何在一個鎖對象上等待並釋放鎖,由於明明就沒有得到鎖對象,何談釋放呢。

wait()的含義:在一個鎖(任意Object)上等待,而且釋放該鎖的全部權。

nodify()含義:喚醒一個鎖對象上調用wait方法致使等待的線程,與wait方法呼應。

1五、wait()方法和notify()/notifyAll()方法在放棄對象監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄對象監視器的時候的區別在於:wait()方法當即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器

1六、爲何要使用線程池

這裏就涉及到一個線程池的設計思想。線程在使用的時候,有很大一部分開銷就是用來建立和銷燬線程,並且有的線程執行的是短任務,這就意味着線程建立和銷燬的代價太大。咱們能夠類比數據鏈接池的思想,像c3p0,dbcp之類的數據鏈接池都在作着相似的工做,那就是幫咱們維護連接/線程。歸根到底就是兩個字:複用

1七、怎麼檢測一個線程是否持有對象監視器

我也是在網上看到一道多線程面試題才知道有方法能夠判斷某個線程是否持有對象監視器:Thread類提供了一個holdsLock(Object obj)方法,當且僅當對象obj的監視器被某條線程持有的時候纔會返回true,注意這是一個static方法,這意味着「某條線程」指的是當前線程

1八、synchronized和ReentrantLock的區別

synchronized是和if、else、for、while同樣的關鍵字,ReentrantLock是類,這是兩者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量,ReentrantLock比synchronized的擴展性體如今幾點上:

(1)ReentrantLock能夠對獲取鎖的等待時間進行設置,這樣就避免了死鎖

(2)ReentrantLock能夠獲取各類鎖的信息

(3)ReentrantLock能夠靈活地實現多路通知

 ( 4 ) Synchronized的使用要比ReetrantLock更簡單一些,並且不用咱們管理鎖,可是ReetrantLock要在使用完以後釋放鎖對象,通常在finally塊中調用release方法。

另外,兩者的鎖機制其實也是不同的。至於這個不一樣點,我會在不一樣的博客裏面詳細介紹。

1九、ConcurrentHashMap的併發度是什麼

ConcurrentHashMap的併發度就是segment的大小,默認爲16,這意味着最多同時能夠有16條線程操做ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優點,任何狀況下,Hashtable能同時有兩條線程獲取Hashtable中的數據嗎?答案是NO!

20、ReadWriteLock是什麼

首先明確一下,不是說ReentrantLock很差,只是ReentrantLock某些時候有侷限。若是使用ReentrantLock,可能自己是爲了防止線程A在寫數據、線程B在讀數據形成的數據不一致,但這樣,若是線程C在讀數據、線程D也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,可是仍是加鎖了,下降了程序的性能。

由於這個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖接口,ReentrantReadWriteLock是ReadWriteLock接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提高了讀寫的性能

相關文章
相關標籤/搜索