1 進程與線程
在Java語言之中最大的特點是支持多線程開發(也是爲數不多支持多線程的編程語言),所以在整個Java技術的學習裏面,如果不能對多線程的概念有一個全面並且細緻的瞭解,則在日後進行一些項目的設計過程之中,尤其是併發訪問設計之中就會出現嚴重的技術缺陷。
如果要想知道進程是什麼,首相就需要了解線程的概念,在傳統的DOS系統的時代,其本身有一個特徵:如果你電腦上出現了病毒,那麼所有的程序將無法執行,因爲傳統的DOS採用的是單進程處理,而單進程處理的最大特點,在同一個時間段上只允許一個程序在執行。
後來到了Windows時代就開始了多進程的設計,於是就表示在一個時間段上可以同時運行多個程序,並且這些程序將進行資源的輪流搶佔,所以在同一個時間段上會有多個程序依次執行,但在同一個時間點上只會有一個進程執行,後來多了多核CPU,由於可以處理的CPU多了,那麼即便有再多的進程出現,也可以比單核CPU處理的速度有所提升。
線程是在進程基礎之上劃分的更小的程序單元,線程是在進程的基礎上創建並且使用的,所以線程依賴於進程的支持,但是線程的啓動速度要比進程快許多,所以當使用多線程進行併發處理的時候,其執行的性能要高於進程。進程是在操作系統上的劃分,而線程是在進程上的劃分。
Java是多線程的編程語言,所以Java在進行併發訪問處理的時候可以得到更高的處理性能。如果要想在Java之中實現多線程的定義,那麼就需要有一個專門的線程主體類進行線程的執行任務的定義,而這個主體類的定義是有要求的,必須實現特定的接口或者繼承特定的父類纔可以完成。
2 Thread類實現多線程
Java裏面提供有一個java.lang.Thread的程序類,那麼只要一個類繼承了此類就表示這個類爲線程的主體類,但是並不是說這個類就可以實現多線程處理了,因爲還需要覆寫Thread類中提供的一個run()方法(public void run()),而這個方法就屬於線程的主方法。
範例:繼承Thread類實現多線程
//實現線程的主體類,繼承Thread類
//實現線程主體方法run()
class MyThread extends Thread{ //繼承Thread類,實現線程的主體類
public String title;
public MyThread() {}
public MyThread(String title) {
this.title = title;
}
@Override //實現線程主體方法run()
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.title + " run: i=" + i);
}
}
}
多線程要執行的功能都應該在run()方法中進行定義,需要說明的是:在正常情況下,如果要想使用一個類中的方法,那麼肯定要產生實例化對象,而後去調用類中提供的方法,但是run()方法是不能夠被直接調用的,因爲這裏面牽扯到操作系統的資源調度問題所以要想啓動多線程必須使用start()方法完成(public void start())。
範例:多線程啓動
public class ThreadDemo {
public static void main(String[] main) {
MyThread threadA = new MyThread("Thread-A");
MyThread threadB = new MyThread("Thread-B");
MyThread threadC = new MyThread("Thread-C");
threadA.start();
threadB.start();
threadC.start();
}
}
通過此時的調用可以發現,雖然調用了是start()方法,但是最終執行的是run()方法,並且所有的線程對象都是交替執行的。那麼爲什麼多線程啓動不直接使用run()方法而必須使用Thread類中的start()方法呢?下面是start()方法源碼分析:
public synchronized void start() {
if (threadStatus != 0) //線程狀態判斷
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); //在start()方法裏面調用了start0()方法
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
private native void start0(); //只定義了方法名稱,但是沒有實現
發現在start()方法裏面會拋出一個“IllegalThreadStateException”異常類對象,但是整個的程序並沒有使用throws或者是明確try…catch處理,因爲該異常一定是RuntimeExcption的子類,每一個線程類的對象只允許啓動一次,如果重複啓動則就拋出此異常,例如,下面的代碼就會拋出異常。
//重複進行線程的啓動
package cn.victor.demo;
class MyThread extends Thread{ //繼承Thread類,實現線程的主體類
public String title;
public MyThread() {}
public MyThread(String title) {
this.title = title;
}
@Override //實現線程主體方法run()
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.title + " run: i=" + i);
}
}
}
public class ThreadDemo {
public static void main(String[] main) {
MyThread threadA = new MyThread("Thread-A");
threadA.start();
threadA.start();
}
}
/*
Exception in thread "main"
java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Unknown Source)
at javasenior/cn.victor.demo.ThreadDemo.main(ThreadDemo.java:27)
*/
在Java程序執行的過程之中考慮到對於不同層次開發者的需求,所以其支持有本地的操作系統函數調用,而這項就被稱爲JNI(Java Native Interface)技術,但是Java開發過程之中並不推薦這樣使用,利用這項技術我們可以使用一些操作系統提供的底層函數進行一些特殊的處理,而在Thread類裏面提供的start0()就表示需要將此方法依賴於不同的操作系統實現。
任何情況下,只要定義了多線程,多線程的啓動永遠只有一種方案:Thread類中的start()方法
3 Runnable接口實現多線程
雖然可以通過Thread的繼承來實現多線程的定義,但是在Java程序裏面對於繼承永遠都是存在有單繼承侷限的,所以在Java裏面又提供有第二種多線程的主體定義結構形式:實現java.lang.Runnable
接口,此接口定義如下:
@FunctionalInterface //從JDK 1.8引入Lambda表達式之後就變爲了函數式接口
public interface Runnable{
public void run();
}
範例:通過Runnable實現多線程的主體類
class MyThread implements Runnable{ //實現Runnable接口,實現線程的主體類
public String title;
public MyThread() {}
public MyThread(String title) {
this.title = title;
}
@Override //實現線程主體方法run()
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(this.title + " run: i=" + i);
}
}
}
但是此時由於不在繼承Thread父類了,那麼對於此時的MyThread類中也就不再支持有start()這個繼承的方法,可是如果不使用Thread.start()方法是無法進行多線程啓動的,那麼這個時候就需要去觀察一下Thread類所提供的構造方法了。
(1)構造方法:public Thread(Runnable target);
範例:啓動多線程
public class ThreadDemo {
public static void main(String[] main) {
new Thread(new MyThread("Thread-A")).start();
new Thread(new MyThread("Thread-B")).start();
new Thread(new MyThread("Thread-C")).start();
}
}
這個時候的多線程實現裏面可以發現,由於只是實現了Runnable接口對象,所以此時線程主體類上就不再有單繼承的侷限,那麼這樣的設計纔是一個標準性的設計。
可以發現從JDK 1.8開始,Runable接口使用了函數式接口定義,所以也可以直接利用Lambda表達式進行線程類實現。
範例:利用Lambda實現多線程
public static void main(String[] main) {
for(int x = 0; x < 3; x++) {
String title = "Thread-" + x;
Runnable run = ()->{
for(int y = 0; y < 10; y++) {
System.out.println(title + " run: y=" + y);
}
};
new Thread(run).start();
}
}
//*************************************
public static void main(String[] main) {
for(int x = 0; x < 3; x++) {
String title = "Thread-" + x;
new Thread(()->{
for(int y = 0; y < 10; y++) {
System.out.println(title + " run: y=" + y);
}
}).start();
}
}
在以後的開發之中對於多線程的實現,優先考慮的就是Runnable接口實現,並且永恆都是通過Thread類對象啓動多線程。
4 Thread與Runnable關係
經過一系列的分析之後可以發現,在多線程的實現過程之中已經有了兩種做法:Thread類、Runnable接口,如果從代碼結構本身來講肯定使用Runnable是最方便的,因爲其可以避免單繼承的侷限,同時也可以更好的進行功能的擴充。
但是從結構上也需要來觀察Thread和Runnable的聯繫,打開Thread類的定義:
public class Thread extends Object implements Runnable{}
發現現在Thread類也是Runnable接口的子類,那麼在之前繼承Thread類的時候實際上覆寫的還是Runnable接口的run()方法,於是此時來觀察一下程序的類結構。
多線程的設計之中,使用了代理設計模式的結構,用戶自定義的線程主體只是負責項目核心功能的實現,而所有的輔助實現全部交由Thread類來處理。
在進行Thread啓動多線程的時候調用的是start()方法,而後找到的是run()方法,當通過Thread類的構造方法傳遞了一個Runnable接口對象的時候,那麼該接口對象將被Thread類中的target屬性所保存,在start()方法執行的時候會調用Thread類中run()方法,而這個run()方法去調用Runnable接口子類被覆寫過的run()方法。
多線程開發的本質實質上是在於多個線程可以進行同一資源的搶佔,那麼Thread主要描述的是線程,而資源的描述是通過Runnable完成的。
範例:利用買票程序來實現多個線程的資源併發訪問
package cn.victor.demo;
class MyThread implements Runnable{ //實現Runnable接口,實現線程的主體類
private int ticket = 5;
@Override //實現線程主體方法run()
public void run() {
for(int i = 0; i < 10; i++) {
if(this.ticket > 0) {
System.out.println("sale ticket" + this.ticket--);
}
}
}
}
public class ThreadDemo {
public static void main(String[] main) {
Runnable thread = new MyThread();
new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}
}
通過內存分析圖來分析本程序的執行結構。
5 Callable接口實現多線程
從最傳統的開發來講如果要進行多線程的實現肯定要依靠的就是Runnable,但是Runnable接口有一個缺點:當線程執行完畢之後無法獲取一個返回值,所以從JDK 1.5之後就提出了一個新的線程實現接口:java.util.concurrent.Callable接口,首先觀察接口的定義:
@FunctionalInterface
public interface Callable<V>{
public V call() throws Exception;
}
可以發現Callable定義的時候可以設置一個泛型,此泛型的類型就是返回的數據的類型,這樣的好處是可以避免向下轉型所帶來的安全隱患。
範例:使用Callable實現多線程
package cn.victor.demo;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyThread implements Callable<String>{
@Override
public String call() throws Exception {
for(int i =0; i < 10; i++) {
System.out.println("Thread run " + i);
}
return "Thread over";
}
}
public class ThreadDemo {
public static void main(String[] main)throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(new MyThread());
new Thread(futureTask).start();
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
面試題:請解釋Runnable與Callable的區別?
(1)Runnable是在JDK 1.0的時候提出的多線程的實現接口,而Callable是在JDK 1.5之後提出來的。
(2)java.lang.Runnable接口之中只提供一個run()方法,並且沒有返回值;
(3)java.util.concurrent.Callable接口提供有call()方法,可以有返回值;
6 多線程運行狀態
對於多線程的開發而言,編寫程序的過程之中總是按照:定義線程主體類,而後通過Thread類進行線程的啓動,但是不意味着調用了start()方法,線程就已經開始運行了,因爲整體的線程處理有自己的一套運行的狀態。
1、任何一個線程的對象都應該使用Thread類進行封裝,所以線程的啓動使用的是start(),但是啓動的時候實際上若干個線程都將進入到一種就緒狀態,現在並沒有執行;
2、進入到就緒狀態之後,就需要等待進行資源的調度,當某一個線程調度成功之後則進入到運行狀態(run()方法),但是所有的線程不可能一直持續執行下去,中間需要產生一些暫停的狀態,例如:某個線程執行一段時間之後就需要讓出資源,而後這個線程就進入到阻塞狀態,隨後重新迴歸到就緒狀態;
3、當run()方法執行完畢之後,實際上該線程的主要任務也就結束了,那麼此時就可以直接進入到停止狀態。