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