Java實習生面試複習(五):Thread線程學習

我是一名很普通的大三學生。我將堅持寫博客,輸出知識的同時鞏固自己的基礎,記錄自己的成長和鍛鍊自己,奧利給!!

如果你覺得內容對你有幫助的話,不如給個贊鼓勵一下更新?(σ゚∀゚)σ…:*☆哎喲不錯哦

多線程也是面試中必問的點,是必備的基礎技能。

Thread(線程)的概念

首先我們要知道線程的概念是什麼,在繼續往下說,大家可以先閉上眼回憶一下:

線程(Thread)是併發編程的基礎,也是程序執行的最小單元,它依託進程而存在。一個進程中可以包含多個線> 程,多線程可以共享一塊內存空間和一組系統資源,因此線程之間的切換更加節省資源、更加輕量化。

線程的狀態有哪些?

線程的狀態在 JDK 1.5 之後以枚舉的方式被定義在 Thread 的源碼中,它總共包含以下 6 個狀態:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

六種狀態分別爲:

  • NEW,新建狀態,線程被創建出來,但尚未啓動時的線程狀態
  • RUNNABLE,就緒狀態,表示可以運行的線程狀態,它可能正在運行,或者是在排隊等待操作系統給它分配 CPU 資源;
    • 比如Thread.start方法就是將線程從NEW狀態 轉換成 RUNNABLE 狀態。
  • BLOCKED,阻塞等待鎖的線程狀態,表示處於阻塞狀態的線程正在等待監視器鎖
    • 比如等待執行 synchronized 代碼塊或者使用 synchronized 標記的方法。
  • WAITING,等待狀態,一個處於等待狀態的線程正在等待另一個線程執行某個特定的動作。
    • 比如,一個線程調用了Object.wait()方法,那它就在等待另一個線程調用Object.notify()Object.notifyAll() 方法。
  • TIMED_WAITING,計時等待狀態,和上者類似,只是多了一個超時時間。
    • 比如調用了有超時時間設置的方法 Object.wait(long timeout)Thread.join(long timeout) 等這些方法時,它纔會進入此狀態;
  • TERMINATED,終止狀態,表示線程已經執行完成。

但是這 6 種狀態並不是線程所有的狀態,只是在 Java 源碼中列舉出的 6 種狀態, Java 線程的處理方法都是圍繞這 6 種狀態的。

線程的工作模式

線程的工作模式:

  • 首先要new Thread()創建線程,此時線程的狀態是NEW,表示線程創建成功但沒有運行。
  • 然後再調用線程的 start() 方法,此時線程就從 NEW(新建)狀態變成了 RUNNABLE(就緒)狀態。
  • 此時線程會判斷要執行的方法中有沒有 synchronized 修飾的代碼塊或方法,如果有並且其他線程也在使用此鎖,那麼線程就會變爲 BLOCKED(阻塞等待)狀態,當其他線程使用完此鎖之後,線程會繼續執行剩餘的方法。
  • 當在遇到 Object#wait、Thread#join、LockSupport#park 這些方法時,線程會變爲 WAITING(等待狀態)狀態,如果是帶了超時時間的等待方法,那麼線程會進入 TIMED_WAITING(計時等待)狀態
  • 當有其他線程執行了 notify() 或 notifyAll() 方法之後,線程被喚醒繼續執行剩餘的業務方法,直到方法執行完成爲止,此時整個線程的流程就執行完了。
    執行流程如下圖所示:
    線程的工作模式
    這裏要區分開BLOCKEDWAITING 的區別:
    雖然二者都有等待的含義,但還是區別很大的,首先它們狀態形成的調用方法不同,其次BLOCKED可以理解爲當前線程還處於活躍狀態,只是在阻塞等待其他線程使用完某個鎖資源;而WAITING則是因爲自身調用了Object.wait()Thread.join()LockSupport.park()而進入等待狀態,只能等待其他線程執行某個特定的動作才能被繼續喚醒,比如當線程因爲調用了Object.wait()而進入WAITING狀態之後,則需要等待另一個線程執行Object.notify()、Object.notifyAll() 才能被喚醒。

線程的使用

創建線程的方式

有幸聽過一節馬哥的公開課小馬哥慕課直播,感興趣的可以看看,視頻很不錯,上和下加一起有六個小時在第12分鐘的時候,有說到其實創建線程的方式從本質上說,只有一種,那就是Thread.run(),因爲不管你是繼承Thread,還是實現Runnable接口,我們可以看源碼:

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    public void run() {
        if (target != null) {
            target.run();
        }
    }

運行線程的方式有兩種,一種當用Runnable接口時間,thread.run會判斷target在決定調用誰的run,而線程thread實際是重寫了該run方法。怎麼用就不我例舉了吧。
說到這裏,可能有同學就疑惑了,明明我平常寫代碼調的是start()不是run()啊?這裏看這篇文章java中多線程執行時,爲何調用的是start()方法而不是run()方法我在補充兩點:

  • start()方法不能被多次調用,多次調用會拋出java.lang.IllegalStateException;而run()方法可以進行多次調用,因爲它只是一個普通的方法。

線程常見的方法

線程的優先級

優先級代表線程執行的機會的大小,優先級高的可能先執行,低的可能後執行,在 Java 源碼中,優先級從低到高分別是 1 到 10,線程默認 new 出來的優先級都是 5,源碼如下:

// 最低優先級
public final static int MIN_PRIORITY = 1;

// 普通優先級,也是默認的
public final static int NORM_PRIORITY = 5;

// 最大優先級
public final static int MAX_PRIORITY = 10;

在程序中我們可以通過 Thread.setPriority() 來設置優先級

join方法

在一個線程中調用其他線程對象的join()方法時 ,這時候當前線程會讓出執行權給其他線程,直到 其他線程執行完或者過了超時時間之後再繼續執行當前線程,join() 源碼如下:

    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) {
            // 其他線程好了之後,當前線程的狀態是 TERMINATED,isAlive 返回 false
            // NEW false
            // RUNNABLE true
            while (isAlive()) {
                // 等待其他線程,一直等待
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                // 等待一定的時間,如果在 delay 時間內,等待的線程仍沒有結束,放棄等待
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

從源碼中可以看出 join() 方法底層還是通過 wait() 方法來實現的。假設我們現在有一道題,保證三個線程的有序執行,讓你去實現,想想用join怎麼實現?

        Thread thread1 = new Thread(() -> {
            System.out.println("t1執行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println("t2執行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println("t3執行");
        }, "t3");

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();

在想想,在看過join的源碼後,你還有什麼方法可以保證線程的有序執行?這裏在沾兩個方法:

        Thread thread1 = new Thread(() -> {
            System.out.println("t1執行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println("t2執行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println("t3執行");
        }, "t3");

        thread1.start();
        while (thread1.isAlive()) {
        }
        thread2.start();
        while (thread2.isAlive()) {
        }
        thread3.start();
        while (thread3.isAlive()) {
        }
        Thread thread1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "執行");
        }, "t1");
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "執行");
        }, "t2");
        Thread thread3 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "執行");
        }, "t3");

        threadStartAndWait(thread1);
        threadStartAndWait(thread2);
        threadStartAndWait(thread3);

    public static void threadStartAndWait(Thread thread) {
        System.out.println(Thread.currentThread().getName());
        if (Thread.State.NEW.equals(thread.getState())) {
            thread.start();
        }

        // 當 JVM Thread 執行完之後,自動就notify()了,鎖的如果是線程對象而不是普通對象,那麼鎖對象執行完畢後,會調用自身對象上的notify();
        while (thread.isAlive()) {  // thread 特殊的object
            // 當線程Thread isAlive() == false 時,thread.wait() 操作會被自動釋放
            synchronized (thread) {
                try {
                    //阻塞的是這個對象所在的線程(通常是主線程)
                    thread.wait(); // 到底是誰在通知Thread -> thread.notify();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

yield方法

yield() 爲本地方法,源碼如下:

    // 當前線程做出讓步,放棄當前 cpu,讓線程重新選擇 cpu,避免線程過度使用 cpu
    // 讓步不是不執行,也有可能重新選中自己
    public static native void yield();

yield() 方法表示給線程調度器一個當前線程願意出讓 CPU 使用權的暗示,但是線程調度器可能會忽略這個暗示。
例如下面這段代碼:

public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("線程:" + Thread.currentThread().getName() + " I:" + i);
                if (i == 5) {
                    Thread.yield();
                }
            }
        };
        Thread t1 = new Thread(runnable, "T1");
        Thread t2 = new Thread(runnable, "T2");
        t1.start();
        t2.start();
}

當你多次執行這段代碼的時候,會發現有時候執行結果會不一樣,這是因爲如上所說,它是不穩定的。

sleep方法

sleep 也是本地 (native) 方法,意思是當前線程會沉睡多久,但是沉睡時不會釋放鎖資源,所以沉睡時,其它線程是無法得到鎖的。

interrupt(),interrupted() 和 isInterrupted() 的區別

interrupt():將調用該方法的對象所表示的線程標記一個停止標記,並不是真的停止該線程。

interrupted():獲取當前線程的中斷狀態,並且會清除線程的狀態標記。是一個是靜態方法。

isInterrupted():獲取調用該方法的對象所表示的線程的中斷狀態,不會清除線程的狀態標記。是一個實例方法。

簡單的中斷案例:t1先執行,但是sleep不釋放鎖資源,在這期間t2等候兩秒鐘還沒拿到鎖就中斷

public class Test {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            testsync();
        },"t1");
        Thread t2 = new Thread(() -> {
            testsync();
        },"t2");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t2.start();
        TimeUnit.SECONDS.sleep(2);
        System.out.println("main");
        /**
         * 如果t2兩秒鐘還拿不到就中斷
         */
        t2.interrupt();
    }
    public static void testsync(){
        try {
            /**
             * lockInterruptibly 和 lock的區別,前者會直接拋出異常可以響應中斷,後者則不可以
             */
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

總結

這篇文章簡單介紹了線程的狀態以及線程的執行流程,還介紹了BLOCKED(阻塞等待)和WAITING(等待)的區別,start()方法和run()方法的區別,以及thread的幾個常見方法

看完這篇文章你能回答出這些問題了嗎?

  • BLOCKED(阻塞等待)和 WAITING(等待)有什麼區別?
  • start() 方法和 run() 方法有什麼區別?
  • 線程的優先級有什麼用?該如何設置?
  • 線程的常用方法有哪些?
  • 怎麼中斷線程?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章