一、程式、流程、執行緒
程式是一組指令的有序集合,也可以將其通俗地理解為若干行程式碼。它本身沒有任何運行的意義,它只是一個靜態的實體,它可能只是一個單純的文字文件,也有可能是經過編譯後產生的可執行檔。
從狹義來說,進程是正在運行的程式的實例;從廣義上來說,進程是一個具有一定獨立功能的程式關於某個資料集合的一次運行活動。進程是作業系統進行資源分配的基本單位。
執行緒是進程中可獨立執行的最小單位,它也是處理器進行獨立調度和分派的基本單位。一個進程可以包含多個線程,每個線程執行自己的任務,同一個進程中的所有線程共享該進程中的資源,如記憶體空間、檔案句柄等。
二、多執行緒程式設計簡介
#1、什麼是多執行緒程式設計
多執行緒程式設計技術是Java語言的重要特點。多執行緒程式設計的意思是將程式任務分成幾個並行的子任務,並將這些子任務交給多個執行緒去執行。
多執行緒程式設計就是以執行緒為基本抽象單位的一種程式設計範式。但是,多執行緒程式設計又不只是使用多個執行緒進行程式設計那麼簡單,其自身又有需要解決的問題。多執行緒程式設計和物件導向程式設計是可以相容的,也就是我們可以在物件導向程式設計的基礎上實現多執行緒程式設計。事實上,Java平台中的一個執行緒就是一個物件。
2、為什麼要用多執行緒程式設計
現在的電腦動輒就是多處理器核心的,而每一個執行緒同一時間只能在一個處理器上。如果只採用單執行緒進行開發,那麼就無法充分利用多核心處理器的資源來提高程式的執行效率。而使用多執行緒進行程式設計時,不同的執行緒可以運行在不同的處理器上。這樣一來,不僅大大提高了對電腦資源的使用率,同時也提高了程式的執行效率。
三、JAVA執行緒API簡介
java.lang.Thread類別就是Java平台對執行緒的實作。 Thread類別或其子類別的一個實例就是一個執行緒。
1、執行緒的建立、啟動、執行
在Java平台中,建立一個執行緒就是建立一個Thread類別(或其子類別)的範例。每個執行緒都有其要執行的任務。執行緒的任務處理邏輯可以在Thread類別的run方法中直接實作或透過該方法進行調用,因此run方法相當於執行緒的任務處理邏輯的入口方法,它應該由Java虛擬機在運行相應執行緒時直接調用,而不應該由應用程式碼進行呼叫。
執行一個執行緒其實就是讓Java虛擬機器執行該執行緒的run方法,從而使任務處理邏輯程式碼得以執行。如果一個執行緒沒有啟動,它的run方法是絕對不會被執行的。為此,首先需要啟動線程。 Thread類別的start方法的作用是啟動對應的執行緒。啟動一個線程的實質是請求虛擬機器運行相應的線程,而這個線程具體何時能夠運行是由線程調度器(線程調度器是作業系統的一部分)決定的。因此,呼叫執行緒的start方法並不意味著執行緒已經開始運行,這個執行緒可能馬上開始運行,也有可能稍後才被運行,也有可能永遠不運行。
以下介紹兩種建立執行緒的方式(實際上還有其他方式,後續文章中會詳細介紹)。在此之前我們先來看看Thread類別的run方法的原始碼:
// Code 1-1@Override public void run() { if (target != null) { target.run(); } }
這個run方法是在介面Runnable中定義的,它不接受參數也沒有回傳值。事實上Runnable接口中也只有這一個方法,因此這個接口是一個函數式接口,這意味著我們可以在需要Runnable的地方使用lambda表達式。 Thread類別實作了這個接口,因此它必須實作這個方法。 target是Thread類別中的一個域,它的型別也是Runnable。 target域表示這個執行緒需要執行的內容,而Thread類別的run方法所做的也只是執行target的run方法。
我們剛剛提到,Java虛擬機會自動呼叫執行緒的run方法。但是,Thread類別的run方法已經定義好了,我們沒有辦法將自己需要執行的程式碼放在Thread類別的run方法中。因此,我們可以考慮其他的方式來影響run方法的行為。第一種就是繼承Thread類別並重寫run方法,這樣JVM在執行執行緒時就會呼叫我們重寫的run方法而不是Thread類別的run方法;第二種方法是將我們要執行的程式碼傳遞給Thread類別的target方法,而剛好Thread類別有幾個建構器可以直接對target進行賦值,這樣一來,JVM在呼叫run方法時執行的仍然是我們傳遞的程式碼。
在Java平台中,每個執行緒都可以擁有自己預設的名字,當然我們也可以在建構Thread類別的實例時為我們的執行緒取一個名字,這個名字便於我們區分不同的執行緒。
下面的程式碼使用上述的兩種方式創建了兩個線程,它們要執行的任務很簡單——列印一行歡迎訊息,並且要包含自己的名字。
public class WelcomeApp { public static void main(String[] args) { Thread thread1 = new WelcomeThread(); Thread thread2 = new Thread(() -> System.out.println("2. Welcome, I'm " + Thread.currentThread().getName())); thread1.start(); thread2.start(); } }class WelcomeThread extends Thread { @Override public void run() { System.out.println("1. Welcome, I'm " + Thread.currentThread().getName()); } }
下面是這個程式運行時輸出的內容:
1. Welcome, I'm Thread-0 2. Welcome, I'm Thread-1
多次執行這個程序,我們可以發現這個程式的輸出也有可能是:
2. Welcome, I'm Thread-1 1. Welcome, I'm Thread-0
這說明,雖然thread1的啟動在thread2之前,但這並不意味著thread1會在thread2之前被執行。
不管採用哪種方式建立線程,一旦線程的run方法執行(由JVM呼叫)結束,對應線程的運行也就結束了。當然,run方法執行結束包括正常結束(run方法正常返回)和程式碼中拋出異常而導致的終止。執行結束的執行緒所佔用的資源(如記憶體空間)會像其他Java物件一樣被JVM回收。
線程屬於“一次性用品”,我們不能透過重新呼叫一個已經運行結束的線程的start方法來使其重新運行。事實上,start方法也只能夠被呼叫一次,多次呼叫同一個Thread實例的start方法會導致其拋出IllegalThreadStateException異常。
2、執行緒的屬性
執行緒的屬性包括執行緒的編號、名稱、類別和優先權, 詳情如下表所示:
上面提到了守護執行緒和使用者執行緒的概念,這裡對它們做一個簡要的說明。依照線程是否會阻止Java虛擬機器正常停止,我們可以將Java中的線程分為守護線程(Daemon Thread)和使用者線程(User Thread,也稱為非守護線程)。線程的daemon屬性用來表示對應線程是否為守護線程。使用者執行緒會阻止Java虛擬機器的正常停止,也就是一個Java虛擬機器只有在其所有使用者執行緒都執行結束(即Thread.run()呼叫未結束)的情況下才能正常停止。而守護線程則不會影響Java虛擬機器的正常停止,也就是應用程式中有守護執行緒在運作也不影響Java虛擬機器的正常停止。因此,守護線程通常用於執行一些重要性不是很高的任務,例如用於監視其他線程的運行情況。
當然,如果Java虛擬機器是強制停止的,例如在Linux系統下使用kill指令強制終止一個Java虛擬機器流程,那麼即使是使用者執行緒也無法阻止Java虛擬機器的停止。
3、Thread類別常用方法
#Java中的任何一段程式碼總是執行在某個執行緒之中。執行當前程式碼的線程就稱為當前線程,Thread.currentThread()可以傳回當前線程。由於同一段程式碼可能會被不同的執行緒執行,因此當前執行緒是相對的,即Thread.currentThread()的回傳值在程式碼實際運行的時候可能對應著不同的執行緒(物件)。
join方法的作用相當於執行該方法的線程和線程調度器說:「我得先暫停一下,等到另外一個線程運行結束後我才能繼續。」
yield靜態方法的作用相當於執行該方法的線程對線程調度器說:「我現在不著急,如果別人需要處理器資源的話先給它用吧。當然,如果沒有其他人要用,我也不介意繼續佔用。」
sleep靜態方法的作用相當於執行該方法的線程對線程調度器說:“我想小憩一會兒,過段時間再叫醒我繼續幹活吧。”
4、Thread类中的废弃方法
虽然这些方法并没有相应的替代品,但是可以使用其他办法来实现,我们会在后续文章中学习这部分内容。
四、无处不在的线程
Java平台本身就是一个多线程的平台。除了Java开发人员自己创建和使用的线程,Java平台中其他由Java虚拟机创建、使用的线程也随处可见。当然,这些线程也是各自有其处理任务。
Java虚拟机启动的时候会创建一个主线程(main线程),该线程负责执行Java程序的入口方法(main方法)。下面的程序打印出主线程的名称:
public class MainThreadDemo { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
该程序会输出“main”,这说明main方法是由一个名为“main”的线程调用的,这个线程就是主线程,它是由JVM创建并启动的。
在多线程编程中,弄清楚一段代码具体是由哪个(或者哪种)线程去负责执行的这点很重要,这关系到性能、线程安全等问题。本系列的后续文章会体现这点。
Java 虚拟机垃圾回收器(Garbage Collector)负责对Java程序中不再使用的内存空间进行回收,而这个回收的动作实际上也是通过专门的线程(垃圾回收线程)实现的,这些线程由Java虚拟机自行创建。
为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码编译为Java虚拟机宿主机处理器可直接执行的机器码。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
Java平台中的线程随处可见,这些线程各自都有其处理任务。
五、线程的层次关系
Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。假设线程A所执行的代码创建了线程B, 那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程。例如, Code 1-2中的线程thread1和thread2是main线程的子线程,main线程是它们的父线程。子线程所执行的代码还可以创建其他线程,因此一个子线程也可以是其他线程的父线程。所以,父线程、子线程是一个相对的称呼。理解线程的层次关系有助于我们理解Java应用程序的结构,也有助于我们后续阐述其他概念。
在Java平台中,一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
一个线程的优先级默认值为该线程的父线程的优先级,即如果我们没有设置或者更改一个线程的优先级,那么这个线程的优先级的值与父线程的优先级的值相等。
不过,Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程。并且,父线程和子线程之间的生命周期也没有必然的联系。比如父线程运行结束后,子线程可以继续运行,子线程运行结束也不妨碍其父线程继续运行。
六、线程的生命周期状态
在Java平台中,一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态。如下图所示:
執行緒的狀態可以透過Thread.getState()呼叫來取得。 Thread.getState()的回傳值型別Thread.State,它是Thread類別內部的一個枚舉型別。 Thread.State所定義的執行緒狀態包括以下幾種:
NEW
:一個己創建而未啟動的執行緒處於該狀態。由於一個執行緒實例只能夠被啟動一次,因此一個執行緒只可能有一次處於該狀態。
RUNNABLE
:此狀態可以被看成複合狀態,它包含兩個子狀態:READY和RUNNING,但實際上Thread.State中並沒有定義這兩種狀態。前者表示處於該狀態的執行緒可以被執行緒調度器進行調度而使之處於RUNNING狀態。後者表示處於該狀態的執行緒正在運行,即對應執行緒物件的run方法所對應的指令正在由處理器執行。執行Thread.yield()的線程,其狀態可能會由RUNNING轉換為READY。處於READY子狀態的執行緒也被稱為活躍執行緒。
BLOCKED
:當執行緒啟動一個阻塞式I/0操作後,或申請一個由其他執行緒持有的獨佔資源(例如鎖)時,對應的執行緒會處於該狀態。處於BLOCKED狀態的執行緒並不會佔用處理器資源。當阻塞式1/0操作完成後,或是執行緒取得了其申請的資源,該執行緒的狀態又可以轉換為RUNNABLE。
WAITING
:一個執行緒執行了某些特定方法之後就會處於這種等待其他執行緒執行另外一些特定操作的狀態。能夠使其執行緒變更為WAITING狀態的方法包括:Object.wait()、Thread.join()和LockSupport.park(Object)。能夠使對應執行緒從WAITING變更為RUNNABLE的對應方法包括:Object.notify()/notifyAll()和LockSupport.unpark(Object))。
TIMED_WAITING
:該狀態和WAITING類似,差別在於處於該狀態的執行緒並非無限制地等待其他執行緒執行特定操作,而是處於帶有時間限制的等待狀態。當其他執行緒沒有在指定時間內執行該執行緒所期望的特定操作時,該執行緒的狀態會自動轉換為RUNNABLE。
TERMINATED
:已經執行結束的執行緒處於該狀態。由於一個執行緒實例只能夠被啟動一次,因此一個執行緒也只可能有一次處於該狀態。 run方法正常傳回或由於拋出異常而提前終止都會導致對應執行緒處於該狀態。
一個執行緒在其整個生命週期中,只可能有一次處於NEW狀態和TERMINATED狀態。
七、多執行緒程式設計的優點
#多執行緒程式設計具有以下優點:
提高系統的吞吐率:多執行緒程式設計使得一個行程中可以有多個並發(即同時進行的)的操作。例如,當一個執行緒因為I/0操作而處於等待時,其他執行緒仍然可以執行其操作。
提高響應性:在使用多執行緒程式設計的情況下,對於GUI軟體(如桌面應用程式)而言,一個慢的操作(例如從伺服器上下載一個大的檔案)並不會導致軟體的介面出現被「凍住」的現象而無法回應使用者的其他操作;對於Web應用程式而言,一個請求的處理慢了並不會影響其他請求的處理。
充分利用多核心處理器資源:如今多核心處理器的裝置越來越普及,就算是手機這樣的消費性裝置也普遍使用多核心處理器。實作適當的多執行緒程式設計有助於我們充分利用裝置的多核心處理器資源,從而避免了資源浪費。
多執行緒程式設計也有自身的問題與風險,包括以下幾個面向:
執行緒安全問題。多個執行緒共享資料的時候,如果沒有採取相應的並發存取控制措施,那麼就可能產生資料一致性問題,例如讀取髒資料(過期的資料)、遺失更新(某些執行緒所做的更新被其他執行緒所做的更新覆蓋)等。
線程活性問題。一個執行緒從其創建到運行結束的整個生命週期會經歷若於狀態。從單一執行緒的角度來看,RUNNABLE狀態是我們所期望的狀態。但實際上,程式碼編寫不當可能導致某些執行緒一直處於等待其他執行緒釋放鎖的狀態(BLOCKED狀態),這種情況稱為死鎖(Deadlock)。當然,一直忙碌的線程也可能會出現問題,它可能面臨活鎖(Livelock)問題,即一個線程一直在嘗試某個操作但就是無法進展。另外,執行緒是一種稀缺的運算資源,一個系統所擁有的處理器數最相比於該系統中存在的執行緒數量而言總是少之又少的。某些情況下可能出現執行緒飢餓(Starvation)的問題,即某些執行緒永遠無法取得處理器執行的機會而永遠處於RUNNABLE狀態的READY子狀態。
上下文切換。處理器從執行一個執行緒轉向執行另外一個執行緒的時候作業系統所需要做的一個動作被稱為上下文切換。由於處理器資源的稀缺性,因此上下文切換可以被視為多執行緒程式設計的必然副產物,它增加了系統的消耗,不利於系統的吞吐率。
相了解更多相關問題請上PHP中文網:JAVA影片教學
#以上是關於JAVA中多執行緒程式設計方法的詳細解析(附實例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!