Java多執行緒程式設計核心技術(一)Java多執行緒技能
1、程序和執行緒
一個程式就是一個程序,而一個程式中的多個任務則被稱為執行緒。
程序是表示資源分配的基本單位,執行緒是程序中執行運算的最小單位,亦是排程執行的基本單位。
舉個例子:
開啟你的計算機上的工作管理員,會顯示出當前機器的所有程序,QQ,360等,當QQ執行時,就有很多子任務在同時執行。比如,當你邊打字傳送表情,邊好友視訊時這些不同的功能都可以同時執行,其中每一項任務都可以理解成“執行緒”在工作。
2、使用多執行緒
在Java的JDK開發包中,已經自帶了對多執行緒技術的支援,可以很方便地進行多執行緒程式設計。實現多執行緒程式設計的方式有兩種,一種是繼承 Thread 類,另一種是實現 Runnable 介面。使用繼承 Thread 類建立執行緒,最大的侷限就是不能多繼承,所以為了支援多繼承,完全可以實現 Runnable 介面的方式。需要說明的是,這兩種方式在工作時的性質都是一樣的,沒有本質的區別。如下所示:
1.繼承 Thread 類
public class MyThread extends Thread { @Override public void run() { //... } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } 複製程式碼
2.實現 Runnable 介面
public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { //... } }).start(); } 複製程式碼
Thread.java 類中的start()方法通知“執行緒規劃器”此執行緒已經準備就緒,等待呼叫執行緒物件的run()方法。這個過程其實就是讓系統安排一個時間來呼叫 Thread 中的 run() 方法,也就是使執行緒得到執行,多執行緒是非同步的,執行緒在程式碼中啟動的順序不是執行緒被呼叫的順序。
Thread構造方法
Thread()
分配新的Thread
物件。 |
Thread(Runnable target)
分配新的Thread
物件。 |
Thread(Runnable target, String name)
分配新的Thread
物件。 |
Thread(String name)
分配新的Thread
物件。 |
Thread(ThreadGroup group, Runnable target)
分配新的Thread
物件。 |
Thread(ThreadGroup group, Runnable target, String name)
分配新的Thread
物件,以便將target
作為其執行物件,將指定的name
作為其名稱,並作為group
所引用的執行緒組的一員。 |
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
分配新的Thread
物件,以便將target
作為其執行物件,將指定的name
作為其名稱,作為group
所引用的執行緒組的一員,並具有指定的 堆疊大小 。 |
Thread(ThreadGroup group, String name)
分配新的Thread
物件。 |
3、例項變數與執行緒安全
自定義執行緒類中的例項變數針對其他執行緒可以有共享與不共享之分。當每個執行緒都有各自的例項變數時,就是變數不共享。共享資料的情況就是多個執行緒可以訪問同一個變數。來看下面的示例:
public class MyThread implements Runnable { private int count = 5; @Override public void run() { count--; System.out.println("執行緒"+Thread.currentThread().getName()+" 計算 count = "+count); } } 複製程式碼
以上程式碼定義了一個執行緒類,實現count變數減一的效果。執行類Runjava程式碼如下:
public class Ruu { public static void main(String[] args) throws InterruptedException { MyThread myThread = new MyThread(); Thread a = new Thread(myThread,"A"); Thread b = new Thread(myThread,"B"); Thread c = new Thread(myThread,"C"); Thread d = new Thread(myThread,"D"); Thread e = new Thread(myThread,"E"); a.start(); b.start(); c.start(); d.start(); e.start(); } } 複製程式碼
列印結果如下:
執行緒C 計算 count = 3 執行緒B 計算 count = 3 執行緒A 計算 count = 2 執行緒D 計算 count = 1 執行緒E 計算 count = 0 複製程式碼
執行緒C,B的列印結果都是3,說明C和B同時對count進行了處理,產生了“非執行緒安全問題”。而我們想要的得到的列印結果卻不是重複的,而是依次遞減的。
在某些JVM中,i--的操作要分成如下3步:
-
取得原有變數的值。
-
計算i-1。
-
對i進行賦值。
在這三個步驟中,如果有多個執行緒同時訪問,那麼一定會出現非執行緒安全問題。
解決方法就是使用 synchronized 同步關鍵字 使各個執行緒排隊執行run()方法。修改後的run()方法:
public class MyThread implements Runnable { private int count = 5; @Override synchronized public void run() { count--; System.out.println("執行緒"+Thread.currentThread().getName()+" 計算 count = "+count); } } 複製程式碼
列印結果:
執行緒B 計算 count = 4 執行緒C 計算 count = 3 執行緒A 計算 count = 2 執行緒E 計算 count = 1 執行緒D 計算 count = 0 複製程式碼
關於System.out.println()方法
先來看System.out.println()方法原始碼:
public void println(String x) { synchronized (this) { print(x); newLine(); } } 複製程式碼
雖然println()方法內部使用 synchronized 關鍵字,但如下所示的程式碼在執行時還是有可能出現非執行緒安全問題的。
System.out.println("執行緒"+Thread.currentThread().getName()+" 計算 count = "+count--); 複製程式碼
原因在於println()方法內部同步,但 i-- 操作卻是在進入 println()之前發生的,所以有發生非執行緒安全問題的概率。
4、多執行緒方法
1. currentThread()方法
currentThread()方法可返回程式碼段正在被哪個執行緒呼叫的資訊。
Thread.currentThread().getName() 複製程式碼
2. isAlive()方法
方法isAlive()的功能是判斷當前的執行緒是否處於活動狀態。
thread.isAlive(); 複製程式碼
3. sleep()方法
方法sleep()的作用是在指定的毫秒數內讓當前"正在執行的執行緒"休眠(暫停執行)。這個"正在執行的執行緒"是指this.currentThread()返回的執行緒。
Thread.sleep() 複製程式碼
4. getId()方法
getId()方法的作用是取得執行緒的唯一標識。
thread.getId() 複製程式碼
5、停止執行緒
停止執行緒是在多執行緒開發時很重要的技術點。停止執行緒並不像break語句那樣乾脆,需要一些技巧性的處理。
在Java中有以下3種方法可以終止正在執行的執行緒:
1)使用退出標誌,使執行緒正常退出,也就是當run()方法完成後執行緒停止。
2)使用stop()方法強行終止執行緒,但是不推薦使用這個方法,因為該方法已經作廢過期,使用後可能產生不可預料的結果。
3)使用interrupt()方法中斷執行緒。
1.暴力法停止執行緒
呼叫stop()方法時會丟擲 java.lang.ThreadDeath 異常,但在通常的情況下,此異常不需要顯示地捕捉。
try { myThread.stop(); } catch (ThreadDeath e) { e.printStackTrace(); } 複製程式碼
方法stop()已經被作廢,因為如果強制讓執行緒停止執行緒則有可能使一些清理性的工作得不到完成。另外一個情況就是對鎖定的物件進行了“解鎖”,導致資料得不到同步的處理,出現數據不一致的情況。示例如下:
public class UserPass { private String username = "aa"; private String password = "AA"; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } synchronized public void println(String username, String password){ this.username = username; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.password = password; } public static void main(String[] args) throws InterruptedException { UserPass userPass = new UserPass(); Thread thread = new Thread(new Runnable() { @Override public void run() { userPass.println("bb","BB"); } }); thread.start(); Thread.sleep(500); thread.stop(); System.out.println(userPass.getUsername()+" "+userPass.getPassword()); } } 複製程式碼
執行結果:
bb AA 複製程式碼
2.異常法停止執行緒
使用interrupt()方法並不會真正的停止執行緒,呼叫interrupt()方法僅僅是在當前執行緒中打了一個停止的標記,並不是真的停止執行緒。
那我們如何判斷該執行緒是否被打上了停止標記,Thread類提供了兩種方法。
interrupted() 測試當前執行緒是否已經中斷。 isInterrupted() 測試執行緒是否已經中斷。 複製程式碼
interrupted() 方法 不止可以判斷當前執行緒是否已經中斷,而且可以會清除該執行緒的中斷狀態。而對於isInterrupted() 方法,只會判斷當前執行緒是否已經中斷,不會清除執行緒的中斷狀態。
僅靠上面的兩個方法可以通過while(!this.isInterrupted()){}
對程式碼進行控制,但如果迴圈外還有其它語句,程式還是會繼續執行的。這時可以丟擲異常從而使執行緒徹底停止。示例如下:
public class MyThread extends Thread { @Override public void run() { try { for (int i=0; i<50000; i++){ if (this.isInterrupted()) { System.out.println("已經是停止狀態了!"); throw new InterruptedException(); } System.out.println(i); } System.out.println("不丟擲異常,我會被執行的哦!"); } catch (Exception e) { //e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { MyThread myThread =new MyThread(); myThread.start(); Thread.sleep(100); myThread.interrupt(); } } 複製程式碼
列印結果:
... 2490 2491 2492 2493 已經是停止狀態了! 複製程式碼
注意
如果執行緒在sleep()狀態下被停止,也就是執行緒物件的run()方法含有sleep()方法,在此期間又執行了thread.interrupt()
方法,則會丟擲java.lang.InterruptedException: sleep interrupted
異常,提示休眠被中斷。
3.return法停止執行緒
return法很簡單,只需要把異常法中的丟擲異常更改為return即可。程式碼如下:
public class MyThread extends Thread { @Override public void run() { for (int i=0; i<50000; i++){ if (this.isInterrupted()) { System.out.println("已經是停止狀態了!"); return;//替換此處 } System.out.println(i); } System.out.println("不進行return,我會被執行的哦!"); } } 複製程式碼
不過還是建議使用“拋異常”來實現執行緒的停止,因為在catch塊中可以對異常的資訊進行相關的處理,而且使用異常能更好、更方便的控制程式的執行流程,不至於程式碼中出現多個return,造成汙染。
6、暫停執行緒
暫停執行緒意味著此執行緒還可以恢復執行。在Java多執行緒中,可以使用 suspend() 方法暫停執行緒,使用 resume()方法恢復執行緒的執行。
這倆方法已經和stop()一樣都被棄用了,因為如果使用不當,極易造成公共的同步物件的獨佔,使得其他執行緒無法訪問公共同步物件。示例如下:
public class MyThread extends Thread { private Integer i = 0; @Override public void run() { while (true) { i++; System.out.println(i); } } public Integer getI() { return i; } public static void main(String[] args) throws InterruptedException { MyThread myThread =new MyThread(); myThread.start(); Thread.sleep(100); myThread.suspend(); System.out.println("main end"); } } 複製程式碼
列印結果:
... 3398 3399 3400 3401 複製程式碼
執行上段程式永遠不會列印main end。出現這樣的原因是,當程式執行到 println() 方法內部停止時,PrintStream物件同步鎖未被釋放。方法 println() 原始碼如下:
public void println(String x) { synchronized (this) { print(x); newLine(); } } 複製程式碼
這導致當前PrintStream物件的println() 方法一直呈“暫停”狀態,並且鎖未被myThread執行緒釋放,而主執行緒中的程式碼System.out.println("main end") 還在傻傻的排隊等待,導致遲遲不能執行列印。
使用 suspend() 和 resume() 方法也容易因為執行緒的暫停而導致資料不同步的情況,示例如下:
public class UserPass2 { private String username = "aa"; private String password = "AA"; public String getUsername() { return username; } public String getPassword() { return password; } public void setValue(String username, String password){ this.username = username; if (Thread.currentThread().getName().equals("a")) { Thread.currentThread().suspend(); } this.password = password; } public static void main(String[] args) throws InterruptedException { UserPass2 userPass = new UserPass2(); new Thread(new Runnable() { @Override public void run() { userPass.setValue("bb","BB"); } },"a").start(); new Thread(new Runnable() { @Override public void run() { System.out.println(userPass.getUsername()+" "+userPass.getPassword()); } },"b").start(); } } 複製程式碼
列印結果:
bb AA 複製程式碼
7、yield()方法
yield() 方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。
public static void yield()暫停當前正在執行的執行緒物件,並執行其他執行緒。 複製程式碼
8、執行緒的優先順序
在作業系統中,執行緒可以劃分優先順序,優先順序較高的執行緒得到的CPU資源較多,也就是CPU優先執行優先順序較高的執行緒物件中的任務。
設定執行緒優先順序有助於幫“執行緒規劃器”確定在下一次選擇哪一個執行緒來優先執行。
設定執行緒優先順序使用setPriority()方法,此方法的JDK原始碼如下:
public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } } 複製程式碼
在Java中,執行緒優先順序劃分為1 ~ 10 這10個等級,如果小於1或大於10,則JDK丟擲異常。
從JDK定義的3個優先順序常量可知,執行緒優先順序預設為5。
public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; 複製程式碼
執行緒優先順序具有繼承性,比如A執行緒啟動B執行緒,則B執行緒的優先順序與A是一樣的。
執行緒優先順序具有規則性,執行緒的優先順序與在程式碼中執行start()方法的順序無關,與優先順序大小有關。
執行緒優先順序具有隨機性,CPU儘量使執行緒優先順序較高的先執行完,但無法百分百肯定。也就是說,執行緒優先順序較高的不一定比執行緒優先順序較低的先執行。
9、守護執行緒
在Java中有兩種執行緒,一種是使用者執行緒,一種守護執行緒。
什麼是守護執行緒?守護執行緒是一種特殊的執行緒,當程序中不存在非守護執行緒了,則守護執行緒自動銷燬。典型的守護執行緒就是垃圾回收執行緒,當程序中沒有非守護執行緒了,則垃圾回收執行緒也就沒有了存在的必要了,自動銷燬。可以簡單地說:任何一個守護執行緒都是非守護執行緒的保姆。
如何設定守護執行緒?通過Thread.setDaemon(false)設定為使用者執行緒,通過Thread.setDaemon(true)設定為守護執行緒。如果不設定屬性,預設為使用者執行緒。
thread.setDaemon(true); 複製程式碼
示例如下:
public class MyThread extends Thread { private int i = 0; @Override public void run() { try { while (true){ i++; System.out.println("i="+i); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { MyThread thread = new MyThread(); thread.setDaemon(true); thread.start(); Thread.sleep(5000); System.out.println("我離開後thread物件也就不再列印了"); } } 複製程式碼
列印結果:
i=1 i=2 i=3 i=4 i=5 我離開後thread物件也就不再列印了 複製程式碼