多線程系列(1):創建線程和線程的常用方法

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。

一:爲什麼要學多線程

  1. 高級面試 :多線程幾乎是面試中必問的題,所以掌握一定的基礎知識是必須的。
  2. 瞭解併發編程:實際工作中很少寫多線程的代碼,這部分代碼一般都被人封裝起來了,在業務中使用多線程的機會也不是很多(看具體項目),雖然代碼中很少會自己去創建線程,但是實際環境中每行代碼卻都是並行執行的,同一時刻大量請求同一個接口,併發可能會產生一些問題,所以也需要掌握一定的併發知識

二:進程與線程

1. 進程

進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。程序運行時系統就會創建一個進程,併爲它分配資源,然後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。

2. 線程

線程是一條執行路徑,是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程可以由很多個線程組成,線程間共享進程的所有資源,每個線程有自己的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就允許多個線程同時運行。同樣多線程也可以實現併發操作,每個請求分配一個線程來處理。

一個正在運行的軟件(如迅雷)就是一個進程,一個進程可以同時運行多個任務( 迅雷軟件可以同時下載多個文件,每個下載任務就是一個線程), 可以簡單的認爲進程是線程的集合。

線程是一條可以執行的路徑。多線程就是同時有多條執行路徑在同時(並行)執行。

3. 進程與線程的關係

一個程序就是一個進程,而一個程序中的多個任務則被稱爲線程。進程是表示資源分配的基本單位,又是調度運行的基本單位。,亦即執行處理機調度的基本單位。 進程和線程的關係:

  • 一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位。

  • 資源分配給進程,同一進程的所有線程共享該進程的所有資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量,即每個線程都有自己的堆棧和局部變量。

  • 處理機分給線程,即真正在處理機上運行的是線程。

  • 線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。

如果把上課的過程比作進程,把老師比作CPU,那麼可以把每個學生比作每個線程,所有學生共享這個教室(也就是所有線程共享進程的資源),上課時學生A向老師提出問題,老師對A進行解答,此時可能會有學生B對老師的解答不懂會提出B的疑問(注意:此時可能老師還沒有對A同學的問題解答完畢),此時老師又向學生B解惑,解釋完之後又繼續回答學生A的問題,同一時刻老師只能向一個學生回答問題(即:當多個線程在運行時,同一個CPU在某一個時刻只能服務於一個線程,可能一個線程分配一點時間,時間到了就輪到其它線程執行了,這樣多個線程在來回的切換)

4. 爲什麼要使用多線程

多線程可以提高程序的效率。

實際生活案例:村長要求喜洋洋在一個小時內打100桶水,可以喜洋洋一個小時只能打25桶水,如果這樣就需要4個小時才能完成任務,爲了在一個小時能夠完成,喜洋洋就請美洋洋、懶洋洋、沸洋洋,來幫忙,這樣4只羊同時幹活,在一小時內完成了任務。原本用4個小時完成的任務現在只需要1個小時就完成了,如果把每隻羊看做一個線程,多隻羊即多線程可以提高程序的效率。

5. 多線程應用場景

  • 一般線程之間比較獨立,互不影響
  • 一個線程發生問題,一般不影響其它線程

三:多線程的實現方式

1. 順序編程

順序編程:程序從上往下的同步執行,即如果第一行代碼執行沒有結束,第二行代碼就只能等待第一行執行結束後才能結束。

public class Main {
    // 順序編程 吃喝示例:當吃飯吃不完的時候,是不能喝酒的,只能吃完晚才能喝酒
    public static void main(String[] args) throws Exception {
		// 先吃飯再喝酒
        eat();
        drink();
    }

    private static void eat() throws Exception {
        System.out.println("開始吃飯?...\t" + new Date());
        Thread.sleep(5000);
        System.out.println("結束吃飯?...\t" + new Date());
    }

    private static void drink() throws Exception {
        System.out.println("開始喝酒?️...\t" + new Date());
        Thread.sleep(5000);
        System.out.println("結束喝酒?...\t" + new Date());
    }
}

這裏寫圖片描述

2. 併發編程

併發編程:多個任務可以同時做,常用與任務之間比較獨立,互不影響。
線程上下文切換:

同一個時刻一個CPU只能做一件事情,即同一時刻只能一個線程中的部分代碼,假如有兩個線程,Thread-0和Thread-1,剛開始CPU說Thread-0你先執行,給你3毫秒時間,Thread-0執行了3毫秒時間,但是沒有執行完,此時CPU會暫停Thread-0執行並記錄Thread-0執行到哪行代碼了,當時的變量的值是多少,然後CPU說Thread-1你可以執行了,給你2毫秒的時間,Thread-1執行了2毫秒也沒執行完,此時CPU會暫停Thread-1執行並記錄Thread-1執行到哪行代碼了,當時的變量的值是多少,此時CPU又說Thread-0又該你,這次我給你5毫秒時間,去執行吧,此時CPU就找出上次Thread-0線程執行到哪行代碼了,當時的變量值是多少,然後接着上次繼續執行,結果用了2毫秒就Thread-0就執行完了,就終止了,然後CPU說Thread-1又輪到你,這次給你4毫秒,同樣CPU也會先找出上次Thread-1線程執行到哪行代碼了,當時的變量值是多少,然後接着上次繼續開始執行,結果Thread-1在4毫秒內也執行結束了,Thread-1也結束了終止了。CPU在來回改變線程的執行機會稱之爲線程上下文切換。

public class Main {
    public static void main(String[] args) {
	    // 一邊吃飯一邊喝酒
        new EatThread().start();
        new DrinkThread().start();
    }
}

class EatThread extends Thread{
    @Override
    public void run() {
        System.out.println("開始吃飯?...\t" + new Date());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("結束吃飯?...\t" + new Date());
    }
}

class DrinkThread extends Thread {
    @Override
    public void run() {
        System.out.println("開始喝酒?️...\t" + new Date());
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("結束喝酒?...\t" + new Date());
    }
}

併發編程,一邊吃飯一邊喝酒總共用時5秒,比順序編程更快,因爲併發編程可以同時運行,而不必等前面的代碼運行完之後才允許後面的代碼

這裏寫圖片描述

本示例主要啓動3個線程,一個主線程main thread、一個吃飯線程(Thread-0)和一個喝酒線程(Thread-1),共三個線程, 三個線程併發切換着執行。main線程很快執行完,吃飯線程和喝酒線程會繼續執行,直到所有線程(非守護線程)執行完畢,整個程序纔會結束,main線程結束並不意味着整個程序結束。
這裏寫圖片描述

  • 順序:代碼從上而下按照固定的順序執行,只有上一件事情執行完畢,才能執行下一件事。就像物理電路中的串行,假如有十件事情,一個人來完成,這個人必須先做第一件事情,然後再做第二件事情,最後做第十件事情,按照順序做。

  • 並行:多個操作同時處理,他們之間是並行的。假如十件事情,兩個人來完成,每個人在某個時間點各自做各自的事情,互不影響

  • 併發:將一個操作分割成多個部分執行並且允許無序處理,假如有十件事情,如果有一個人在做,這個人可能做一會這個不想做了,再去做別的,做着做着可能也不想做了,又去幹其它事情了,看他心情想幹哪個就幹哪個,最終把十件事情都做完。如果有兩個人在做,他們倆先分一下,比如張三做4件,李四做6件,他們各做自己的,在做自己的事情過程中可以隨意的切換到別的事情,不一定要把某件事情幹完再去幹其它事情,有可能一件事做了N次才做完。

通常一臺電腦只有一個cpu,多個線程屬於併發執行,如果有多個cpu,多線程併發執行有可能變成並行執行。
這裏寫圖片描述

3. 多線程創建方式

  • 繼承 Thread
  • 實現 Runable
  • 實現 Callable
①:繼成java.lang.Thread, 重寫run()方法
public class Main {
    public static void main(String[] args) {
        new MyThread().start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

Thread 類

package java.lang;
public class Thread implements Runnable {
	// 構造方法
	public Thread(Runnable target);
	public Thread(Runnable target, String name);
	
	public synchronized void start();
}

Runnable 接口

package java.lang;

@FunctionalInterface
public interface Runnable {
    pubic abstract void run();
}

②:實現java.lang.Runnable接口,重寫run()方法,然後使用Thread類來包裝

public class Main {
    public static void main(String[] args) {
    	 // 將Runnable實現類作爲Thread的構造參數傳遞到Thread類中,然後啓動Thread類
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

可以看到兩種方式都是圍繞着Thread和Runnable,繼承Thread類把run()寫到類中,實現Runnable接口是把run()方法寫到接口中然後再用Thread類來包裝, 兩種方式最終都是調用Thread類的start()方法來啓動線程的。
兩種方式在本質上沒有明顯的區別,在外觀上有很大的區別,第一種方式是繼承Thread類,因Java是單繼承,如果一個類繼承了Thread類,那麼就沒辦法繼承其它的類了,在繼承上有一點受制,有一點不靈活,第二種方式就是爲了解決第一種方式的單繼承不靈活的問題,所以平常使用就使用第二種方式

其它變體寫法:

public class Main {
    public static void main(String[] args) {
        // 匿名內部類
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
            }
        }).start();

        // 尾部代碼塊, 是對匿名內部類形式的語法糖
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
            }
        }.start();

        // Runnable是函數式接口,所以可以使用Lamda表達式形式
        Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
        new Thread(runnable).start();
    }
}

③:實現Callable接口,重寫call()方法,然後包裝成java.util.concurrent.FutureTask, 再然後包裝成Thread

Callable:有返回值的線程,能取消線程,可以判斷線程是否執行完畢

public class Main {
    public static void main(String[] args) throws Exception {
    	 // 將Callable包裝成FutureTask,FutureTask也是一種Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // get方法會阻塞調用的線程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}


class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");

        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);

        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
        return sum;
    }
}

Callable 也是一種函數式接口

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

FutureTask

public class FutureTask<V> implements RunnableFuture<V> {
	// 構造函數
	public FutureTask(Callable<V> callable);
	
	// 取消線程
	public boolean cancel(boolean mayInterruptIfRunning);
	// 判斷線程
	public boolean isDone();
	// 獲取線程執行結果
	public V get() throws InterruptedException, ExecutionException;
}

RunnableFuture

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

三種方式比較:

  • Thread: 繼承方式, 不建議使用, 因爲Java是單繼承的,繼承了Thread就沒辦法繼承其它類了,不夠靈活
  • Runnable: 實現接口,比Thread類更加靈活,沒有單繼承的限制
  • Callable: Thread和Runnable都是重寫的run()方法並且沒有返回值,Callable是重寫的call()方法並且有返回值並可以藉助FutureTask類來判斷線程是否已經執行完畢或者取消線程執行
  • 當線程不需要返回值時使用Runnable,需要返回值時就使用Callable,一般情況下不直接把線程體代碼放到Thread類中,一般通過Thread類來啓動線程
  • Thread類是實現Runnable,Callable封裝成FutureTask,FutureTask實現RunnableFuture,RunnableFuture繼承Runnable,所以Callable也算是一種Runnable,所以三種實現方式本質上都是Runnable實現

四:線程的狀態

  1. 創建(new)狀態: 準備好了一個多線程的對象,即執行了new Thread(); 創建完成後就需要爲線程分配內存
  2. 就緒(runnable)狀態: 調用了start()方法, 等待CPU進行調度
  3. 運行(running)狀態: 執行run()方法
  4. 阻塞(blocked)狀態: 暫時停止執行線程,將線程掛起(sleep()、wait()、join()、沒有獲取到鎖都會使線程阻塞), 可能將資源交給其它線程使用
  5. 死亡(terminated)狀態: 線程銷燬(正常執行完畢、發生異常或者被打斷interrupt()都會導致線程終止)

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

五:Thread常用方法

Thread

public class Thread implements Runnable {
    // 線程名字
    private volatile String name;
    // 線程優先級(1~10)
    private int priority;
    // 守護線程
    private boolean daemon = false;
    // 線程id
    private long tid;
    // 線程組
    private ThreadGroup group;
    
    // 預定義3個優先級
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
    
    
    // 構造函數
    public Thread();
    public Thread(String name);
    public Thread(Runnable target);
    public Thread(Runnable target, String name);
    // 線程組
    public Thread(ThreadGroup group, Runnable target);
    
    
    // 返回當前正在執行線程對象的引用
    public static native Thread currentThread();
    
    // 啓動一個新線程
    public synchronized void start();
    // 線程的方法體,和啓動線程沒毛關係
    public void run();
    
    // 讓線程睡眠一會,由活躍狀態改爲掛起狀態
    public static native void sleep(long millis) throws InterruptedException;
    public static void sleep(long millis, int nanos) throws InterruptedException;
    
    // 打斷線程 中斷線程 用於停止線程
    // 調用該方法時並不需要獲取Thread實例的鎖。無論何時,任何線程都可以調用其它線程的interruptf方法
    public void interrupt();
    public boolean isInterrupted()
    
    // 線程是否處於活動狀態
    public final native boolean isAlive();
    
    // 交出CPU的使用權,從運行狀態改爲掛起狀態
    public static native void yield();
    
    public final void join() throws InterruptedException
    public final synchronized void join(long millis)
    public final synchronized void join(long millis, int nanos) throws InterruptedException
    
    
    // 設置線程優先級
    public final void setPriority(int newPriority);
    // 設置是否守護線程
    public final void setDaemon(boolean on);
    // 線程id
    public long getId() { return this.tid; }
    
    
    // 線程狀態
    public enum State {
        // new 創建
        NEW,

        // runnable 就緒
        RUNNABLE,

        // blocked 阻塞
        BLOCKED,

        // waiting 等待
        WAITING,

        // timed_waiting
        TIMED_WAITING,

        // terminated 結束
        TERMINATED;
    }
}

public static void main(String[] args) {
    // main方法就是一個主線程

    // 獲取當前正在運行的線程
    Thread thread = Thread.currentThread();
    // 線程名字
    String name = thread.getName();
    // 線程id
    long id = thread.getId();
    // 線程優先級
    int priority = thread.getPriority();
    // 是否存活
    boolean alive = thread.isAlive();
    // 是否守護線程
    boolean daemon = thread.isDaemon();

    // Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
    System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
}

0. Thread.currentThread()
public static void main(String[] args) {
    Thread thread = Thread.currentThread();
    // 線程名稱
    String name = thread.getName();
    // 線程id
    long id = thread.getId();
    // 線程已經啓動且尚未終止
    // 線程處於正在運行或準備開始運行的狀態,就認爲線程是“存活”的
    boolean alive = thread.isAlive();
    // 線程優先級
    int priority = thread.getPriority();
    // 是否守護線程
    boolean daemon = thread.isDaemon();
    
    // Thread[name=main,id=1,alive=true,priority=5,daemon=false]
    System.out.println("Thread[name=" + name + ",id=" + id + ",alive=" + alive + ",priority=" + priority + ",daemon=" + daemon + "]");
}

1. start() 與 run()
public static void main(String[] args) throws Exception {
   new Thread(()-> {
       for (int i = 0; i < 5; i++) {
           System.out.println(Thread.currentThread().getName() + " " + i);
           try { Thread.sleep(200); } catch (InterruptedException e) { }
       }
   }, "Thread-A").start();

   new Thread(()-> {
       for (int j = 0; j < 5; j++) {
           System.out.println(Thread.currentThread().getName() + " " + j);
           try { Thread.sleep(200); } catch (InterruptedException e) { }
       }
   }, "Thread-B").start();
}

start(): 啓動一個線程,線程之間是沒有順序的,是按CPU分配的時間片來回切換的。
這裏寫圖片描述

public static void main(String[] args) throws Exception {
    new Thread(()-> {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            try { Thread.sleep(200); } catch (InterruptedException e) { }
        }
    }, "Thread-A").run();

    new Thread(()-> {
        for (int j = 0; j < 5; j++) {
            System.out.println(Thread.currentThread().getName() + " " + j);
            try { Thread.sleep(200); } catch (InterruptedException e) { }
        }
    }, "Thread-B").run();
}

注意:執行結果都是main主線程
這裏寫圖片描述

run(): 調用線程的run方法,就是普通的方法調用,雖然將代碼封裝到兩個線程體中,可以看到線程中打印的線程名字都是main主線程,run()方法用於封裝線程的代碼,具體要啓動一個線程來運行線程體中的代碼(run()方法)還是通過start()方法來實現,調用run()方法就是一種順序編程不是併發編程。

有些面試官經常問一些啓動一個線程是用start()方法還是run()方法,爲了面試而面試。

2. sleep() 與 interrupt()
public static native void sleep(long millis) throws InterruptedException;
public void interrupt();

sleep(long millis): 睡眠指定時間,程序暫停運行,睡眠期間會讓出CPU的執行權,去執行其它線程,同時CPU也會監視睡眠的時間,一旦睡眠時間到就會立刻執行(因爲睡眠過程中仍然保留着鎖,有鎖只要睡眠時間到就能立刻執行)。

  • sleep(): 睡眠指定時間,即讓程序暫停指定時間運行,時間到了會繼續執行代碼,如果時間未到就要醒需要使用interrupt()來隨時喚醒
  • interrupt(): 喚醒正在睡眠的程序,調用interrupt()方法,會使得sleep()方法拋出InterruptedException異常,當sleep()方法拋出異常就中斷了sleep的方法,從而讓程序繼續運行下去
public static void main(String[] args) throws Exception {
    Thread thread0 = new Thread(()-> {
        try {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,讓我睡10秒,中間有事叫我,zZZ。。。");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要繼續幹活了");
        }
    });
    thread0.start();

    // 這裏睡眠只是爲了保證先讓上面的那個線程先執行
    Thread.sleep(2000);

    new Thread(()-> {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,別睡了,起來幹活了!!!");
        // 無需獲取鎖就可以調用interrupt
        thread0.interrupt();
    }).start();
}

這裏寫圖片描述

3. wait() 與 notify()

wait、notify和notifyAll方法是Object類的final native方法。所以這些方法不能被子類重寫,Object類是所有類的超類,因此在程序中可以通過this或者super來調用this.wait(), super.wait()

  • wait(): 導致線程進入等待阻塞狀態,會一直等待直到它被其他線程通過notify()或者notifyAll喚醒。該方法只能在同步方法中調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。wait(long timeout): 時間到了自動執行,類似於sleep(long millis)
  • notify(): 該方法只能在同步方法或同步塊內部調用, 隨機選擇一個(注意:只會通知一個)在該對象上調用wait方法的線程,解除其阻塞狀態
  • notifyAll(): 喚醒所有的wait對象

注意:

  • Object.wait()和Object.notify()和Object.notifyall()必須寫在synchronized方法內部或者synchronized塊內部
  • 讓哪個對象等待wait就去通知notify哪個對象,不要讓A對象等待,結果卻去通知B對象,要操作同一個對象

Object

public class Object {
	public final void wait() throws InterruptedException;
	public final native void wait(long timeout) throws InterruptedException;
	public final void wait(long timeout, int nanos) throws InterruptedException;
	
	
	public final native void notify();
	public final native void notifyAll();
}

WaitNotifyTest

public class WaitNotifyTest {
    public static void main(String[] args) throws Exception {
        WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
        new Thread(() -> {
            try {
                waitNotifyTest.printFile();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                waitNotifyTest.printFile();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t睡覺1秒中,目的是讓上面的線程先執行,即先執行wait()");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            waitNotifyTest.notifyPrint();
        }).start();
    }

    private synchronized void printFile() throws InterruptedException {
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
        this.wait();
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印結束。。。");
    }

    private synchronized void notifyPrint() {
        this.notify();
        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成...");
    }
}

這裏寫圖片描述

wait():讓程序暫停執行,相當於讓當前,線程進入當前實例的等待隊列,這個隊列屬於該實例對象,所以調用notify也必須使用該對象來調用,不能使用別的對象來調用。調用wait和notify必須使用同一個對象來調用。
這裏寫圖片描述

this.notifyAll();
這裏寫圖片描述

4. sleep() 與 wait()
① Thread.sleep(long millis): 睡眠時不會釋放鎖
public static void main(String[] args) throws InterruptedException {
    Object lock = new Object();

    new Thread(() -> {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                try { Thread.sleep(1000); } catch (InterruptedException e) { }
            }
        }
    }).start();

    Thread.sleep(1000);

    new Thread(() -> {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
            }
        }
    }).start();
}

因main方法中Thread.sleep(1000)所以上面的線程Thread-0先被執行,當循環第一次時就會Thread.sleep(1000)睡眠,因爲sleep並不會釋放鎖,所以Thread-1得不到執行的機會,所以直到Thread-0執行完畢釋放鎖對象lock,Thread-1才能拿到鎖,然後執行Thread-1;
這裏寫圖片描述

5. wait() 與 interrupt()

wait(): 方法的作用是釋放鎖,加入到等待隊列,當調用interrupt()方法後,線程必須先獲取到鎖後,然後才拋出異常InterruptedException 。注意: 在獲取鎖之前是不會拋出異常的,只有在獲取鎖之後纔會拋異常

所有能拋出InterruptedException的方法都可以通過interrupt()來取消的

public static native void sleep(long millis) throws InterruptedException;
public final void wait() throws InterruptedException;
public final void join() throws InterruptedException;
public void interrupt()

notify()和interrupt()
從讓正在wait的線程重新運行這一點來說,notify方法和intterrupt方法的作用有些類似,但仍有以下不同之處:

  • notify/notifyAll是java.lang.Object類的方法,喚醒的是該實例的等待隊列中的線程,而不能直接指定某個具體的線程。notify/notifyAll喚醒的線程會繼續執行wait的下一條語句,另外執行notify/notifyAll時線程必須要獲取實例的鎖

  • interrupte方法是java.lang.Thread類的方法,可以直接指定線程並喚醒,當被interrupt的線程處於sleep或者wait中時會拋出InterruptedException異常。執行interrupt()並不需要獲取取消線程的鎖。

  • 總之notify/notifyAll和interrupt的區別在於是否能直接讓某個指定的線程喚醒、執行喚醒是否需要鎖、方法屬於的類不同

6. interrupt()

有人也許認爲“當調用interrupt方法時,調用對象的線程就會InterruptedException異常”, 其實這是一種誤解,實際上interrupt方法只是改變了線程的“中斷狀態”而已,所謂中斷狀態是一個boolean值,表示線程是否被中斷的狀態。

public class Thread implements Runnable {
	public void interrupt() {
		中斷狀態 = true;
	}
	
	// 檢查中斷狀態
	public boolean isInterrupted();
	
	// 檢查中斷狀態並清除當前線程的中斷狀態
	public static boolean interrupted() {
		// 僞代碼
		boolean isInterrupted = isInterrupted();
		中斷狀態 = false;
	}
}	

假設Thread-0執行了sleep、wait、join中的一個方法而停止運行,在Thread-1中調用了interrupt方法,此時線程Thread-0的確會拋出InterruptedException異常,但這其實是sleep、wait、join中的方法內部會對線程的“中斷狀態”進行檢查,如果中斷狀態爲true,就會拋出InterruptedException異常。假如某個線程的中斷狀態爲true,但線程體中卻沒有調用或者沒有判斷線程中斷狀態的值,那麼線程則不會拋出InterruptedException異常。

isInterrupted() 檢查中斷狀態
若指定線程處於中斷狀態則返回true,若指定線程爲非中斷狀態,則反回false, isInterrupted() 只是獲取中斷狀態的值,並不會改變中斷狀態的值。

interrupted()
檢查中斷狀態並清除當前線程的中斷狀態。如當前線程處於中斷狀態返回true,若當前線程處於非中斷狀態則返回false, 並清除中斷狀態(將中斷狀態設置爲false), 只有這個方法纔可以清除中斷狀態,Thread.interrupted的操作對象是當前線程,所以該方法並不能用於清除其它線程的中斷狀態。

interrupt()與interrupted()

  • interrupt():打斷線程,將中斷狀態修改爲true
  • interrupted(): 不打斷線程,獲取線程的中斷狀態,並將中斷狀態設置爲false

這裏寫圖片描述

public class InterrupptTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        boolean interrupted = thread.isInterrupted();
        // interrupted=false
        System.out.println("interrupted=" + interrupted);

        thread.interrupt();

        boolean interrupted2 = thread.isInterrupted();
        // interrupted2=true
        System.out.println("interrupted2=" + interrupted2);

        boolean interrupted3 = Thread.interrupted();
        // interrupted3=false
        System.out.println("interrupted3=" + interrupted3);
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        synchronized (this) {
            try {
                wait();
            } catch (InterruptedException e) {
                // InterruptedException	false
                System.out.println("InterruptedException\t" + Thread.currentThread().isInterrupted());
            }
        }
    }
}


這裏寫圖片描述

② object.wait(long timeout): 會釋放鎖
public class SleepWaitTest {
    public static void main(String[] args) throws InterruptedException {
        SleepWaitTest object = new SleepWaitTest();

        new Thread(() -> {
            synchronized (object) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
                try {
                    object.wait(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印結束。。。");
            }
        }).start();

		 // 先上面的線程先執行
        Thread.sleep(1000);

        new Thread(() -> {
            synchronized (object) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                }
            }
        }).start();
    }
}

因main方法中有Thread.sleep(1000)所以上面的線程Thread-0肯定會被先執行,當Thread-0被執行時就拿到了object對象鎖,然後進入wait(5000)5秒鐘等待,此時wait釋放了鎖,然後Thread-1就拿到了鎖就執行線程體,Thread-1執行完後就釋放了鎖,當等待5秒後Thread-0就能再次獲取object鎖,這樣就繼續執行後面的代碼。wait方法是釋放鎖的,如果wait方法不釋放鎖那麼Thread-1是拿不到鎖也就沒有執行的機會的,事實是Thread-1得到了執行,所以說wait方法會釋放鎖

這裏寫圖片描述

③ sleep與wait的區別
  • sleep在Thread類中,wait在Object類中
  • sleep不會釋放鎖,wait會釋放鎖
  • sleep使用interrupt()來喚醒,wait需要notify或者notifyAll來通知
5.join()

讓當前線程加入父線程,加入後父線程會一直wait,直到子線程執行完畢後父線程才能執行。當我們調用某個線程的這個方法時,這個方法會掛起調用線程,直到被調用線程結束執行,調用線程纔會繼續執行。

將某個線程加入到當前線程中來,一般某個線程和當前線程依賴關係比較強,必須先等待某個線程執行完畢才能執行當前線程。一般在run()方法內使用

join() 方法:

public final void join() throws InterruptedException {
        join(0);
}


public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
    	 // 循環檢查線程的狀態是否還活着,如果死了就結束了,如果活着繼續等到死
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}


public final synchronized void join(long millis, int nanos) throws InterruptedException {

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException("nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }

    join(millis);
}


JoinTest

public class JoinTest {
    public static void main(String[] args) {
        new Thread(new ParentRunnable()).start();
    }
}

class ParentRunnable implements Runnable {
    @Override
    public void run() {
        // 線程處於new狀態
        Thread childThread = new Thread(new ChildRunable());
        // 線程處於runnable就緒狀態
        childThread.start();
        try {
            // 當調用join時,parent會等待child執行完畢後再繼續運行
            // 將某個線程加入到當前線程
            childThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 5; i++) {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父線程 running");
        }
    }
}

class ChildRunable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子線程 running");
        }
    }
}

程序進入主線程,運行Parent對應的線程,Parent的線程代碼分兩段,一段是啓動一個子線程,一段是Parent線程的線程體代碼,首先會將Child線程加入到Parent線程,join()方法會調用join(0)方法(join()方法是普通方法並沒有加鎖,join(0)會加鎖),join(0)會執行while(isAlive()) { wait(0);} 循環判斷線程是否處於活動狀態,如果是繼續wait(0)知道isAlive=false結束掉join(0), 從而結束掉join(), 最後回到Parent線程體中繼續執行其它代碼。

在Parent調用child.join()後,child子線程正常運行,Parent父線程會等待child子線程結束後再繼續運行。
這裏寫圖片描述

  • join() 和 join(long millis, int nanos) 最後都調用了 join(long millis)。

  • join(long millis, int nanos)和join(long millis)方法 都是synchronized。

  • join() 調用了join(0),從源碼可以看到join(0)不斷檢查當前線程是否處於Active狀態。

  • join() 和 sleep() 一樣,都可以被中斷(被中斷時,會拋出 InterrupptedException 異常);不同的是,join() 內部調用了wait(),會出讓鎖,而 sleep() 會一直保持鎖。

6. yield()

交出CPU的執行時間,不會釋放鎖,讓線程進入就緒狀態,等待重新獲取CPU執行時間,yield就像一個好人似的,當CPU輪到它了,它卻說我先不急,先給其他線程執行吧, 此方法很少被使用到,

/**
 * A hint to the scheduler that the current thread is willing to yield
 * its current use of a processor. The scheduler is free to ignore this
 * hint.
 *
 * <p> Yield is a heuristic attempt to improve relative progression
 * between threads that would otherwise over-utilise a CPU. Its use
 * should be combined with detailed profiling and benchmarking to
 * ensure that it actually has the desired effect.
 *
 * <p> It is rarely appropriate to use this method. It may be useful
 * for debugging or testing purposes, where it may help to reproduce
 * bugs due to race conditions. It may also be useful when designing
 * concurrency control constructs such as the ones in the
 * {@link java.util.concurrent.locks} package.
 */
public static native void yield();

這裏寫圖片描述

public static void main(String[] args) {
    new Thread(new Runnable() {
        int sum = 0;
        @Override
        public void run() {
            long beginTime=System.currentTimeMillis();
            for (int i = 0; i < 99999; i++) {
                sum += 1;
                // 去掉該行執行用2毫秒,加上271毫秒
                Thread.yield();
            }
            long endTime=System.currentTimeMillis();
            System.out.println("用時:"+ (endTime - beginTime) + " 毫秒!");
        }
    }).start();
}

sleep(long millis) 與 yeid()

  • sleep(long millis): 需要指定具體睡眠的時間,不會釋放鎖,睡眠期間CPU會執行其它線程,睡眠時間到會立刻執行
  • yeid(): 交出CPU的執行權,不會釋放鎖,和sleep不同的時當再次獲取到CPU的執行,不能確定是什麼時候,而sleep是能確定什麼時候再次執行。兩者的區別就是sleep後再次執行的時間能確定,而yeid是不能確定的
  • yield會把CPU的執行權交出去,所以可以用yield來控制線程的執行速度,當一個線程執行的比較快,此時想讓它執行的稍微慢一些可以使用該方法,想讓線程變慢可以使用sleep和wait,但是這兩個方法都需要指定具體時間,而yield不需要指定具體時間,讓CPU決定什麼時候能再次被執行,當放棄到下次再次被執行的中間時間就是間歇等待的時間
7. setDaemon(boolean on)

線程分兩種:

  • 用戶線程:如果主線程main停止掉,不會影響用戶線程,用戶線程可以繼續運行。
  • 守護線程:如果主線程死亡,守護線程如果沒有執行完畢也要跟着一塊死(就像皇上死了,帶刀侍衛也要一塊死),GC垃圾回收線程就是守護線程
public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            IntStream.range(0, 5).forEach(i -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\ti=" + i);
            });
        }
    };
    thread.start();


    for (int i = 0; i < 2; i++) {
        System.out.println(Thread.currentThread().getName() + "\ti=" + i);
    }
    System.out.println("主線程執行結束,子線程仍然繼續執行,主線程和用戶線程的生命週期各自獨立。");
}

這裏寫圖片描述

public static void main(String[] args) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            IntStream.range(0, 5).forEach(i -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\ti=" + i);
            });
        }
    };
    thread.setDaemon(true);
    thread.start();


    for (int i = 0; i < 2; i++) {
        System.out.println(Thread.currentThread().getName() + "\ti=" + i);
    }
    System.out.println("主線程死亡,子線程也要陪着一塊死!");
}

這裏寫圖片描述

六 線程組

可以對線程分組,分組後可以統一管理某個組下的所有線程,例如統一中斷所有線程

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent;
    String name;
    int maxPriority;
    
    Thread threads[];
    
    private ThreadGroup() {
        this.name = "system";
        this.maxPriority = Thread.MAX_PRIORITY;
        this.parent = null;
    }
    
    public ThreadGroup(String name) {
        this(Thread.currentThread().getThreadGroup(), name);
    }
    
    public ThreadGroup(ThreadGroup parent, String name) {
        this(checkParentAccess(parent), parent, name);
    }
    
    // 返回此線程組中活動線程的估計數。 
    public int activeGroupCount();
    
    // 中斷此線程組中的所有線程。
    public final void interrupt();
}

public static void main(String[] args) {
    String mainThreadGroupName = Thread.currentThread().getThreadGroup().getName();
    System.out.println(mainThreadGroupName);
    // 如果一個線程沒有指定線程組,默認爲當前線程所在的線程組
    new Thread(() -> { }, "my thread1").start();

    ThreadGroup myGroup = new ThreadGroup("MyGroup");
    myGroup.setMaxPriority(5);

    Runnable runnable = () -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        String groupName = threadGroup.getName();
        ThreadGroup parentGroup = threadGroup.getParent();
        String parentGroupName = parentGroup.getName();
        ThreadGroup grandpaThreadGroup = parentGroup.getParent();
        String grandpaThreadGroupName = grandpaThreadGroup.getName();
        int maxPriority = threadGroup.getMaxPriority();
        int activeCount = myGroup.activeCount();

        // system <- main <- MyGroup(1) <- my thread2
        System.out.println(MessageFormat.format("{0} <- {1} <- {2}({3}) <- {4}",
                grandpaThreadGroupName,
                parentGroupName,
                groupName,
                activeCount,
                Thread.currentThread().getName()));
    };

    new Thread(myGroup, runnable, "my thread2").start();
}

線程組與線程組之間是有父子關係的,自定義線程組的父線程組是main線程組,main線程組的父線程組是system線程組。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章