了解java多線程

前言

今天我進一步了解了java多線程的相關知識,根據幾個相關問題進行學習并作出了解答,這篇文章將用來記錄我學習到的有關知識。

問題列表

  1. 怎么實現多線程?
  2. 正確啟動線程的方式
  3. 線程中常用的方法說明
  4. 如何停止線程?
  5. 如和中斷線程?
  6. 線程的生命周期
  7. 線程的異常處理
  8. 死鎖的解決方案

1. 怎么實現多線程?

實現多線程的方法有兩種:

  1. 繼承Thread類、并重寫run方法。
  2. 實現Runnable接口、并重寫run方法。

關于這兩種方法有一個比較,如圖:

2. 正確啟動線程的方式

  1. 如果線程類繼承了Thread類,那么在主方法中可以之間創建該線程類對象,使該對象調用start()方法即可啟動線程。
  2. 如果線程類實現了Runnable接口,那么在主方法中同樣需要創建該線程類對象,然后作為Thread的構造方法的參數創建一個線程,再調用start()方法即可啟動線程。

現在對剛剛上面的兩個問題進行總結:


實現線程的兩種方式.png

3. 線程中常用的方法說明

sleep方法:
當在當前線程中調用Thread.sleep(long millis)方法時,當前線程會進入阻塞狀態。millis參數指定了線程睡眠的時間,單位是毫秒。 當時間結束之后,線程會重新進入就緒狀態。
代碼演示:

public class SleepTest extends Thread {
    static int i=0;
    public void run(){
        while(++i < 5) {
            System.out.println(Thread.currentThread().getName()+ "輸出" + i);
            try {
                Thread.sleep(1000);     //間隔一秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        SleepTest t = new SleepTest();
        t.start();
    }
}

運行結果:

結果

結果的輸出是由間隔時間的。

join方法:
它是Thread的實例方法,在當前線程里的另外一個線程調用了join()方法時,當前線程會進入阻塞狀態等待這個調用了join()方法的線程執行完畢后,再由阻塞狀態進入到就緒狀態。
代碼演示:

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new app("t1"));
        Thread t2 = new Thread(new app("t2"));
        Thread t3 = new Thread(new app("t3"));
        t1.start();
        t1.join();   //這里會等待t1線程執行完畢才會開啟t2線程
        
        t2.start();  //同上
        t2.join();
        
        t3.start();
        t3.join();
    }
    
    static class app implements Runnable{
        int i = 0;
        String name;
        public app(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            while(i < 5) {
                System.out.println(name + "輸出" + i++);
            }
        }
    }
}

運行結果:

結果

yield方法:
它是Thread的靜態方法,如果在當前的線程中寫了“Thread.yield”,那么就會使當前線程主動放棄cpu資源,進入就緒狀態,讓其它的線程來獲取cpu資源,也有可能當前的線程還會獲取cpu資源。
代碼演示:

public class YieldTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new app("t1"));
        Thread t2 = new Thread(new app("t2"));
        
        t1.start();
        t2.start();
    }
    
    static class app implements Runnable{
        int i = 0;
        String name;
        public app(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            while(i < 8){
                if(i == 5) {
                    Thread.yield();
                    System.out.println(name+"放棄cpu資源");
                }
                System.out.println(name+"輸出了"+i);
                i++;
            }
        }
    }
}

運行結果:

結果

wait、notify/notifyAll方法:
它們是Object的方法,需要用到synchronized關鍵字,一般在synchronized 同步代碼塊里使用 wait()、notify/notifyAll() 方法。
1.wait()使當前線程阻塞,前提是必須先獲得鎖,當線程執行wait()方法時候,會釋放當前的鎖,然后讓出CPU,進入等待狀態。
2.只有當 notify/notifyAll() 被執行時候,才會喚醒一個或多個正處于等待狀態的線程,然后繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到wait(),再次釋放鎖。
代碼演示:

public class LockTest {
    
    static Object lock = new Object();
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new app1("t1"));
        Thread t2 = new Thread(new app2("t2"));
        t2.start();
        Thread.sleep(3000);
        t1.start();
    }
    static class app1 implements Runnable{
        String name;
        public app1(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            synchronized(lock) {
                while(++i < 10) {
                    if(i == 5) {
                        System.out.println(name+"線程喚醒其他線程");
                        lock.notify();
                    }
                    System.out.println(name+"輸出"+i);
                }
            }
        }
    }
    static class app2 implements Runnable{
        String name;
        public app2(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            synchronized(lock){
                if(i != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(name+"線程被喚醒");
                System.out.println(name+"輸出"+i);
            }
        }
    }
}

這段代碼里面有兩個線程,讓t1線程一直輸出直到i等于5的時候,喚醒被阻塞的t2線程,但喚醒t2線程不代表釋放鎖,會先等待t1線程執行完畢后才會執行t2線程。
運行結果:

結果

4. 如何停止線程?

首先,stop()方法不推薦使用,因為stop()會讓線程戛然而止,使得在實際開發中不能知道線程還有哪些工作還沒做、完成了哪些工作,還有我們不能夠去做一些應有的清理工作。

正確的停止線程方法:設置退出標志

public class StopThread extends Thread{

    public void run() {
        test01 t = new test01();
        Thread thread = new Thread(t);
        //開始線程
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //停止線程
        t.flag = false;
    }
    public static void main(String[] args) {
        Thread stopThread = new Thread(new StopThread());
        stopThread.start();
    }
    
    static class test01 implements Runnable{

        //設置線程停止的標志
        //volatile保證了線程可以正確的讀取其他線程寫入的值
        volatile boolean flag = true;

        @Override
        public void run() {
            System.out.println("線程開始.......");
            while(flag) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("線程正在進行.....");
            }
            System.out.println("線程停止.......");
        }   
    }
}

運行結果:

結果

5. 如何中斷線程?

調用interrupt()方法:
當線程調用了某些方法,比如sleep()方法而進入一種阻塞狀態,此時該線程如果調用了interrupt()方法,會產生兩個結果:1.線程的中斷狀態被清除(退出阻塞狀態)2.會拋出一個InterruptedException異常,表明線程被中斷了。
代碼演示:

public class InterruptThread extends Thread{

    public void run() {
        while(true) {
            System.out.println("線程正在進行.....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("中斷阻塞狀態");
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread stopThread = new Thread(new InterruptThread());
        //開始線程
        System.out.println("線程開始.......");
        stopThread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中斷線程
        stopThread.interrupt();
    }
}

運行結果:

結果

雖然主線程已經執行結束,但stopThread 線程卻還會繼續進行,這里只是為了演示interrupt()方法中斷阻塞狀態的功能。

方法總結

6. 線程的生命周期

線程的生命周期

相關解釋:
1.就緒狀態:創建了線程對象后,調用了線程的start()方法(注意此時線程只是進入了線程隊列,等待獲取cpu服務,具備了運行條件,但并不一定已經開始運行)。
2.運行狀態:處于就緒狀態的線程,一旦獲取了cpu資源,便進入到運行狀態,開始執行run()方法里面的邏輯。
3.終止狀態:線程的run()方法執行完畢,或者線程調用了stop()方法(此方法已過時,不推薦使用),線程便進入終止狀態。
4.阻塞狀態:一個正在執行的線程在某種情況下,由于某種原因(比如線程調用了sleep()方法、wait()方法、join()方法)而暫時讓出了cpu資源,暫停了自己的執行,便進入到阻塞狀態。

7. 線程的異常處理

關于線程的異常處理我是這么理解的,每一個線程在運行的時候都是相互獨立的,那么它出現的異常應該由各自的線程自己來處理。

下面嘗試不在線程中處理異常,嘗試在主線程中處理異常:

public class EpTest implements Runnable{

    public static void main(String[] args) {
        EpTest instance = new EpTest();
        try{
            Thread t1 = new Thread(instance);
            t1.start();
        }catch (Exception e){
            System.out.println("主線程捕獲線程中的異常");
        }
    }
    @Override
    public void run() {
            throw new RuntimeException();
    }
}
Exception in thread "Thread-0" java.lang.RuntimeException
    at EpTest.run(EpTest.java:19)
    at java.base/java.lang.Thread.run(Thread.java:834)

結果顯示此時這個主線程并不能處理該異常
那么處理線程中出現的異常有以下我學習到的幾種方法:

  1. 直接在線程中使用try、cath語句處理異常。
 @Override
    public void run() {
        try{
            ...
        }catch (Exception e){
            ...
        }
    }
  1. 對于不在線程中處理的異常,可以交給異常處理器處理UncaughtExceptionHandler
public class EpTest implements Runnable{

    public static void main(String[] args) {
        EpTest instance = new EpTest();
        Thread t1 = new Thread(instance);
        t1.setUncaughtExceptionHandler(new EpHandler());
        t1.start();
    }
    @Override
    public void run() {
            throw new RuntimeException();
    }
}
class EpHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t,Throwable e){
        System.out.println("捕獲未處理的異常");
    }
}

首先需要自定義一個異常處理器:

class EpHandler implements Thread.UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t,Throwable e){
        System.out.println("捕獲未處理的異常");
    }
}

接著在主方法中設置這個異常處理器:

public static void main(String[] args) {
        EpTest instance = new EpTest();
        Thread t1 = new Thread(instance);
        //調用Thread類的setUncaughtExceptionHandler()設置異常處理器
        t1.setUncaughtExceptionHandler(new EpHandler());
        t1.start();
    }

3.使用線程池

public class EpTest2 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new EpTest2());
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(t1);
        executorService.shutdown();
    }

    @Override
    public void run() {
        //在run方法中設置異常處理器
        Thread.currentThread().setUncaughtExceptionHandler(new EpHandler());
        throw new RuntimeException();
    }
}

需要注意的是,只有通過execute()方法提交任務,才能將它拋出的異常交給未捕獲異常處理器。

另外的一種方法:

public class EpTest2 implements Runnable {
    public static void main(String[] args) {
        Thread t1 = new Thread(new EpTest2());
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<?> future=executorService.submit(t1);
        executorService.shutdown();
        try {
            //拋出異常
            future.get();
        }catch (Exception e){
            System.out.println("捕獲未處理的異常");
        }
    }

    @Override
    public void run() {
        throw new RuntimeException();
    }
}

這種方法通過submit()方法提交的任務由于拋出了異常而結束,該異常將被Future.get()封裝在ExecutionException中重新拋出。

8. 死鎖的解決方案

線程死鎖是指由于兩個或者多個線程互相持有對方所需要的資源,導致這些線程處于等待狀態,無法前往執行。當線程互相持有對方所需要的資源時,會互相等待對方釋放資源,如果線程都不主動釋放所占有的資源,將產生死鎖。

產生死鎖的必要條件:

(1)互斥條件:一個資源每次只能被一個線程使用,直到被該進程釋放。
(2)請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
(3)不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪。
(4)循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。

解決死鎖的問題:

  1. 保證加鎖的順序(按照一定的順序給線程加鎖)
  2. 設置加鎖時限(線程嘗試獲取鎖的時候加上一定的時限,超過時限則放棄對該鎖的請求,并釋放自己占有的鎖)

相關教程:
細說多線程之Thread VS Runnable
https://www.imooc.com/learn/312
Java Socket應用
https://www.imooc.com/learn/161
Java高并發之魂:synchronized深度解析
https://www.imooc.com/learn/1086
Java多線程之內存可見性
https://www.imooc.com/learn/352

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,986評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,492評論 1 15
  • 林炳文Evankaka原創作品。轉載自http://blog.csdn.net/evankaka 本文主要講了ja...
    ccq_inori閱讀 668評論 0 4
  • 1.解決信號量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 906評論 0 1
  • 一擴展javalangThread類二實現javalangRunnable接口三Thread和Runnable的區...
    和帥_db6a閱讀 497評論 0 1