BAT面試官有點懵系列,Java多線程你真的理解透徹了嗎?帶你玩轉一次多線程!Let's go!別再out了!

在這裏插入圖片描述


神標題引入

Java語言提供了非常優秀的多線程支持,程序可以通過非常簡單,簡單不能再簡單的方式來創建和啓動多線程!!!接下來,帶你玩轉一次多線程!!!走起!!!


線程和進程

一定要分清楚線程和進程的區別,幾乎所有的操作系統都有“進程”這一概念!一任務,一程序。每一個運行中的程序就是一個進程!當程序運行時,其內部包含了多個順序執行流,每一個順序執行流就是一個線程!

進程有以下三種特徵:

  • 獨立性:進程是系統中獨立存在的實體,可擁有自己的獨立資源,每個進程都有自己私有的地址空間。在沒有經過進程本身允許的情況下,用戶不能夠直接訪問其它進程的地址空間!

  • 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中動的指令集合。在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,這些概念在程序中都是不具備的。

  • 併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會互相影響。

多線程則擴展了多進程的概念,使得同一個進程可以同時併發處理多個任務。線程(Thread)也被稱作輕量級進程( Lightweight Process),線程是進程的執行單元。就像進程在操作系統中的地位一樣,線程在程序中是獨立的、併發的執行流。當進程被初始化後,主線程就被創建了。對於絕大多數的應用程序來說,通常僅要求有一個主線程,但也可以在該進程內創建多條順序執行流,這些順序執行流就是線程,每個線程也是互相獨立的。

線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程。線程可以擁有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源,它與父進程的其他線程共享該進程所擁有的全部資源。

最簡單的話闡述,就是 一個程序運行後至少有一個進程,一個進程裏可以包含多個線程,但至少要包含一個線程。


多線程的優勢

  • 進程之間不能共享內存,但線程之間共享內存非常容易。
  • 系統創建進程時需要爲該進程重新分配系統資源,但創建線程則代價小得多,因此使用多線程來實現多任務併發比多進程的效率高。
  • Java語言內置了多線程功能支持,而不是單純地作爲底層操作系統的調度方式,從而簡化了Java的多線程編程。

線程創建方式

Java使用Thread類代表線程,所有的線程對象都必須是 Thread類 或其 子類的實例

繼承Thread類來創建和啓動

  1. 定義Thread類的子類,並寫該類的run()方法,該run()方法的方法體就代表了線程需要完成
    的任務。因此把run()方法稱爲線程執行體。
  2. 創建Thread子類的實例,即創建了線程對象。
  3. 調用線程對象的start()方法來啓動該線程。
public class Main extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //獲取線程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        //實例化後調用start啓動
        new Main().start();

		//實例化後調用start啓動
        new Main().start();
    }


    /**
     * 繼承Thread重寫run方法
     */
    @Override
    public void run() {
        System.out.println("Thread start!Thread name:" + Thread.currentThread().getName());
    }
}

運行共啓動三個線程,一個主線程,兩個子線程!
在這裏插入圖片描述
主線程的執行體不是由run()方法確定的,而是由main方法確定的,main方法的方法體代表主線程的線程執行體。

  • Thread.currentThread()是Thread類的靜態方法,該方法返回正在執行的線程對象!
  • getName(): 該方法是Thread類的實例方法,該方法返回調用該方法的線程名字。
  • setName(): 設置線程的名字!

特別注意:
使用繼承Thread類的方法來創建線程類時,多個線程之間無法共享線程類的實例變量。


實現Runnable接口重寫run方法創建線程類

  1. 定義Runnable接口的實現類,並重寫該接口的run0方法,該run(方法的方法體同樣是該線程的線程執行體;
  2. 創建Runnable實現類的實例,並以此實例作爲Thread的target 來創建Thread對象,該Thread對象纔是真正的線程對象;
  3. 調用線程對象的start(方法來啓動該線程。

Runnable 對象僅僅作爲 Thread 對象的target, Runnable 實現類裏包含的 run() 方法僅作爲線程執行體。而實際的線程對象依然是Thread實例,只是該Thread 線程負責執行其 targetrun() 方法。

public class Main implements Runnable {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //獲取線程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        Main main = new Main();
        new Thread(main, "Thread ONE").start();
        new Thread(main, "Thread TWO").start();

    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

開啓兩個線程,各for循環100次打印Thread Name。

在這裏插入圖片描述


使用 Callable 和 Future 創建線程

從Java5開始,Java提供了Callable 接口,Callable接口提供了一個 call() 方法可以作爲線程執行體,但 call() 方法比 run() 方法功能更強大。

  • call() 方法可以有返回值;
  • call() 方法可以聲明拋出異常。

因此完全可以提供一個 Callable 對象作爲Thread的target,而該線程的線程執行體就是該Callable對象的call()方法。問題是: Callable 接口是Java 5新增的接口,而且它不是Runnable接口的子接口,所以Callable對象不能直接作爲Thread的target。
Java 5提供了Future 接口來代表Callable接口裏 call() 方法的返回值,併爲Future 接口提供了一個FutureTask實現類,該實現類實現了Future 接口,並實現了Runnable 接口一可以作爲Thread類的target。值得注意的是:Callable接口有泛型限制,Callable接口裏的泛型形參類型與call() 方法返回值類型相同。而且Callable接口是函數式接口,因此可使用Lambda表達式創建Callable對象。

創建並啓動有返回值的線程的步驟:

  1. 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,且該 call() 方法有返回值,再創建 Callable 實現類的實例。從Java 8開始,可以直接使用 Lambda 表達式創建 Callable 對象;
  2. 使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該Callable 對象的 call() 方法的返回值;
  3. 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程;
  4. 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //獲取線程的名字
        System.out.println("Thread name:" + Thread.currentThread().getName());

        //創建Main對象
        Main main = new Main();

        //使用Lambda表達式創建Callable<Integer>對象
        //FutureTask包裝Callable<Integer>對象
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {

                int temp = 0;
                while (temp < 10) {
                    temp++;
                    System.out.println("Thread name:" + Thread.currentThread().getName());

                }

                return temp;
            }
        });

        //啓動線程
        new Thread(task).start();

        try {

            //線程返回值
            Integer integer = task.get();

            System.out.println("Thread return:" + integer);

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        
    }
}

當線程順序執行完畢後,然後返回!
在這裏插入圖片描述

上面程序中使用 Lambda 表達式直接創建了 Callable 對象,這樣就無須先創建 Callable 實現類,再創建 Callable 對象了。實現 Callable 接口與實現 Runnable 接口並沒有太大的差別,只是 Callablecall() 方法允許聲明拋出異常,而且允許帶返回值。


三種創建線程方式做出對比

通過繼承 Thread 類或實現 RunnableCallable 接口都可以實現多線程,不過實現 Runnable 接口與實現 Callable 接口的方式基本相同,只是 Callable 接口裏定義的方法有返回值,可以聲明拋出異常而已。因此可以將實現 Runnable 接口和實現 Callable 接口可以理解爲同一種方式!

採用實現Runnable、Callable 接口的方式創建多線程的優缺點:

  • 線程類只是實現了 Runnable 接口或 Callable 接口,還可以繼承其他類。
  • 在這種方式下,多個線程可以共享同一個 target 對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
  • 劣勢是,編程稍稍複雜,如果需要訪問當前線程,則必須使 Thread.currentThread() 方法。採用繼承 Thread 類的方式創建多線程的優缺點:
  • 劣勢是,因爲線程類已經繼承了 Thread 類,所以不能再繼承其他父類。
  • 優勢是,編寫簡單,如果需要訪問當前線程,則無須使用 Thread.currentThread() 方法, 直接使用 this 即可獲得當前線程。

所以推薦採用實現Runnable接口、Callable 接口的方式來創建多線程。


線程生命週期

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態, 在線程的生命週期中,它要經過 新建(New)、就緒( Runnable)、運行( Running)、阻塞( Blocked)和死亡(Dead) 5種狀態。尤其是當線程啓動以後,它不可能一直“霸佔”着CPU獨自運行,所以CPU需要在多條線程之間切換,於是線程狀態也會多次在運行、阻塞之間切換。


新建狀態

當使用new關鍵字創建的線程之後,此時此刻線程就處於新建狀態!不會執行線程的執行體!

就緒狀態

當線程對象調用了 start() 方法之後,該線程處於就緒狀態,Java 虛擬機會爲其創建方法調用棧和程序計數器,處於這個狀態中的線程並沒有開始運行,只是表示該線程可以運行了。至於該線程何時開始運行,取決於JVM裏線程調度器的調度。

運行狀態
如果線程處於就緒狀態,並獲得了CPU,開始執行 run() 內方法體,此時線程就處於運行狀態!

阻塞狀態
當線程處於運行狀態時候,如果需要在線程運行的過程中被中斷,使其它線程獲得執行的機會。所有現代的桌面和服務器操作系統都採用搶佔式調度策略,但一些小型設備如手機則可能採用協作式調度策略,在這樣的系統中,只有當一個線程調用了它的 sleep()yield() 方法後纔會放棄所佔用的資源一也就是必須由該線程主動放棄所佔用的資源。

當發生下列情況時,線程就處於阻塞狀態:

  • 線程調用 sleep() 方法主動放棄所佔用的處理器資源。
  • 線程調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞。
  • 線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。
  • 線程在等待某個通知。
  • 程序調用了線程的 suspend() 方法將該線程掛起。但這個方法容易導致死鎖,所以應該儘量避免使用該方法。

被阻塞的線程在合適的時候會重新進入就緒狀態

當處於以下情況時,線程會重新處於就需狀態:

  • 調用 sleep() 方法的線程經過了指定時間;
  • 線程調用的阻塞式IO方法已經返回;
  • 線程成功地獲得了試圖取得的同步監視器;
  • 線程正在等待某個通知時,其他線程發出了一個通知;
  • 處於掛起狀態的線程被調用了 resume() 恢復方法。

線程死亡
線程會以如下三種方式結束,結束後就處於死亡狀態。

  • run()call() 方法執行完成,線程正常結束;
  • 線程拋出一個未捕獲的ExceptionError
  • 直接調用該線程的 stop() 方法來結束該線程一該方法容 易導致死鎖,通常不推薦使用。

生命週期流程圖如下:

在這裏插入圖片描述


*值得注意的是:只能對處於新建狀態的線程調用start()方法, 否則將引發IllegalThreadStateException 異常。線程死亡後請不要試圖再次調用start()再次啓動,他已經死亡了! 程序只能對新建狀態的線程調用 start() 方法,對新建狀態的線程兩次調用 start() 方法也是錯誤的。這都會引發 IllegalThreadStateException 異常。


線程控制

Java的線程支持提供了一些便捷的工具方法,可以很好地控制線程的執行。

join線程

Thread提供了讓一個線程等待另一個線程完成的方法 join() 方法。 當在某個程序執行流中調用其他線程的 join() 方法時,調用線程將被阻塞,直到被 join() 方法加入的join線程執行完爲止。

join() 方法通常由使用線程的程序調用,以將大問題劃分成許多小問題,每個小問題分配一個線程。當所有的小問題都得到處理後,再調用主線程來進一步操作。

public class JoinThread extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        new JoinThread("threadA").start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                JoinThread joinThread = new JoinThread("被join的線程");
                joinThread.start();
                try {
                    joinThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("Main thread");

    }

    /**
     * 構造設置線程名字
     *
     * @param threadName
     */
    public JoinThread(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        System.out.println("Thread name:" + getName());
    }
}

在這裏插入圖片描述

從上方代碼和運行結果不難看出,最後打印的主線程,也就意味着主線程必須等待“被join 的線程”這個線程執行完畢後纔會執行!

join 線程有三種重載的方式:

  • join( ):等待被join的線程執行完成。
  • join(long millis) : 等待被join的線程的時間最長爲
  • join(long millis, int nanos):等待被join的線程的時間最長爲 millis 毫秒加 nanos 毫微秒。

後臺線程

後臺線程,從字面上就可以看出,它是運行在後臺的線程,後臺線程有一個特徵:如果所有的前臺線程都死亡,後臺線程會自動死亡。 可以調用Thread對象中的setDaemon(true) 方法可將指定線程設置成後臺線程。

代碼模擬:

public class ThreadTest extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();

        //設置爲後臺線程
        threadTest.setDaemon(true);

        threadTest.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("Main thread");
        }
    }

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

在這裏插入圖片描述
當Main - 主線程執行完畢後,子線程也隨之結束,而沒有繼續循環下去!另外,Thread還提供了 isDaemon( ) 方法,用於判斷指定線程是否是後臺線程!

一定要注意,前臺線程死亡後,JVM會通知後臺線程死亡,但從它接收指令到做出響應,需要一定時間。而且要將某個線程設置爲後臺線程,必須在該線程啓動之前設置,也就是說,setDaemon(true) 必須在 start( ) 方法之前調用,否則會引發IllegalThreadStateException異常。


線程睡眠

這個很好理解,就是讓線程進入暫停狀態,Thread 提供了 sleep( ) 方法,可以指定線程睡眠多長時間,單位:毫秒。

public class ThreadTest extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        //創建線程
        ThreadTest threadTest = new ThreadTest();

        //啓動縣城
        threadTest.start();


    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("thread run " + i);
            
            try {
                //睡眠一秒後繼續順序執行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


線程讓步yieId

yield( ) 方法是一個和 sleep( ) 方法有點相似的方法,它也是Thread類提供的一個靜態方法,它也可以讓當前正在執行的線程暫停,但它不會阻塞該線程,它只是將該線程轉入就緒狀態。yield( ) 只是讓當前線程暫停一下,讓系統的線程調度器重新調度一次, 完全可能的情況是:當某個線程調用了 yield( ) 方法暫停之後,線程調度器又將其調度出來重新執行。

案例代碼

public class ThreadTest extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadTest threadTest1 = new ThreadTest("線程ONE");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("線程TWO");
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println(getName() + i);

            //當i等於20的時候,做出線程讓步
            if (i == 20) {
                Thread.yield();
            }
        }
    }
}


線程優先級控制

Thread 類提供了 setPriority(int newPriority)getPriority( ) 方法來設置和返回指定線程的優先級,其中 setPriority( ) 方法的參數可以是一個整數,範圍是1~10之間,也可以使用 Thread 類的如下三個靜態常量。

public class ThreadTest extends Thread {

    /**
     * 程憶難
     * https://myhub.blog.csdn.net/
     *
     * @param args
     */
    public static void main(String[] args) {

        ThreadTest threadTest1 = new ThreadTest("高優先級");

        //設置優先級
        threadTest1.setPriority(MAX_PRIORITY);
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("低優先級");

        //設置優先級
        threadTest1.setPriority(NORM_PRIORITY);
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println(getName() + i);

            //當i等於20的時候,做出線程讓步
            if (i == 20) {
                Thread.yield();
            }
        }
    }
}

  • MAX PRIORITY: 其值是10;
  • MIN PRIORITY: 其值是1;
  • NORM PRIORITY: 其值是5。

簡而言之,setPriority 值越高,線程獲得的執行機會也就越多!不推薦直接設置數值
,Windows 2000僅提供了7個優先級。因此應該儘量避免直接爲線程指定優先級,而應該使用MAX_ PRIORITY、 MIN_ PRIORITY和NORM PRIORITY三個靜態常量來設置優先級,這樣纔可以保證程序具有最好的可移植性。


線程同步的“必要性”

現在來假設一種場景,我們都知道12306每逢春運,購票壓力都很大,有多個窗口都在賣票,這就相當於多線程,現在考慮,會不會遇到這種情況,假如還剩下最後一張票沒有賣,現在同時有兩個窗口同時查詢,都顯示爲最後一張票,也就意味着兩個窗口都可以操作把這張票賣出去,當都操作成功後,那後臺數據庫中的一張票就是賣出去兩張,最後爲-1張。又或者是同時查詢同時賣票,賣出的與剩餘的數量對應不上!當然,12306不可能出現這個問題,我們只是舉例發揮想象!來解決這個算法問題!

現在來寫賣票算法!

定義票剩餘數量(模擬12306票務數據庫)

public class TiketAdmin {

    private int tiketNum;

    public TiketAdmin(int tiketNum) {
        this.tiketNum = tiketNum;
    }

    public int getTiketNum() {
        return tiketNum;
    }

    public void setTiketNum(int tiketNum) {
        this.tiketNum = tiketNum;
    }
}

初始化票務,模擬兩個窗口取票

/**
 * @author CSDN程憶難
 * @link https://myhub.blog.csdn.net
 */
public class ThreadTest extends Thread {

    //總票數
    private static TiketAdmin tiketAdmin;


    public static void main(String[] args) {

        //初始化剩餘票數
        tiketAdmin = new TiketAdmin(100);

        ThreadTest threadTest1 = new ThreadTest("窗口1");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("窗口2");
        threadTest2.start();
    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {

        if (tiketAdmin.getTiketNum() > 0) {
            try {
                sleep(300 + tiketAdmin.getTiketNum());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //如果票數大於0,那就賣一張票
            if (tiketAdmin.getTiketNum() > 0) {

                //模擬賣票
                tiketAdmin.setTiketNum(tiketAdmin.getTiketNum() - 1);

                //打印剩餘票數
                System.out.println(getName() + "賣了一張,剩餘票數:" + tiketAdmin.getTiketNum());
            }
        }

    }
}

兩個線程(兩個窗口),同時賣出票,剩餘99張!但這賣出了兩張票,跟總餘票不對應,這樣的線程是不安全的!
在這裏插入圖片描述

我們發現,這樣的邏輯本身就存在很大的問題,票數根本對不上!怎麼解決呢,所以要實現線程同步!

爲了解決這個問題,Java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。


synchronized線程同步

現在加上synchronized

/**
 * @author CSDN程憶難
 * @link https://myhub.blog.csdn.net
 */
public class ThreadTest extends Thread {

    //總票數
    private static TiketAdmin tiketAdmin;


    public static void main(String[] args) {

        //初始化剩餘票數
        tiketAdmin = new TiketAdmin(100);

        ThreadTest threadTest1 = new ThreadTest("窗口1");
        threadTest1.start();

        ThreadTest threadTest2 = new ThreadTest("窗口2");
        threadTest2.start();


    }

    public ThreadTest(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {

        synchronized (tiketAdmin) {
            if (tiketAdmin.getTiketNum() > 0) {
                try {
                    sleep(300 + tiketAdmin.getTiketNum());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //如果票數大於0,那就賣一張票
                if (tiketAdmin.getTiketNum() > 0) {

                    //模擬賣票
                    tiketAdmin.setTiketNum(tiketAdmin.getTiketNum() - 1);

                    //打印剩餘票數
                    System.out.println(getName() + "賣了一張,剩餘票數:" + tiketAdmin.getTiketNum());
                }
            }
        }

    }
}

現在數據對了!下面解釋一下synchronized!
在這裏插入圖片描述

與同步代碼塊對應,Java的多線程安全支持還提供了同步方法,同步方法就是使用synchronized 關鍵字來修飾某個方法,則該方法稱爲同步方法。對於synchronized 修飾的實例方法(非static方法)而言,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。

線程安全具有以下特徵:

  • 該類的對象可以被多個線程安全地訪問;
  • 每個線程調用該對象的任意方法之後都將得到正確結果;
  • 每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。.

釋放同步監視器鎖定

任何線程進入同步代碼塊、同步方法之前,必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器的鎖定呢?程序無法顯式釋放對同步監視器的鎖定,線程會在如下幾種情況下釋放對同步監視器的鎖定。

  • 當前線程的同步方法、同步代碼塊執行結束,當前線程即釋放同步監視器。
  • 當前線程在同步代碼塊、同步方法中遇到breakreturn 終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。
  • 當前線程在同步代碼塊、同步方法中出現了未處理的ErrorException, 導致了該代碼塊、該方法異常結束時,當前線程將會釋放同步監視器。
  • 當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait()方法,則當前線程暫停,並釋放同步監視器。在如下所示的情況下,線程不會釋放同步監視器。
  • 線程執行同步代碼塊或同步方法時,程序調用Thread.sleep()Thread.yield() 方法來暫停當前線程的執行,當前線程不會釋放同步監視器。
  • 線程執行同步代碼塊時,其他線程調用了該線程的 suspend() 方法將該線程掛起,該線程不會釋放同步監視器。當然,程序應該儘量避免使用 suspend()resume() 方法來控制線程。

Lock對象控制線程同步

import java.util.concurrent.locks.ReentrantLock;

public class TiketAdmin {
    
    private final ReentrantLock reentrantLock = new ReentrantLock();

    
    /**
     * 賣票操作
     */
    public void sellTiket() {
        
        reentrantLock.lock();
        try {
            //保證線程安全代碼
        } finally {
            reentrantLock.unlock();
        }
    }
}

Lock提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,Lock 允許實現更靈活的結構,可以具有差別很大的屬性,並且支持多個相關的Condition對象。
Java 8新增了新型的StampedLock類,在大多數場景中它可以替代傳統的ReentrantReadWriteLockReentrantReadWriteLock爲讀寫操作提供了三種鎖模式: WritingReadingOptimisticReading


死鎖

死鎖是這樣的,當兩個線程都在等待對方釋放鎖的時候,這就會發生死鎖!由於Thread類的suspend()方法也很容易導致死鎖,所以Java不再推薦使用該方法來
暫停線程的執行。所以多線程編程時應該採取措施避免死鎖出現。


線程通信

Object實現線程通信

爲了實現這種功能,可以藉助於Object類提供的wait()notify()notifyAll() 三個方法,這三個方法並不屬於Thread類,而是屬於Object類。但這三個方法必須由同步監視器對象來調用,這可分成以下兩種情況:

  • 對於使用synchronized修飾的同步方法,因爲該類的默認實例(this) 就是同步監視器,所以可以在同步方法中直接調用這三個方法。
  • 對於使用synchronized修飾的同步代碼塊,同步監視器是synchronized後括號裏的對象,所以必須使用該對象調用這三個方法。

關於wait()notify()notifyAll() 這三個方法,相關解釋:

  • wait(): 導致當前線程等待,直到其他線程調用該同步監視器的 notify() 方法或notifyAll()方法來喚醒該線程。該 wait() 方法有三種形式一無時間參數的 wait 一直等待,直到其他線程通知)、帶毫秒參數的 wait() 和帶毫秒、毫微秒參數的 wait() 這兩種方法都是等待指定時間後自動甦醒。調用 wait() 方法的當前線程會釋放對該同步監視器的鎖定。
  • notify():喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定後使用 wait() 方法,纔可以執行被喚醒的線程。
  • notifyAll():喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,纔可以執行被喚醒的線程。

Callable創建線程

Condition類提供瞭如下三個方法:

  • await(): 類似於隱式同步監視器上的wait()方法,導致當前線程等待,直到其他線程調用該Condition的signal()方法或signalAll()方法來喚醒該線程。該await()方法有更多變體,如long awaitNanos(longnanosTimeout)、void awaitUninterruptiblyOawaitUntil(Date deadline)等,可以完成更豐富的等待操作。
  • signal(): 喚醒在此Lock對象上等待的單個線程。如果所有線程都在該Lock對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該Lock 對象的鎖定後使用await(),纔可以執行被喚醒的線程。
  • signalAll(): 喚醒在此Lock對象上等待的所有線程。只有當前線程放棄對該Lock對象的鎖定後,纔可以執行被喚醒的線程。

線程池

系統啓動一個新線程的成本是比較高的,因爲它涉及與操作系統交互。在這種情形下,使用線程池可以很好地提高性能,尤其是當程序中需要創建大量生存期很短暫的線程時,更應該考慮使用線程池。與數據庫連接池類似的是,線程池在系統啓動時即創建大量空閒的線程,程序將一個Runnable對象或Callable對象傳給線程池,線程池就會啓動一個線程來 執行它們的 run()call() 方法,當 run()call() 方法執行結束後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個 Runnable 對象的 run()call() 方法。

Java 5新增了一個 Executors 工廠類來產生線程池,該工廠類包含如下幾個靜態工廠方法來創建線程池:

  • newCachedThreadPool): 創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中;
  • newFixedThreadPool(int nThreads);創建一個可重用的、 具有固定線程數的線程池。
  • newSingleThreadExecutor() :創建一個只 有單線程的線程池,它相當於調用newFixedThread Pool() 方法時傳入參數爲1;
  • newScheduledThreadPool(int corePoolSize): 創建具有指定線程數的線程池,它可以在指定延遲後執行線程任務。corePoolSize指池中所保存的線程數,即使線程是空閒的也被保存在線程池內;
  • newSingleThreadScheduledExecutor(): 創建只有一個線程的線程池,它可以在指定延遲後執行線程任務;
  • ExecutorService new WorkStealingPool(int parallelism):創建持有足夠的線程的線程池來支持給定的並行級別,該方法還會使用多個隊列來減少競爭;
  • ExecutorService new WorkStealingPool():該方法是前一個方法的簡化版本。如果當前機器有4個CPU,則目標並行級別被設置爲4,也就是相當於爲前一個方法傳入4作爲參數。

使用線程池來執行線程任務的步驟如下:

  1. 調用 Executors 類的靜態工廠方法創建一個 ExecutorService 對象,該對象代表一個線程池;
  2. 創建 Runnable 實現類或 Callable 實現類的實例,作爲線程執行任務;
  3. 調用 ExecutorService 對象的 submit() 方法來提交 Runnable 實例或Callable實例;
  4. 當不想提交任何任務時,調用 ExecutorService 對象的 shutdown() 方法來關閉線程池。

代碼演示:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author CSDN程憶難
 * @link https://myhub.blog.csdn.net
 */
public class ThreadPack {


    public static void main(String[] args) {


        //固定線程數
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        //Lambda創建線程
        Runnable runnable = () -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("線程:" + Thread.currentThread().getName());
            }
        };

        //提交到線程池兩個
        executorService.submit(runnable);
        executorService.submit(runnable);

        //關閉線程池
        executorService.shutdown();
    }
}

可以看到,在交替執行
在這裏插入圖片描述

上面程序中創建 Runnable 實現類與最開始創建線程池並沒有太大差別,創建了Runnable 實現類之後程序沒有直接創建線程、啓動線程來執行該 Runnable 任務,而是通過線程池來執行該任務!

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