Java技術棧——Java多線程詳述
- 一.多線程
-
- 1.1多線程概述
- 1.2 程式運作原理
-
- 1.2.1 搶占式排程詳解
- 1.3 主線程
- 1.4 Thread類
- 1.5 建立線程
-
- 1.5.1 run()與start()
- 1.5.2 繼承Thread類原理
- 1.5.3 多線程的記憶體圖解
- 1.5.4 擷取線程名稱
- 1.6 建立線程方式—實作Runnable接口
-
- 1.6.1 Runnable的優點
- 1.7 線程的匿名内部類使用
- 二、線程池
-
- 2.1 線程池概念
- 2.2 使用線程池方式--Runnable接口
- 2.3 使用線程池方式—Callable接口
一.多線程
1.1多線程概述
在了解學習多線程之前,我們先要熟悉了解幾個關于多線程有關的概念。
程序:程序指正在運作的程式。确切的來說,當一個程式進入記憶體運作,即變成一個程序,程序是處于運作過程中的程式,并且具有一定獨立功能。

線程:線程是程序中的一個執行單元,負責目前程序中程式的執行,一個程序中至少有一個線程。一個程序中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。
簡而言之:一個程式運作後至少有一個程序,一個程序中可以包含多個線程。什麼是多線程呢? 多線程定義:在一個程式中,這些獨立運作的程式片段叫作“線程”。即就是一個程式中有多個線程在同時執行。
我們可以通過程式執行流程,來區分單線程程式與多線程程式的不同:
單線程程式:即,若有多個任務隻能依次執行。當上一個任務執行結束後,下一個任務開始執行。如接水,有一個水龍頭,一個人接完,下一個人才能開始接水。
多線程程式:即,若有多個任務可以同時執行。如在飲水機處接水,溫水處與熱水處可以同時放水。
1.2 程式運作原理
分時排程:所有線程輪流使用 CPU 的使用權,平均配置設定每個線程占用 CPU 的時間。分時排程可以在分時排程類中公平地分布處理資源。核心的其他部分可以在短時間内獨占處理器,而不會縮短使用者察覺的響應時間。在Java中可以設定一個或多個程序的優先級級别,優先級的級别範圍通常為 0 到 +10(不指定的話,java預設建立為5),值越低,優先級越高。
搶占式排程:這裡有必要說明一下,搶占式排程是實時排程的一種。優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會随機選擇一個(線程随機性),Java使用的為搶占式排程。
1.2.1 搶占式排程詳解
現在大部分電腦作業系統都支援多程序并發運作,即支援多個軟體同時運作,比如打開了微信,并且同時聽着某易音樂,然後在CSDN上編寫分享部落格,“感覺上,這些運用在同時進行。”實際上,CPU(中央處理器)使用搶占式排程模式在多個線程間進行着高速的切換(針對于某一個核來說),我們根本就沒有察覺到,對于CPU的一個核而言,某個時刻,隻能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運作。是以,仔細想想就會發現,多線程程式并不能提高程式的運作速度,但能夠提高程式運作效率,讓CPU的使用率更高。
1.3 主線程
再來看看我們最開始之前,學習常用的場景,當我們在dos指令行中輸入java空格類名回車後,啟動JVM,并且加載對應的class檔案。虛拟機并會從main方法開始執行我們的程式代碼,一直把main方法的代碼執行完成。如果在執行過程遇到循環時間比較長的代碼,那麼在循環之後的其他代碼是不會被馬上執行的。如下代碼示範:
class Demo{
String name;
Demo(String name){
this.name = name;
}
void show() {
for (int i=1;i<=10000 ;i++ ) {
System.out.println("name="+name+",i="+i);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
Demo demo = new Demo("CSDN");
Demo demo2 = new Demo("NDSC");
demo.show();
demo2.show();
}
}
若在上述代碼中show方法中的循環執行次數很多,這時在demo.show();下面的代碼是不會馬上執行的,并且在dos視窗會看到不停的輸出name=CSDN,i=++,這樣的語句。是因為:jvm啟動後,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之為主線程。當程式的主線程執行時,如果遇到了循環而導緻程式在指定位置停留時間過長,則無法馬上執行下面的程式,需要等待循環結束後能夠執行。
那麼,能否實作一個主線程負責執行其中一個循環,再由另一個線程負責其他代碼的執行,最終實作多部分代碼同時執行的效果?當然可以,Java中的多線程技術能夠實作同時執行。
1.4 Thread類
該如何建立線程呢?通過API中搜尋,查到Thread類。通過閱讀Thread類中的描述,我們能夠了解到Thread是程式中的執行線程,并且Java 虛拟機是支援并允許應用程式并發地運作多個執行線程。
構造方法
Thread()構造方法摘要 | 描述 |
---|---|
Thread() | 配置設定線程對象 |
Thread(String name) | 配置設定新的線程對象,将指定的name作為其線程名稱 |
常用方法
類型 | 名稱 | 描述 |
---|---|---|
void | start() | 使該線程開始執行;Java虛拟機實際上調用的是該線程的run()方法 |
void | run() | 該線程具體要執行的操作 |
static void | sleep(long m) | 讓目前正在執行的線程休眠毫秒,(暫停執行) |
1.5 建立線程
建立新執行線程有兩種方法。
繼承Thread:将類聲明為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。建立對象,開啟線程。run方法相當于其他線程的main方法。
繼承Thread建立線程的步驟:
1 定義一個類繼承Thread。
2 重寫run方法。
3 建立子類對象,就是建立線程對象。
4 調用start方法,開啟線程并讓線程執行,此時jvm會去調用run方法。
實作Runnable:聲明一個實作 Runnable 接口的類,該類會實作 run 方法,然後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。
main
//測試類
public class Demo {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo("線程Demo!");
//開啟新線程
td.start();
//在主方法中執行for循環
for (int i = 0; i < 10; i++) {
System.out.println("main線程!"+i);
}
}
}
建立線程類
//建立線程類
public class ThreadDemo extends ThreadDemo {
public ThreadDemo (String name) {
//調用父類的String參數的構造方法,指定線程的名稱
super(name);
}
//重寫run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
1.5.1 run()與start()
注意:線程對象調用 run方法和調用start方法差別?
在了解這個問題之前,我們應該清楚什麼是run()方法,什麼是start()方法;
run():就是繼承Thread類,或者實作runnable要實作的方法,本質上是一個成員函數,但并不是多線程的方式,就是一個普通的方法。我們從源碼就能看出就是簡單的普通方法的調用。
run()源碼
@Override
public void run() {
// 簡單的運作,不會新起線程,target 是 Runnable
if (target != null) {
target.run();
}
}
start():要了解start方法,我們最好從源碼入手,start 方法的源碼也沒幾行代碼,注釋也比較詳細,最主要的是 start0() 方法。
start()源碼
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
// 沒有初始化,抛出異常
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
// 是否啟動的辨別符
boolean started = false;
try {
// start0() 是啟動多線程的關鍵
// 這裡會建立一個新的線程,是一個 native 方法
// 執行完成之後,新的線程已經在運作了
start0();
// 主線程執行
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
start0()源碼
start0()的源碼隻有一行,為了友善了解start執行過程以及日後的學習,總結了start0的每一步調用執行過程。
- Step1::start0方法實際上調用的是jvm.cpp檔案的JVM_StartThread方法,(是否建立線程,以及能否建立線程,比如記憶體已滿,無法繼續建立新的線程,都是在這一步進行判斷的);
- Step2:如果能建立,則調用JavaThread方法建立線程(包括初始化相關變量),就在建立線程的時候,傳入了java_start,做為線程運作函數的初始位址;
- Step3:當子線程完成初始化之後,父線程會執行Thread::start方法,設定線程狀态為RUNNABLE;
- Step4:這時候子線程就可以開始執行thread->run()方法了。
Java技術棧——Java多線程詳述一.多線程二、線程池
補充:start0為什麼會被标記成native本地方法。衆所周知,Java其最大的優點之一就是跨平台性,start() 方法調用 start0() 方法後,該線程并不一定會立馬執行,隻是将線程變成了可運作狀态(NEW —> RUNNABLE)。具體什麼時候執行,取決于 CPU ,由 CPU 統一排程。可以在不同系統上運作,每個系統的 CPU 排程算法不一樣,是以就需要做不同的處理,這件事情就隻能交給 JVM 來實作了,start0() 方法自然就表标記成了 native。
是以綜上,線程對象調用run方法不開啟線程,僅是對象調用方法。線程對象調用start開啟線程,并讓jvm調用run方法在開啟的線程中執行。
1.5.2 繼承Thread類原理
我們為什麼要繼承Thread類,并調用其的start方法才能開啟線程呢?繼承Thread類:因為Thread類用來描述線程,具備線程應該有的功能。那為什麼不直接建立Thread類的對象呢?如下代碼:
Thread t1 = new Thread();
//這樣做沒有錯,但是該start調用的是Thread類中的run方法
//這個run方法沒有做什麼事情,更重要的是這個run方法中并沒有定義我們需要讓線程執行的代碼。
t1.start();
建立線程是為了建立程式單獨的執行路徑,讓多部分代碼實作同時執行。也就是說線程建立并執行需要給定線程要執行的任務。對于之前所講的主線程,它的任務定義在main函數中。自定義線程需要執行的任務都定義在run方法中。Thread類run方法中的任務并不是我們所需要的,隻有重寫這個run方法。既然Thread類已經定義了線程任務的編寫位置(run方法),那麼隻要在編寫位置(run方法)中定義任務代碼即可。是以進行了重寫run方法動作。
1.5.3 多線程的記憶體圖解
多線程執行時,在記憶體中的運作方式其實很簡單:多線程執行時,在棧記憶體中,其實每一個執行線程都有一片自己所屬的棧記憶體空間。進行方法的壓棧和彈棧。當執行線程的任務結束了,線程自動在棧記憶體中釋放了。但是當所有的執行線程都結束了,那麼程序就結束了。
1.5.4 擷取線程名稱
開啟的線程都會有自己的獨立運作棧記憶體,而這些線程都是有其預設的名字的,當然也可以自定義線程名。根據Thread類的API文檔整理如下。
函數名 | 功能 |
---|---|
Thread.currentThread() | 擷取目前線程對象 |
Thread.currentThread().getName() | 擷取目前線程對象的名稱 |
class MyThread extends Thread {
MyThread(String name){
super(name);
}
//複寫其中的run方法
public void run(){
for (int i=1;i<=100 ;i++ ){
System.out.println(Thread.currentThread().getName()+",i="+i);
}
}
}
class ThreadDemo {
public static void main(String[] args) {
//建立兩個線程任務
MyThread d = new MyThread();
MyThread d2 = new MyThread();
//沒有開啟新線程, 在主線程調用run方法
d.run();
//開啟一個新線程,新線程調用run方法
d2.start();
}
}
通過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1…注意:進行多線程程式設計時,不要忘記了Java程式運作是從主線程開始,main方法就是主線程的線程執行内容。
1.6 建立線程方式—實作Runnable接口
建立線程的另一種方法是聲明實作 Runnable 接口的類。該類然後實作 run 方法。然後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。檢視Runnable接口說明文檔:Runnable接口用來指定每個線程要執行的任務。包含了一個 run 的無參數抽象方法,需要由接口實作類重寫該方法。
實作Runnable接口,建立線程的步驟:
- 1、定義類實作Runnable接口。
- 2、覆寫接口中的run方法。。
- 3、建立Thread類的對象
- 4、将Runnable接口的子類對象作為參數傳遞給Thread類的構造函數。
- 5、調用Thread類的start方法開啟線程。
示例
public class RunnableDemo {
public static void main(String[] args) {
//建立線程執行目标類對象
Runnable runnable = new MyRunnable();
//将Runnable接口的子類對象作為參數傳遞給Thread類的構造函數
Thread thread = new Thread(runn);
Thread thread2 = new Thread(runn);
//開啟線程
thread.start();
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main線程:正在執行!"+i);
}
}
}
線程執行類示例
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("線程!"+i);
}
}
}
1.6.1 Runnable的優點
第二種方式實作Runnable接口避免了單繼承的局限性,是以較為常用。實作Runnable接口的方式,更加的符合面向對象,線程分為兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一起。一旦建立Thread類的子類對象,既是線程對象,有又有線程任務。實作runnable接口,将線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。
1.7 線程的匿名内部類使用
使用線程的内匿名内部類方式,可以友善的實作每個線程執行不同的線程任務操作。
- 方式1:建立線程對象時,直接重寫Thread類中的run方法
new Thread() {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ ">>>>" + i);
}
}
}.start();
- 方式2:使用匿名内部類的方式實作Runnable接口,重新Runnable接口中的run方法
Runnable runnable = new Runnable() {
public void run() {
for (int i = 0; i < 100; 1x++) {
System.out.println(Thread.currentThread().getName()+ ">>>>" + i);
}
}
};
new Thread(runnable ).start();
二、線程池
2.1 線程池概念
線程池,其實就是一個容納多個線程的容器,其中的線程可以反複使用,省去了頻繁建立線程對象的操作,無需反複建立線程而消耗過多資源。
在java中,如果每個請求到達就建立一個新線程,開銷是相當大的。在實際使用中,建立和銷毀線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的使用者請求的時間和資源要多的多。除了建立和銷毀線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個jvm裡建立太多的線程,可能會使系統由于過度消耗記憶體或“切換過度”而導緻系統資源不足。為了防止資源不足,需要采取一些辦法來限制任何給定時刻處理的請求數目,盡可能減少建立和銷毀線程的次數,特别是一些資源耗費比較大的線程的建立和銷毀,盡量利用已有對象來進行服務。
線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務重複使用線程,線程建立的開銷就被分攤到了多個任務上了,而且由于在請求到達時線程已經存在,是以消除了線程建立所帶來的延遲。這樣,就可以立即為請求服務,使用應用程式響應更快。另外,通過适當的調整線程中的線程數目可以防止出現資源不足的情況。
2.2 使用線程池方式–Runnable接口
通常,線程池都是通過線程池工廠建立,再調用線程池中的方法擷取線程,再通過線程去執行任務方法。
Executors:線程池建立工廠類
- public static ExecutorService newFixedThreadPool(int nThreads):傳回線程池對象
- ExecutorService:線程池類
- Future<?> submit(Runnable task):擷取線程池中的某一個線程對象,并執行
- Future接口:用來記錄線程任務執行完畢後産生的結果。線程池建立與使用
使用線程池中線程對象的步驟,代碼示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
//建立線程池,包含10個線程
ExecutorService service = Executors.newFixedThreadPool(10);
RunnableDemo rd = new RunnableDemo ();
//從線程池中擷取線程對象,然後調用RunnableDemo 中的run()
service.submit(rd);
//注意:submit方法調用結束後,程式并不終止,是因為線程池控制了線程的關閉。将使用完的線程又歸還到了線程池中
//關閉線程池
//service.shutdown();
}
}
Runnable接口實作類
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("線程示例啟動");
System.out.println("線程: " +Thread.currentThread().getName());
System.out.println("線程關閉"+Thread.currentThread().getName());
}
}
2.3 使用線程池方式—Callable接口
lCallable接口:與Runnable接口功能相似,用來指定線程的任務。其中的call()方法,用來傳回線程任務執行完畢後的結果,call方法可抛出異常。
- Future submit(Callable task):擷取線程池中的某一個線程對象,并執行線程中的call()方法
-
Future接口:用來記錄線程任務執行完畢後産生的結果。
代碼示例:
public class ThreadPoolDemo {
public static void main(String[] args) {
//建立線程池,包含10個線程
ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
//建立Callable對象
CallableDemo cd = new CallableDemo ();
service.submit(c);
service.submit(c);
}
}
Callable接口實作類,call方法可抛出異常、傳回線程任務執行完畢後的結果
public class CallableDemo implements Callable {
@Override
public Object call() throws Exception {
System.out.println("線程示例:call");
System.out.println("線程: " +Thread.currentThread().getName());
System.out.println("線程結束:"+Thread.currentThread().getName());
return null;
}
}