征服Android面試官路漫漫(一):線程攻略,夯實基礎很重要!

征服Android面試官路漫漫,吾將上下而求索~

本文章講解的內容是Java線程,建議對着示例項目閱讀文章,示例項目鏈接:ThreadDemo

本文章分析的相關的源碼基於Java Development Kit(JDK) 13

概述

在說線程的概念之前,先說下進程的概念,進程是代碼在數據集合上的一次運行活動,它是系統進行資源分配和調度的基本單位。一個進程至少有一個線程,線程是進程中的實體,線程本身是不會獨立存在的,進程中的多個線程可以共享進程的資源(例如:內存地址、文件I/O等),也可以獨立調度。

有以下三種方式實現線程:

  • 使用內核線程實現
  • 使用用戶線程實現
  • 使用用戶線程和輕量級線程混合實現

Java語言統一處理了不同硬件和操作系統平臺的線程操作,一個線程是一個已經執行start()方法而且還沒結束的java.lang.Thread類的實例,其中Thread類的所有關鍵方法都是本地方法(Native Method)來的,這意味着這些方法沒有使用或者無法使用平臺相關的手段來實現。

線程狀態切換

Java語言定義了六種線程狀態,要注意的是,在任意一個時間點,一個線程有且只有五種線程狀態的其中一種,這五種線程狀態如下所示:

新建(New):線程創建後尚未啓動的狀態。
運行(Runable):線程正在等待着CPU爲它分配執行時間,進入就緒(Ready)狀態,等到CPU分配執行時間後,線程才真正執行,進入正在運行(Running)狀態。
無限期等待(Waiting):這種狀態下的線程不會被CPU分配執行時間,它們需要其他線程顯示地喚醒。以下方法會讓線程進入這種狀態:

  • 沒有設置參數Timeout的Object.wait()方法
  • 沒有設置參數Timeout的Thread.join()方法
  • LockSupport.park方法

限期等待(Timed Waiting):這種狀態下的線程不會被CPU分配執行時間,但是它無需其他線程顯示地喚醒,會在一定時間內由系統自動喚醒。以下方法會讓線程進入這種狀態:

  • Thread.sleep()方法
  • 有設置參數Timeout的Object.wait()方法
  • 有設置參數Timeout的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUntil()方法

阻塞(Block):線程被阻塞的狀態,在等待着一個排他鎖。
結束(Terminated):線程已終止,並且已經結束執行的狀態。

線程創建和運行

Java語言提供了三種創建線程的方式,如下所示:

繼承Thead類並且重寫run方法

代碼如下所示:

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class TanJiaJunThreadTest {

    private static class TanJiaJunThread extends Thread {

        @Override
        public void run() {
            super.run();
            System.out.println("譚嘉俊");
        }

    }

    public static void main(String[] args) {
        // 創建線程
        TanJiaJunThread thread = new TanJiaJunThread();
        // 啓動線程
        thread.start();
    }

}

這種方式的優點是在run方法內可以使用this獲取當前線程,無須使用Thread.currentThread方法;缺點是因爲Java的類只能繼承一個類,所以繼承Thread類之後,就不能繼承其他類了,而且因爲任務和代碼沒有分離,如果多個線程執行相同的任務時,需要多份任務代碼。

要注意的是,調用了start方法後,線程正在等待着CPU爲它分配執行時間,進入就緒(Ready)狀態,等到CPU分配執行時間後,線程才真正執行,進入正在運行(Running)狀態。

實現Runnable接口的run方法

代碼如下所示:

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class TanJiaJunRunnableTest {

    private static class TanJiaJunRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("譚嘉俊");
        }

    }

    public static void main(String[] args) {
        // 創建TanJiaJunRunnable對象
        TanJiaJunRunnable runnable = new TanJiaJunRunnable();
        // 創建線程
        Thread thread = new Thread(runnable);
        // 啓動線程
        thread.start();
    }

}

這種方式的優點是因爲Java的類可以實現多個接口,所以這個類就可以繼承自己需要的類了,而且任務和代碼分離,如果多個線程執行相同的任務時,可以公用同一個任務的代碼,如果需要對它們區分,可以添加參數進行區分。

使用FutureTask類

代碼如下所示:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class TanJiaJunFutureTaskTest {

    private static class TanJiaJunCallable implements Callable<String> {

        @Override
        public String call() {
            return "譚嘉俊";
        }

    }

    public static void main(String[] args) {
        // 創建FutureTask對象
        FutureTask<String> futureTask = new FutureTask<>(new TanJiaJunCallable());
        // 創建線程
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            // 等待任務執行完畢,並且得到返回值
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

}

前面兩種方式都沒有返回值,FutureTask可以有返回值。

wait和notify

wait()方法、wait(long timeoutMillis)方法、wait(long timeoutMillis, int nanos)方法、notify()方法和notifyAll()方法都是Object類的方法。

wait系列方法

當一個線程調用共享變量的wait系列方法時,這個線程進入等待狀態,直到使用下面兩種方式纔會被喚醒:

  • 其他線程調用該共享變量的notify系列方法(notify()方法或者notifyAll()方法)。
  • 其他線程調用該共享變量所在的線程的interrupt()方法後,該線程拋出InterruptedException異常返回。

要注意的是,需要獲取到該共享變量的監視器鎖才能調用wait方法,否則會拋出IllegalMonitorStateException異常,可以使用以下兩種方式獲得對象的監視器鎖:

調用被關鍵字synchronized修飾的方法,代碼如下所示:

Object object = new Object();

private synchronized void test() {
    try {
        // 調用變量object的wait()方法
        object.wait();
    } catch(InterruptedException e) {
         e.printStackTrace();
    }
}

執行同步代碼塊,代碼如下所示:

Object object = new Object();

synchronized(object) {
    try {
        // 調用變量object的wait()方法
        object.wait();
    } catch(InterruptedException e) {
         e.printStackTrace();
    }
}

wait()

源碼如下所示:

// Object.java
public final void wait() throws InterruptedException {
    wait(0L);
}

這個方法實際上調用了wait(long timeoutMillis)方法,參數timeoutMillis的值是0L。它的行爲和調用wait(0L, 0)方法是一致的。

wait(long timeoutMillis)

源碼如下所示:

// Object.java
public final native void wait(long timeoutMillis) throws InterruptedException;

參數timeoutMillis是等待的最大時間,也就是超時時間,單位是毫秒。它的行爲和調用wait(timeoutMillis, 0)方法是一致的。

要注意的是,如果傳入了負數的timeoutMillis,就會拋出IllegalArgumentException異常。

wait(long timeoutMillis, int nanos)

源碼如下所示:

// Object.java
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
    if (timeoutMillis < 0) {
        throw new IllegalArgumentException("timeoutMillis value is negative");
    }

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

    if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
        timeoutMillis++;
    }

    wait(timeoutMillis);
}

這個方法實際上調用了wait(long timeoutMillis)方法;參數timeoutMillis是等待的最大時間,也就是超時時間,單位是毫秒;參數nanos是額外的時間,單位是納秒,範圍是0~999999(包括999999)。

只有在參數nanos大於0的時候,參數timeoutMillis纔會自增。

notify系列方法

在一個線程上調用共享變量的notify方法後,會喚醒這個共享變量上調用wait系列方法後進入等待狀態的線程。要注意的是,一個共享變量可能有多個線程在等待,具體喚醒哪個等待的線程是隨機的。

被喚醒的線程不能立即從wait系列方法返回後繼續執行,它需要獲取到該共享變量的監視器鎖才能返回,也就是說,喚醒它的線程釋放了該共享變量的監視器鎖,被喚醒的線程不一定能獲取到該共享變量的監視器鎖,因爲該線程還需要和其他線程去競爭這個監視器鎖,只有競爭到這個監視器鎖後才能繼續執行。

只有當前線程獲取到該共享變量的監視器鎖後,才能調用該共享變量的notify系列方法,否則會拋出IllegalMonitorStateException異常。

notify()方法

源碼如下所示:

// Object.java
@HotSpotIntrinsicCandidate
public final native void notify();

notifyAll()方法

源碼如下所示:

// Object.java
@HotSpotIntrinsicCandidate
public final native void notifyAll();

notifyAll()方法可以喚醒所有在該共享變量上因爲調用wait系列方法而進入等待狀態的線程。

sleep()方法--讓線程睡眠

sleep()方法是Thread類的一個靜態方法。當一個正在執行的線程調用了這個方法後,調用線程會暫時讓出指定睡眠時間的執行權,不參與CPU的調度,但是不會讓出該線程所擁有的監視器鎖。指定的睡眠時間到了後,sleep()方法會正常返回,線程處於就緒狀態,然後參與CPU調度,獲取到CPU資源後繼續運行。

要注意的是,如果在睡眠期間其他線程調用了該線程的interrupt()方法中斷了該線程,就會在調用sleep方法的地方拋出InterruptedException異常而返回。

源碼如下所示:

// Thread.java
public static native void sleep(long millis) throws InterruptedException;

下面來看一個生產者消費者問題(Producer-Consumer Problem)的例子:
Repository類是一個存儲庫,存放產品,代碼如下所示:

package producerconsumerproblem;

import java.util.LinkedList;
import java.util.Queue;

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class Repository {

    // 隊列的最大容量是10
    private static final int MAX_SIZE = 10;
    // 創建隊列
    private final Queue<Object> queue = new LinkedList<>();

    void produce() {
        synchronized (queue) {
            while (queue.size() == MAX_SIZE) {
                try {
                    System.out.println("生產者(線程名字:" + Thread.currentThread().getName() + "):存儲庫已滿");
                    // 當隊列滿了後,調用變量queue的wait()方法,生產者線程,並且釋放Queue對象的監視器鎖
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 如果隊列還沒滿,就創建新的Object對象,並且在隊尾入列
            queue.offer(new Object());
            System.out.println("生產者(線程名字:" + Thread.currentThread().getName() + "):生產了一個產品");
            // 通知其他生產者線程和消費者線程
            queue.notifyAll();
        }
    }

    void consume() {
        synchronized (queue) {
            while (queue.size() == 0) {
                try {
                    System.out.println("消費者(線程名字:" + Thread.currentThread().getName() + "):存儲庫是空");
                    // 當隊列空了後,調用變量queue的wait()方法,消費者線程進入等待狀態,並且釋放Queue對象的監視器鎖
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 如果隊列存在元素,就將隊頭元素出列
            queue.poll();
            System.out.println("消費者(線程名字:" + Thread.currentThread().getName() + "):消費了一個產品");
            // 通知其他消費者線程和生產者線程
            queue.notifyAll();
        }
    }

}

以下是測試代碼,我先把生產者所在的線程睡眠(sleep)一秒,把消費者所在的線程睡眠三秒,這樣就可以製造出生產速度大於消費速度的場景,代碼如下所示:

package producerconsumerproblem;

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class ProducerConsumerProblemTest {

    // 生產者線程
    private static class Producer implements Runnable {

        private Repository repository;

        Producer(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            // 循環執行
            while (true) {
                try {
                    // 讓生產者線程睡眠一秒
                    Thread.sleep(1000);
                    // 調用存儲庫的produce()方法,生產者生產產品
                    repository.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }

    }

    // 消費者線程
    private static class Consumer implements Runnable {

        private Repository repository;

        Consumer(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            // 循環執行
            while (true) {
                try {
                    // 讓消費者線程睡眠三秒
                    Thread.sleep(3000);
                    // 調用存儲庫的consume()方法,消費者消費產品
                    repository.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }

    }

    public static void main(String[] args) {
        // 創建存儲庫
        Repository repository = new Repository();

        // 創建三個生產者線程,並且讓它們運行
        for (int i = 0; i < 3; i++) {
            new Thread(new Producer(repository)).start();
        }

        // 創建三個消費者線程,並且讓它們運行
        for (int i = 0; i < 3; i++) {
            new Thread(new Consumer(repository)).start();
        }
    }

}

運行上面的代碼,大約十秒後手動結束進程,結果如下所示:

/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=63240:/Applications/IntelliJ IDEA CE.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Users/tanjiajun/IdeaProjects/ThreadDemo/out/production/ThreadDemo producerconsumerproblem.ProducerConsumerProblemTest
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
消費者(線程名字:Thread-3):消費了一個產品
消費者(線程名字:Thread-5):消費了一個產品
消費者(線程名字:Thread-4):消費了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-2):存儲庫已滿
生產者(線程名字:Thread-1):存儲庫已滿
消費者(線程名字:Thread-3):消費了一個產品
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-1):存儲庫已滿
消費者(線程名字:Thread-5):消費了一個產品
消費者(線程名字:Thread-4):消費了一個產品
生產者(線程名字:Thread-1):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-2):存儲庫已滿
生產者(線程名字:Thread-1):存儲庫已滿
生產者(線程名字:Thread-0):存儲庫已滿
消費者(線程名字:Thread-3):消費了一個產品
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-0):存儲庫已滿
生產者(線程名字:Thread-1):存儲庫已滿
消費者(線程名字:Thread-4):消費了一個產品
消費者(線程名字:Thread-5):消費了一個產品
生產者(線程名字:Thread-1):生產了一個產品
生產者(線程名字:Thread-0):生產了一個產品
生產者(線程名字:Thread-2):存儲庫已滿
生產者(線程名字:Thread-0):存儲庫已滿
生產者(線程名字:Thread-1):存儲庫已滿

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

然後我把生產者所在的線程睡眠三秒,把消費者所在的線程睡眠一秒,這樣就可以製造出生產速度小於消費速度的場景,代碼如下所示:

package producerconsumerproblem;

/**
 * Created by TanJiaJun on 2020/8/30.
 */
class ProducerConsumerProblemTest {

    // 生產者線程
    private static class Producer implements Runnable {

        private Repository repository;

        Producer(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            // 循環執行
            while (true) {
                try {
                    // 讓生產者線程睡眠三秒
                    Thread.sleep(3000);
                    // 調用存儲庫的produce()方法,生產者生產產品
                    repository.produce();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }

    }

    // 消費者線程
    private static class Consumer implements Runnable {

        private Repository repository;

        Consumer(Repository repository) {
            this.repository = repository;
        }

        @Override
        public void run() {
            // 循環執行
            while (true) {
                try {
                    // 讓消費者線程睡眠一秒
                    Thread.sleep(1000);
                    // 調用存儲庫的consume()方法,消費者消費產品
                    repository.consume();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
            }
        }

    }

    public static void main(String[] args) {
        // 創建存儲庫
        Repository repository = new Repository();

        // 創建三個生產者線程,並且讓它們運行
        for (int i = 0; i < 3; i++) {
            new Thread(new Producer(repository)).start();
        }

        // 創建三個消費者線程,並且讓它們運行
        for (int i = 0; i < 3; i++) {
            new Thread(new Consumer(repository)).start();
        }
    }

}

運行上面的代碼,大約十秒後手動結束進程,結果如下所示:

/Library/Java/JavaVirtualMachines/jdk-13.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=63256:/Applications/IntelliJ IDEA CE.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Users/tanjiajun/IdeaProjects/ThreadDemo/out/production/ThreadDemo producerconsumerproblem.ProducerConsumerProblemTest
消費者(線程名字:Thread-3):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
消費者(線程名字:Thread-5):存儲庫是空
生產者(線程名字:Thread-0):生產了一個產品
消費者(線程名字:Thread-3):消費了一個產品
消費者(線程名字:Thread-5):存儲庫是空
生產者(線程名字:Thread-1):生產了一個產品
消費者(線程名字:Thread-4):消費了一個產品
生產者(線程名字:Thread-2):生產了一個產品
消費者(線程名字:Thread-5):消費了一個產品
消費者(線程名字:Thread-3):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
消費者(線程名字:Thread-5):存儲庫是空
生產者(線程名字:Thread-0):生產了一個產品
消費者(線程名字:Thread-3):消費了一個產品
消費者(線程名字:Thread-5):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
生產者(線程名字:Thread-1):生產了一個產品
消費者(線程名字:Thread-5):消費了一個產品
生產者(線程名字:Thread-2):生產了一個產品
消費者(線程名字:Thread-4):消費了一個產品
消費者(線程名字:Thread-3):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
消費者(線程名字:Thread-5):存儲庫是空
生產者(線程名字:Thread-0):生產了一個產品
消費者(線程名字:Thread-3):消費了一個產品
消費者(線程名字:Thread-5):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
消費者(線程名字:Thread-4):消費了一個產品
消費者(線程名字:Thread-5):消費了一個產品
消費者(線程名字:Thread-3):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
消費者(線程名字:Thread-5):存儲庫是空
生產者(線程名字:Thread-0):生產了一個產品
消費者(線程名字:Thread-3):消費了一個產品
消費者(線程名字:Thread-5):存儲庫是空
消費者(線程名字:Thread-4):存儲庫是空
生產者(線程名字:Thread-2):生產了一個產品
生產者(線程名字:Thread-1):生產了一個產品
消費者(線程名字:Thread-4):消費了一個產品
消費者(線程名字:Thread-5):消費了一個產品

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

上面的結果都符合預期,我解釋一下,當發現隊列滿了後,就會調用變量queue的wait()方法,該生產者線程就會被進入等待狀態,並且釋放Queue對象的監視器鎖,讓其他生產者線程和消費者線程去競爭這個監視器鎖,打破了死鎖產生的四個條件中的請求並持有條件,避免發生死鎖,同樣的,當發現隊列空了後,也會調用變量queue的wait()方法,該消費者線程會進入等待狀態,並且釋放Queue對象的監視器鎖,讓其他消費者線程和生產者線程去競爭這個監視器鎖,打破了死鎖的四個條件中的請求並持有條件,避免發生死鎖。

join系列方法--等待線程執行終止

join系列方法是Thread類的一個普通方法。它可以處理一些需要等待某幾個任務完成後才能繼續往下執行的場景。

join()方法

源碼如下所示:

// Thread.java
public final void join() throws InterruptedException {
    join(0);
}

這個方法實際上調用了join(final long millis)方法,參數millis的值是0。

join(final long millis)方法

源碼如下所示:

// Thread.java
public final synchronized void join(final long millis)
throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            final long startTime = System.nanoTime();
            long delay = millis;
            do {
                wait(delay);
            } while (isAlive() && (delay = millis -
                    TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        throw new IllegalArgumentException("timeout value is negative");
    }
}

參數millis是等待時間,單位是毫秒。

要注意的是,如果傳入了負數的millis,就會拋出IllegalArgumentException異常。

join(long millis, int nanos)方法

源碼如下所示:

// Thread.java
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 > 0 && millis < Long.MAX_VALUE) {
        millis++;
    }

    join(millis);
}

這個方法實際上調用了join(final long millis)方法;參數millis是等待時間,單位是毫秒;參數nanos是額外的時間,單位是納秒,範圍是0~999999(包括999999)。

只有在參數nanos大於0的時候,參數millis纔會自增。

我在寫深入瞭解volatile關鍵字這篇文章的時候,其中一個例子使用到了這個方法,代碼如下所示:

/**
 * Created by TanJiaJun on 2020-08-16.
 */
class VolatileDemo {

    private static final int THREADS_COUNT = 10;

    private static volatile int value = 0;

    private static void increase() {
        // 對value變量進行自增操作
        value++;
    }

    public static void main(String[] args) {
        // 創建10個線程
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++)
                    // 每個線程對value變量進行1000次自增操作
                    increase();
            });
            threads[i].start();
        }
        // 主線程等待子線程運行結束
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("value的值:" + value);
    }

}

在這個示例代碼中調用join()方法目的是爲了讓這十個子線程運行結束後,主線程才結束,保證這十個子線程都能全部運行結束。

yield()--讓出CPU執行權

yield()方法是Thread類的一個靜態方法。當一個線程調用這個方法後,當前線程告訴線程調度器讓出CPU執行權,但是線程調度器可以無條件忽略這個請求,如果成功讓出後,線程處於就緒狀態,它會從線程就緒隊列中獲取一個線程優先級最高的線程,當然也有可能調度到剛剛讓出CPU執行權的那個線程來獲取CPU執行權。源碼如下所示:

// Thread.java
public static native void yield();

它和sleep()方法的區別是:當線程調用sleep()方法時,它會被阻塞指定的時間,在這個期間線程調度器不會去調度其他線程,而當線程調用yield()方法時,線程只是讓出自己剩餘的CPU時間片,線程還是處於就緒狀態,並沒有被阻塞,線程調度器在下一次調度時可能還會調度到這個線程執行。

線程中斷

在Java中,線程中斷是一種線程間的協作模式。

要注意的是,通過設置線程的中斷標誌並不能立刻終止線程的執行,而是通過被中斷的線程的中斷標誌自行處理。

interrupt()方法

interrupt()方法可以中斷線程,如果是在其他線程調用該線程的interrupt()方法,會通過checkAccess()方法檢查權限,這有可能拋出SecurityException異常。假設有兩個線程,分別是線程A和線程B,當線程A正在運行時,線程B可以調用線程A的interrupt()方法來設置線程A的中斷標誌爲true並且立即返回,前面也提到過,設置標誌僅僅是設置標誌而已,線程A實際上還在運行,還沒被中斷;如果線程A因爲調用了wait系列方法、join()方法或者sleep()方法而被阻塞,這時候線程B調用線程A的interrupt()方法,線程A會在調用這些方法的地方拋出InterruptedException異常。源碼如下所示:

// Thread.java
public void interrupt() {
    if (this != Thread.currentThread()) {
        checkAccess();

        // 線程可能在IO操作中阻塞
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0(); // 設置中斷標誌
                b.interrupt(this);
                return;
            }
        }
    }

    // 設置中斷標誌
    interrupt0();
}

isInterrupted()方法

isInterrupted()方法可以用來檢測當前線程是否被中斷,如果是就返回true,否則返回false。源碼如下所示:

// Thread.java
public boolean isInterrupted() {
    return isInterrupted(false);
}

interrupted()方法

interrupted()方法是Thread類的一個靜態方法。它可以用來檢測當前線程是否被中斷,如果是就返回true,否則返回false。它和上面提到的isInterrupted()方法的不同的是:如果發現當前線程被中斷,就會清除中斷標誌,並且這個方法是靜態方法,可以直接調用Thread.interrupted()使用。源碼如下所示:

// Thread.java
public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

isInterrupted()方法和interrupted()方法都是調用了isInterrupted(boolean ClearInterrupted)方法,源碼如下所示:

// Thread.java
@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean ClearInterrupted);

參數ClearInterrupted是用來判斷是否需要重置中斷標誌。

從源碼可得知,interrupted()方法是通過獲取當前線程的中斷標誌,而不是獲取調用interrupted()方法的實例對象的中斷標誌。

線程上下文切換

在多線程編程中,線程的個數一般大於CPU的個數,但是每一個CPU只能被一個線程使用,爲了讓用戶感覺多個線程在同時執行,CPU的資源分配策略採用的是時間片輪換策略,時間片輪換策略是指給每個線程分配一個時間片,線程會在時間片內佔用CPU執行任務。

線程上下文切換是指當前線程使用完時間片後,處於就緒狀態,並且讓出CPU給其他線程佔用,同時保存當前線程的執行現場,用於再次執行時恢復執行現場。

線程死鎖

線程死鎖是指兩個或者兩個以上的線程在執行的過程中,因爲互相競爭資源而導致互相等待的現象,如果沒有外力的情況下,它們會一直互相等待,導致無法繼續執行下去。

死鎖的產生必須具備以下四個條件:

  • 互斥條件:指資源只能由一個線程佔用,如果其他線程要請求使用該資源,就只能等待,直到佔用資源的線程釋放該資源。
  • 請求並持有條件:指一個線程已經佔有至少一個資源,但是還想請求佔用新的資源,而新的資源被其他線程佔用,所以當前線程會被阻塞,但是阻塞的同時不釋放自己獲取的資源。
  • 不可剝奪條件:指線程獲取到的資源在自己使用完畢之前不能被其他線程佔用,只有在自己使用完畢後纔會釋放該資源。
  • 環路等待條件:指發生在死鎖時,必然存在一個線程——資源的環形鏈,舉個例子:有一個線程集合{Thead0, Thread1, Thread2, ……, Threadn),其中Thread0等待Thread1佔用的資源,Thread1等待Thread2佔用的資源,Thread2等待Thread3佔用的資源,……,Threadn等待Thread0佔用的資源。

那如何避免死鎖呢?只要打破其中一個條件就可以避免死鎖,不過基於操作系統的特性,只有請求並持有條件和環路等待條件是可以破壞的。

用戶線程和守護線程

Java中的線程分爲兩類:用戶線程(User Thread)和守護線程(Daemon Thread)。在Java虛擬機啓動的時候會調用main方法,main方法所在的線程就是一個用戶線程,同時Java虛擬機還會啓動很多守護線程,例如:垃圾回收線程。

只需要調用Thread類的setDaemon(boolean on)方法,並且參數on設爲true,就可以使該線程成爲守護線程。

用戶線程和守護線程的區別是當最後一個用戶線程結束後,Java虛擬機進程纔會正常結束,而守護線程是否結束不影響Java虛擬機進程的結束。

總結一下:

如果我們希望在主線程結束後,子線程繼續工作,等到子線程結束後才讓Java虛擬機進程結束,我們可以把線程設爲用戶線程;如果我們希望在主線程結束後,Java虛擬機進程也立即結束,我們可以把線程設爲守護線程。

面試複習路線,梳理知識,提升儲備

自己的知識準備得怎麼樣,這直接決定了你能否順利通過一面和二面,所以在面試前來一個知識梳理,看需不需要提升自己的知識儲備是很有必要的。

關於知識梳理,這裏再分享一下我面試這段時間的複習路線:(以下體系的複習資料是我從各路大佬收集整理好的)

  • 架構師築基必備技能
  • Android高級UI與FrameWork源碼
  • 360°全方面性能調優
  • 解讀開源框架設計思想
  • NDK模塊開發
  • 微信小程序
  • Hybrid 開發與Flutter

知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結:

《960全網最全Android開發筆記》

《379頁Android開發面試寶典》

歷時半年,我們整理了這份市面上最全面的安卓面試題解析大全
包含了騰訊、百度、小米、阿里、樂視、美團、58、獵豹、360、新浪、搜狐等一線互聯網公司面試被問到的題目。熟悉本文中列出的知識點會大大增加通過前兩輪技術面試的機率。

如何使用它?

1.可以通過目錄索引直接翻看需要的知識點,查漏補缺。
2.五角星數表示面試問到的頻率,代表重要推薦指數

《507頁Android開發相關源碼解析》

只要是程序員,不管是Java還是Android,如果不去閱讀源碼,只看API文檔,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀源碼,不僅限於閱讀各大系統源碼,還包括各種優秀的開源庫。

資料太多,全部展示會影響篇幅,暫時就先列舉這些部分截圖,以上資源均免費分享,以上內容均放在了開源項目:github 中已收錄,大家可以自行獲取(或者關注主頁掃描加微信獲取)。

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