從執行緒到併發程式設計
什麼是執行緒
說起執行緒,還是得從程式說起。那麼程式是什麼呢?現代作業系統在執行一個程式時,會為其建立一個程式。比如你電腦上開啟個QQ
或者是啟動一個Java
程式,作業系統都會為其建立一個程式。而執行緒是作業系統的最小排程單元,一個程式中可以有多個執行緒。OS
排程會讓多個執行緒之間高速切換,讓我們以為是多個執行緒在同時執行。
執行緒的建立與銷燬
執行緒的建立
那麼怎麼去建立一個執行緒呢。在Java
中我們可以有以下三種方式來建立執行緒:
-
繼承
Thread
類,重寫run
方法。public class ThreadDemo1 extends Thread { @Override public void run() { System.out.println("extends thread run"); } public static void main(String[] args) { ThreadDemo1 thread1 = new ThreadDemo1(); ThreadDemo1 thread2 = new ThreadDemo1(); thread1.start(); thread2.start(); } }
-
實現
Runnable
介面,重寫run
方法。public class ThreadDemo2 implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " implements runnable run"); } public static void main(String[] args) { new Thread(new ThreadDemo2(), "thread1").start(); new Thread(new ThreadDemo2(), "thread2").start(); } }
-
實現
Callable
介面,重寫call
方法,實現帶返回值的執行緒。public class ThreadDemo3 implements Callable<String> { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = newFixedThreadPool(1); ThreadDemo3 thread = new ThreadDemo3(); Future<String> future = executorService.submit(thread); System.out.println(future.get()); executorService.shutdown(); } @Override public String call() throws Exception { System.out.println(Thread.currentThread().getName() + " implements callable"); return Thread.currentThread().getName(); } }
終止執行緒
-
interrupt
中斷標誌前面看完了如何建立一個執行緒,那麼又怎麼去終止一個執行緒呢。以前的
Thread
類中有個stop
方法可以用來終止執行緒,而現在已經被標記過期了,其實也不建議使用stop
方法來終止執行緒,為什麼呢!因為我想用過Linux
系統的都知道kill -9
吧,stop
方法與其類似,stop
方法會強制殺死執行緒,而不管執行緒中的任務是否執行完畢。那麼我們如何更加優雅的去終止一個執行緒呢。這裡
Thread
類為我們提供了一個interrupt
方法。當我們需要終止一個執行緒,可以呼叫它的
interrupt
方法,相當於告訴這個執行緒你可以終止了,而不是暴力的殺死該執行緒,執行緒會自行中斷,我們可以使用isInterrupted
方法來判斷執行緒是否已經終止了:這段程式碼可以測試到,如果interrupt
方法無法終止執行緒,那麼這個執行緒將會是死迴圈,而無法結束。這裡使用interrupt
以一種更加安全中斷執行緒。public class InterruptDemo { private static int i; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while(!Thread.currentThread().isInterrupted()){ i++; } System.out.println("result: " + i); }, "interrupt-test"); thread.start(); TimeUnit.SECONDS.sleep(2); thread.interrupt(); } }
-
volatile
共享變數作為中斷標誌這裡先不介紹
volatile
的記憶體語義以及原理,它可以解決共享變數的記憶體可見性問題。使其他執行緒可以及時看到被volatile
變數修飾的共享變數的變更。所以我們也可以使用volatile
來達到中斷執行緒的目的。public class VolatileDemo { private volatile static boolean flag = false; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { long i = 0L; while (!flag) { i++; } System.out.println(i); }, "volatile-demo"); thread.start(); System.out.println("volatile-demo is start"); Thread.sleep(1000); flag = true; } }
比如上面示例中的程式碼,我們可以控制在特定的地方,改變共享變數,來達到讓執行緒退出。
執行緒復位
-
interrupted
前面說了使用
interrupt
可以告訴執行緒可以中斷了,執行緒同時也提供了另外一個方法即Thread.interrupted()
可以已經設定過中斷標誌的執行緒進行復位。public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (true) { boolean isInterrupted = Thread.currentThread().isInterrupted(); if(isInterrupted){ System.out.println("before: " + isInterrupted); Thread.interrupted(); // 對執行緒進行復位,中斷標識為false System.out.println("after: " + Thread.currentThread().isInterrupted()); } } }, "InterruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); // 設定中斷標識為true } }
輸出結果:
before: true after: false
通過
demo
可以看到執行緒確實是先被設定了中斷標識,後又被複位。 -
異常復位
除了使用
interupted
來設定中斷復位,還有一種情況,就是對丟擲InterruptedException
異常的方法,在InterruptedExceptio
丟擲之前,JVM
會先把執行緒的中斷標識位清除,然後才會丟擲InterruptedException
,這個時候如果呼叫isInterrupted
方法,將會返回false
,例如:public class InterruptDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { while (true) { try { Thread.sleep(10000); } catch (InterruptedException e) { // 丟擲InterruptedException會將復位標識設定為false e.printStackTrace(); } } }, "InterruptDemo"); thread.start(); TimeUnit.SECONDS.sleep(1); thread.interrupt(); // 設定中斷標誌為true TimeUnit.SECONDS.sleep(1); System.out.println(thread.isInterrupted()); } }
輸出結果:
java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at top.felixu.chapter1.lifecycle.InterruptDemo.lambda$main$0(InterruptDemo.java:48) at java.lang.Thread.run(Thread.java:748) false
通過例子可以看到,在丟擲異常之後,
isInterrupted
確實是又變成了false
。
為什麼要併發程式設計
單執行緒有時候也可以解決問題啊,那麼我們為什麼還要併發程式設計呢,很大程度上是因為更好的利用CPU
資源,提升我們系統的效能。根據摩爾定律(當價格不變時,積體電路上可容納的元器件的數目,約每隔18-24個月便會增加一倍,效能也將提升一倍。換言之,每一美元所能買到的電腦效能,將每隔18-24個月翻一倍以上。這一定律揭示了資訊科技進步的速度。)推算,不久就會有超強的計算能力,然而,事情並未像預料的那樣發展。2004年,Intel宣佈4GHz晶片的計劃推遲到2005年,然後在2004年秋季,Intel宣佈徹底取消4GHz的計劃。現在雖然有4GHz的晶片但頻率極限已逼近,而且近10年停留在4GHz,也就是摩爾定律應該是失效了。既然單核CPU
的計算能力短期無法提升了,多核CPU
在此時應運而生。單執行緒畢竟只可能跑在一個核心上,浪費了CPU
的資源,從而催生了併發程式設計,併發程式設計是為了發揮出多核CPU
的計算能力,提升效能。
頂級電腦科學家
Donald Ervin Knuth
如此評價這種情況:在我看來,這種現象(併發)或多或少是由於硬體設計者無計可施了導致的,他們將摩爾定律的責任推給了軟體開發者。
併發程式設計總結起來說大致有以下優點:
充分利用
CPU
,提高計算能力。方便對業務的拆分。比如一個購物流程,我們可以拆分成下單,減庫存等,利用多執行緒來加快響應。
對於需要阻塞的場景,可以非同步處理,來減少阻塞。
對於執行效能,可以通過多執行緒平行計算。
併發程式設計有哪些問題
看起來好像多執行緒確實很好,那麼我們就可以儘量多的去開執行緒了嘛。也並不是這樣的,多執行緒的效能也受多方面因素所影響:
-
時間片的切換
時間片是
CPU
分配給執行緒執行的時間,即便是單核CPU
也是可以通過時間片的切換使多個執行緒切換執行,讓我們覺得是多個執行緒在同時執行,因為時間片的切換是非常快的,我們感覺不到的。每次切換執行緒是需要時間的,而且切換的時候需要儲存當前執行緒的狀態,以便切換回來的時候可以繼續執行。所以當執行緒較多的時候,切換時間片所帶來的消耗也同樣可觀。那麼有沒有什麼姿勢可以解決這個問題呢,是有的:- 無鎖併發程式設計:多執行緒在競爭鎖時會引起上下文的切換,可以使用對資料
Hash
取模分段的思想來避免使用鎖。 -
CAS
演算法:可以使用Atomic
包中相關原子操作,來避免使用鎖。 - 使用最少執行緒:根據業務需求建立執行緒數,過多的建立執行緒會造成執行緒閒置和資源浪費。
- 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。
- 無鎖併發程式設計:多執行緒在競爭鎖時會引起上下文的切換,可以使用對資料
-
死鎖
為了保證多執行緒的正確性,很多時候,我們都會使用鎖,它是一個很好用的工具,然而在一些時候,不正確的姿勢會造成死鎖問題,進而引發系統不可用。下面我們就來看一個死鎖案例:
public class DeadLockDemo { public static void main(String[] args) { new DeadLockDemo().deadLock(); } private void deadLock() { Object o1 = new Object(); Object o2 = new Object(); Thread one = new Thread(() -> { synchronized (o1) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2) { System.out.println(Thread.currentThread().getName()); } } }, "thread-one"); Thread two = new Thread(() -> { synchronized (o2) { synchronized (o1) { System.out.println(Thread.currentThread().getName()); } } }, "thread-two"); one.start(); two.start(); } }
執行之後便會發現程式無法終止了,那麼究竟發生了什麼呢?我們通過
jps
命令來檢視一下當前Java
的PID
。$ jps 1483 DeadLockDemo
可以看到當前的程式
PID
為1483
(每個人的都不一樣,得自己執行哦),接下來我們使用jstack
命令dump
出當前程式的執行緒資訊,看一下究竟發生了什麼。jstack 1483 . . . . . .省略部分資訊 "thread-two" #12 prio=5 os_prio=31 tid=0x00007fbba9956800 nid=0x5603 waiting for monitor entry [0x0000700011058000] java.lang.Thread.State: BLOCKED (on object monitor) at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32) - waiting to lock <0x000000076ada81b8> (a java.lang.Object) - locked <0x000000076ada81c8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread-one" #11 prio=5 os_prio=31 tid=0x00007fbba8033800 nid=0xa803 waiting for monitor entry [0x0000700010f55000] java.lang.Thread.State: BLOCKED (on object monitor) at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24) - waiting to lock <0x000000076ada81c8> (a java.lang.Object) - locked <0x000000076ada81b8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) . . . . . .省略部分資訊 Found one Java-level deadlock: ============================= "thread-two": waiting to lock monitor 0x00007fbba9006eb8 (object 0x000000076ada81b8, a java.lang.Object), which is held by "thread-one" "thread-one": waiting to lock monitor 0x00007fbba90082a8 (object 0x000000076ada81c8, a java.lang.Object), which is held by "thread-two" Java stack information for the threads listed above: =================================================== "thread-two": at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$1(DeadLockDemo.java:32) - waiting to lock <0x000000076ada81b8> (a java.lang.Object) - locked <0x000000076ada81c8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$2/381259350.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) "thread-one": at top.felixu.section1.deadlock.DeadLockDemo.lambda$deadLock$0(DeadLockDemo.java:24) - waiting to lock <0x000000076ada81c8> (a java.lang.Object) - locked <0x000000076ada81b8> (a java.lang.Object) at top.felixu.section1.deadlock.DeadLockDemo$$Lambda$1/1607521710.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.
從上面來看,兩個執行緒都是阻塞狀態,都在等待別的執行緒釋放鎖,但是永遠都等不到,從而形成了死鎖。那麼平常開發過程中儘量按以下操作來避免不必要的死鎖(當然有時候不注意還是會莫名死鎖,得
dump
資訊加以分析才能找出問題的):- 避免一個執行緒同時獲取多個鎖。
- 儘量避免一個執行緒在鎖內同時獲取多個資源,儘量保證每個鎖內只佔有一個資源。
- 嘗試使用定時鎖,使用
lock.tryLock(timeout)
來替代使用內部鎖機制。 - 對於資料庫鎖,加鎖和解鎖必須在一個資料庫連線裡,否則會出現解鎖失敗的情況。
-
軟體和硬體資源的限制
程式跑在伺服器上,必然受到伺服器等方面的限制。
- 硬體資源限制:一般指磁碟讀寫速度、頻寬、
CPU
效能等方面 - 軟體資源限制:一般指資料庫連線數、
Socket
連線數等方面
- 硬體資源限制:一般指磁碟讀寫速度、頻寬、
所以,如何合理的使用執行緒需要我們在實踐中具體去分析。
參考自《Java併發程式設計的藝術》
相關文章
- 併發程式設計之多執行緒執行緒安全程式設計執行緒
- 併發程式設計之:執行緒程式設計執行緒
- 併發程式設計與執行緒安全程式設計執行緒
- java併發程式設計——執行緒池Java程式設計執行緒
- java併發程式設計——執行緒同步Java程式設計執行緒
- Java併發程式設計:Java執行緒Java程式設計執行緒
- Java併發程式設計之執行緒安全、執行緒通訊Java程式設計執行緒
- java併發程式設計 | 執行緒詳解Java程式設計執行緒
- 併發程式設計——如何終止執行緒程式設計執行緒
- 併發程式設計之:執行緒池(一)程式設計執行緒
- 併發程式設計之多執行緒基礎程式設計執行緒
- Java併發程式設計-執行緒基礎Java程式設計執行緒
- 多執行緒併發程式設計“鎖”事執行緒程式設計
- Java併發程式設計:執行緒池ThreadPoolExecutorJava程式設計執行緒thread
- Java 併發程式設計 | 執行緒池詳解Java程式設計執行緒
- 併發程式設計之:深入解析執行緒池程式設計執行緒
- iOS多執行緒之併發程式設計-4iOS執行緒程式設計
- java併發程式設計:執行緒池的使用Java程式設計執行緒
- 併發程式設計 —— 談談執行緒中斷程式設計執行緒
- 併發程式設計之執行緒安全性程式設計執行緒
- 多執行緒程式設計,處理多執行緒的併發問題(執行緒池)執行緒程式設計
- Java併發程式設計之執行緒篇之執行緒中斷(三)Java程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒簡介(二)Java程式設計執行緒
- Python 併發程式設計之執行緒池/程式池Python程式設計執行緒
- Python併發程式設計之執行緒池/程式池Python程式設計執行緒
- Java併發程式設計之執行緒篇之執行緒的由來(一)Java程式設計執行緒
- Java執行緒與併發程式設計實踐----額外的執行緒能力Java執行緒程式設計
- Java併發程式設計序列之執行緒狀態Java程式設計執行緒
- 併發程式設計之 執行緒協作工具類程式設計執行緒
- 《java併發程式設計的藝術》執行緒池Java程式設計執行緒
- 42、併發程式設計之多執行緒理論篇程式設計執行緒
- 43、併發程式設計之多執行緒實操篇程式設計執行緒
- Java併發程式設計——深入理解執行緒池Java程式設計執行緒
- 併發程式設計 建立執行緒的三種方式程式設計執行緒
- Java併發(一)----程式、執行緒、並行、併發Java執行緒並行
- Java併發程式設計學習筆記----執行緒池Java程式設計筆記執行緒
- python併發程式設計之多執行緒理論部分Python程式設計執行緒
- 【架構】Java併發程式設計——執行緒池的使用架構Java程式設計執行緒