- Java多線程編程核心技術
- 高洪巖
- 2745字
- 2019-01-01 01:10:23
1.2 使用多線程
想學習一個技術就要“接近”它,所以在本節,首先用一個示例來接觸一下線程。
一個進程正在運行時至少會有1個線程在運行,這種情況在Java中也是存在的。這些線程在后臺默默地執行,比如調用public static void main()方法的線程就是這樣的,而且它是由JVM創建的。
創建示例項目callMainMethodMainThread,創建Test.java類。代碼如下:
package test; public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
程序運行后的效果如圖1-5所示。

圖1-5 主線程main
在控制臺中輸出的main其實就是一個名稱叫作main的線程在執行main()方法中的代碼。另外需要說明一下,在控制臺輸出的main和main方法沒有任何的關系,僅僅是名字相同而已。
1.2.1 繼承Thread類
在Java的JDK開發包中,已經自帶了對多線程技術的支持,可以很方便地進行多線程編程。實現多線程編程的方式主要有兩種,一種是繼承Thread類,另一種是實現Runnable接口。
但在學習如何創建新的線程前,先來看看Thread類的結構,如下:
public class Thread implements Runnable
從上面的源代碼中可以發現,Thread類實現了Runnable接口,它們之間具有多態關系。
其實,使用繼承Thread類的方式創建新線程時,最大的局限就是不支持多繼承,因為Java語言的特點就是單根繼承,所以為了支持多繼承,完全可以實現Runnable接口的方式,一邊實現一邊繼承。但用這兩種方式創建的線程在工作時的性質是一樣的,沒有本質的區別。
本節來看一下第一種方法。創建名稱為t1的Java項目,創建一個自定義的線程類MyThread.java,此類繼承自Thread,并且重寫run方法。在run方法中,寫線程要執行的任務的代碼如下:
package com.mythread.www; public class MyThread extends Thread { @Override public void run() { super.run(); System.out.println("MyThread"); } }
運行類代碼如下:
package test; import com.mythread.www.MyThread; public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); System.out.println("運行結束!"); } }
運行結果如圖1-6所示。

圖1-6 運行結果
從圖1-6中的運行結果來看,MyThread.java類中的run方法執行的時間比較晚,這也說明在使用多線程技術時,代碼的運行結果與代碼執行順序或調用順序是無關的。
線程是一個子任務,CPU以不確定的方式,或者說是以隨機的時間來調用線程中的run方法,所以就會出現先打印“運行結束!”后輸出“MyThread”這樣的結果了。
注意 如果多次調用start()方法,則會出現異常Exception in thread"main"java.lang.IllegalThreadStateException。
上面介紹了線程的調用的隨機性,下面將在名稱為randomThread的Java項目中演示線程的隨機性。
創建自定義線程類MyThread.java,代碼如下:
package mythread; public class MyThread extends Thread { @Override public void run() { try { for (int i = 0; i < 10; i++) { int time = (int) (Math.random() * 1000); Thread.sleep(time); System.out.println("run=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
再創建運行類Test.java,代碼如下:
package test; import mythread.MyThread; public class Test { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.setName("myThread"); thread.start(); for (int i = 0; i < 10; i++) { int time = (int) (Math.random() * 1000); Thread.sleep(time); System.out.println("main=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
在代碼中,為了展現出線程具有隨機特性,所以使用隨機數的形式來使線程得到掛起的效果,從而表現出CPU執行哪個線程具有不確定性。
Thread.java類中的start()方法通知“線程規劃器”此線程已經準備就緒,等待調用線程對象的run()方法。這個過程其實就是讓系統安排一個時間來調用Thread中的run()方法,也就是使線程得到運行,啟動線程,具有異步執行的效果。如果調用代碼thread.run()就不是異步執行了,而是同步,那么此線程對象并不交給“線程規劃器”來進行處理,而是由main主線程來調用run()方法,也就是必須等run()方法中的代碼執行完后才可以執行后面的代碼。
以異步的方式運行的效果如圖1-7所示。

圖1-7 隨機被執行的線程
另外還需要注意一下,執行start()方法的順序不代表線程啟動的順序。創建測試用的項目名稱為z,類MyThread.java代碼如下:
package extthread; public class MyThread extends Thread { private int i; public MyThread(int i) { super(); this.i = i; } @Override public void run() { System.out.println(i); } }
運行類Test.java代碼如下:
package test; import extthread.MyThread; public class Test { public static void main(String[] args) { MyThread t11 = new MyThread(1); MyThread t12 = new MyThread(2); MyThread t13 = new MyThread(3); MyThread t14 = new MyThread(4); MyThread t15 = new MyThread(5); MyThread t16 = new MyThread(6); MyThread t17 = new MyThread(7); MyThread t18 = new MyThread(8); MyThread t19 = new MyThread(9); MyThread t110 = new MyThread(10); MyThread t111 = new MyThread(11); MyThread t112 = new MyThread(12); MyThread t113 = new MyThread(13); t11.start(); t12.start(); t13.start(); t14.start(); t15.start(); t16.start(); t17.start(); t18.start(); t19.start(); t110.start(); t111.start(); t112.start(); t113.start(); } }
程序運行后的結果如圖1-8所示。

圖1-8 線程啟動順序與start()執行順序無關
1.2.2 實現Runnable接口
如果欲創建的線程類已經有一個父類了,這時就不能再繼承自Thread類了,因為Java不支持多繼承,所以就需要實現Runnable接口來應對這樣的情況。
創建項目t2,繼續創建一個實現Runnable接口的類MyRunnable,代碼如下:
package myrunnable; public class MyRunnable implements Runnable { @Override public void run() { System.out.println("運行中!"); } }
如何使用這個MyRunnable.java類呢?這就要看一下Thread.java的構造函數了,如圖1-9所示。

圖1-9 Thread構造函數
在Thread.java類的8個構造函數中,有兩個構造函數Thread(Runnable target)和Thread(Runnable target,String name)可以傳遞Runnable接口,說明構造函數支持傳入一個Runnable接口的對象。運行類代碼如下:
public class Run { public static void main(String[] args) { Runnable runnable=new MyRunnable(); Thread thread=new Thread(runnable); thread.start(); System.out.println("運行結束!"); } }
運行結果如圖1-10所示。

圖1-10 運行結果
圖1-10所示的打印結果沒有什么特殊之處。
使用繼承Thread類的方式來開發多線程應用程序在設計上是有局限性的,因為Java是單根繼承,不支持多繼承,所以為了改變這種限制,可以使用實現Runnable接口的方式來實現多線程技術。這也是上面的示例介紹的知識點。
另外需要說明的是,Thread.java類也實現了Runnable接口,如圖1-11所示。

圖1-11 類Thread實現Runnable接口
那也就意味著構造函數Thread(Runnable target)不光可以傳入Runnable接口的對象,還可以傳入一個Thread類的對象,這樣做完全可以將一個Thread對象中的run()方法交由其他的線程進行調用。
1.2.3 實例變量與線程安全
自定義線程類中的實例變量針對其他線程可以有共享與不共享之分,這在多個線程之間進行交互時是很重要的一個技術點。
(1)不共享數據的情況
不共享數據的情況如圖1-12所示。

圖1-12 不共享數據
下面通過一個示例來看下數據不共享情況。
創建實驗用的Java項目,名稱為t3,MyThread.java類代碼如下:
public class MyThread extends Thread { private int count = 5; public MyThread(String name) { super(); this.setName(name);//設置線程名稱 } @Override public void run() { super.run(); while (count > 0) { count--; System.out.println("由 " + this.currentThread().getName() + " 計算,count=" + count); } } }
運行類Run.java代碼如下:
public class Run { public static void main(String[] args) { MyThread a=new MyThread("A"); MyThread b=new MyThread("B"); MyThread c=new MyThread("C"); a.start(); b.start(); c.start(); } }
不共享數據運行結果如圖1-13所示。

圖1-13 不共享數據的運行結果
由圖1-13可以看到,一共創建了3個線程,每個線程都有各自的count變量,自己減少自己的count變量的值。這樣的情況就是變量不共享,此示例并不存在多個線程訪問同一個實例變量的情況。
如果想實現3個線程共同對一個count變量進行減法操作的目的,該如何設計代碼呢?
(2)共享數據的情況
共享數據的情況如圖1-14所示。

圖1-14 共享數據
共享數據的情況就是多個線程可以訪問同一個變量,比如在實現投票功能的軟件時,多個線程可以同時處理同一個人的票數。
下面通過一個示例來看下數據共享情況。
創建t4測試項目,MyThread.java類代碼如下:
public class MyThread extends Thread { private int count=5; @Override public void run() { super.run(); count--; //此示例不要用for語句,因為使用同步后其他線程就得不到運行的機會了, //一直由一個線程進行減法運算 System.out.println("由 "+this.currentThread().getName()+" 計算,count="+count); } }
運行類Run.java代碼如下:
public class Run { public static void main(String[] args) { 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(); } }
運行結果如圖1-15所示。

圖1-15 共享數據運行結果
從圖1-15中可以看到,線程A和B打印出的count值都是3,說明A和B同時對count進行處理,產生了“非線程安全”問題。而我們想要得到的打印結果卻不是重復的,而是依次遞減的。
在某些JVM中,i--的操作要分成如下3步:
1)取得原有i值。
2)計算i-1。
3)對i進行賦值。
在這3個步驟中,如果有多個線程同時訪問,那么一定會出現非線程安全問題。
其實這個示例就是典型的銷售場景:5個銷售員,每個銷售員賣出一個貨品后不可以得出相同的剩余數量,必須在每一個銷售員賣完一個貨品后其他銷售員才可以在新的剩余物品數上繼續減1操作。這時就需要使多個線程之間進行同步,也就是用按順序排隊的方式進行減1操作。更改代碼如下:
public class MyThread extends Thread { private int count=5; @Override synchronized public void run() { super.run(); count--; System.out.println("由 "+this.currentThread().getName()+" 計算,count="+count); } }
重新運行程序,就不會出現值一樣的情況了,如圖1-16所示。

圖1-16 方法調用被同步
通過在run方法前加入synchronized關鍵字,使多個線程在執行run方法時,以排隊的方式進行處理。當一個線程調用run前,先判斷run方法有沒有被上鎖,如果上鎖,說明有其他線程正在調用run方法,必須等其他線程對run方法調用結束后才可以執行run方法。這樣也就實現了排隊調用run方法的目的,也就達到了按順序對count變量減1的效果了。synchronized可以在任意對象及方法上加鎖,而加鎖的這段代碼稱為“互斥區”或“臨界區”。
當一個線程想要執行同步方法里面的代碼時,線程首先嘗試去拿這把鎖,如果能夠拿到這把鎖,那么這個線程就可以執行synchronize里面的代碼。如果不能拿到這把鎖,那么這個線程就會不斷地嘗試拿這把鎖,直到能夠拿到為止,而且是有多個線程同時去爭搶這把鎖。
本節中出現了一個術語“非線程安全”。非線程安全主要是指多個線程對同一個對象中的同一個實例變量進行操作時會出現值被更改、值不同步的情況,進而影響程序的執行流程。下面再用一個示例來學習一下如何解決“非線程安全”問題。
創建t4_threadsafe項目,來實現一下非線程安全的環境。LoginServlet.java代碼如下:
package controller; //本類模擬成一個Servlet組件 public class LoginServlet { private static String usernameRef; private static String passwordRef; public static void doPost(String username, String password) { try { usernameRef = username; if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
線程ALogin.java代碼如下:
package extthread; import controller.LoginServlet; public class ALogin extends Thread { @Override public void run() { LoginServlet.doPost("a", "aa"); } }
線程BLogin.java代碼如下:
package extthread; import controller.LoginServlet; public class BLogin extends Thread { @Override public void run() { LoginServlet.doPost("b", "bb"); } }
運行類Run.java代碼如下:
public class Run { public static void main(String[] args) { ALogin a = new ALogin(); a.start(); BLogin b = new BLogin(); b.start(); } }
程序運行后的效果如圖1-17所示。

圖1-17 非線程安全
解決這個“非線程安全”的方法也是使用synchronized關鍵字。更改代碼如下:
synchronized public static void doPost(String username, String password) { try { usernameRef = username; if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
程序運行后效果如圖1-18所示。

圖1-18 排隊進入方法
1.2.4 留意i--與System.out.println()的異常
在前面章節中,解決非線程安全問題使用的是synchronized關鍵字,本節將通過程序案例細化一下println()方法與i++聯合使用時“有可能”出現的另外一種異常情況,并說明其中的原因。
創建名稱為sameNum的項目,自定義線程MyThread.java代碼如下:
package extthread; public class MyThread extends Thread { private int i = 5; @Override public void run() { System.out.println("i=" + (i--) + " threadName=" + Thread.currentThread().getName()); //注意:代碼i--由前面項目中單獨一行運行改成在當前項目中在println()方法中直接進行打印 } }
運行類Run.java代碼如下:
package test; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread run = new MyThread(); Thread t1 = new Thread(run); Thread t2 = new Thread(run); Thread t3 = new Thread(run); Thread t4 = new Thread(run); Thread t5 = new Thread(run); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
程序運行后根據概率還是會出現非線程安全問題,如圖1-19所示。

圖1-19 出現非線程安全問題
本實驗的測試目的是:雖然println()方法在內部是同步的,但i--的操作卻是在進入println()之前發生的,所以有發生非線程安全問題的概率,如圖1-20所示。

圖1-20 println內部同步
所以,為了防止發生非線程安全問題,還是應繼續使用同步方法。