1. 概念
線程,CPU調度的基本單位,被包裹在進程裏面,同一個進程裏的線程共享同一片內存空間。其中,守護線程是特殊的線程,守護線程自創建伊始會在後臺爲用戶線程提供服務,其生命貫徹進程的整個生命週期。
2. 特點
- 輕型實體:這種輕體現在線程是程序執行流的最小單位,執行時僅向系統請求執行所必須的一點兒資源。
- 共享進程內存空間:同一個進程裏的線程擁有同一個內存空間地址(進程空間地址),共享這片內存空間,同一個進程裏的線程互相通信不需要調用內核。
- 併發執行:線程通過互奪CPU資源實現併發執行,這種併發效果不是嚴格意義的同時執行,而是在多個線程之間高速切換,以至於給人併發執行的錯覺。
3. 組成
- 線程ID:線程標識符
- 當前指令指針(PC)
- 寄存器集合:存儲單元寄存器的集合
- 堆棧:兩種數據結構
4. 狀態
- 運行:線程佔有處理機正在執行
- 阻塞:線程在等待某個事件(如某個信號量),邏輯上不可執行
- 就緒:線程一切準備就緒,等待處理機執行,邏輯上可執行
5. 生命週期
- 新建狀態
- 通過
new
方法新建一個線程,該線程被加載進堆內存,此時的線程不具有運行所必須的資源
- 通過
- 可運行態
- 調用線程的
start()
方法,使該線程獲得運行所需的一點系統資源,同時調用run()
方法。
- 調用線程的
- 不可運行態
- 以下方法會使線程進入不可運行態
- 線程調用
sleep()
方法進入睡眠 - 線程調用
wait()
方法 - 發生I/O阻塞
- 線程調用
- 以下方法可使線程脫離不可運行態
sleep()
時間結束- 被notify()喚醒
- 等待輸入輸出完成
- 以下方法會使線程進入不可運行態
- 消亡態
- 當線程的
run()
方法走到生命的盡頭,線程進入消亡態,消亡了的線程不能再被start()
。
- 當線程的
5. 多線程
- 多個線程併發執行的過程稱作多線程
6. Java實現多線程的兩個方法
1、 繼承Thread類,重寫run()
方法
代碼示例
class SubThread extends Thread{
public void run(){
... ; //線程體,線程所要實現的功能
}
}
public class TestThread{
public static void main(String[] args){
SubThread st = new SubThread();
st.start();
}
}
2、 實現Runable方法,重寫run()
方法,並使用Thread構造函數構造線程
代碼示例
class SubThread implememts Runable{
public void run(){
... ; //線程體,線程所要實現的功能
}
}
public class TestThread{
public static void main(String[] args){
SubThread r = new SubThread();
Thread st = new Thread(r);
st.start()
}
7. 繼承 VS 實現
實現的方式優於繼承的方式,原因如下:
- 繼承實現多線程難免會走進單繼承的尷尬,而實現的方式則可以避免這個情況實現多繼承。
- 在子線程中需要對共享數據進行操作的時候,實現的方式只要在實現Runable的類中定義一個普通的變量就可以實現數據共享,因爲在實例化時實現Runable的對象只被創建一次,之後以該對象爲參數實例化的線程共享同一片內存數據;而繼承的方式每次實例化一個對象會重新開闢一個內存空間,如果要操作同一片內存空間就不得不用
static
修飾,這種方式修飾的內存空間生命週期長。
8. 線程的方法
start()
:開啓一個線程,獲得運行所需的系統資源;調用run()
方法run()
:線程體,線程要實現的主體功能sleep(Long l)
:顯式地讓線程睡眠l毫秒,睡眠時讓出CPU執行權join()
:暫停當前線程,讓調用該方法的線程參與進來,並在該線程執行結束後纔開始執行當前線程currentThread()
:返回當前佔有處理機的線程setName()
:設置線程名字getName()
:返回線程名字yield()
:讓出CPU的執行權,但並不是說讓出CPU執行權就一定是下一個線程執行,因爲有可能讓出執行權後又搶到執行權,繼續執行。isAlive()
:判斷一個線程是否消亡
代碼示例
public class TestThread{
public static void main(String[] args){
SubThread st = new SubThread();
st.start();
//st.run(); 該方法僅僅是調用st對象的run()方法,而不是一個線程,執行這一步的還是主線程
//st.start(); 一個線程在消亡之後就等同於人類的死亡,是不會有重新開始的
st.sleep(1000); //這裏要闡述線程和對象的關係,線程是線程,對象是對象,即使這裏使用SubThread對象
//調用的sleep()方法,但是實際上是主線程在執行這個方法,兩者不衝突
}
}
9. 線程的優先級
線程的優先級並不是絕對的優先,而是混沌的。被給予了高優先級的線程僅僅只是在概率上有較大概率搶到CPU執行權,而非絕對。線程預設的優先級別有以下三個:
- NORM_PRIORITY = 5
- MIN_PRIORITY = 1
- MAX_PRIORITY = 10
其中,一般我們創建一個線程優先級都爲NORM_PRIORITY。
設置和獲得線程優先級的方法如下:
- setPriority(int i)
- getPriority()
PS:線程創建時繼承父類線程優先級。線程優先級在1~10之間,離開這個範圍虛擬機會報java.lang.IllegalArgumentException錯誤。
10. 線程的同步機制
1. 線程的安全問題
代碼示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(true){
if(num >= 1){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(num--);
}else{
break;
}
}
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
上述程序其中一次輸出結果如下:
20
19
18
17
16
15
14
13
12
11
10
10
9
8
7
6
5
4
3
2
1
1
由此可見上述程序是存在安全隱患的,程序的本意是讓兩個線程相互協作打印出20到1,然而由程序可以看出,出現了一些重複值,這些重複值的出現意味着該程序是線程不安全的。
出現上述線程安全的原因是:當線程t1通過if條件判斷後,還沒來得及執行num--
的的操作就失去了CPU操作權,而後線程t2進來自然會導致一些數字被打印多次。解決這種線程不安全的問題的關鍵是:當某個線程進入某個共享區域後,對該區域數據進行操作期間其他線程必須不能再進入該區域,直到這個線程對該區域的操作完畢。
2. 解決線程安全問題的兩個方案
- 當多個線程嘗試操作共享數據時,將操作共享數據的代碼塊用
synchrosized(mutex)
聲明爲同步代碼塊。
代碼示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(true){
synchronized(this){
if(num >= 1){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}else{
break;
}
}
}
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
其中mutex是互斥鎖,它可以是任意對象,但必須是同一對象,不同對象代表不同鎖。在這裏用this表示用RunnableImpl實例化的對象本身。
- 將對共享數據操作部分封裝成一個方法,用
synchrosized
修飾。
代碼示例
class RunnableImpl implements Runnable{
private int num = 20;
public void run(){
while(num >= 1){
printNum();
}
}
public synchronized void printNum(){
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(num--);
}
}
public class TestThread{
public static void main(String[] args){
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri);
Thread t2 = new Thread(ri);
t1.start();
t2.start();
}
}
上述解決方案是將共享數據操作放進一個synchronized聲明的方法中,該解決方案的互斥鎖默認爲this。
其實,解決這種線程問題的方法就是提出“鎖”的概念,當一個線程進入共享數據操作時就上鎖(lockd),直到該線程對操作完畢。實質上當某個線程進入被synchronized修飾的代碼塊或方法時,該方法塊的狀態改變,只有被同步監視器(mutex)標記的線程可進入該區域,當此條線程離開,狀態恢復。示意圖如下(只是想試試GIF圖製作,請多包涵):
11. 線程的通信
線程的通信主要是三個方法:
- wait():使線程掛起,並交出CPU執行權,被掛起的線程會被放進等待隊列中等待喚醒
- notify():喚醒等待隊列中優先級最高的一個線程
- notifyAll():喚醒所有線程
首先,這些方法不是Java.lang.Thread裏的方法,而是Java.lang.Object的方法,也就是所有的對象都具有該方法,但該方法只能使用在同步代碼塊或同步方法中。