關於Java中線程的生命週期,首先看一下下面這張較爲經典的圖:java
新建狀態(New):當線程對象對建立後,即進入了新建狀態,如:Thread t = new MyThread();算法
就緒狀態(Runnable):當調用線程對象的 start() 方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經作好了準備,隨時等待CPU調度執行,並非說執行了 t.start() 此線程當即就會執行;編程
運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;數組
阻塞狀態(Blocked):處於運行狀態中的線程因爲某種緣由,暫時放棄對CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的緣由不一樣,阻塞狀態又能夠分爲三種:緩存
1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;安全
2.同步阻塞:線程在獲取 synchronized 同步鎖失敗(由於鎖被其它線程所佔用),它會進入同步阻塞狀態;服務器
3.其餘阻塞:經過調用線程的 sleep() 或 join() 或發出了I/O請求時,線程會進入到阻塞狀態。當 sleep() 狀態超時、 join() 等待線程終止或者超時、或者 I/O 處理完畢時,線程從新轉入就緒狀態。多線程
死亡狀態(Dead):線程執行完了或者因異常退出了 run() 方法,該線程結束生命週期。架構
Thread類中經常使用的方法:併發
start():啓動線程,該方法調用後,線程不會當即執行,當JVM調用run方法時纔會真正執行。舉例:start是開車前的打火操做,此時汽車不會走的。run方法至關於掛擋擡離合。
setName(String name):設置線程的名字。
getName():獲取線程的名字。
currentThread():獲取當前線程的對象,在使用實現 Runnable 接口去建立線程的時候,就能夠使用該方法獲取線程的對象了。
setPriority(int newPriority):設置線程的優先級,1~10之間的整數,數字越大優先級越高。具備較高線程優先級的線程對象僅表示此線程具備較多的執行機會,而非優先執行。每一個線程默認的優先級都與建立它的線程的優先級相同。main線程默認具備普通優先級。
sleep(long millis):讓當前的正在執行的線程暫停指定的時間,並進入阻塞狀態。待睡眠時間事後會自動甦醒,從而繼續執行任務。在其睡眠的時間段內,該線程因爲不是處於就緒狀態,所以不會獲得執行的機會。即便此時系統中沒有任何其餘可執行的線程,處於 sleep() 中的線程也不會執行。所以 sleep() 方法經常使用來暫停線程執行。前面有講到,當調用了新建的線程的 start() 方法後,線程進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,若是但願這個新線程必然性的當即執行,直接調用原來線程的 sleep(1) 便可。
interrupt():
若是線程處於正常活動狀態,那麼會將該線程的中斷標誌設置爲 true。被設置中斷標誌的線程可能不會當即終端,而是繼續正常運行,不受影響。小時後家人叫你回家吃飯,你能夠選擇在外面繼續玩耍一會以後再回去吃飯。
喚醒正在睡眠的線程,調用interrupt方法會拋出一個 InterruptedException 的異常。
Thread 類中的 stop 方法已經不建議使用了,該方法過於暴力。而上面中的 interrupt 方法並不能中止線程,那麼該如何正確的中止線程呢?Thread 類中有一個 isInterrupted() 方法,它會返回一個 boolean 類型的值,當調用 interrupt() 方法以後,isInterrupted() 方法會返回true。咱們能夠在多線程的代碼中添加判斷,當 isInterrupted() 方法會返回 true 時,手動的拋出一個異常,經過這種方式去中止線程。
yield():當前線程在執行該方法以後會進行禮讓。即原本CPU會執行A線程,可是在A線程中調用了 yield() 方法,此時CPU會放棄A線程的執行權,可是放棄的時間不肯定,有可能剛放棄,A線程立刻又得到了CPU的執行權。yield() 方法還與線程優先級有關,當某個線程調用 yiled() 方法從運行狀態轉換到就緒狀態後,CPU從就緒狀態線程隊列中只會選擇與該線程優先級相同或優先級更高的線程去執行。舉例:坐公交車或地鐵時,看到老人上車後,你會起身讓座,從你起身到老人坐下,這段時間是不肯定的,而且也有可能你剛起身讓座,老人表示一站就到目的地不想作了,此時你會繼續坐回座位上。
join():線程加入,能夠理解爲兩個線程的合併,有兩個線程A和B,A線程須要等B線程執行完成以後再執行,此時就能夠使用join方法。當B線程調用join方法後,A線程內部至關於調用了 wait() 方法進入到等待狀態,直到B線程執行結束後,A線程纔會被喚醒。這時A和B兩個線程能夠當作合併爲一個線程而進行同步執行。
setDaemon(true):設置後臺線程(守護線程)。後臺線程主要是爲其餘線程(相對能夠稱之爲前臺線程)提供服務,或「守護線程」。如JVM中的垃圾回收線程。生命週期:後臺線程的生命週期與前臺線程生命週期有必定關聯。主要體如今:當全部的前臺線程都進入死亡狀態時,後臺線程會自動死亡。設置後臺線程:調用 Thread 對象的 setDaemon(true) 方法能夠將指定的線程設置爲後臺線程。
判斷線程是不是後臺線程:調用thread對象的 isDeamon() 方法。
注:main 線程默認是前臺線程,前臺線程中建立的子線程默認是前臺線程,後臺線程中建立的線程默認是後臺線程。調用 setDeamon(true) 方法將前臺線程設置爲後臺線程時,須要在 start() 方法調用以前。前臺線程都死亡後,JVM通知後臺線程死亡,但從接收指令到做出響應,須要必定的時間。
Java中線程的建立常見有如三種基本形式
1.繼承 Thread 類,重寫該類的 run() 方法。
class MyThread extends Thread { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread myThread1 = new MyThread(); // 建立一個新的線程myThread1 此線程進入新建狀態 Thread myThread2 = new MyThread(); // 建立一個新的線程myThread2 此線程進入新建狀態 myThread1.start(); // 調用start()方法使得線程進入就緒狀態 myThread2.start(); // 調用start()方法使得線程進入就緒狀態 } } } }
如上所示,繼承 Thread 類,經過重寫 run() 方法定義了一個新的線程類 MyThread ,其中 run() 方法的方法體表明瞭線程須要完成的任務,稱之爲線程執行體。當建立此線程類對象時,一個新的線程得以建立,並進入到線程新建狀態。經過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不必定會立刻得以執行,這取決於CPU調度時機。
2.實現 Runnable 接口,並重寫該接口的 run() 方法,該 run() 方法一樣是線程執行體,建立 Runnable 實現類的實例,並以此實例做爲 Thread 類的 target 來建立 Thread 對象,該 Thread 對象纔是真正的線程對象。
class MyRunnable implements Runnable { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Runnable myRunnable = new MyRunnable(); // 建立一個Runnable實現類的對象 Thread thread1 = new Thread(myRunnable); // 將myRunnable做爲Thread target建立新的線程 Thread thread2 = new Thread(myRunnable); thread1.start(); // 調用start()方法使得線程進入就緒狀態 thread2.start(); } } } }
那麼 Thread 和 Runnable 之間究竟是什麼關係呢?咱們首先來看下面這個例子。
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Runnable myRunnable = new MyRunnable(); Thread thread = new MyThread(myRunnable); thread.start(); } } } } class MyRunnable implements Runnable { private int i = 0; @Override public void run() { System.out.println("in MyRunnable run"); for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } } class MyThread extends Thread { private int i = 0; public MyThread(Runnable runnable){ super(runnable); } @Override public void run() { System.out.println("in MyThread run"); for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
一樣的,與實現 Runnable 接口建立線程方式類似,不一樣的地方在於
Thread thread = new MyThread(myRunnable);
那麼這種方式能夠順利建立出一個新的線程麼?答案是確定的。至於此時的線程執行體究竟是 MyRunnable 接口中的 run() 方法仍是 MyThread 類中的 run() 方法呢?經過輸出咱們知道線程執行體是 MyThread 類中的 run() 方法。其實緣由很簡單,由於 Thread 類自己也是實現了 Runnable 接口,而 run() 方法最早是在 Runnable 接口中定義的方法。
public interface Runnable { public abstract void run(); }
咱們看一下 Thread 類中對 Runnable 接口中 run() 方法的實現:
@Override public void run() { if (target != null) { target.run(); } }
也即,當執行到 Thread 類中的 run() 方法時,會首先判斷 target 是否存在,存在則執行 target 中的 run() 方法,也就是實現了 Runnable 接口並重寫了 run() 方法的類中的 run() 方法。可是上述給到的列子中,因爲多態的存在,根本就沒有執行到 Thread 類中的 run() 方法,而是直接先執行了運行時類型即 MyThread 類中的 run() 方法。
3.使用 Callable 和 Future 接口建立線程。具體是建立 Callable 接口的實現類,並實現 call() 方法。並使用 FutureTask 類來包裝 Callable 實現類的對象,且以此 FutureTask 對象做爲 Thread 對象的 target 來建立線程。
public class ThreadTest { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 建立MyCallable對象 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask對象做爲Thread對象的target建立新的線程 thread.start(); //線程進入到就緒狀態 } } System.out.println("主線程for循環執行完畢.."); try { int sum = ft.get(); //取得新建立的新線程中的call()方法返回的結果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { private int i = 0; // 與run()方法不一樣的是,call()方法具備返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; } }
首先,咱們發現,在實現 Callable 接口中,此時再也不是 run() 方法了,而是 call() 方法,此 call() 方法做爲線程執行體,同時還具備返回值!在建立新的線程時,是經過 FutureTask 來包裝 MyCallable 對象,同時做爲了 Thread 對象的 target 。那麼看下 FutureTask 類的定義:
public class FutureTask<V> implements RunnableFuture<V> { //.... }
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
咱們發現 FutureTask 類其實是同時實現了 Runnable 和 Future 接口,由此才使得其具備 Future 和 Runnable 雙重特性。經過 Runnable 特性,能夠做爲 Thread 對象的 target ,而 Future 特性,使得其能夠取得新建立線程中的call()方法的返回值。
執行此程序,咱們發現 sum = 4950 永遠都是最後輸出的。而「主線程for循環執行完畢..」則極可能是在子線程循環中間輸出。由CPU的線程調度機制,咱們知道,「主線程for循環執行完畢..」的輸出時機是沒有任何問題的,那麼爲何 sum =4950 會永遠最後輸出呢?
緣由在於經過 ft.get() 方法獲取子線程 call() 方法的返回值時,當子線程此方法還未執行完畢,ft.get() 方法會一直阻塞,直到 call() 方法執行完畢才能取到返回值。
上述主要講解了三種常見的線程建立方式,對於線程的啓動而言,都是調用線程對象的start()方法,須要特別注意的是:不能對同一線程對象兩次調用start()方法。
就緒狀態轉換爲運行狀態:當此線程獲得處理器資源;
運行狀態轉換爲就緒狀態:當此線程主動調用yield()方法或在運行過程當中失去處理器資源。
運行狀態轉換爲死亡狀態:當此線程線程執行體執行完畢或發生了異常。
此處須要特別注意的是:當調用線程的 yield() 方法時,線程從運行狀態轉換爲就緒狀態,但接下來CPU調度就緒狀態中的哪一個線程具備必定的隨機性,所以,可能會出現A線程調用了 yield() 方法後,接下來CPU仍然調度了A線程的狀況。
因爲實際的業務須要,經常會遇到須要在特定時機終止某一線程的運行,使其進入到死亡狀態。目前最通用的作法是設置 boolean 型的變量,當條件知足時,使線程執行體快速執行完畢。如:
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); } if(i == 40){ myRunnable.stopThread(); } } } } class MyRunnable implements Runnable { private boolean stop; @Override public void run() { for (int i = 0; i < 100 && !stop; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public void stopThread() { this.stop = true; } }
synchronized
一、synchronized 關鍵字的做用域有二種:
① 是某個對象實例內,synchronized aMethod(){} 能夠防止多個線程同時訪問這個對象的 synchronized 方法(若是一個對象有多個 synchronized 方法,只要一個線程訪問了其中的一個synchronized 方法,其它線程不能同時訪問這個對象中任何一個 synchronized 方法)。這時,不一樣的對象實例的 synchronized 方法是不相干擾的。也就是說,其它線程照樣能夠同時訪問相同類的另外一個對象實例中的 synchronized 方法;
② 是某個類的範圍,synchronized static aStaticMethod{} 防止多個線程同時訪問這個類中的 synchronized static 方法。它能夠對類的全部對象實例起做用。
二、除了方法前用 synchronized 關鍵字,synchronized 關鍵字還能夠用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的做用域是當前對象;
三、synchronized 關鍵字是不能繼承的,也就是說,基類的方法 synchronized f(){} 在繼承類中並不自動是 synchronized f(){},而是變成了 f(){}。繼承類須要你顯式的指定它的某個方法爲 synchronized 方法;
總的說來,synchronized 關鍵字能夠做爲函數的修飾符,也可做爲函數內的語句,也就是平時說的同步方法和同步語句塊。若是再細的分類,synchronized 可做用於 instance 變量、object reference(對象引用)、static 函數和 class literals (類名稱字面常量)身上。
在進一步闡述以前,咱們須要明確幾點:
A.不管 synchronized 關鍵字加在方法上仍是對象上,它取得的鎖都是對象,而不是把一段代碼或函數看成鎖――並且同步方法極可能還會被其餘線程的對象訪問。
B.每一個對象只有一個鎖(lock)與之相關聯。
C.實現同步是要很大的系統開銷做爲代價的,甚至可能形成死鎖,因此儘可能避免無謂的同步控制。
討論synchronized用到不一樣地方對代碼產生的影響:
假設P一、P2是同一個類的不一樣對象,這個類中定義瞭如下幾種狀況的同步塊或同步方法,P一、P2就均可以調用它們。
一、把synchronized看成函數修飾符時,示例代碼以下:
Public synchronized void methodAAA(){ //…. }
這也就是同步方法,那這時 synchronized 鎖定的是調用這個同步方法對象。也就是說,當一個對象P1在不一樣的線程中執行這個同步方法時,它們之間會造成互斥,達到同步的效果。可是這個對象所屬的 Class 所產生的另外一對象P2卻能夠任意調用這個被加了 synchronized 關鍵字的方法。
上邊的示例代碼等同於以下代碼:
public void methodAAA(){ synchronized (this){ //(1) //….. } }
(1)處的 this 指的就是調用這個方法的對象,如P1。可見同步方法實質是將 synchronized 做用於 object reference。――那個拿到了P1對象鎖的線程,才能夠調用P1的同步方法,而對P2而言,P1這個鎖與它絕不相干,程序也可能在這種情形下襬脫同步機制的控制,形成數據混亂:(
2.同步塊,示例代碼以下:
public void method3(SomeObject so){ synchronized(so){ //….. }
這時,鎖就是 so 這個對象,誰拿到這個鎖誰就能夠運行它所控制的那段代碼。當有一個明確的對象做爲鎖時,就能夠這樣寫程序,但當沒有明確的對象做爲鎖,只是想讓一段代碼同步時,能夠建立一個特殊的 instance 變量(它得是一個對象)來充當鎖:
class Foo implements Runnable{ private byte[] lock = new byte[0]; // 特殊的instance變量 Public void methodA(){ synchronized(lock) { //… } } //….. }
注:零長度的byte數組對象建立起來將比任何對象都經濟――查看編譯後的字節碼:生成零長度的byte[]對象只需3條操做碼,而Object lock = new Object()則須要7行操做碼。
3.將synchronized做用於static 函數,示例代碼以下:
Class Foo{ public synchronized static void methodAAA(){ // 同步的static 函數 //…. } public void methodBBB(){ synchronized(Foo.class) // class literal(類名稱字面常量) } }
代碼中的 methodBBB() 方法是把 class literal 做爲鎖的狀況,它和同步的 static 函數產生的效果是同樣的,取得的鎖很特別,是當前調用這個方法的對象所屬的類(Class,而再也不是由這個Class產生的某個具體對象了)。
記得在《Effective Java》一書中看到過將 Foo.class 和 P1.getClass() 用於做同步鎖還不同,不能用 P1.getClass() 來達到鎖這個Class的目的。P1指的是由Foo類產生的對象。
能夠推斷:若是一個類中定義了一個 synchronized 的 static 函數A,也定義了一個 synchronized 的 instance 函數B,那麼這個類的同一對象 Obj 在多線程中分別訪問A和B兩個方法時,不會構成同步,由於它們的鎖都不同。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class。
volatile關鍵字:
理解volatile關鍵字的做用的前提是要理解Java內存模型,volatile關鍵字的做用主要有兩個:
① 多線程主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變量,保證了其在多線程之間的可見性,即每次讀取到volatile變量,必定是最新的數據;
② 代碼底層執行不像咱們看到的高級語言—-Java程序這麼簡單,它的執行是Java代碼–>字節碼–>根據字節碼執行對應的C/C++代碼–>C/C++代碼被編譯成彙編語言–>和硬件電路交互,現實中,爲了獲取更好的性能JVM可能會對指令進行重排序,多線程下可能會出現一些意想不到的問題。使用volatile則會對禁止語義重排序,固然這也必定程度上下降了代碼執行效率
從實踐角度而言,volatile的一個重要做用就是和CAS結合,保證了原子性,詳細的能夠參見java.util.concurrent.atomic包下的類,好比AtomicInteger。
ThreadLocal 是Java裏一種特殊的變量。每一個線程都有一個 ThreadLocal 就是每一個線程都擁有了本身獨立的一個變量,競爭條件被完全消除了。若是爲每一個線程提供一個本身獨有的變量拷貝,將大大提升效率。首先,經過複用減小了代價高昂的對象的建立個數。其次,你在沒有使用高代價的同步或者不變性的狀況下得到了線程安全。
簡單說 ThreadLocal 就是一種以空間換時間的作法,在每一個 Thread 裏面維護了一個以開地址法實現的 ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,天然就沒有線程安全方面的問題了。
一、避免線程的建立和銷燬帶來的性能開銷。
二、避免大量的線程間因互相搶佔系統資源致使的阻塞現象。
三、可以對線程進行簡單的管理並提供定時執行、間隔執行等功能。
四、使用線程池還能夠根據項目靈活地控制併發的數目。
線程池的類型
① newFixedThreadPool:建立一個指定工做線程數量的線程池。每當提交一個任務就建立一個工做線程,若是工做線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
特色:是一個典型且優秀的線程池,它具備線程池提升程序效率和節省建立線程時所耗的開銷的優勢。可是,在線程池空閒時,即線程池中沒有可運行任務時,它不會釋放工做線程,還會佔用必定的系統資源。
② newCachedThreadPool:建立一個可緩存的線程池。這種類型的線程池特色是:
1).工做線程的建立數量幾乎沒有限制(其實也有限制的,數目爲Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
2).若是長時間沒有往線程池中提交任務,即若是工做線程空閒了指定的時間(默認爲1分鐘),則該工做線程將自動終止。終止後,若是你又提交了新的任務,則線程池從新建立一個工做線程。
特色:在線程池空閒時,即線程池中沒有可運行任務時,它會釋放工做線程,從而釋放工做線程所佔用的資源。可是,但當出現新任務時,又要建立一新的工做線程,又要必定的系統開銷。而且在使用CachedThreadPool時,必定要注意控制任務的數量,不然,因爲大量線程同時運行,頗有會形成系統癱瘓。
③ newSingleThreadExecutor:建立一個單線程化的Executor,即只建立惟一的工做者線程來執行任務,若是這個線程異常結束,會有另外一個取代它,保證順序執行。
單工做線程最大的特色是可保證順序地執行各個任務,而且在任意給定的時間不會有多個線程是活動的 。 該線程池存在的意義是:在線程死後(或發生異常時)從新啓動一個線程來替代原來的線程繼續執行下去。
④ newScheduleThreadPool:建立一個定長的線程池,並且支持定時的以及週期性的任務執行,相似於Timer。
線程池的關閉
ThreadPoolExecutor 提供了兩個方法,用於線程池的關閉,分別是 shutdown() 和 shutdownNow()。
shutdown():不會當即的終止線程池,而是要等全部任務緩存隊列中的任務都執行完後才終止,但不再會接受新的任務。
shutdownNow():當即終止線程池,並嘗試打斷正在執行的任務,而且清空任務緩存隊列,返回還沒有執行的任務。
線程池的注意事項:
在使用線程池時需注意線程池大小與性能的關係,注意併發風險、死鎖、資源不足和線程泄漏等問題。
(1) 線程池大小:多線程應用並不是線程越多越好,須要根據系統運行的軟硬件環境以及應用自己的特色決定線程池的大小。通常來講,若是代碼結構合理的話,線程數目與CPU 數量相適合便可。若是線程運行時可能出現阻塞現象,可相應增長池的大小;若有必要可採用自適應算法來動態調整線程池的大小,以提升CPU 的有效利用率和系統的總體性能。
(2) 併發錯誤:多線程應用要特別注意併發錯誤,要從邏輯上保證程序的正確性,注意避免死鎖現象的發生。
(3) 線程泄漏:這是線程池應用中一個嚴重的問題,當任務執行完畢而線程沒能返回池中就會發生線程泄漏現象。
一、start() 方法和 run() 方法的區別?
只有調用了 start() 方法,纔會表現出多線程的特性,不一樣線程的 run() 方法裏面的代碼交替執行。若是隻是調用 run() 方法,那麼代碼仍是同步執行的,必須等待一個線程的 run() 方法裏面的代碼所有執行完畢以後,另一個線程才能夠執行其 run() 方法裏面的代碼。
二、Runnable 接口和 Callable 接口的區別?
Callable 是在 JDK1.5 增長的,接口中的 call() 方法是有返回值的,是一個泛型,和 Future、FutureTask 配合能夠用來獲取異步執行的結果。也即 Callable 能夠返回裝載有計算結果的 Future 對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。經過 Future 對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。
Runnable() 的 run() 方法不能夠拋出異常,Callable 的 call() 方法也能夠拋出異常。
這實際上是頗有用的一個特性,由於多線程相比單線程更難、更復雜的一個重要緣由就是由於多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候咱們指望的數據是否已經賦值完畢?沒法得知,咱們能作的只是等待這條多線程的任務執行完畢而已。而 Callable + Future/FutureTask 卻能夠獲取多線程運行的結果,能夠在等待時間太長沒獲取到須要的數據的狀況下取消該線程的任務,真的是很是有用。
三、CyclicBarrier 和 CountDownLatch 的區別?
兩個看上去有點像的類,都在java.util.concurrent(JUC)下,均可以用來表示代碼運行到某個點上,兩者的區別在於:
① CyclicBarrier 的某個線程運行到某個點上以後,該線程即中止運行,直到全部的線程都到達了這個點,全部線程才從新運行;CountDownLatch 則不是,某線程運行到某個點上以後,只是給某個數值-1而已,該線程繼續運行。
② CyclicBarrier 只能喚起一個任務,CountDownLatch 能夠喚起多個任務。
③ CyclicBarrier 可重用,CountDownLatch 不可重用,計數值爲0該 CountDownLatch 就不可再用了。
四、submit() 和 execute() 的區別?
線程池中的execute方法即開啓線程執行池中的任務。還有一個方法submit也能夠作到,它的功能是提交指定的任務去執行而且返回Future對象,即執行的結果。
二者的三個區別:
① 接收的參數不同;
② submit有返回值,而execute沒有;
用到返回值的例子,好比說我有不少個作validation的task,我但願全部的task執行完,而後每一個task告訴我它的執行結果,是成功仍是失敗,若是是失敗,緣由是什麼。而後我就能夠把全部失敗的緣由綜合起來發給調用者。
③ submit方便Exception處理意思就是若是你在你的task裏會拋出checked或者unchecked exception,而你又但願外面的調用者可以感知這些exception並作出及時的處理,那麼就須要用到submit,經過捕獲Future.get拋出的異常。
JDK5日後,任務分兩類:一類是實現了 Runnable 接口的類,一類是實現了 Callable 接口的類。二者均可以被 ExecutorService 執行,它們的區別是:
execute(Runnable r) 沒有返回值。能夠執行任務,但沒法判斷任務是否成功完成。——實現 Runnable 接口;
submit(Runnable r) 返回一個 future。能夠用這個 future 來判斷任務是否成功完成。——實現 Callable 接口。
五、sleep() 方法和 wait() 方法有什麼區別?
sleep() 方法和 wait()方法均可以用來放棄CPU必定的時間,不一樣點在於若是線程持有某個對象的監視器,sleep() 方法不會放棄這個對象的監視器,wait() 方法會放棄這個對象的監視器。
六、wait() 方法和notify() / notifyAll() 方法在放棄對象監視器時有什麼區別?
wait() 方法和 notify() / notifyAll() 方法在放棄對象監視器時的區別在於:wait() 方法當即釋放對象監視器,notify() / notifyAll() 方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器。
七、synchronized 和 ReentrantLock 的區別?
synchronized 是和 if、else、for、while 同樣的關鍵字,ReentrantLock 是類,這是兩者的本質區別。
既然 ReentrantLock 是類,那麼它就提供了比 synchronized 更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量,ReentrantLock 比 synchronized 的擴展性體如今幾點上:
① ReentrantLock 能夠對獲取鎖的等待時間進行設置,這樣就避免了死鎖;
② ReentrantLock 能夠獲取各類鎖的信息;
③ ReentrantLock 能夠靈活地實現多路通知;
另外,兩者的鎖機制其實也是不同的。ReentrantLock 底層調用的是 Unsafe 的 park 方法加鎖,synchronized 操做的應該是對象頭中 mark word(這點不肯定)。
八、鎖和同步的對比?
鎖的使用場景有不少,如共享實例變量、共享鏈接資源時以及包括併發包中BlockingQueue、ConcurrentHashMap等併發集合中都大量使用了鎖。基體上使用同步的地方均可以改爲鎖來用,可是使用鎖的地方不必定能改爲同步來用。
① 同步synchronized算是一個關鍵詞,是來來修飾方法的,可是鎖lock是一個實例變量,經過調用lock()方法來取得鎖
② 只能同步方法,而不能同步變量和類,鎖也是同樣
③ 同步沒法保證線程取得方法執行的前後順序。鎖能夠設置公平鎖來確保。
④ 沒必要同步類中全部的方法,類能夠同時擁有同步和非同步方法。
⑤ 若是線程擁有同步和非同步方法,則非同步方法能夠被多個線程自由訪問而不受鎖的限制。鎖也是同樣。
⑥ 線程睡眠時,它所持的任何鎖都不會釋放。
⑦ 線程能夠得到多個鎖。好比,在一個對象的同步方法裏面調用另一個對象的同步方法,則獲取了兩個對象的同步鎖。
⑧ 同步損害併發性,應該儘量縮小同步範圍。同步不但能夠同步整個方法,還能夠同步方法中一部分代碼塊。
⑨ 在使用同步代碼塊時候,應該指定在哪一個對象上同步,也就是說要獲取哪一個對象的鎖。例如:
最後,還須要說的一點是。若是使用鎖,那麼必定的注意編寫代碼,但不很容易出現死鎖!避免方法後文後講。
八、FutureTask與Future的關係?
FutureTask 表示一個異步運算的任務,FutureTask 類是 Future 的一個實現,並實現了 Runnable,因此可經過 Excutor (線程池) 來執行,也可傳遞給 Thread 對象執行。FutureTask裏面能夠傳入一個Callable的具體實現類,能夠對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操做。固然,因爲FutureTask也是Runnable接口的實現類,因此FutureTask也能夠放入線程池中。
若是在主線程中須要執行比較耗時的操做時,但又不想阻塞主線程時,能夠把這些做業交給 Future 對象在後臺完成,當主線程未來須要時,就能夠經過 Future 對象得到後臺做業的計算結果或者執行狀態。 Executor 框架利用 FutureTask 來完成異步任務,並能夠用來進行任何潛在的耗時的計算。通常 FutureTask 多用於耗時的計算,主線程能夠在完成本身的任務後,再去獲取結果。FutureTask 類既能夠使用 new Thread(Runnable r) 放到一個新線程中跑,也能夠使用 ExecutorService.submit(Runnable r) 放到線程池中跑,並且兩種方式均可以獲取返回結果,但實質是同樣的,即若是要有返回結果那麼構造函數必定要注入一個 Callable 對象。
九、什麼是線程安全?
一句話解釋:若是你的代碼在多線程下執行和在單線程下執行永遠都能得到同樣的結果,那麼你的代碼就是線程安全的。
這個問題有值得一提的地方,就是線程安全也是有幾個級別的:
① 不可變
像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新建立一個,所以這些不可變對象不須要任何同步手段就能夠直接在多線程環境下使用
② 絕對線程安全
無論運行時環境如何,調用者都不須要額外的同步措施。要作到這一點一般須要付出許多額外的代價,Java中標註本身是線程安全的類,實際上絕大多數都不是線程安全的。不過絕對線程安全的類,Java中也有,比方說 CopyOnWriteArrayList、CopyOnWriteArraySet。
③ 相對線程安全
相對線程安全也就是咱們一般意義上所說的線程安全,像 Vector 這種,add、remove 方法都是原子操做,不會被打斷,但也僅限於此,若是有個線程在遍歷某個 Vector、有個線程同時在 add 這個 Vector,99%的狀況下都會出現 ConcurrentModificationException,也就是fail-fast機制。
④ 線程非安全
ArrayList、LinkedList、HashMap等都是線程非安全的類。
十、Java中如何獲取到線程 dump 文件?
死循環、死鎖、阻塞、頁面打開慢等問題,打線程 dump 是最好的解決問題的途徑。所謂線程 dump 也就是線程堆棧,獲取到線程堆棧有兩步:
① 獲取到線程的 pid,能夠經過使用 jps 命令,在Linux環境下還能夠使用 ps -ef | grep java;
② 打印線程堆棧,能夠經過使用 jstack pid 命令,在Linux環境下還能夠使用 kill -3 pid;
另外,Thread 類提供了一個 getStackTrace() 方法也能夠用於獲取線程堆棧。這是一個實例方法,所以此方法是和具體線程實例綁定的,每次獲取獲取到的是具體某個線程當前運行的堆棧。
十一、一個線程若是出現了運行時異常會怎麼樣?
若是這個異常沒有被捕獲的話,這個線程就中止執行了。另外重要的一點是:若是這個線程持有某個對象的監視器,那麼這個對象監視器會被當即釋放。
十二、如何在兩個線程之間共享數據?
經過在線程之間共享對象就能夠了,而後經過 wait/notify/notifyAll、await/signal/signalAll 進行喚起和等待,比方說阻塞隊列 BlockingQueue 就是爲線程之間共享數據而設計的。
1三、生產者消費者模型的做用是什麼?
這個問題很重要:
① 經過平衡生產者的生產能力和消費者的消費能力來提高整個系統的運行效率,這是生產者消費者模型最重要的做用;
② 解耦,這是生產者消費者模型附帶的做用,解耦意味着生產者和消費者之間的聯繫少,聯繫越少越能夠獨自發展而不須要收到相互的制約。
1四、爲何 wait() 方法和 notify()/notifyAll() 方法要在同步塊中被調用?
這是JDK強制的,wait() 方法和 notify()/notifyAll() 方法在調用前都必須先得到對象的鎖。
1五、wait() 方法和 notify()/notifyAll() 方法在放棄對象監視器時有什麼區別?
區別在於:wait()方法當即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器。
1六、怎麼檢測一個線程是否持有對象監視器?
Thread 類提供了一個 holdsLock(Object obj) 方法,當且僅當對象 obj 的監視器被某條線程持有的時候纔會返回 true,注意這是一個 static 方法,這意味着「某條線程」指的是當前線程。
1七、ConcurrentHashMap 的併發度是什麼?
ConcurrentHashMap 的併發度就是 segment 的大小,默認爲16,這意味着最多同時能夠有16條線程操做 ConcurrentHashMap,這也是 ConcurrentHashMap 對 Hashtable 的最大優點,任何狀況下,Hashtable 都不能同時有兩條線程獲取Hashtable中的數據。
1八、ReadWriteLock是什麼?
首先明確一下,不是說ReentrantLock很差,只是ReentrantLock某些時候有侷限。若是使用ReentrantLock,可能自己是爲了防止線程A在寫數據、線程B在讀數據形成的數據不一致,但這樣,若是線程C在讀數據、線程D也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,可是仍是加鎖了,下降了程序的性能。
由於這個,才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提高了讀寫的性能。
1九、Linux環境下如何查找哪一個線程使用CPU最長?
這是一個比較偏實踐的問題,這種問題我以爲挺有意義的。能夠這麼作:
① 獲取項目的 pid,jps 或者 ps -ef | grep java,這個前面有講過;
② top -H -p pid,順序不能改變。
這樣就能夠打印出當前的項目,每條線程佔用CPU時間的百分比。注意這裏打出的是 LWP,也就是操做系統原生線程的線程號。
使用」top -H -p pid」+」jps pid」能夠很容易地找到某條佔用CPU高的線程的線程堆棧,從而定位佔用CPU高的緣由,通常是由於不當的代碼操做致使了死循環。注意,」top -H -p pid」打出來的LWP是十進制的,」jps pid」打出來的本地線程號是十六進制的,轉換一下,就能定位到佔用CPU高的線程的當前線程堆棧了。
20、Java編程寫一個會致使死鎖的程序?
死鎖:線程A和線程B相互等待對方持有的鎖致使程序無限死循環下去。幾個步驟:
① 兩個線程裏面分別持有兩個Object對象:lock1 和 lock2。這兩個 lock 做爲同步代碼塊的鎖;
② 線程1的 run() 方法中同步代碼塊先獲取 lock1 的對象鎖,Thread.sleep(xxx),時間50毫秒差很少,而後接着獲取 lock2 的對象鎖。這麼作主要是爲了防止線程1啓動一會兒就連續得到了 lock1 和 lock2 兩個對象的對象鎖
③ 線程2的 run() 方法中同步代碼塊先獲取 lock2 的對象鎖,接着獲取 lock1 的對象鎖,固然這時 lock1 的對象鎖已經被線程1鎖持有,線程2確定是要等待線程1釋放 lock1 的對象鎖。這樣,線程1″睡覺」結束時,線程2已經獲取了 lock2 的對象鎖了,線程1再嘗試獲取 lock2 的對象鎖,便被阻塞,此時一個死鎖就造成了。
產生死鎖的簡單代碼:
public class DeadLock{ private final Object left = new Object(); private final Object right = new Object(); public void leftRight() throws Exception{ synchronized (left){ Thread.sleep(2000); synchronized (right){ System.out.println("leftRight end!"); } } } public void rightLeft() throws Exception{ synchronized (right){ Thread.sleep(2000); synchronized (left){ System.out.println("rightLeft end!"); } } } }
寫兩個線程分別調用它們:
public class Thread0 extends Thread{ private DeadLock dl; public Thread0(DeadLock dl){ this.dl = dl; } public void run(){ try{ dl.leftRight(); } catch (Exception e){ e.printStackTrace(); } } } public class Thread1 extends Thread{ private DeadLock dl; public Thread1(DeadLock dl){ this.dl = dl; } public void run(){ try{ dl.rightLeft(); } catch (Exception e){ e.printStackTrace(); } } }
寫個main函數調用一下:
public static void main(String[] args){ DeadLock dl = new DeadLock(); Thread0 t0 = new Thread0(dl); Thread1 t1 = new Thread1(dl); t0.start(); t1.start(); while(true); }
結果,什麼語句都不會打印,由於死鎖了。
如何定位死鎖問題:
① jps得到當前Java虛擬機進程的pid:
② jstack打印堆棧。jstack打印內容的最後其實已經報告發現了一個死鎖,咱們分析死鎖產生的緣由,看前面的部分:
避免死鎖的方式
① 讓程序每次至多隻能得到一個鎖。固然,在多線程環境下,這種狀況一般並不現實;
② 設計時考慮清楚鎖的順序,儘可能減小嵌在的加鎖交互數量;
③ 既然死鎖的產生是兩個線程無限等待對方持有的鎖,那麼只要等待時間有個上限不就行了。固然synchronized不具有這個功能,可是咱們能夠使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法能夠指定一個超時時限,在等待超過該時限以後變回返回一個失敗信息。
2一、怎麼喚醒一個阻塞的線程?
若是線程是由於調用了 wait()、sleep()或者join() 方法而致使的阻塞,能夠中斷線程,而且經過拋出 InterruptedException 來喚醒它;若是線程遇到了IO阻塞,無能爲力,由於IO是操做系統實現的,Java代碼並無辦法直接接觸到操做系統。
2二、不可變對象對多線程有什麼幫助?
前面有提到過的一個問題,不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。
2三、什麼是多線程的上下文切換?
是指CPU控制權由一個已經正在運行的線程切換到另一個就緒並等待獲取CPU執行權的線程的過程。
2四、若是你提交任務時,線程池隊列已滿,這時會發生什麼?
若是你使用的 LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於 LinkedBlockingQueue 能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務;若是你使用的是有界隊列比方說 ArrayBlockingQueue 的話,任務首先會被添加到 ArrayBlockingQueue 中,若是 ArrayBlockingQueue 滿了,則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy。
2五、Java中用到的線程調度算法是什麼?
搶佔式。一個線程用完CPU以後,操做系統會根據線程優先級、線程飢餓狀況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。
2六、Thread.sleep(0)的做用是什麼?
因爲Java採用搶佔式的線程調度算法,所以可能會出現某條線程經常獲取到CPU控制權的狀況,爲了讓某些優先級比較低的線程也能獲取到CPU控制權,能夠使用Thread.sleep(0)手動觸發一次操做系統分配時間片的操做,這也是平衡CPU控制權的一種操做。
2七、什麼是自旋?
不少 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界作忙循環,這就是自旋。若是作了屢次忙循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。
2八、什麼是Java內存模型?
Java內存模型定義了一種多線程訪問Java內存的規範。簡單總結一下Java內存模型的幾部份內容:
① Java內存模型將內存分爲了主內存和工做內存。類的狀態,也就是類之間共享的變量,是存儲在主內存中的,每次Java線程用到這些主內存中的變量的時候,會讀一次主內存中的變量,並讓這些內存在本身的工做內存中有一份拷貝,運行本身線程代碼的時候,用到這些變量,操做的都是本身工做內存中的那一份。在線程代碼執行完畢以後,會將最新的值更新到主內存中去;
② 定義了幾個原子操做,用於操做主內存和工做內存中的變量;
③ 定義了 volatile 變量的使用規則;
④ happens-before,即先行發生原則,定義了操做A必然先行發生於操做B的一些規則,好比在同一個線程內控制流前面的代碼必定先行發生於控制流後面的代碼、一個釋放鎖unlock的動做必定先行發生於後面對於同一個鎖進行鎖定 lock 的動做等等,只要符合這些規則,則不須要額外作同步措施,若是某段代碼不符合全部的 happens-before 規則,則這段代碼必定是線程非安全的。
2九、什麼是CAS?
CAS,全稱爲Compare and Swap,即比較-替換。假設有三個操做數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,纔會將內存值修改成B並返回true,不然什麼都不作並返回false。固然CAS必定要 volatile 變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,不然舊的預期值A對某條線程來講,永遠是一個不會變的值A,只要某次CAS操做失敗,永遠都不可能成功。
30、什麼是樂觀鎖和悲觀鎖?
① 樂觀鎖:對於併發間操做產生的線程安全問題持樂觀狀態,樂觀鎖認爲競爭不老是會發生,所以它不須要持有鎖,將比較-替換這兩個動做做爲一個原子操做嘗試去修改內存中的變量,若是失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
② 悲觀鎖:對於併發間操做產生的線程安全問題持悲觀狀態,悲觀鎖認爲競爭老是會發生,所以每次對某資源進行操做時,都會持有一個獨佔的鎖,就像 synchronized,無論三七二十一,直接上了鎖就操做資源了。
3一、什麼是AQS?
AQS全稱爲 AbstractQueuedSychronizer,翻譯過來應該是抽象隊列同步器。
若是說java.util.concurrent的基礎是CAS的話,那麼AQS就是整個Java併發包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS實際上以雙向隊列的形式鏈接全部的 Entry,比方說 ReentrantLock,全部等待的線程都被放在一個 Entry 中並連成雙向隊列,前面一個線程使用 ReentrantLock 好了,則雙向隊列實際上的第一個Entry開始運行。
AQS定義了對雙向隊列全部的操做,而只開放了 tryLock() 和 tryRelease() 方法給開發者使用,開發者能夠根據本身的實現重寫 tryLock() 和 tryRelease() 方法,以實現本身的併發功能。
3二、單例模式的線程安全性?
首先要說的是單例模式的線程安全意味着:某個類的實例在多線程環境下只會被建立一次出來。單例模式有不少種的寫法,我總結一下:
① 餓漢式單例模式的寫法:線程安全;
② 懶漢式單例模式的寫法:非線程安全;
③ 雙檢鎖單例模式的寫法:線程安全。
3三、Semaphore 有什麼做用?
Semaphore 就是一個信號量,它的做用是限制某段代碼塊的併發數。Semaphore 有一個構造函數,能夠傳入一個 int 型整數n,表示某段代碼最多隻有n個線程能夠訪問,若是超出了n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此能夠看出若是 Semaphore 構造函數中傳入的 int 型整數n=1,至關於變成了一個 synchronized 了。
3四、Hashtable的size()方法中明明只有一條語句」return count」,爲何還要作同步?
某個方法中若是有多條語句,而且都在操做同一個類變量,那麼在多線程環境下不加鎖,勢必會引起線程安全問題,這很好理解,可是size()方法明明只有一條語句,爲何還要加鎖?
本身的理解主要有兩點:
① 同一時間只能有一條線程執行固定類的同步方法,可是對於類的非同步方法,能夠多條線程同時訪問。因此,這樣就有問題了,可能線程A在執行 Hashtable 的 put() 方法添加數據,線程B則能夠正常調用size()方法讀取 Hashtable 中當前元素的個數,那讀取到的值可能不是最新的,可能線程A添加了完了數據,可是沒有對size++,線程B就已經讀取size了,那麼對於線程B來講讀取到的 size 必定是不許確的。而給 size() 方法加了同步以後,意味着線程B調用 size() 方法只有在線程A調用put方法完畢以後才能夠調用,這樣就保證了線程安全性;
② CPU執行代碼,執行的不是Java代碼,這點很關鍵,必定得記住。Java代碼最終是被翻譯成彙編代碼執行的,彙編代碼纔是真正能夠和硬件電路交互的代碼。即便你看到Java代碼只有一行,甚至你看到Java代碼編譯以後生成的字節碼也只有一行,也不意味着對於底層來講這句語句的操做只有一個。一句」return count」假設被翻譯成了三句彙編語句執行,徹底可能執行完第一句,線程就切換了。
3五、線程類的構造方法、靜態塊是被哪一個線程調用的?
這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 new 這個線程類所在的線程所調用的,而 run() 方法裏面的代碼纔是被線程自身所調用的。
若是說上面的說法讓你感到困惑,那麼我舉個例子,假設 Thread2 中 new 了 Thread1,main 函數中 new 了 Thread2,那麼:
① Thread2 的構造方法、靜態塊是 main 線程調用的,Thread2 的 run() 方法是 Thread2 本身調用的;
② Thread1 的構造方法、靜態塊是 Thread2 調用的,Thread1 的 run() 方法是 Thread1 本身調用的。
3五、同步方法和同步塊,哪一個是更好的選擇?
同步塊,意味着同步塊以外的代碼是異步執行的,這比同步整個方法更提高代碼的效率。請知道一條原則:同步的範圍越小越好。
藉着這一條,額外提一點,雖然說同步的範圍越少越好,可是在Java虛擬機中仍是存在着一種叫作鎖粗化的優化方法,這種方法就是把同步範圍變大。這是有用的,比方說 StringBuffer,它是一個線程安全的類,天然最經常使用的 append() 方法是一個同步方法,咱們寫代碼的時候會反覆 append 字符串,這意味着要進行反覆的加鎖->解鎖,這對性能不利,由於這意味着Java虛擬機在這條線程上要反覆地在內核態和用戶態之間進行切換,所以Java虛擬機會將屢次 append() 方法調用的代碼進行一個鎖粗化的操做,將屢次的 append 的操做擴展到 append() 方法的頭尾,變成一個大的同步塊,這樣就減小了加鎖–>解鎖的次數,有效地提高了代碼執行的效率。
3六、高併發、任務執行時間短的業務怎樣使用線程池?併發不高、任務執行時間長的業務怎樣使用線程池?併發高、業務執行時間長的業務怎樣使用線程池?
併發編程網上看到的一個問題,這個問題很是好、很是實際、很是專業。關於這個問題,我的見解是:
① 高併發、任務執行時間短的業務,線程池線程數能夠設置爲CPU核數+1,減小線程上下文的切換;
② 併發不高、任務執行時間長的業務要區分開看:
a)假如是業務時間長集中在IO操做上,也就是IO密集型的任務,由於IO操做並不佔用CPU,因此不要讓全部的CPU閒下來,能夠加大線程池中的線程數目,讓CPU處理更多的業務;
b)假如是業務時間長集中在計算操做上,也就是計算密集型任務,這個就沒辦法了,和①同樣吧,線程池中的線程數設置得少一些,減小線程上下文的切換。
③ 併發高、業務執行時間長,解決這種類型任務的關鍵不在於線程池而在於總體架構的設計,看看這些業務裏面某些數據是否能作緩存是第一步,增長服務器是第二步,至於線程池的設置,設置參考②。最後,業務執行時間長的問題,也可能須要分析一下,看看能不能使用中間件對任務進行拆分和解耦。
一、搶佔式調度策略
Java運行時系統的線程調度算法是搶佔式的。Java運行時系統支持一種簡單的固定優先級的調度算法。若是一個優先級比其餘任何處於可運行狀態的線程都高的線程進入就緒狀態,那麼運行時系統就會選擇該線程運行。新的優先級較高的線程搶佔了其餘線程。可是Java運行時系統並不搶佔同優先級的線程。換句話說,Java運行時系統不是分時的。然而,基於Java Thread類的實現系統多是支持分時的,所以編寫代碼時不要依賴分時。當系統中的處於就緒狀態的線程都具備相同優先級時,線程調度程序採用一種簡單的、非搶佔式的輪轉的調度順序。
二、時間片輪轉調度策略
有些系統的線程調度採用時間片輪轉調度策略。這種調度策略是從全部處於就緒狀態的線程中選擇優先級最高的線程分配必定的CPU時間運行。該時間事後再選擇其餘線程運行。只有當線程運行結束、放棄(yield)CPU或因爲某種緣由進入阻塞狀態,低優先級的線程纔有機會執行。若是有兩個優先級相同的線程都在等待CPU,則調度程序以輪轉的方式選擇運行的線程。