這篇文章咱們總結一下 Java線程的基礎,打好基礎,后面幾篇再學多線程的同步控制中的各種鎖、線程通信等方面的知識時就會覺得更容易些。
本文的大綱如下:
在計算機系統里每個進程(Process)都代表著一個運行著的程序,比如打開微信,系統就會為微信開一個進程--進程是對運行時程序的封裝,是系統進行資源調度和分配的基本單位。
一個進程下可以有很多個線程,還拿微信舉例子,我們用微信的時候除了給好友收發消息,還可以在里面看公眾號,看公眾號的時候,也不影響我們的微信收到其他人發給我們的消息,這就以為著運行的微信的進程,還開啟了多個線程來同時完成這些子任務。
線程是進程的子任務,是CPU調度和分派的基本單位,用于保證程序的實時性,實現進程內部的并發,線程同時也是操作系統可識別的最小執行和調度單位。
在 Java 里線程是程序執行的載體,我們寫的代碼就是由線程運行的。有的時候為了增加程序的執行效率,我們不得不使用多線程進行編程,雖然多線程能最大化程序利用 CPU 的效率,但是程序用多寫成進行任務處理,也是BUG的高發地,主要原因還是多線程環境下有些問題一旦被疏忽就會造成執行結果不符合預期的BUG。
平時我們寫代碼思考問題時的習慣思維是單線程的,寫多線程的時候得刻意切換一下才行,這就要求我們要了解清楚線程在不同運行條件下所表現出來的行為才行。
首先我們來看一下在 Java 中是怎么表示線程的。
到目前為止,我們寫的所有 Java 程序代碼都是在由JVM給創建的主線程(Main Thread) 中執行的。Java 線程就像一個虛擬 CPU,可以在運行的 Java 應用程序中執行 Java 代碼。當一個 Java 應用程序啟動時,它的入口方法 main() 方法由主線程執行。主線程(Main Thread)是一個由 Java 虛擬機創建的運行應用程序的特殊線程。
我們在 Java 里萬物皆對象,所以系統的線程在 Java 里也是用對象表示的,線程是類 java.lang.Thread 類或者其子類的實例。在 Java 應用程序內部, 我們可以通過線程對象創建和啟動更多線程,這些線程可以與主線程并行執行應用程序的代碼。
下面看一下怎么在 Java 程序里創建和啟動線程。
在 Java 中創建一個線程,就是創建一個Thread類的實例
Thread thread = new Thread();
啟動線程就是調用Thread對象的start()方法
thread.start();
當然,這個例子沒有指定線程要執行的代碼,所以線程將在啟動后立即停止。 讓線程執行邏輯,需要給線程對象指定執行體。
有兩種方法可以給線程指定要執行的代碼。
其實,還有第三種給線程指定執行代碼的方法,不過細究下來算是第二種方法的特殊使用方式,下面我們看看這三種指定線程執行方法體的方式,以及它們之間的區別。
通過繼承Thread類創建線程的步驟:
(1) 定義 Thread 類的子類,并覆蓋該類的 run() 方法。run() 方法的方法體就代表了線程要完成的任務,因此把 run() 方法稱為執行體。
(2) 創建 Thread 子類的實例,即創建了線程對象。
(3) 調用線程對象的 start() 方法來啟動該線程。
package com.learnthread;public class ThreadFirstRunDemo { public static void main(String[] args) { // 實例化線程對象 MyThread threadA = new MyFirstThread("線程-A"); MyThread threadB = new MyFirstThread("線程-B"); // 啟動線程 threadA.start(); threadB.start(); } static class MyFirstThread extends Thread { private int ticket = 5; MyThread(String name) { super(name); } @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票"); ticket--; } } }}
上面的程序,主線程啟動調用A、B兩個線程的start() 后,并沒有通過調用wait() 等待他們執行結束。A、B兩個線程的執行體,會并發地被系統執行,等線程都直接結束后,程序才會退出。
Runnable 接口的定義如下,只有一個 run() 方法的定義:
package java.lang;public interface Runnable { public abstract void run();}
其實,Thread 類實現的也是 Runnable 接口。 在 Thread 類的重載構造方法里,支持接收一個實現了 Runnale 接口的對象作為其 target 參數來初始化線程對象。
public class Thread implements Runnable { ... public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } ... public Thread(Runnable target, String name) { init(null, target, name, 0); } ...}
通過實現 Runnable 接口創建線程的步驟如下:
(1) 定義 Runnable 接口的實現,實現該接口的 run 方法。該 run 方法的方法體同樣是線程的執行體。
(2) 創建 Runnable 實現類的實例,并以此實例作為 Thread 的 target 參數來創建 Thread 對象,該 Thread 對象才是真正的線程對象。
(3) 調用線程對象的 start 方法來啟動線程并執行。
package com.learnthread;public class RunnableThreadDemo { public static void main(String[] args) { // 實例化線程對象 Thread threadA = new Thread(new MyThread(), "Runnable 線程-A"); Thread threadB = new Thread(new MyThread(), "Runnable 線程-B"); // 啟動線程 threadA.start(); threadB.start(); } static class MyThread implements Runnable { private int ticket = 5; @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票"); ticket--; } } }}
運行上面例程會有以下輸出,同樣程序會在所以線程執行完后退出。
Runnable 線程-B 賣出了第 5 張票Runnable 線程-B 賣出了第 4 張票Runnable 線程-B 賣出了第 3 張票Runnable 線程-B 賣出了第 2 張票Runnable 線程-B 賣出了第 1 張票Runnable 線程-A 賣出了第 5 張票Runnable 線程-A 賣出了第 4 張票Runnable 線程-A 賣出了第 3 張票Runnable 線程-A 賣出了第 2 張票Runnable 線程-A 賣出了第 1 張票Process finished with exit code 0
既然是給 Thread 傳遞 Runnable 接口的實現對象即可,那么除了普通的定義類實現接口的方式,我們還可以使用匿名類和 Lambda 表達式的方式來定義 Runnable 的實現。
Thread threadA = new Thread(new Runnable() { private int ticket = 5; @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票"); ticket--; } }}, "Runnable 線程-A");
Runnable runnable = () -> { System.out.println("Lambda Runnable running"); };Thread threadB = new Thread(runnable, "Runnable 線程-B");
因為,Lambda 是無狀態的,定義不了內部屬性,這里就舉個簡單的打印一行輸出的例子了,理解一下這種用法即可。
上面兩種方法雖然能指定線程執行體里要執行的任務,但是都沒有返回值,如果想讓線程的執行體方法有返回值,且能被外部創建它的父線程獲取到返回值,就需要結合J.U.C 里提供的 Callable、Future 接口來實現線程的執行體方法才行。
J.U.C 是 java.util.concurrent 包的縮寫,提供了很多并發編程的工具類,后面會詳細學習。
Callable 接口只聲明了一個方法,這個方法叫做 call():
package java.util.concurrent;public interface Callable<V> { V call() throws Exception;}
Future 就是對于具體的 Callable 任務的執行進行取消、查詢是否完成、獲取執行結果的。可以通過 get 方法獲取 Callable 的 call 方法的執行結果,但是要注意該方法會阻塞直到任務返回結果。
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;}
Java 的 J.U.C 里給出了 Future 接口的一個實現 FutureTask,它同時實現了 Future 和 Runnable 接口,所以,FutureTask 既可以作為 Runnable 被線程執行,又可以作為 Future 得到 Callable 的返回值。
下面是一個 Callable 實現類和 FutureTask 結合使用讓主線程獲取子線程執行結果的一個簡單的示例:
package com.learnthread;import java.util.concurrent.Callable;import java.util.concurrent.FutureTask;public class CallableDemo implements Callable<Integer> { @Override public Integer call() { int i = 0; for (i = 0; i < 20; i++) { if (i == 5) { break; } System.out.println(Thread.currentThread().getName() + " " + i); } return i; } public static void main(String[] args) { CallableDemo tt = new CallableDemo(); FutureTask<Integer> ft = new FutureTask<>(tt); Thread t = new Thread(ft); t.start(); try { System.out.println(Thread.currentThread().getName() + " " + ft.get()); } catch (Exception e) { e.printStackTrace(); } }}
上面我們把 FutureTask 作為 Thread 構造方法的 Runnable 類型參數 target 的實參,在它的基礎上創建線程, 執行邏輯。所以本質上 Callable + FutureTask 這種方式也是第二種通過實現 Runnable 接口給線程指定執行體的,只不過是由 FutureTask 包裝了一層,由它的 run 方法再去調用 Callable 的 call 方法。例程運行后的輸出如下:
Thread-0 0Thread-0 1Thread-0 2Thread-0 3Thread-0 4main 5
Callable 更常用的方式是結合線程池來使用,在線程池接口 ExecutorService 中定義了多個可接收 Callable 作為線程執行任務的方法 submit、invokeAny、invokeAll 等,這個等學到線程池了我們再去學習。
在剛開始接觸和學習 Java 線程相關的知識時,一個常見的錯誤是,在創建線程的線程里,調用 Thread 對象的 run() 方法而不是調用 start() 方法。
Runnable myRunnable = new Runnable() { @Override public void run() { System.out.println("Anonymous Runnable running"); }};Thread newThread = new Thread(myRunnable);newThread.run(); // 應該調用 newThread.start();
起初你可能沒有注意到這么干有啥錯,因為 Runnable 的 run() 方法正常地被執行,輸出了我們想要的結果。
但是,這么做 run() 不會由我們剛剛創建的新線程執行,而是由創建 newThread 對象的線程執行的 。要讓新創建的線程--newThread 調用 myRunnable 實例的 run() 方法,必須調用 newThread.start() 方法才行。
Thread 線程常用的方法有以下這些:
方法 | 描述 |
run | 線程的執行實體,不需要我們主動調用,調用線程的start() 就會執行run() 方法里的執行體 |
start | 線程的啟動方法。 |
Thread.currentThread | Thread 類提供的靜態方法,返回對當前正在執行的線程對象的引用。 |
setName | 設置線程名稱。 |
getName | 獲取線程名稱。 |
setPriority | 設置線程優先級。Java 中的線程優先級的范圍是 [1,10],一般來說,高優先級的線程在運行時會具有優先權。可以通過 thread.setPriority(Thread.MAX_PRIORITY) 的方式設置,默認優先級為 5。 |
getPriority | 獲取線程優先級。 |
setDaemon | 設置線程為守護線程。 |
isDaemon | 判斷線程是否為守護線程。 |
isAlive | 判斷線程是否啟動。 |
interrupt | 中斷線程的運行。 |
Thread.interrupted | 測試當前線程是否已被中斷。 |
join | 可以使一個線程強制運行,線程強制運行期間,其他線程無法運行,必須等待此線程完成之后才可以繼續執行。 |
Thread.sleep | 靜態方法。將當前正在執行的線程休眠。 |
Thread.yield | 靜態方法。將當前正在執行的線程暫停,讓出CPU,讓其他線程執行。 |
使用 Thread.sleep 方法可以使得當前正在執行的線程進入休眠狀態。 使用 Thread.sleep 需要向其傳入一個整數值,這個值表示線程將要休眠的毫秒數。 Thread.sleep 方法可能會拋出 InterruptedException,因為異常不能跨線程傳播回主線程中,因此必須在本地進行處理。線程中拋出的其它異常也同樣需要在本地進行處理。
public class ThreadSleepDemo { public static void main(String[] args) { new Thread(new MyThread("線程A", 500)).start(); new Thread(new MyThread("線程B", 1000)).start(); new Thread(new MyThread("線程C", 1500)).start(); } static class MyThread implements Runnable { /** 線程名稱 */ private String name; /** 休眠時間 */ private int time; private MyThread(String name, int time) { this.name = name; this.time = time; } @Override public void run() { try { // 休眠指定的時間 Thread.sleep(this.time); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name + "休眠" + this.time + "毫秒。"); } }}
上面例程開啟了3個線程,在各自的線程執行體里讓各自線程休眠了 500、1000 和 1500 ms ,線程 C 休眠結束后,整個程序退出。
線程A休眠500毫秒。線程B休眠1000毫秒。線程C休眠1500毫秒。Process finished with exit code 0
當一個線程運行時,另一個線程可以直接通過 interrupt 方法中斷其運行狀態。
public class ThreadInterruptDemo { public static void main(String[] args) { MyThread mt = new MyThread(); // 實例化Runnable實現類的對象 Thread t = new Thread(mt, "線程"); // 實例化Thread對象 t.start(); // 啟動線程 try { Thread.sleep(2000); // 主線程休眠2秒 } catch (InterruptedException e) { System.out.println("主線程休眠被終止"); } t.interrupt(); // 中斷 mt 線程的執行 } static class MyThread implements Runnable { @Override public void run() { System.out.println("1、進入run()方法"); try { Thread.sleep(10000); // 線程休眠10秒 System.out.println("2、已經完成了休眠"); } catch (InterruptedException e) { System.out.println("3、MyThread線程休眠被終止"); return; // 返回調用處 } System.out.println("4、run()方法正常結束"); } }}
如果一個線程的 run 方法執行一個無限循環,并且沒有執行 sleep 等會拋出 InterruptedException 的操作,那么調用線程的 interrupt 方法就無法使線程提前結束。
不過調用 interrupt 方法會設置線程的中斷標記,此時被設置中斷標記的線程再調用 interrupted 方法會返回 true。因此可以在線程的執行體循環體中使用 interrupted 方法來判斷當前線程是否處于中斷狀態,從而提前結束線程。
看下面這個,可以有效終止線程執行的示例:
package com.learnthread;import java.util.concurrent.TimeUnit;public class ThreadInterruptEffectivelyDemo { public static void main(String[] args) throws Exception { MyTask task = new MyTask(); Thread thread = new Thread(task, "線程-A"); thread.start(); TimeUnit.MILLISECONDS.sleep(50); thread.interrupt(); } private static class MyTask implements Runnable { private volatile long count = 0L; @Override public void run() { System.out.println(Thread.currentThread().getName() + " 線程啟動"); // 通過 Thread.interrupted 和 interrupt 配合來控制線程終止 while (!Thread.interrupted()) { System.out.println(count++); } System.out.println(Thread.currentThread().getName() + " 線程終止"); } }}
主線程在啟動線程-A后,主動休眠50毫秒,線程-A的執行體里會不斷打印計數器的值,等休眠結束后主線程通過調用線程-A的 interrupt 方法設置了線程的中斷標記,這時線程-A的執行體中通過 Thread.interrupted() 就能判斷出線程被設置了中斷狀態,隨后結束執行退出。
(1) 什么是守護線程?
(2) 為什么需要守護線程?
守護線程的優先級比較低,用于為系統中的其它對象和線程提供服務。典型的應用就是垃圾回收器。
(3) 如何使用守護線程?
可以使用 isDaemon 方法判斷線程是否為守護線程。
可以使用 setDaemon 方法設置線程為守護線程。
public class ThreadDaemonDemo { public static void main(String[] args) { Thread t = new Thread(new MyThread(), "線程"); t.setDaemon(true); // 此線程在后臺運行 System.out.println("線程 t 是否是守護進程:" + t.isDaemon()); t.start(); // 啟動線程 } static class MyThread implements Runnable { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + "在運行。"); } } }}
java.lang.Thread.State 中定義了 6 種不同的線程狀態,在給定的一個時刻,線程只能處于其中的一個狀態。 下圖給出了這六種狀態,注意中間的 Ready 和 Running 都屬于 Runnable 就緒狀態。
以下是各狀態的說明,以及狀態間的聯系:
下面這張圖更生動地展示了線程狀態切換的時機和觸發條件(圖片來自網絡,出處下方飲用鏈接1)。
引用鏈接:
本文鏈接:http://www.www897cc.com/showinfo-26-51228-0.html老后端被借調去寫Java了,含淚總結的Java多線程編程基礎
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: PHP老矣,尚能飯否?