java多線程及多線程編程
一、線程、進程、多線程的概念
Java 給多線程編程提供了內置的支持。
一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
進程:一個進程包括由操作系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到所有的非守護線程都結束運行後才能結束。
【注意】區分線程和進程的概念
進程:應用程序的執行實例,有獨立的內存空間和系統資源
線程:CPU調度和分派的基本單位,進程中執行運算的最小單位,可完成一個獨立的順序控制流程
進程和線程的關係
(1)一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。
(2)資源分配給進程,同一進程的所有線程共享該進程的所有資源。 同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。
(3)處理機分給線程,即真正在處理機上運行的是線程。
(4)線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。
多線程是多任務的一種特別的形式,但多線程使用了更小的資源開銷。
多線程能滿足程序員編寫高效率的程序來達到充分利用 CPU 的目的。
【注意】多線程就是分時利用 CPU,宏觀上讓所有線程一起執行 ,也叫併發。
二、一個線程的生命週期(5種狀態)
線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖是一個線程完整的生命週期:
新建狀態: 使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。
就緒狀態:當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。
運行狀態:如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。
阻塞狀態:如果一個線程執行了sleep(睡眠)、 suspend(掛起) 等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:
- 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。調用wait(),使該線程處於等待池(wait blocked pool),直到notify()/notifyAll(),線程被喚醒被放到鎖定池(lock blocked pool ),釋放同步鎖使線程回到可運行狀態(Runnable)。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。對Running狀態的線程加同步鎖(Synchronized)使其進入(lock blocked pool ),同步鎖被釋放進入可運行狀態(Runnable)。
- 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
死亡狀態:
一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。
三、線程的優先級
每一個 Java 線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。
Java 線程的優先級是一個整數,其取值範圍是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。
具有較高優先級的線程對程序更重要,並且應該在低優先級的線程之前分配處理器資源。但是,線程優先級不能保證線程執行的順序,而且非常依賴於平臺。
四、創建一個線程
Java 提供了三種創建線程的方法:
- 通過實現
Runnable接口
; - 通過繼承
Thread 類
本身; - 通過
Callable 和 Future
創建線程。
(1) 通過實現 Runnable 接口來創建線程
創建一個線程,最簡單的方法是創建一個實現 Runnable 接口的類。
爲了實現 Runnable,一個類只需要執行一個方法調用 run(),聲明如下:
public void run()
可以重寫該方法,重要的是理解的 run() 可以調用其他方法,使用其他類,並聲明變量,就像主線程一樣。
在創建一個實現 Runnable 接口的類之後,你可以在類中實例化一個線程對象。
Thread 定義了幾個構造方法,下面的這個是我們經常使用的:
Thread(Runnable threadOb,String threadName);
這裏,threadOb 是一個實現 Runnable 接口的類的實例,並且 threadName 指定新線程的名字。
新線程創建之後,你調用它的 start() 方法它纔會運行。
void start();
下面是一個創建線程並開始讓它執行的實例:
//實例
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;
RunnableDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 讓線程睡眠一會
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo( "Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo( "Thread-2");
R2.start();
}
}
運行結果如下:
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
(2)通過繼承Thread來創建線程
創建一個線程的第二種方法是創建一個新的類,該類繼承 Thread 類,然後創建一個該類的實例。
繼承類必須重寫 run() 方法,該方法是新線程的入口點。它也必須調用 start() 方法才能執行。
該方法儘管被列爲一種多線程實現方式,但是本質上也是實現了 Runnable 接口的一個實例。
//實例
class ThreadDemo extends Thread {
private Thread t;
private String threadName;
ThreadDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 讓線程睡眠一會
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
public class TestThread {
public static void main(String args[]) {
ThreadDemo T1 = new ThreadDemo( "Thread-1");
T1.start();
ThreadDemo T2 = new ThreadDemo( "Thread-2");
T2.start();
}
}
運行結果如下:
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
—Thread 方法
下面列出Thread類的一些重要方法:
測試線程是否處於活動狀態。 上述方法是被Thread對象調用的。下面的方法是Thread類的靜態方法。
實例參考鏈接:https://www.runoob.com/java/java-multithreading.html
(3)通過 Callable 和 Future 創建線程
- 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。
- 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
- 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。
- 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。
//實例
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的線程").start();
}
}
try
{
System.out.println("子線程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
五、創建線程的三種方式的對比
- 採用實現 Runnable、Callable 接口的方式創建多線程時,線程類只是實現了 Runnable 接口或 Callable 接口,還可以繼承其他類。
- 使用繼承 Thread 類的方式創建多線程時,編寫簡單,如果需要訪問當前線程,則無需使用 Thread.currentThread() 方法,直接使用 this 即可獲得當前線程。
六、線程的幾個主要概念
在多線程編程時,你需要了解以下幾個概念:
1、線程同步:即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作, 其他線程才能對該內存地址進行操作,而其他線程又處於等待狀態,實現線程同步的方法有很多,臨界區對象就是其中一種。
學習鏈接:
https://blog.csdn.net/ETalien_/article/details/86552623
https://www.cnblogs.com/XHJT/p/3897440.html
https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5
2、線程間通信:
①同步;②while輪詢的方式;③wait/notify機制;④管道通信。
具體內容參考下面第一個鏈接。
學習鏈接:
https://www.cnblogs.com/hapjin/p/5492619.html
https://www.jianshu.com/p/9218692cb209
https://blog.csdn.net/z714405489/article/details/82814133
3、線程死鎖:
學習鏈接:
https://blog.csdn.net/ls5718/article/details/51896159
https://www.cnblogs.com/xrq730/p/4853713.html
https://www.jianshu.com/p/68c0fef7b63e
4、線程控制:掛起、停止和恢復
補充一:線程狀態
線程共包括以下 5 種狀態:
- 新建狀態(New): 線程對象被創建後,就進入了新建狀態。例如,Thread thread = new Thread()。生成線程對象,並沒有調用該對象的 start 方法,這是線程處於創建狀態。
- 就緒狀態(Runnable): 也被稱爲“可執行狀態”。線程對象被創建後,其它線程調用了該對象的start()方法,從而來啓動該線程,但是此時線程調度程序還沒有把該線程設置爲當前線程,此時處於就緒狀態。在線程運行之後,從等待或者睡眠中回來之後,也會處於就緒狀態。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
- 運行狀態(Running): 線程獲取CPU權限進行執行。線程調度程序將處於就緒狀態的線程設置爲當前線程,此時線程就進入了運行狀態,開始運行 run 函數當中的代碼。需要注意的是,線程只能從就緒狀態進入到運行狀態。
- 阻塞狀態(Blocked): 阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
—(1)等待阻塞 – 通過調用線程的wait()方法,讓線程等待某工作的完成。
—(2)同步阻塞 – 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態。
— (3) 其他阻塞 – 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
線程正在運行的時候,被暫停,通常是爲了等待某個時間的發生(比如說某項資源就緒)之後再繼續運行。sleep,suspend,wait 等方法都可以導致線程阻塞。- 死亡狀態(Dead): 線程執行完了或者因異常退出了run()方法,該線程結束生命週期。如果一個線程的 run 方法執行結束或者調用 stop 方法後,該線程就會死亡。對於已經死亡的線程,無法再使用 start 方法令其進入就緒。
補充二:Thread類中和start()和run()的區別
每個線程都是通過某個特定 Thread 對象所對應的方法 run() 來完成其操作的,方法 run() 稱爲線程體。通過調用 Thread 類的 start() 方法來啓動一個線程。
先調用 start 後調用 run,這麼麻煩,爲了不直接調用 run?
因爲就是爲了實現多線程的優點,沒這個 start 不行。
1.start() 方法來啓動線程,真正實現了多線程運行。這時無需等待 run 方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用 Thread 類的 start() 方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。 然後通過此 Thread 類調用方法 run() 來完成其運行操作的, 這裏方法 run() 稱爲線程體,它包含了要執行的這個線程的內容, run 方法運行結束, 此線程終止。然後 CPU 再調度其它線程。
2.run() 方法當作普通方法的方式調用。程序還是要順序執行,要等待 run 方法體執行完畢後,纔可繼續執行下面的代碼; 程序中只有主線程——這一個線程, 其程序執行路徑還是隻有一條, 這樣就沒有達到寫線程的目的。
【注意】多線程就是分時利用 CPU,宏觀上讓所有線程一起執行 ,也叫併發。
補充三:java的多線程,Object類的線程方法
notify() :通知一個在對象上等待的線程,使其從wait()返回,而返回的前提是該線程獲取到了對象的鎖。
notifyAll(): 通知所有等待在該對象上的線程。
wait():調用該方法的線程進入WAITING狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意,調用wait()方法後,會釋放對象的鎖。
wait(long) :超時等待一段時間,這裏的參數是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回。
wait(long, int) : 對於超時時間更細粒度的控制,可以達到毫秒。
參考鏈接:
[1] https://www.runoob.com/java/java-multithreading.html
[2] https://www.cnblogs.com/wxd0108/p/5479442.html