所謂的多線程編程本質上是並發編程,並發編程的本質是指發揮出所有硬件的最大性能。
Java 是為數不多的真正支持有多線程並發編程的開發語言。所以Java 在整體的處理性能上是最高的。
如果要了解線程的性能問題,那么首先要先解決的就是清楚什么叫做進程?
從計算機發展的歷史來講,傳統的硬件只有一個 CPU(單核心的CPU),於是為了發揮出硬件的全部性能,引入了多進程的編程模式。
多進程是指在沒有擴展原始系統硬件資源的情況下,利用一些算法,可以實現多個進程的並行執行。在同一個時間段上,會有多個進程並發執行,但是在同一個時間點上只會有一個進程執行。
線程實在進程基礎上的進一步划分,可以達到更快的處理性能,任何一個進程的啟動速度實際上都是非常緩慢的,所以線程本質上的設計性能上要遠遠高於進程,但是線程不可能離開進程存活。
每一個進程的執行都必須有一個獨立執行它的 CPU,所以所有的資源共享里只有 CPU是無法進行共享的,每個進程都有一個自己的中央處理單元。如果要想實現CPU的共享,那么就必須利用線程來描述。
隨着硬件和軟件的發展,硬件中的CPU出現了多核的狀態,理論上多核CPU的多進程執行稱為並行編程,並非所有的CPU都可以超出若干個線程出來,一般來講,每一塊CPU 只會有一個線程執行,但是有些CPU 可以使用超線程技術,設計出若干個多線程的執行狀態。
在進程和線程的概念之上實際上還有一個所謂的”纖程“,在線程基礎上的進一步划分,但有些地方稱之為”協程“。Java並沒有支持多協程編程(以后可能有)。現在比較常見的編程語言里:Kotlin(Android 第二代產品)和Python都是支持多協程編程的。
所有的Java 程序執行需要通過一個主方法完成,主方法會作為程序的起點。
若要進行多線程編程,也要有一個線程的起點結構。這個結構就稱為線程類,所有的線程類都是有繼承要求的,可以有三種實現方式:
- 繼承Thread類;
- 實現Runnable接口;
- 實現Callable接口;
1)繼承Thread實現多線程
java.lang.Thread 是由系統定義的一個線程處理類,任何的子類只需要繼承此類,就可以得到一個線程處理的子類,在繼承時一定要覆寫Thread 類中的 run() 方法,那么這個方法成為線程啟動的主方法存在。
范例:定義一個線程的主體類:
如果要想正常的進行多線程的並發執行,那么就要調用本機操作系統提供的底層的函數支持,所以多線程的啟動並不是依靠 run()完成的,他需要通過 Start()方法進行啟動。而所有的Start() 啟動后將調用 run()方法中定義的方法體。
public void start()
范例:啟動多線程
class MyThread extends Thread{ private String name; public MyThread(String name){ this.name =name; } @Override public void run() { //覆寫run()方法 for (int i =0; i<50;i++){ System.out.println("【"+this.name+"- 線程】運行,i="+i); } } } public class MyBlog2 { public static void main(String[] args) { MyThread threadA = new MyThread("線程A"); MyThread threadB = new MyThread("線程B"); MyThread threadC = new MyThread("線程C"); threadA.start(); //通過Thread 類繼承而來 threadB.start(); threadC.start(); } }
運行結果(隨機抽取):
................... 【線程A- 線程】運行,i=49 【線程C- 線程】運行,i=0 【線程B- 線程】運行,i=0 【線程C- 線程】運行,i=1 【線程B- 線程】運行,i=1
..................
通過執行的結果,所有的線程對象屬於交替的執行過程,並且都在交替執行run() 方法定義的方法體。進一步的解釋:關於start() 方法?
首先觀察start() 的實現源代碼:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); @Override public void run() { if (target != null) { target.run(); } }
在每一個start()方法里都會拋出 “IllegalThreadStateException” 異常,而此異常是 RuntimeException 的一個子類,用戶可以根據自己的需要選擇性處理,此異常在重復啟動多線程的時候才會拋出。
此時會發現 start()方法里定義了一個 start0()方法,而start0()方法沒有方法體,但是使用了一個native的關鍵字定義,此關鍵字的作用在於此操作將交由底層實現。
在Java開發領域存在一個稱為 JNI(Java Native Iterface)的技術,利用Java 技術調用底層操作系統函數,但是一般JavaEE 中這個開發較少見,因為這樣寫,Java的可移植性就會喪失,在之前的Android 開發里,JNI技術比較常見。
public class MyBlog2 { public static void main(String[] args) { MyThread threadA = new MyThread("線程A"); MyThread threadB = new MyThread("線程B"); MyThread threadC = new MyThread("線程C"); threadA.start(); //通過Thread 類繼承而來 threadB.start(); threadC.start(); threadC.start(); //讓他重復啟動 } }
Exception in thread "main" java.lang.IllegalThreadStateException at java.base/java.lang.Thread.start(Thread.java:794) at ThreadTest.MyBlog2.main(MyBlog2.java:24) 【線程A- 線程】運行,i=0 【線程C- 線程】運行,i=0 ....................................
//省略下面。。
一個線程只允許啟動一次。
2)實現 Runnable 接口
除了使用 Thread 類實現多線程之外,也可以使用java.lang .Runnable 接口來完成,首先觀察一下Runnable 接口。
@FunctionalInterface public interface Runnable{ public void run(); }
在Runnable 接口中同樣存在有一個 run() 方法,此方法作為線程主方法存在。
使用Runnable實現多線程:
class MyThread implements Runnable{ private String name; public MyThread(String name){ this.name =name; } @Override public void run() { //覆寫run()方法 for (int i =0; i<50;i++){ System.out.println("【"+this.name+"- 線程】運行,i="+i); } } }
在之前繼承了 Thread類實現的多線程,會自動將父類的 start方法繼承而來,但若使用Runnable來實現,該接口並沒有提供 start() 方法,同時關鍵型的問題是:多線程的啟動只能夠依靠Thread 類的 start() 方法。所以此時關注一下Thread類里提供的構造方法:
public Thread(Runnable target)
這個構造方法里面需要接受 Runnable 接口對象的實例。那么此時只需要按照標准調用即可。
范例:啟動多線程:
public class MyBlog2 { public static void main(String[] args) { MyThread threadA = new MyThread("線程A"); MyThread threadB = new MyThread("線程B"); MyThread threadC = new MyThread("線程C"); new Thread(threadA).start(); new Thread(threadB).start(); new Thread(threadC).start(); } }
//程序執行結果(隨機抽取):
【線程B- 線程】運行,i=0 【線程C- 線程】運行,i=0 【線程C- 線程】運行,i=1 【線程C- 線程】運行,i=2 【線程C- 線程】運行,i=3 //..........省略后面的結果了。。。。。。
那么此時就實現了與之前完全相同的操作功能,,但是很明顯這樣的實現避免了繼承帶來的單繼承的局限,所以更適合項目的編寫,同時在JDK1.8之后,Runnable成為了一個函數時接口,所以此時代碼也可以使用 Lambda表達式進行定義。
范例:使用Lambda 實現多線程:
public class MyBlog2 { public static void main(String[] args) { String names[] = new String[]{"線程A","線程B","線程C"}; for (String name :names) { new Thread(()->{ for (int i =0; i<50;i++){ System.out.println("【"+name+"- 線程】運行,i="+i); } }).start(); } } }
運行結果(部分抽取): 【線程A- 線程】運行,i=3 【線程B- 線程】運行,i=26 【線程C- 線程】運行,i=30 【線程B- 線程】運行,i=27 【線程B- 線程】運行,i=28
從JDK1.8 之后 Lambda表達式的出現實際上可以達到簡化線程類定義的功能。
- Thread 類與Runnable 接口的關系
在JDK 1.0 的時代就提供了 Thread類和 Runnable 接口,所以這兩個實現方案就經常被人拿來比較,我將從兩者實現的關聯及區別進行說明,首先觀察一下 Thread 類的定義結構:
public class Thread extends Object implements Runnable
可以發現此時的 Thread 類也是 Runnable 接口的子類,所以可以得到如下結構圖:
解釋問題:為什么Thread 接受了 Runnable 接口對象之后會去調用真實線程中的 run() 方法?
1、關注 Thread類中接受 Runnable 接口對象的構造方法:
public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); }
this.target = target;
private Runnable target;
當Runnable 接口傳到 Thread 類中之后,會自動利用Thread類中的target 屬性保存 Runnable 接口實例。
2、觀察 Thread類中的 run() 方法(調用start()就調用Thread 類中的run()方法)
@Override public void run(){ if (target != null){ target.run(); } }
可以發現在 Thread.run()方法定義的時候會判斷是否有 target 實例,如果有實例,則會調用相應的run()方法;
在兩種多線程實現方法下,實際上Runnable 接口相比較 Thread 而言,可以更加方便的描述數據共享的概念,即多個線程並行操作同一個資源(方法體)。
范例:觀察資源共享:
class Mythread implements Runnable{ private String name; private int ticket = 20; //共買20張票 @Override public void run() { for (int x = 0; x < 50; x++) { if (this.ticket >0 ){ System.out.println("- 賣票:"+ticket--); } } } } public class MyBlog1 { public static void main(String[] args) { Mythread threadBody =new Mythread();//定義多線程的公共處理 new Thread(threadBody).start(); new Thread(threadBody).start(); new Thread(threadBody).start(); } }
運行結果(部分抽取):
- 賣票:20
- 賣票:19
- 賣票:18
- 賣票:16
- 賣票:17
- 賣票:14
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name =name;
}
@Override
public void run() {
//覆寫run()方法
for (int i =0; i<50;i++){
System.out.println("【"+this.name+"- 線程】運行,i="+i);
}
}
}
public class MyBlog2 {
public static void main(String[] args) {
MyThread threadA = new MyThread("線程A");
MyThread threadB = new MyThread("線程B");
MyThread threadC = new MyThread("線程C");
threadA.start(); //通過Thread 類繼承而來
threadB.start();
threadC.start();
}
}