線程基本知識
什么是線程安全性?
當多個線程訪問某個類時,這個類始終都能表現出正確的行為,那么可以認為這個類是線程安全的。
在線程安全的類中封裝了必要的同步機制,因此使用它時無需再考慮同步什么是線程?和進程有什么區別?
線程是操作系統能夠進行運算調度的最小單元,被包含在進程中,是進程的實際運行單元。
線程是進程的子集,不同的進程使用不同的內存空間,而所有線程共享相同內存空間。線程中的狀態有哪些?
新建(new) 可運行(runnable) 運行(running) 阻塞(block) 死亡(dead)多線程有什么用?
- 發揮多核CPU的優勢
- 單核應用多線程,防止阻塞
- 便于建模
Java中如何使用線程?不同場景如何使用?
Thread類實例就是一個線程,但是它需要調用Runnable接口來執行線程任務,因此使用線程可以繼承Thread類或者實現Runnable接口來實現線程。如果已經繼承了其他類,那么只能選擇實現Runnable接口。
Thread類內的start方法用于啟動新線程,其內部調用了run方法,但是如果直接調用run方法,只會在原線程中調用,而沒有新線程啟動。多次start一個線程會怎么樣?為什么?
start方法會真正開啟一個線程,threadStatus從NEW變為RUNNABLE,start之前會檢查threadStatus是否為NEW,否則所有調用會出錯illeaglthreadstateexception。線程運行時發生異常會怎么樣?如何停止一個運行的線程?
如果異常沒有被捕獲則線程停止,Thread.UncaughtExceptionHandler是用于處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler并將線程和異常作為參數傳遞給handler的uncaughtException()方法進行處理。在Java中,線程方法的異常(無論是checked還是unchecked exception),都應該在線程代碼邊界之內(run方法內)進行try catch并處理掉.換句話說,我們不能捕獲從線程中逃逸的異常。
目前沒有一個兼容且線程安全的方法來停止線程,當run或者call方法執行完畢后線程會自動結束,如果需要手動結束線程,可以用volatile變量來退出run或者取消任務來中斷線程。線程類的構造方法、靜態塊是被哪個線程調用的?
線程類的構造方法、靜態塊是被new這個線程類所在的線程所調用的,而run方法里面的代碼才是被線程自身所調用的。
線程使用
Runnable和Callable有什么不同?
Callable中的call方法可以返回值和拋出異常,還可以返回裝載有計算結果的Future對象,而Runnable則沒有這些功能。Java中CyclicBarrier 和 CountDownLatch有什么不同?
CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。CountDownLatch一般用于某個線程A等待若干個其他線程執行完任務之后,它才執行;
而CyclicBarrier一般用于一組線程互相等待至某個狀態,然后這一組線程再同時執行;
另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。
線程同步
什么是競態條件?
競態條件是指由于不恰當的執行時序而出現不正確的結果的情況。
多線程對一些資源的競爭會產生競態條件,因為線程間的隨機競爭會導致程序在并發下出現問題。
引發競態條件的原因一般有兩種:操作非原子性,先檢查后執行;
對于基本類型的增減操作,是讀取-修改-寫入的三個獨立操作的序列,其結果依賴之前的狀態。
先檢查后執行,在檢查和執行之間,原來的觀察結果可能變得無效,從而導致問題。什么是原子操作?
原子操作是不可分割的,在執行完畢之前不會被任何其它任務或事件中斷。如何在兩個線程間共享數據?
通過共享對象或者是使用像阻塞隊列這樣并發的數據結構Thread類中的yield方法有什么作用?
Yield方法可以暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法而且只保證當前線程放棄CPU占用,而不能保證使其它線程一定能占用CPU,執行yield()的線程有可能在進入到暫停
狀態后馬上又被執行。Java多線程中調用wait()和 sleep()方法有什么不同?
wait()方法用于線程間通信,如果等待條件為真且其它線程被喚醒時,它會釋放鎖,而sleep()方法僅僅釋放CPU資源或者讓當前線程停止執行一段時間,但不會釋放鎖。Java中notify 和 notifyAll有什么區別?為什么這些方法不在thread類里面?為什么要在同步塊中調用?
鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由于這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因為wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。如果另外的一個線程調用了相同對象的notifyAll()方法,那么處于該對象的等待池中的線程就會全部進入該對象的鎖池中,準備爭奪鎖的擁有權。如果另外的一個線程調用了相同對象的notify()方法,那么僅僅有一個處于該對象的等待池中的線程(隨機)會進入該對象的鎖池.
JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。
為了避免wait和notify之間產生競態條件Java中interrupted 和 isInterrupted方法的區別?
interrupted()和isInterrupted()的主要區別是前者會將中斷狀態清除而后者不會。多線程中的忙循環是什么?
忙循環就是用循環讓一個線程等待,不像傳統方法wait(), sleep()或
yield()它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這么做的目的是為了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可能會在另一個內核運行,這樣會重建緩存。為了避免重建緩存和減少等待重建的時間就可以使用它了。為什么應該在循環中檢查等待條件?
處于等待狀態的線程可能會收到錯誤警報和偽喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。因此,當一個等待線程醒來時,不能認為它原來的等待狀態仍然是有效的,在notify
()方法調用之后和等待線程醒來之前這段時間等待狀態可能會改變。如何避免死鎖?死鎖和活鎖的區別?
死鎖滿足以下條件:
- 互斥條件:一個資源每次只能被一個進程使用。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標志位、排序,規定所有的進程申請資源必須以一定的順序(升序或降序)等操作來避免死鎖。
處于活鎖的線程或進程的狀態是不斷改變的,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
synchronized鎖普通方法和鎖靜態方法有什么異同?
靜態方法上的鎖是鎖住這個類的,普通方法上的鎖是鎖住這個對象的。類鎖和對象鎖不同,他們之間不會產生互斥。Java中synchronized 和 ReentrantLock 有什么不同?
- ReentrantLock 擁有Synchronized相同的并發性和內存語義,此外還多了公平鎖,定時鎖等候和中斷鎖等
- synchronized是在JVM層面上實現的,在代碼執行時出現異常,JVM會自動釋放鎖定,但是lock是通過代碼實現解鎖的
Java中的ReadWriteLock是什么?
一個ReadWriteLock維護一對關聯的鎖,一個用于只讀操作一個用于寫。在沒有寫線程 的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨占的。實現了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能。什么是自旋鎖?
自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環的方式去嘗試獲取鎖,如果做了多次忙循環發現還沒有獲得鎖,再阻塞。這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。
Java中的同步集合和并發集合的區別?
同步集合和并發集合都為并發和多線程提供了合適的線程安全集合,不過并發集合的可擴展性更高。1.5并發集合更像ConcurrentHashMap,線程安全提供了鎖分離,內部分區,CAS算法等技術。什么是ThreadLocal變量?
每個線程都有一個ThreadLocal就是每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了。
首先,通過復用減少了代價高昂的對象的創建個數。
其次,在沒有使用高代價的同步或者不變性的情況下獲得了線程安全。volatile 變量和 atomic 變量有什么不同?
Volatile變量可以確保先行關系,即寫操作會發生在后續的讀操作之前
, 但它并不能保證原子性。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把當前值加一。什么是不可變對象,它對寫并發應用有什么幫助?如何創建不可變對象?
Immutable對象保證了對象的內存可見性。可以在沒有同步的情況下共享,降低了對該對象進行并發訪問時的同步化開銷。創建不可變對象步驟:
- 將所有的成員聲明為私有的
- 通過構造方法初始化所有成員
- 對變量不要提供setter方法
- 在getter方法中,返回對象的拷貝。
- 什么是CAS? 什么是AQS?
- CAS,全稱為Compare and Swap,即比較-替換,是java.util.concurrent的基礎。假設有三個操作數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,才會將內存值修改為B并返回true,否則什么都不做并返回false。當然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,否則舊的預期值A對某條線程來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功。
- AQS,全稱為AbstractQueuedSychronizer(抽象隊列同步器),是整個Java并發包的核心,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS實際上以雙向隊列的形式連接所有的Entry,比方說ReentrantLock,所有等待的線程都被放在一個Entry中并連成雙向隊列,前面一個線程使用ReentrantLock好了,則雙向隊列實際上的第一個Entry開始運行。AQS定義了對雙向隊列所有的操作,而只開放了tryLock和tryRelease方法給開發者使用,開發者可以根據自己的實現重寫tryLock和tryRelease方法,以實現自己的并發功能。
線程池
- 什么是線程池? 為什么要使用它?
假設一個服務器完成一項任務所需時間為:T1 創建線程時間,T2 在線程中執行任務的時間,T3 銷毀線程時間。如果:T1 + T3 遠大于 T2,則可以采用線程池,以提高服務器性能。
一個線程池包括以下四個基本組成部分:
1、線程池管理器(ThreadPool):用于創建并管理線程池,包括 創建線程池,銷毀線程池,添加新任務;
2、工作線程(PoolWorker):線程池中線程,在沒有任務時處于等待狀態,可以循環的執行任務;
3、任務接口(Task):每個任務必須實現的接口,以供工作線程調度任務的執行,它主要規定了任務的入口,任務執行完后的收尾工作,任務的執行狀態等;
4、任務隊列(taskQueue):用于存放沒有處理的任務。提供一種緩沖機制。
線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者一些空閑的時間段。線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目
- 常用的線程池有幾種?這幾種線程池之間有什么區別和聯系?線程池的實現原理是怎么樣的?
通常使用Executors 獲取線程池,常用的線程池有以下幾種:
(1)CachedThreadPool :
- 按需創建新的線程,如果沒有可用線程則創建新的線程,之前用過的線程可能會再次被使用;
- 因為空閑線程會被移除線程池,因此,如果線程池長時間不被使用也不會消耗系統資源
(2)FixedThreadPool :
- 在任何情況下最多只有nThread個線程工作,多余的Task將會被存放到隊列中等待;
- 如果線程在執行任務中被終止,終止之前會創建其他的線程代替原來的;
- 線程將會一直存在在線程池中,直到調用shutDown()方法
(3)ScheduledThreadPool :
- 核心線程數將會一直存在線程池中,除非設置了allowCoreThreadTimeOut
- 可以設置線程的執行時間
(4)SingleThreadExecutor:
- 可保證順序地執行各個任務
- 任意給定的時間不會有多個線程是活動的 。
實現原理
以上線程池都是是調用了ThreadPoolExecutor這個類的構造,也就是說當我們得到的ExecutorService實際是ThreadPoolExecutor的實例。
- 高并發、任務執行時間短的業務怎樣使用線程池?并發不高、任務執行時間長的業務怎樣使用線程池?并發高、業務執行時間長的業務怎樣使用線程池?
- 高并發、任務執行時間短的業務,線程池線程數可以設置為CPU核數+1,減少線程上下文的切換
- 并發不高、任務執行時間長的業務要區分開看:
a. IO密集型任務: 因為IO操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大線程池中的線程數目,讓CPU處理更多的業務
b. CPU密集型任務:這個就沒辦法了,和1一樣吧,線程池中的線程數設置得少一些,減少線程上下文的切換 - 并發高、業務執行時間長:解決這種類型任務的關鍵不在于線程池而在于整體架構的設計,第一步業務里數據做緩存,第二步增加服務器,最后引入中間件,對任務進行拆分和解耦
Java線程池中submit()和 execute()方法有什么區別?
execute()方法的返回類型是void,它定義在Executor接口中,
而submit()方法可以返回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴展了Executor接口什么是阻塞式方法?
阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,這里的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之后才會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。什么是FutureTask?
FutureTask表示一個可以取消的異步運算。它有啟動和取消運算、查詢運算是否完成和取回運算結果等方法。
Future: 只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞。
Runnable:可以對調用了Callable和Runnable的對象進行包裝,由于FutureTask也是調用了Runnable接口,所以它可以提交給Executor來執行。提交任務時線程池隊列已滿,此時會發生什么?
Java線程池會將提交的任務先置于工作隊列中,在從工作隊列中獲取。那么工作隊列就有兩種實現策略:無界隊列和有界隊列。無界隊列不存在飽和的問題,但是其問題是當請求持續高負載的話,任務會無腦的加入工作隊列,那么很可能導致內存等資源溢出或者耗盡。而有界隊列不會帶來高負載導致的內存耗盡的問題,但是有引發工作隊列已滿情況下,線程池按工作隊列飽和策略處理這種情況。
飽和策略分為:Abort 策略(默認), CallerRuns 策略,Discard策略,DiscardOlds策略。Java中的fork join框架是什么?
fork join框架是專門為了那些可以遞歸劃分成許多子模塊設計的,目的是將所有可用的處理能力用來提升程序的性能。優勢是它使用了工作竊取算法,可以完成更多任務的工作線程可以從其它線程中竊取任務來執行。
JVM中的線程
- Java內存模型是什么?
Java內存模型規定Java程序在不同的內存架構,CPU和操作系統間有確定性行為。這為一個內存所做的變動能被其他線程可見提供了保證,它們之間是先行發生關系,確保了:
- 線程內的代碼按先后順序執行——程序次序規則
- 線程的所有操作必須在線程的start調用之后——線程啟動規則
- 線程的所有操作都會在線程終止之前結束——線程終止規則
- 對象的終結操作必須在這個對象構造完成之后——對象終結規則
- 前一個對volatile的寫在后一個volatile讀之前——volatile變量規則
- 解鎖操作必須發生在另一個鎖定操作之前——管程鎖定規則
Java中堆和棧有什么不同?
因為棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用于存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。
而堆是所有線程共享的一片公用內存區域。對象都在堆里創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile變量就可以發揮作用了。它要求線程從主存中讀取變量的值,不從工作內存的緩存變量中讀取。如何獲取線程堆棧(用于排查死鎖)?如何控制線程的棧堆大小?
在Windows可以使用Ctrl +Break組合鍵來獲取線程堆棧,Linux下用kill
-3命令。
也可以用jps找到線程id,在用jstack獲取具體線程堆棧。
-Xss參數用來控制線程的堆棧大小Java中用到的線程調度算法是什么
搶占式。一個線程用完CPU之后,操作系統會根據線程優先級、線程饑餓情況等數據算出一個總的優先級并分配下一個時間片給某個線程執行。Thread.sleep(0)的作用是什么
由于Java采用搶占式的線程調度算法,因此可能會出現某條線程常常獲取到CPU控制權的情況,為了讓某些優先級比較低的線程也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次操作系統分配時間片的操作,這也是平衡CPU控制權的一種操作。
實踐
- 生產者消費者模型的作用是什么?
- 通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的運行效率
- 解耦,解耦意味著生產者和消費者之間的聯系少,聯系越少越可以獨自發展而不需要收到相互的制約
如何寫代碼來解決生產者消費者問題?
比較低級的辦法是用wait和notify來解決這個問題,比較贊的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型有三個線程T1,T2,T3,怎么確保它們按順序執行?
可以用線程類的join()方法在一個線程中啟動另一個線程,另外一個線程完成該線程繼續執行。為了確保三個線程的順序你應該先啟動最后一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最后完成。
或者用conditions的await和signal和指定喚醒特定線程。假如有Thread1、Thread2、Thread3、Thread4四條線程分別統計C、D、E、F四個盤的大小,所有線程都統計完畢交給Thread5線程去做匯總,應當如何實現?
- 用concurrent包下的CountDownLatch
- 用concurrent包下的CyclicBarrier
- Linux環境下如何查找哪個線程使用CPU最長
- 獲取項目的pid,jps或者ps -ef | grep java
- 使用"top -H -p pid"+"jps pid",打印出當前的項目,每條線程占用CPU時間的百分比。
- 如何寫出線程安全的單例模式?
http://blog.csdn.net/cselmu9/article/details/51366946
- 餓漢(推薦)
private static MySingleton instance = new MySingleton();
private MySingleton(){}
public static MySingleton getInstance() { return instance; }
- static代碼塊
private static MySingleton instance = null;
private MySingleton(){}
static{ instance = new MySingleton(); }
public static MySingleton getInstance() { return instance; }
- 枚舉類
public class ClassFactory{
private enum MyEnumSingleton{
singletonFactory;
private MySingleton instance;
//枚舉類的構造方法在類加載是被實例化
private MyEnumSingleton(){ instance = new MySingleton();}
public MySingleton getInstance(){ return instance; }
}
public static MySingleton getInstance(){ return MyEnumSingleton.singletonFactory.getInstance(); }
}
class MySingleton{//需要獲實現單例的類,比如數據庫連接Connection
public MySingleton(){}
}
- 雙檢鎖
//使用volatile關鍵字保其可見性
volatile private static MySingleton instance = null;
private MySingleton(){}
public static MySingleton getInstance() {
try { //懶漢式
if(instance != null){}else{
Thread.sleep(300); //創建實例之前可能會有準備工作
synchronized (MySingleton.class) {
if(instance == null){//二次檢查
instance = new MySingleton(); } } }
} catch (InterruptedException e) { e.printStackTrace(); }
return instance;
}
- 寫出3條你遵循的多線程最佳實踐
- 給線程起個有意義的名字。
- 避免鎖定和縮小同步的范圍
- 多用同步類少用wait 和 notify
- 多用并發集合少用同步集合