在上一篇文章中併發編程的優缺點談到了爲何花功夫去學習併發編程的技術,也就是說咱們必須瞭解到併發編程的優缺點,咱們在什麼狀況下能夠去考慮開啓多個線程去實現咱們的業務,固然使用多線程咱們應該着重注意一些什麼,在上一篇文章中會有一些討論。那麼,說了這麼多,不管是針對面試仍是實際工做中做爲一名軟件開發人員都應該具有這樣的技能。萬事開頭難,接下來就應該瞭解如何新建一個線程?線程狀態是怎樣轉換的?關於線程狀態的操做是怎樣的?這篇文章就主要圍繞這三個方面來聊一聊。java
一個java程序從main()方法開始執行,而後按照既定的代碼邏輯執行,看似沒有其餘線程參與,但實際上java程序天生就是一個多線程程序,包含了:
(1)分發處理髮送給給JVM信號的線程;
(2)調用對象的finalize方法的線程;
(3)清除Reference的線程;
(4)main線程,用戶程序的入口;面試
那麼,如何在用戶程序中新建一個線程了,只要有三種方式:編程
經過實現callable接口這三種方式,下面看具體demo。安全
public class CreateThreadDemo { public static void main(String[] args) { //1.繼承Thread Thread thread = new Thread() { @Override public void run() { System.out.println("繼承Thread"); super.run(); } }; thread.start(); //2.實現runable接口 Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("實現runable接口"); } }); thread1.start(); //3.實現callable接口 ExecutorService service = Executors.newSingleThreadExecutor(); Future<String> future = service.submit(new Callable() { @Override public String call() throws Exception { return "經過實現Callable接口"; } }); try { String result = future.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
三種新建線程的方式具體看以上註釋,須要主要的是:多線程
另外因爲FeatureTask也實現了Runable接口也能夠利用上面第二種方式(實現Runable接口)來新建線程;併發
此圖來源於《JAVA併發編程的藝術》一書中,線程是會在不一樣的狀態間進行轉換的,java線程線程轉換圖如上圖所示。線程建立以後調用start()方法開始運行,當調用wait(),join(),LockSupport.lock()方法線程會進入到WAITING狀態,而一樣的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增長了超時等待的功能,也就是調用這些方法後線程會進入TIMED_WAITING狀態,當超時等待時間到達後,線程會切換到Runable的狀態,另外當WAITING和TIMED _WAITING狀態時能夠經過Object.notify(),Object.notifyAll()方法使線程轉換到Runable狀態。當線程出現資源競爭時,即等待獲取鎖的時候,線程會進入到BLOCKED阻塞狀態,當線程獲取鎖時,線程進入到Runable狀態。線程運行結束後,線程進入到TERMINATED狀態,狀態轉換能夠說是線程的生命週期。另外須要注意的是:異步
用一個表格將上面六種狀態進行一個總結概括。ide
除了新建一個線程外,線程在生命週期內還有須要基本操做,而這些操做會成爲線程間一種通訊方式,好比使用中斷(interrupted)方式通知實現線程間的交互等等,下面就將具體說說這些操做。
中斷能夠理解爲線程的一個標誌位,它表示了一個運行中的線程是否被其餘線程進行了中斷操做。中斷比如其餘線程對該線程打了一個招呼。其餘線程能夠調用該線程的interrupt()方法對其進行中斷操做,同時該線程能夠調用 isInterrupted()來感知其餘線程對其自身的中斷操做,從而作出響應。另外,一樣能夠調用Thread的靜態方法 interrupted()對當前線程進行中斷操做,該方法會清除中斷標誌位。須要注意的是,當拋出InterruptedException時候,會清除中斷標誌位,也就是說在調用isInterrupted會返回false。post
下面結合具體的實例來看一看學習
public class InterruptDemo { public static void main(String[] args) throws InterruptedException { //sleepThread睡眠1000ms final Thread sleepThread = new Thread() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } super.run(); } }; //busyThread一直執行死循環 Thread busyThread = new Thread() { @Override public void run() { while (true) ; } }; sleepThread.start(); busyThread.start(); sleepThread.interrupt(); busyThread.interrupt(); while (sleepThread.isInterrupted()) ; System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted()); System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); } }
輸出結果
sleepThread isInterrupted: false busyThread isInterrupted: true
開啓了兩個線程分別爲sleepThread和BusyThread, sleepThread睡眠1s,BusyThread執行死循環。而後分別對着兩個線程進行中斷操做,能夠看出sleepThread拋出InterruptedException後清除標誌位,而busyThread就不會清除標誌位。
另外,一樣能夠經過中斷的方式實現線程間的簡單交互, while (sleepThread.isInterrupted()) 表示在Main中會持續監測sleepThread,一旦sleepThread的中斷標誌位清零,即sleepThread.isInterrupted()返回爲false時纔會繼續Main線程纔會繼續往下執行。所以,中斷操做能夠看作線程間一種簡便的交互方式。通常在結束線程時經過中斷標誌位或者標誌位的方式能夠有機會去清理資源,相對於武斷而直接的結束線程,這種方式要優雅和安全。
join方法能夠看作是線程間協做的一種方式,不少時候,一個線程的輸入可能很是依賴於另外一個線程的輸出,這就像兩個好基友,一個基友先走在前面忽然看見另外一個基友落在後面了,這個時候他就會在原處等一等這個基友,等基友遇上來後,就兩人攜手並進。其實線程間的這種協做方式也符合現實生活。在軟件開發的過程當中,從客戶那裏獲取需求後,須要通過需求分析師進行需求分解後,這個時候產品,開發纔會繼續跟進。若是一個線程實例A執行了threadB.join(),其含義是:當前線程A會等待threadB線程終止後threadA纔會繼續執行。關於join方法一共提供以下這些方法:
public final synchronized void join(long millis) public final synchronized void join(long millis, int nanos) public final void join() throws InterruptedException
Thread類除了提供join()方法外,另外還提供了超時等待的方法,若是線程threadB在等待的時間內尚未結束的話,threadA會在超時以後繼續執行。join方法源碼關鍵是:
while (isAlive()) { wait(0); }
能夠看出來當前等待對象threadA會一直阻塞,直到被等待對象threadB結束後即isAlive()返回false的時候纔會結束while循環,當threadB退出時會調用notifyAll()方法通知全部的等待線程。下面用一個具體的例子來講說join方法的使用:
public class JoinDemo { public static void main(String[] args) { Thread previousThread = Thread.currentThread(); for (int i = 1; i <= 10; i++) { Thread curThread = new JoinThread(previousThread); curThread.start(); previousThread = curThread; } } static class JoinThread extends Thread { private Thread thread; public JoinThread(Thread thread) { this.thread = thread; } @Override public void run() { try { thread.join(); System.out.println(thread.getName() + " terminated."); } catch (InterruptedException e) { e.printStackTrace(); } } } }
輸出結果爲:
main terminated. Thread-0 terminated. Thread-1 terminated. Thread-2 terminated. Thread-3 terminated. Thread-4 terminated. Thread-5 terminated. Thread-6 terminated. Thread-7 terminated. Thread-8 terminated.
在上面的例子中一個建立了10個線程,每一個線程都會等待前一個線程結束纔會繼續運行。能夠通俗的理解成接力,前一個線程將接力棒傳給下一個線程,而後又傳給下一個線程......
public static native void sleep(long millis)方法顯然是Thread的靜態方法,很顯然它是讓當前線程按照指定的時間休眠,其休眠時間的精度取決於處理器的計時器和調度器。須要注意的是若是當前線程得到了鎖,sleep方法並不會失去鎖。sleep方法常常拿來與Object.wait()方法進行比價,這也是面試常常被問的地方。
sleep() VS wait()
二者主要的區別:
public static native void yield();這是一個靜態方法,一旦執行,它會是當前線程讓出CPU,可是,須要注意的是,讓出的CPU並非表明當前線程再也不運行了,若是在下一次競爭中,又得到了CPU時間片當前線程依然會繼續運行。另外,讓出的時間片只會分配給當前線程相同優先級的線程。什麼是線程優先級了?下面就來具體聊一聊。
現代操做系統基本採用時分的形式調度運行的線程,操做系統會分出一個個時間片,線程會分配到若干時間片,當前時間片用完後就會發生線程調度,並等待這下次分配。線程分配到的時間多少也就決定了線程使用處理器資源的多少,而線程優先級就是決定線程須要或多或少分配一些處理器資源的線程屬性。
在Java程序中,經過一個整型成員變量Priority來控制優先級,優先級的範圍從1~10.在構建線程的時候能夠經過**setPriority(int)**方法進行設置,默認優先級爲5,優先級高的線程相較於優先級低的線程優先得到處理器時間片。須要注意的是在不一樣JVM以及操做系統上,線程規劃存在差別,有些操做系統甚至會忽略線程優先級的設定。
sleep() VS yield()
另外須要注意的是,sleep()和yield()方法,一樣都是當前線程會交出處理器資源,而它們不一樣的是,sleep()交出來的時間片其餘線程均可以去競爭,也就是說都有機會得到當前線程讓出的時間片。而yield()方法只容許與當前線程具備相同優先級的線程可以得到釋放出來的CPU時間片。
守護線程是一種特殊的線程,就和它的名字同樣,它是系統的守護者,在後臺默默地守護一些系統服務,好比垃圾回收線程,JIT線程就能夠理解守護線程。與之對應的就是用戶線程,用戶線程就能夠認爲是系統的工做線程,它會完成整個系統的業務操做。用戶線程徹底結束後就意味着整個系統的業務任務所有結束了,所以系統就沒有對象須要守護的了,守護線程天然而然就會退。當一個Java應用,只有守護線程的時候,虛擬機就會天然退出。下面以一個簡單的例子來表述Daemon線程的使用。
public class DaemonDemo { public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { @Override public void run() { while (true) { try { System.out.println("i am alive"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("finally block"); } } } }); daemonThread.setDaemon(true); daemonThread.start(); //確保main線程結束前能給daemonThread可以分到時間片 try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } } 複製代碼
輸出結果爲:
i am alive finally block i am alive
上面的例子中daemodThread run方法中是一個while死循環,會一直打印,可是當main線程結束後daemonThread就會退出因此不會出現死循環的狀況。main線程先睡眠800ms保證daemonThread可以擁有一次時間片的機會,也就是說能夠正常執行一次打印「i am alive」操做和一次finally塊中"finally block"操做。緊接着main 線程結束後,daemonThread退出,這個時候只打印了"i am alive"並無打印finnal塊中的。所以,這裏須要注意的是守護線程在退出的時候並不會執行finnaly塊中的代碼,因此將釋放資源等操做不要放在finnaly塊中執行,這種操做是不安全的
線程能夠經過setDaemon(true)的方法將線程設置爲守護線程。而且須要注意的是設置守護線程要先於start()方法,不然會報
Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1365) at learn.DaemonDemo.main(DaemonDemo.java:19)
這樣的異常,可是該線程仍是會執行,只不過會當作正常的用戶線程執行。