一、基礎概念
多線程的學習從一些概念開始,進程和線程,並發與並行,同步與異步,高並發。
幾乎所有的操作系統都支持同時運行期多個任務,所有運行中的任務通常就是一個進程,進程是處於運行過程中的程序,進程是操作系統進行資源分配和調度的一個獨立單位。
進程有三個如下特征:
-
獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個進程都擁有自己私有的地址空間。在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。
-
動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念,進程具有自己的生命周期和各種不同的狀態,這些概念在程序中部是不具備的。
-
並發性:多個進程可以在單個處理器上並發執行,多個進程之間不會互相影響。
線程是進程的組成部分,一個進程可以擁有多個線程,而線程必須有一個父進程,線程可以有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源。比如使用QQ時,我們可以同事傳文件,發送圖片,聊天,這就是多個線程在進行。
線程可以完成一定的任務,線程能夠獨立運行的,它不知道有其他線程的存在,線程的執行是搶占式的,當前線程隨時可能被掛起。
總之:一個程序運行后至少有一個進程,一個進程里可以有多個線程,但至少要有一個線程。
1.2 並發和並行
並發和並行是比較容易混淆的概念,他們都表示兩個或者多個任務一起執行,但並發側重多個任務交替執行,同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。而並行確實真正的同時執行,有多條指令在多個處理器上同時執行,並行的前提條件就是多核CPU。
1.3 同步和異步
同步和異步通常用來形容一次方法調用。同步方法調用一旦開始,調用者必須等到方法調用返回后,才能繼續后續的行為。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會立即返回,調用者可以繼續后續的操作。
1.4 高並發
高並發一般是指在短時間內遇到大量操作請求,非常具有代表性的場景是秒殺活動與搶票,高並發是互聯網分布式系統架構設計中必須考慮的因素之一,高並發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),並發用戶數等。
多線程在這里只是在同/異步角度上解決高並發問題的其中的一個方法手段,是在同一時刻利用計算機閑置資源的一種方式
1.5 多線程的好處
線程在程序中是獨立的、並發的執行流,擁有獨立的內存單元,多個線程共享父進程里的全部資源,線程共享的環境有進程的代碼段,進程的公有數據等,利用這些共享數據,線程很容易實現相互之間的通信,可以提高程序的運行效率。
多線程的好處主要有:
-
進程之間不能共享內存,但線程之間共享內存非常容易。
-
系統創建進程時需要給進程重新分配系統資源,但創建線程代價小得多,所以使用多線程實現多任務並發比多進程效率高
-
Java語言內置了多線程功能支持。
二、創建多線程
上面講了多線程的一些概念,都有些抽象,下面將學習如何使用多線程,創建多線程的方式有三種。
2.1 繼承Thread類創建
繼承Thread創建並啟動多線程有三個步驟:
-
定義類並繼承Thread,重寫run()方法,run()方法中為需要多線程執行的任務。
-
創建該類的實例,即創建了線程對象。
-
調用實例的start()方法啟動線程。
public class FirstThread extends Thread { private int i=0; public void run() { for (; i < 100; i++) { //獲取當前線程名稱 System.out.println(this.getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的靜態方法currentThread,獲取當前線程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //創建線程並啟動 new FirstThread().start(); new FirstThread().start(); } } } }
運行結果可以看到兩個線程的i並不是連續的,說明他們並不共享數據。
2.2 實現Runnable接口
實現Runnable接口創建並啟動多線程也有以下步驟:
-
定義類並繼承Runnable接口,重寫run()方法,run()方法中為需要多線程執行的任務。
-
創建該類的實例,並以此實例作為target為參數來創建Thread對象,這個Thread對象才是真正的多線程對象。
public class SecondThread implements Runnable { private int i = 0; @Override public void run() { for (; i < 100; i++) { //此時想要獲取到多線程對象,只能使用Thread.currentThread()方法 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { //Thread的靜態方法currentThread,獲取當前線程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //創建線程並啟動 SecondThread secondThread=new SecondThread(); new Thread(secondThread,"線程一").start(); new Thread(secondThread,"線程二").start(); } } } }
2.3 使用Callable和Future
Callable是Runnable的增加版,主要是接口中的call()方法可以有返回值,並且可以申明拋出異常,使用Callable創建的步驟如下:
-
定義類並繼承Callable接口,重寫call()方法,run()方法中為需要多線程執行的任務。
-
創建類實例,使用FutureTask來包裝對象實例,
-
使用FutureTask對象作為Thread的target來創建多線程,並啟動線程。
-
調用FutureTask對象的get()方法來獲取子線程結束后的返回值。
public class ThirdThread { public static void main(String[] args) { //使用lambda表達式 FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> { int i = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + "的循環變量i的值:" + i); } return i; }); for (int i = 0; i < 100; i++) { //Thread的靜態方法currentThread,獲取當前線程 System.out.println(Thread.currentThread().getName()); if (i == 20) { //創建線程並啟動 new Thread(task, "有返回值的線程").start(); } } try { System.out.println("線程的返回值:" + task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
這里使用了lambda表達式,不使用表達式的方式也很簡單,可以去源碼中查看。Callable與Runnable方式基本相同,只不過增加了返回值且可允許聲明拋出異常。
使用三種方式都可以創建線程,且方式也相對簡單,大體分為實現接口和實現Thread類兩種,這兩種都各有優缺點。
繼承接口實現:
- 優點:除了繼承接口之外,還可以繼承其他類。這種方式多個線程共享一個target對象,可以處理用於共同資源的情況。
- 缺點:編程稍微復雜一些,並且沒有直接獲取當前線程對象的方式,必須使用Thread.currentThread()方式。
基礎Thread類:
-
優點:編程簡單
-
缺點:不能繼承其他類
三、操作多線程
3.1 多線程的狀態
線程狀態是線程中非常重要的一個概念,然而我看過很多資料,線程的狀態理解有很多種方式,很多人將其分為五個基本狀態:新建、就緒、運行、阻塞、死亡,但在狀態枚舉中並不是這五個狀態,我不知道是什么原因(有大神可以解答更好),只能按照枚舉中的狀態根據自己的理解。
-
初始(NEW):新創建了一個線程對象,但還沒有調用start()方法,而且就算調用了改方法也不代表狀態立即改變。
-
運行(RUNNABLE):在運行的狀態肯定就處於RUNNABLE狀態。
-
阻塞(BLOCKED):表示線程阻塞,或者說線程已經被掛起了。
-
等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
-
超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間后自行返回。
-
終止(TERMINATED):表示該線程已經執行完畢。
狀態流程圖如下:
理解:初始狀態很好理解,這個時候其實還不能被稱為一個線程,因為他還沒被啟動,當調用start()方法后,線程正式啟動,但是也不代表立即就改變了狀態。
運行狀態中其實包含兩種狀態,運行中(RUNING)和就緒(READY)。
就緒狀態表示你有資格運行,只要CPU還未調度到你,就處於就緒狀態,有幾個狀態會是線程狀態編程就緒狀態
-
調用線程的start()方法。
-
當前線程sleep()方法結束,其他線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖。
-
當前線程時間片用完了,調用當前線程的yield()方法。
-
鎖池里的線程拿到對象鎖后。
運行中(RUNING)狀態比較好理解,線程調度程序選擇了當前線程作。
阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。
等待狀態是指線程沒有被CPU分配執行時間,需要等待,這種等待是需要被顯示的喚醒,否則會無限等待下去。
超時等待狀態是這現在沒有被CPU分配執行時間,需要等待,不過這種等待不需要被顯示的喚醒,會設置一定的時間后zi懂喚醒。
死亡狀態也很好理解,說明線程方法被執行完成,或者出錯了,線程一旦進入這個狀態就代表徹底的結束
如何中斷線程是多線程開發的重要技術點,說明中斷線程不像break語句那樣簡單干脆,中斷線程處理不好的話會出現難以定位的錯誤,中斷線程表示要求線程在完成任務之前停止正在做的操作,很明顯我們必須妥善的處理,否則會出現奇怪的錯誤,stop()方法可以停止線程,但最好不用它,因為他是不安全的,並且已經被廢棄,正在被使用的是interrupted。
interrupted並不是馬上停止線程,而是給線程打一個停止標記,將線程的中斷狀態設置為true,這類似老板讓你好好工作,但是到底好不好工作要看你自己。
public class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 50000; i++) { System.out.println("i=" + (i + 1)); } } public static void main(String[] args) { try { MyThread myThread = new MyThread(); myThread.start(); Thread.sleep(2000); myThread.interrupt(); } catch (InterruptedException e) { e.printStackTrace(); } } }
結果顯示還是會打印5萬行數據。
Thread提供了兩個方法來判斷線程是否終止
-
static boolean interrupted():判斷當前線程是否中斷,清除中斷標志。
-
boolean isInterrupted():判斷線程是否中斷,不清除中斷標志。
Thread.currentThread().interrupt(); System.out.println("是否停止1?=" + Thread.interrupted());//true,執行方法后清除了標記 System.out.println("是否停止2?=" + Thread.interrupted());//false
當調用了interrupted后線程未真正的停止,但已經有了標志狀態,也就是說我們可以通過標志狀態來對我們的多線程執行的方法進行處理。
public class FiveThread extends Thread { @Override public void run() { for (int i = 0; i < 500000; i++) { if (this.isInterrupted()) { System.out.println("已經是停止狀態了!退出!"); break; } System.out.println("i=" + (i + 1)); } System.out.println("666"); } public static void main(String[] args) { try { FiveThread thread = new FiveThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } }
異常法停止線程
這樣雖然可以實現退出for循環,但是在for循環之外的代碼依然會被執行,很明顯這樣沒有達到效果,這個時候我們可以拋出異常:
@Override public void run() { try { for (int i = 0; i < 500000; i++) { if (this.isInterrupted()) { System.out.println("已經是停止狀態了!退出!"); throw new InterruptedException(); } System.out.println("i=" + (i + 1)); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("拋出了錯誤!"); } System.out.println("666"); }
當然我們也可以使用return方式進行處理,但還是拋出異常處理比較好,可以讓線程中斷事件得到傳播。
public class JoinThread extends Thread { private Thread thread; public JoinThread(Thread thread) { this.thread = thread; } @Override public void run() { try { thread.join(); for (int i = 0; i < 10; i++) { System.out.println(thread.getName() + "的執行 " + i); } } catch (InterruptedException e) { e.printStackTrace(); } } 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; } } }
在上面的例子中一個創建了10個線程,每個線程都會等待前一個線程結束才會繼續運行。可以通俗的理解成接力,前一個線程將接力棒傳給下一個線程,然后又傳給下一個線程。
join方法有三個重載:
-
join():等待被join的線程執行完成。
-
join(long millis):等待被join的線程millis毫秒,如到時仍未執行完成,則不再等待。
-
join(long millis,int nanos):等待被join的線程millis毫秒加nanos微妙。
3.4 sleep
想讓當前的線程暫停一段時間,並進入阻塞狀態,就可以使用sleep方法,這是一個Thread的靜態方法,使用也很簡單。一旦調用了sleep方法,線程就不會獲得執行的機會,即是沒有其他線程執行,sleep方法不會失去鎖。sleep方法經常會拿來Object.wait()方法進行比較。
兩者主要的區別:
-
sleep()方法是Thread的靜態方法,而wait是Object實例方法
-
wait()方法必須要在同步方法或者同步塊中調用,也就是必須已經獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放占有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉對象鎖;
-
sleep()方法在休眠時間達到后如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知后,才會離開等待池,並且再次獲得CPU時間片才會繼續執行。
3.5 yield
yield也是一個靜態方法,一旦執行,它會讓當前線程讓出CPU,使線程進入就緒狀態,但是,需要注意的是,讓出的CPU並不是代表當前線程不再運行了,如果在下一次競爭中,又獲得了CPU時間片當前線程依然會繼續運行。另外,讓出的時間片只會分配給當前線程相同優先級的線程。
public class YieldThread extends Thread { public YieldThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 50; i++) { //獲取當前線程名稱 System.out.println(this.getName() + " " + i); if (i == 20) { Thread.yield(); } } } public static void main(String[] args) { YieldThread yieldThread = new YieldThread("高級"); //yieldThread.setPriority(Thread.MAX_PRIORITY); yieldThread.start(); YieldThread yieldThread2 = new YieldThread("低級"); //yieldThread2.setPriority(Thread.MIN_PRIORITY); yieldThread2.start(); } }
在未設置線程優先級時,第一個線程執行到20時會讓給第二個線程運行,可是在設置了線程優先級后,高級線程在執行yield后發現沒有比他優先級更高的線程,就又會開始執行,並不會中斷。
四、線程優先級
每個線程執行時都有一定的優先級,優先級高的線程獲得更多的執行機會,優先級低的線程則獲得少的執行機會,線程的默認級別與創建它的父線程級別相同,main的級別為普通級別,那么由他創建的線程都是普通級別。
setPriority可以設置線程的優先級,范圍在1-10之間,也可以使用三個靜態常量:
-
MAX_PRIORITY:值為10
-
MIN_PRIORITY:值為1
-
NORM_PRIORITY:值為5
當線程的優先級既有它的規律性,即cpu盡量將資源給優先級高的,但是也有一定的隨機性,優先級較高的不一定先執行完run方法。
五、守護線程
Java中有兩種線程,一種是用戶線程,一種是守護線程,守護線程是一種特殊的線程,就和它的名字一樣,它是系統的守護者。
典型的守護線程是垃圾回收線程,當進程中沒有非守護線程了,那守護線程就沒有必要存在了,用戶線程就可以認為是系統的工作線程,它會完成整個系統的業務操作。用戶線程完全結束后就意味着整個系統的業務任務全部結束了,因此系統就沒有對象需要守護的了,守護線程自然而然就會退。當一個Java應用,只有守護線程的時候,虛擬機就會自然退出。
public class DaemonThread extends Thread { @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"); } } } public static void main(String[] args) { DaemonThread daemonThread = new DaemonThread(); daemonThread.setDaemon(true); daemonThread.start(); //確保main線程結束前能給daemonThread能夠分到時間片 try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } }
這個例子中的線程如果不設置為守護線程,是一個死循環,會一直執行,當我們把它設置為守護線程后,在主線程執行完成后,守護線程也會退出,但是需要注意的是守護線程在退出的時候並不會執行finnaly塊中的代碼,所以將釋放資源等操作不要放在finnaly塊中執行,這種操作是不安全的。
線程可以通過setDaemon(true)的方法將線程設置為守護線程。並且需要注意的是設置守護線程要先於start()方法。