並發編程-01-多執行緒基礎

本文參考自螞蟻課堂相關內容

1.執行緒與進程區別

每個正在系統上運行的程式都是一個進程。每個進程包含一到多個執行緒。

執行緒是一組指令的集合,或者是程式的特殊段,它可以在程式里獨立執行,也可以把它理解為程式碼運行的上下文。所以執行緒基本上是輕量級的進程,它負責在單個程式里執行多任務。通常由作業系統負責多個執行緒的調度和執行。

使用執行緒可以把佔據時間長的程式中的任務放到後台去處理,程式的運行速度可能加快,在一些等待的任務實現上如用戶輸入、文件讀寫和網路收發數據等,執行緒就比較有用了。在這種情況下可以釋放一些珍貴的資源如記憶體佔用等。

如果有大量的執行緒,會影響性能,因為作業系統需要在他們之間切換,更多的執行緒需要更多的記憶體空間,執行緒的中止需要考慮其對程式運行的影響。通常塊模型數據是在多個執行緒間共享的,需要放置執行緒死鎖情況的發生

總結:進程是所有執行緒的集合,每一個執行緒是進程中的一條執行路徑。

3.多執行緒應用場景

主要能體現到多執行緒提高程式效率

舉例:迅雷多執行緒下載、資料庫連接池、分批發送簡訊等。

4.同步與非同步概念

5.多執行緒創建方式

(1)第一種:繼承Thread類,重寫run方法

// 1.什麼是執行緒 執行緒是一條執行路徑,每個執行緒互不影響。
// 2.什麼是多執行緒,多執行緒在一個進程中,有多條不同的執行路徑,並且同時執行。目的是為了提高程式效率。
// 3.在一個進程中,一定會有主執行緒
// 4.如果連執行緒主執行緒都沒有,怎麼執行程式。
// 執行緒幾種分類 用戶執行緒,守護執行緒
// 主執行緒 子執行緒 GC執行緒

// 創建執行緒的幾種方法
//   1.繼承Thread類 重寫run方法
//   2.實現runnable介面
//   3.使用匿名內部類方式
//   4.使用執行緒池進行管理(企業常用)

// 1.繼承Thread類 重寫run方法,run方法中,需要執行緒執行程式碼
class CreateThread1 extends Thread {

    // run方法中編寫 多執行緒需要執行的程式碼
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子--i:" + i);
        }
    }
}

public class Thread001 {

    // 交替執行
    public static void main(String[] args) {
        System.out.println("-----main--主執行緒開始-----");
        // 1.創建一個執行緒
        CreateThread1 thread1 = new CreateThread1();
        // 2.開始執行執行緒 主頁 開啟執行緒不是調用run方法 而是start方法
        thread1.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主--i:"+i);
        }
        System.out.println("-----main--主執行緒結束-----");
    }
}

(2)第二種:實現Runnable介面,重寫run方法

// 1.實現runnable介面,重寫run方法
class CreateThread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子--i:" + i);
        }
    }
}

public class Thread002 {
    public static void main(String[] args) {
        System.out.println("-----main--主執行緒開始-----");
        // 1.創建一個執行緒
        CreateThread2 createThread2 = new CreateThread2();
        Thread thread2 = new Thread(createThread2);
        // 2.啟動執行緒
        thread2.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主--i:"+i);
        }
        System.out.println("-----main--主執行緒結束-----");
    }
}

(3)第三種:使用匿名內部類方式

public class Thread003 {
    public static void main(String[] args) {
        System.out.println("-----main--主執行緒開始-----");
        // 1.創建一個執行緒
//        Thread thread3 = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 100; i++) {
//                    System.out.println("子--i:" + i);
//                }
//            }
//        });
        // lambda
        Thread thread3 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("子--i:" + i);
            }
        });
        // 2.啟動執行緒
        thread3.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("主--i:"+i);
        }
        System.out.println("-----main--主執行緒結束-----");
    }
}

(4)第四種:執行緒池管理(企業常用)

(5)Q&A

​ Q1:使用繼承Thread類好還是使用實現Runnable介面好?

​ A1:使用實現runnable介面好,因為實現了介面還可以繼續繼承,繼承了類之後不能再繼承。

​ Q2:啟動執行緒時調用start方法還是run方法?

​ A2:啟動執行緒調用的是start方法,不是run方法,調用run方法只是使用實例調用方法。

6.獲取執行緒對象以及名稱

常用執行緒api方法
start() 啟動執行緒
currentThread() 獲取當前執行緒對象
getID() 獲取當前執行緒ID,Thread編號,改變好從0開始
getName() 獲取當前執行緒名稱
sleep(long mill) 休眠執行緒
stop() 停止執行緒
常用執行緒構造函數
Thread() 分配一個新的Thread對象
Thread(String name) 分配一個新的Thread對象,具有指定的name
Thread(Runnable r) 分配一個新的Thread對象
Thread(Runnable r, String name) 分配一個新的Thread對象

7.多執行緒運行狀態

執行緒從創建、運行到結束總是處於下面五個狀態之一:新建狀態、就緒狀態、運行狀態、阻塞狀態、死亡狀態

(1)新建狀態

當用new操作符創建一個執行緒時,例如new Thread(r),執行緒還沒有開始運行,此時執行緒處在新建狀態。當一個執行緒處於新建狀態時,程式還沒有開始運行執行緒中的程式碼。

(2)就緒狀態

一個新創建的執行緒並不自動開始執行,要執行執行緒,必須調用執行緒的start()方法。當執行緒對象調用start()方法即啟動了執行緒,start()方法創建執行緒運行所需的系統資源,並調度執行緒運行run()方法。當start()方法返回後,執行緒就處於就緒狀態。

(3)運行狀態

當執行緒獲得CPU時間片後,它才進入運行狀態,真正開始執行run()方法。

(4)阻塞狀態

執行緒運行過程中,可能由於各種原因進入阻塞狀態:

1.執行緒通過調用sleep方法進入睡眠狀態;

2.執行緒調用一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的調用者;

3.執行緒試圖得到一個鎖,而該鎖正被其他執行緒持有;

4.執行緒在等待某個觸發條件;

(5)死亡狀態

有兩個原因會導致執行緒死亡:

1.run方法正常退出而自然死亡;

2.一個未捕獲的異常終止了run方法而使執行緒猝死;

為了確定執行緒在當前是否存活著(就是要麼是可運行的,要麼是阻塞了),需要使用isAlive()方法。如果是可運行或被阻塞,這個方法返回true,如果執行緒仍舊是new狀態且不是可運行的,或者執行緒死亡了,則返回false。

8.守護執行緒

Java中有兩種執行緒,一種是用戶執行緒,另一種是守護執行緒。

用戶執行緒是指用戶自定義創建的執行緒,主執行緒通知,用戶執行緒不會停止

守護執行緒是指當進程不存在或主執行緒停止,守護執行緒也會被停止。

使用setDaemon(true)方法設置為守護執行緒

public class DaemonThread {
    public static void main(String[] args) {
        System.out.println("主執行緒執行開始");
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("子執行緒");
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 10; i++) {
            try {
                thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主執行緒");
        }
        System.out.println("主執行緒執行完畢!");
    }
}
// 結果
thread.setDaemon(true); // 主執行緒執行完子執行緒也停止
// 不設置或設置為false 則子執行緒在主執行緒執行結束後也一直執行

9.join()方法作用

當在主執行緒當中執行到t1.join()方法時,就認為主線應該把執行權讓給t1

(1)需求:

創建一個執行緒,子執行緒執行完畢後,主執行緒才能執行

public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "i:" + i);
                }
            }
        });
        t1.start();
        // 當在主執行緒當中執行到t1.join()方法時,就認為主執行緒應該把執行權讓給t1
        t1.join();
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main-i:" + i);
        }
    }
}
// 結果
// 調用join()方法
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
main:0
main:1
main:2
main:3
main:4
main:5
main:6
main:7
main:8
main:9

// 不調用join()方法
main:0
Thread-0:0
main:1
Thread-0:1
main:2
Thread-0:2
main:3
Thread-0:3
main:4
Thread-0:4
main:5
Thread-0:5
main:6
Thread-0:6
main:7
Thread-0:7
main:8
Thread-0:8
main:9
Thread-0:9

(2)優先順序

現代作業系統基本採用時分的形式調度運行的執行緒,執行緒分配得到的時間片的多少決定了執行緒使用處理器資源的多少,也對應了執行緒優先順序這個概念。在JAVA執行緒中,通過一個 int priority來控制優先順序,範圍為1-10,其中10最高,默認值為5。下面是源碼(基於1.8)中關於priority的一些量和方法。

class PriorityThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().toString() + ":" + i);
        }
    }
}
public class Priority {
    public static void main(String[] args) {
        PriorityThread priorityThread = new PriorityThread();
        Thread t1 = new Thread(priorityThread);
        Thread t2 = new Thread(priorityThread);
        t1.start();
        // 注意設置了優先順序,不代表每次都一定會被執行。只是CPU調度會優先分配
        t1.setPriority(10);
        t2.start();
    }
}
// 結果
Thread[Thread-0,10,main]:0
Thread[Thread-0,10,main]:1
Thread[Thread-0,10,main]:2
Thread[Thread-0,10,main]:3
Thread[Thread-0,10,main]:4
Thread[Thread-0,10,main]:5
Thread[Thread-0,10,main]:6
Thread[Thread-0,10,main]:7
Thread[Thread-0,10,main]:8
Thread[Thread-0,10,main]:9
Thread[Thread-1,5,main]:0
Thread[Thread-1,5,main]:1
Thread[Thread-1,5,main]:2
Thread[Thread-1,5,main]:3
Thread[Thread-1,5,main]:4
Thread[Thread-1,5,main]:5
Thread[Thread-1,5,main]:6
Thread[Thread-1,5,main]:7
Thread[Thread-1,5,main]:8
Thread[Thread-1,5,main]:9

(3)Yield方法

Thread.yield()方法的作用:暫停當前正在執行的執行緒,並執行其他執行緒。(可能沒有效果)

yield()讓當前正在運行的執行緒回到可運行狀態,以允許具有相同優先順序的其他執行緒獲得運行的機會。因此,使用yield()的目的是讓具有相同優先順序的執行緒之間能夠適當的輪換執行。但是,實際上無法保證yield()達到讓步的目的,因為,讓步的執行緒可能被執行緒調度程式再次選中。

結論:大多數情況下,yield()將導致執行緒從運行狀態轉到可運行狀態,但有可能沒有效果。

10.作業題

Q1:現在有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

A1:

public class HomeWork {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t1,i:" + i);
            }
            System.out.println("-----end-----");
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t2,i:" + i);
            }
            System.out.println("-----end-----");
        });
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t3,i:" + i);
            }
            System.out.println("-----end-----");
        });
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        t3.start();
    }
}
// 結果
t1,i:0
t1,i:1
t1,i:2
t1,i:3
t1,i:4
t1,i:5
t1,i:6
t1,i:7
t1,i:8
t1,i:9
-----end-----
t2,i:0
t2,i:1
t2,i:2
t2,i:3
t2,i:4
t2,i:5
t2,i:6
t2,i:7
t2,i:8
t2,i:9
-----end-----
t3,i:0
t3,i:1
t3,i:2
t3,i:3
t3,i:4
t3,i:5
t3,i:6
t3,i:7
t3,i:8
t3,i:9
-----end-----

11.面試題

Q1:進程與執行緒的區別?

A1:進程是所有執行緒的集合,每一個執行緒是進程中的一條執行路徑。

Q2:為什麼要用多執行緒?

A2:提高程式效率。

Q3:多執行緒創建方式?

A3:繼承Thread類或者Runnable介面。

Q4:是繼承Thread類好還是實現Runnable介面好?

A4:Runnable介面好,因為實現了介面還可以繼續繼承。繼承Thread類不能再繼承。

Q5:你在哪裡用到了多執行緒?

A5:主要能體現到多執行緒提高程式效率。

​ 舉例:分批發送簡訊、迅雷多執行緒下載等。