100道多線程面試題,和麪試官扯皮沒問題了!

目前Java的面試中,可以說多線程是必問的。在我們學習Java時,這也是非常重要的一個部分。以下給大家分享了100道多線程相關面試題,不打沒有準備的戰。加油!

文章目錄

1.什麼是進程?

進程是系統中正在運行的一個程序,程序一旦運行就是進程。

進程可以看成程序執行的一個實例。進程是系統資源分配的獨立實體,每個進程都擁有獨立的地址空間。一個進程無法訪問另一個進程的變量和數據結構,如果想讓一個進程訪問另一個進程的資源,需要使用進程間通信,比如管道,文件,套接字等。

2.什麼是線程?

是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。

3.線程的實現方式?

1.繼承Thread類

2.實現Runnable接口

3.使用Callable和Future

4.Thread 類中的start() 和 run() 方法有什麼區別?

1.start()方法來啓動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。 然後通過此Thread類調用方法run()來完成其運行操作的, 這裏方法run()稱爲線程體,它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止。然後CPU再調度其它線程。
2.run()方法當作普通方法的方式調用。程序還是要順序執行,要等待run方法體執行完畢後,纔可繼續執行下面的代碼; 程序中只有主線程——這一個線程, 其程序執行路徑還是隻有一條, 這樣就沒有達到寫線程的目的。

5.線程NEW狀態

new創建一個Thread對象時,並沒處於執行狀態,因爲沒有調用start方法啓動改線程,那麼此時的狀態就是新建狀態。

6.線程RUNNABLE狀態

線程對象通過start方法進入runnable狀態,啓動的線程不一定會立即得到執行,線程的運行與否要看cpu的調度,我們把這個中間狀態叫可執行狀態(RUNNABLE)。

7.線程的RUNNING狀態

一旦cpu通過輪詢貨其他方式從任務可以執行隊列中選中了線程,此時它才能真正的執行自己的邏輯代碼。

8.線程的BLOCKED狀態

線程正在等待獲取鎖。

  • 進入BLOCKED狀態,比如調用了sleep,或者wait方法
  • 進行某個阻塞的io操作,比如因網絡數據的讀寫進入BLOCKED狀態
  • 獲取某個鎖資源,從而加入到該鎖的阻塞隊列中而進入BLOCKED狀態

9.線程的TERMINATED狀態

TERMINATED是一個線程的最終狀態,在該狀態下線程不會再切換到其他任何狀態了,代表整個生命週期都結束了。

下面幾種情況會進入TERMINATED狀態:

  • 線程運行正常結束,結束生命週期
  • 線程運行出錯意外結束
  • JVM Crash 導致所有的線程都結束

10.線程狀態轉化圖

image-20200501131131886

11.i–與System.out.println()的異常

示例代碼:

public class XkThread extends Thread {

    private int i = 5;

    @Override
    public void run() {
       System.out.println("i=" + (i——————) + " threadName=" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        XkThread xk = new XkThread();
        Thread t1 = new Thread(xk);
        Thread t2 = new Thread(xk);
        Thread t3 = new Thread(xk);
        Thread t4 = new Thread(xk);
        Thread t5 = new Thread(xk);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

結果:

i=5 threadName=Thread-1
i=2 threadName=Thread-5
i=5 threadName=Thread-2
i=4 threadName=Thread-3
i=3 threadName=Thread-4

雖然println()方法在內部是同步的,但i——————的操作卻是在進入println()之前發生的,所以有發生非線程安全的概率。

println()源碼:

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

12.如何知道代碼段被哪個線程調用?

   System.out.println(Thread.currentThread().getName());

13.線程活動狀態?

public class XKThread extends Thread {

    @Override
    public void run() {
        System.out.println("run run run is "  + this.isAlive() );
    }

    public static void main(String[] args) {
        XKThread xk = new XKThread();
        System.out.println("begin ——— " + xk.isAlive());
        xk.start();
        System.out.println("end ————— " + xk.isAlive());

    }
}

14.sleep()方法

方法sleep()的作用是在指定的毫秒數內讓當前的“正在執行的線程”休眠(暫停執行)。

15.如何優雅的設置睡眠時間?

jdk1.5 後,引入了一個枚舉TimeUnit,對sleep方法提供了很好的封裝。

比如要表達2小時22分55秒899毫秒。

Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

可以看到表達的含義更清晰,更優雅。

16.停止線程

run方法執行完成,自然終止。

stop()方法,suspend()以及resume()都是過期作廢方法,使用它們結果不可預期。

大多數停止一個線程的操作使用Thread.interrupt()等於說給線程打一個停止的標記, 此方法不回去終止一個正在運行的線程,需要加入一個判斷才能可以完成線程的停止。

17.interrupted 和 isInterrupted

interrupted : 判斷當前線程是否已經中斷,會清除狀態。

isInterrupted :判斷線程是否已經中斷,不會清除狀態。

18.yield

放棄當前cpu資源,將它讓給其他的任務佔用cpu執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得cpu時間片。

測試代碼:(cpu獨佔時間片)

public class XKThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用時 = " + (endTime - beginTime) + " 毫秒! ");
    }

    public static void main(String[] args) {
        XKThread xkThread = new XKThread();
        xkThread.start();
    }

}

結果:

用時 = 20 毫秒! 

加入yield,再來測試。(cpu讓給其他資源導致速度變慢)

public class XKThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            Thread.yield();
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用時 = " + (endTime - beginTime) + " 毫秒! ");
    }

    public static void main(String[] args) {
        XKThread xkThread = new XKThread();
        xkThread.start();
    }

}

結果:

用時 = 38424 毫秒! 

19.線程的優先級

在操作系統中,線程可以劃分優先級,優先級較高的線程得到cpu資源比較多,也就是cpu有限執行優先級較高的線程對象中的任務,但是不能保證一定優先級高,就先執行。

Java的優先級分爲1~10個等級,數字越大優先級越高,默認優先級大小爲5。超出範圍則拋出:java.lang.IllegalArgumentException。

20.優先級繼承特性

線程的優先級具有繼承性,比如a線程啓動b線程,b線程與a優先級是一樣的。

21.誰跑的更快?

設置優先級高低兩個線程,累加數字,看誰跑的快,上代碼。

public class Run extends Thread{

    public static void main(String[] args) {
        try {
            ThreadLow low = new ThreadLow();
            low.setPriority(2);
            low.start();

            ThreadHigh high = new ThreadHigh();
            high.setPriority(8);
            high.start();

            Thread.sleep(2000);
            low.stop();
            high.stop();
            System.out.println("low  = " + low.getCount());
            System.out.println("high = " + high.getCount());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

class ThreadHigh extends Thread {
    private int count = 0;

    public int getCount() {
        return count;
    }


    @Override
    public void run() {
        while (true) {
            count++;
        }
    }
}

class ThreadLow extends Thread {
    private int count = 0;

    public int getCount() {
        return count;
    }


    @Override
    public void run() {
        while (true) {
            count++;
        }
    }
}

結果:

low  = 1193854568
high = 1204372373

22.線程種類

Java線程有兩種,一種是用戶線程,一種是守護線程。

23.守護線程的特點

守護線程是一個比較特殊的線程,主要被用做程序中後臺調度以及支持性工作。當Java虛擬機中不存在非守護線程時,守護線程纔會隨着JVM一同結束工作。

24.Java中典型的守護線程

GC(垃圾回收器)

25.如何設置守護線程

Thread.setDaemon(true)

PS:Daemon屬性需要再啓動線程之前設置,不能再啓動後設置。

25.Java虛擬機退出時Daemon線程中的finally塊一定會執行?

Java虛擬機退出時Daemon線程中的finally塊並不一定會執行。

代碼示例:

public class XKDaemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(),"xkDaemonRunner");
        thread.setDaemon(true);
        thread.start();

    }

    static class DaemonRunner implements Runnable {

        @Override
        public void run() {
            try {
                SleepUtils.sleep(10);
            } finally {
                System.out.println("Java小咖秀 daemonThread finally run …");
            }

        }
    }
}

結果:


沒有任何的輸出,說明沒有執行finally。

26.設置線程上下文類加載器

​ 獲取線程上下文類加載器

public ClassLoader getContextClassLoader() 

​ 設置線程類加載器(可以打破Java類加載器的父類委託機制)

public void setContextClassLoader(ClassLoader cl)

27.join

join是指把指定的線程加入到當前線程,比如join某個線程a,會讓當前線程b進入等待,直到a的生命週期結束,此期間b線程是處於blocked狀態。

28.什麼是synchronized?

synchronized關鍵字可以時間一個簡單的策略來防止線程干擾和內存一致性錯誤,如果一個對象是對多個線程可見的,那麼對該對想的所有讀寫都將通過同步的方式來進行。

29.synchronized包括哪兩個jvm重要的指令?

monitor enter 和 monitor exit

30.synchronized關鍵字用法?

可以用於對代碼塊或方法的修飾

31.synchronized鎖的是什麼?

普通同步方法 —————> 鎖的是當前實力對象。

靜態同步方法—————> 鎖的是當前類的Class對象。

同步方法快 —————> 鎖的是synchonized括號裏配置的對象。

32.Java對象頭

synchronized用的鎖是存在Java對象頭裏的。對象如果是數組類型,虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,用2字寬存儲對象頭。

Tips:32位虛擬機中一個字寬等於4字節。

33.Java對象頭長度

image-20200418215447539

34.Java對象頭的存儲結構

32位JVM的Mark Word 默認存儲結構

image-20200418220122794

35.Mark Word的狀態變化

Mark Word 存儲的數據會隨着鎖標誌爲的變化而變化。

image-20200418220322880

64位虛擬機下,Mark Word是64bit大小的

image-20200418224055558

36.鎖的升降級規則

Java SE 1.6 爲了提高鎖的性能。引入了“偏向鎖”和輕量級鎖“。

Java SE 1.6 中鎖有4種狀態。級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態。

鎖只能升級不能降級。

37.偏向鎖

大多數情況,鎖不僅不存在多線程競爭,而且總由同一線程多次獲得。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中記錄存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行 cas操作來加鎖和解鎖,只需測試一下對象頭 Mark Word裏是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖,如果失敗,則需要測試下Mark Word中偏向鎖的標示是否已經設置成1(表示當前時偏向鎖),如果沒有設置,則使用cas競爭鎖,如果設置了,則嘗試使用cas將對象頭的偏向鎖只想當前線程。

38.關閉偏向鎖延遲

java6和7中默認啓用,但是會在程序啓動幾秒後才激活,如果需要關閉延遲,

-XX:BiasedLockingStartupDelay=0。

39.如何關閉偏向鎖

JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

Tips:如果你可以確定程序的所有鎖通常情況處於競態,則可以選擇關閉。

40.輕量級鎖

線程在執行同步塊,jvm會現在當前線程的棧幀中創建用於儲存鎖記錄的空間。並將對象頭中的Mark Word複製到鎖記錄中。然後線程嘗試使用cas將對象頭中的Mark Word替換爲之鄉鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

41.輕量鎖的解鎖

輕量鎖解鎖時,會使原子操作cas將 displaced Mark Word 替換回對象頭,如果成功則表示沒有競爭發生,如果失敗,表示存在競爭,此時鎖就會膨脹爲重量級鎖。

42.鎖的優缺點對比

image-20200419110938271

43.什麼是原子操作

不可被中斷的一個或一系列操作

44.Java如何實現原子操作

Java中通過鎖和循環cas的方式來實現原子操作,JVM的CAS操作利用了處理器提供的CMPXCHG指令來實現的。自旋CAS實現的基本思路就是循環進行CAS操作直到成功爲止。

45.CAS實現原子操作的3大問題

ABA問題,循環時間長消耗資源大,只能保證一個共享變量的原子操作

46.什麼是ABA問題

問題:

因爲cas需要在操作值的時候,檢查值有沒有變化,如果沒有變化則更新,如果一個值原來是A,變成了B,又變成了A,那麼使用cas進行檢測時會發現發的值沒有發生變化,其實是變過的。

解決:

添加版本號,每次更新的時候追加版本號,A-B-A —> 1A-2B-3A。

從jdk1.5開始,Atomic包提供了一個類AtomicStampedReference來解決ABA的問題。

47.CAS循環時間長佔用資源大問題

如果jvm能支持處理器提供的pause指令,那麼效率會有一定的提升。

一、它可以延遲流水線執行指令(de-pipeline),使cpu不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,有些處理器延遲時間是0。

二、它可以避免在退出循環的時候因內存順序衝突而引起的cpu流水線被清空,從而提高cpu執行效率。

48.CAS只能保證一個共享變量原子操作

一、對多個共享變量操作時,可以用鎖。

二、可以把多個共享變量合併成一個共享變量來操作。比如,x=1,k=a,合併xk=1a,然後用cas操作xk。

Tips:java 1.5開始,jdk提供了AtomicReference類來保證飲用對象之間的原子性,就可以把多個變量放在一個對象來進行cas操作。

49.volatile關鍵字

volatile 是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性“。

Java語言規範第3版對volatile定義如下,Java允許線程訪問共享變量,爲了保證共享變量能準確和一致的更新,線程應該確保排它鎖單獨獲得這個變量。如果一個字段被聲明爲volatile,Java線程內存模型所有線程看到這個變量的值是一致的。

50.等待/通知機制

一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作。

WechatIMG36051.wait

方法wait()的作用是使當前執行代碼的線程進行等待,wait()是Object類通用的方法,該方法用來將當前線程置入“預執行隊列”中,並在 wait()所在的代碼處停止執行,直到接到通知或中斷爲止。

在調用wait之前線程需要獲得該對象的對象級別的鎖。代碼體現上,即只能是同步方法或同步代碼塊內。調用wait()後當前線程釋放鎖。

52.notify

notify()也是Object類的通用方法,也要在同步方法或同步代碼塊內調用,該方法用來通知哪些可能燈光該對象的對象鎖的其他線程,如果有多個線程等待,則隨機挑選出其中一個呈wait狀態的線程,對其發出 通知 notify,並讓它等待獲取該對象的對象鎖。

53.notify/notifyAll

notify等於說將等待隊列中的一個線程移動到同步隊列中,而notifyAll是將等待隊列中的所有線程全部移動到同步隊列中。

54.等待/通知經典範式

等待

synchronized(obj) {
		while(條件不滿足) {
				obj.wait();
		}
		執行對應邏輯
}

通知

synchronized(obj) {
	  改變條件
		obj.notifyAll();
}

55.ThreadLocal

主要解決每一個線程想綁定自己的值,存放線程的私有數據。

56.ThreadLocal使用

獲取當前的線程的值通過get(),設置set(T) 方式來設置值。

public class XKThreadLocal {

    public static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) {
        if (threadLocal.get() == null) {
            System.out.println("未設置過值");
            threadLocal.set("Java小咖秀");
        }
        System.out.println(threadLocal.get());
    }

}

輸出:

未設置過值
Java小咖秀

Tips:默認值爲null

57.解決get()返回null問題

通過繼承重寫initialValue()方法即可。

代碼實現:

public class ThreadLocalExt extends ThreadLocal{

    static ThreadLocalExt threadLocalExt = new ThreadLocalExt();

    @Override
    protected Object initialValue() {
        return "Java小咖秀";
    }

    public static void main(String[] args) {
        System.out.println(threadLocalExt.get());
    }
}

輸出結果:

Java小咖秀

58.Lock接口

鎖可以防止多個線程同時共享資源。Java5前程序是靠synchronized實現鎖功能。Java5之後,併發包新增Lock接口來實現鎖功能。

59.Lock接口提供 synchronized不具備的主要特性

image-20200419193439709

60.重入鎖 ReentrantLock

支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖。除此之外,該鎖的還支持獲取鎖時的公平和非公平性選擇。

61.重進入是什麼意思?

重進入是指任意線程在獲取到鎖之後能夠再次獲鎖而不被鎖阻塞。

該特性主要解決以下兩個問題:

一、鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是則再次成功獲取。

二、所得最終釋放。線程重複n次是獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。

62.ReentrantLock默認鎖?

默認非公平鎖

代碼爲證:

 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

63.公平鎖和非公平鎖的區別

公平性與否針對獲取鎖來說的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。

64.讀寫鎖

讀寫鎖允許同一時刻多個讀線程訪問,但是寫線程和其他寫線程均被阻塞。讀寫鎖維護一個讀鎖一個寫鎖,讀寫分離,併發性得到了提升。

Java中提供讀寫鎖的實現類是ReentrantReadWriteLock。

65.LockSupport工具

定義了一組公共靜態方法,提供了最基本的線程阻塞和喚醒功能。

image-20200419223303672

66.Condition接口

提供了類似Object監視器方法,與 Lock配合使用實現等待/通知模式。

67.Condition使用

代碼示例:

public class XKCondition {
    Lock lock = new ReentrantLock();
    Condition cd = lock.newCondition();

    public void await() throws InterruptedException {
        lock.lock();
        try {
            cd.await();//相當於Object 方法中的wait()
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        lock.lock();
        try {
            cd.signal(); //相當於Object 方法中的notify()
        } finally {
            lock.unlock();
        }
    }
  
}

68.ArrayBlockingQueue?

一個由數據支持的有界阻塞隊列,此隊列FIFO原則對元素進行排序。隊列頭部在隊列中存在的時間最長,隊列尾部存在時間最短。

69.PriorityBlockingQueue?

一個支持優先級排序的無界阻塞隊列,但它不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。

70.DelayQueue?

是一個支持延時獲取元素的使用優先級隊列的實現的無界阻塞隊列。隊列中的元素必須實現Delayed接口和 Comparable接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。

71.Java併發容器,你知道幾個?

ConcurrentHashMap、CopyOnWriteArrayList 、CopyOnWriteArraySet 、ConcurrentLinkedQueue、

ConcurrentLinkedDeque、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、

LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、SynchronousQueue、

LinkedTransferQueue、DelayQueue

72.ConcurrentHashMap

併發安全版HashMap,java7中採用分段鎖技術來提高併發效率,默認分16段。Java8放棄了分段鎖,採用CAS,同時當哈希衝突時,當鏈表的長度到8時,會轉化成紅黑樹。(如需瞭解細節,見jdk中代碼)

73.ConcurrentLinkedQueue

基於鏈接節點的無界線程安全隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部,當我們獲取一個元素時,它會返回隊列頭部的元素。它採用cas算法來實現。(如需瞭解細節,見jdk中代碼)

74.什麼是阻塞隊列?

阻塞隊列是一個支持兩個附加操作的隊列,這兩個附加操作支持阻塞的插入和移除方法。

1、支持阻塞的插入方法:當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。

2、支持阻塞的移除方法:當隊列空時,獲取元素的線程會等待隊列變爲非空。

75.阻塞隊列常用的應用場景?

常用於生產者和消費者場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列正好是生產者存放、消費者來獲取的容器。

76.Java裏的阻塞的隊列

ArrayBlockingQueue:    數組結構組成的 |有界阻塞隊列
LinkedBlockingQueue:   鏈表結構組成的|有界阻塞隊列
PriorityBlockingQueue:  支持優先級排序|無界阻塞隊列
DelayQueue:            優先級隊列實現|無界阻塞隊列
SynchronousQueue:      不存儲元素| 阻塞隊列
LinkedTransferQueue:   鏈表結構組成|無界阻塞隊列
LinkedBlockingDeque:   鏈表結構組成|雙向阻塞隊列

77.Fork/Join

java7提供的一個用於並行執行任務的框架,把一個大任務分割成若干個小任務,最終彙總每個小任務結果的後得到大任務結果的框架。

78.工作竊取算法

是指某個線程從其他隊列裏竊取任務來執行。當大任務被分割成小任務時,有的線程可能提前完成任務,此時閒着不如去幫其他沒完成工作線程。此時可以去其他隊列竊取任務,爲了減少競爭,通常使用雙端隊列,被竊取的線程從頭部拿,竊取的線程從尾部拿任務執行。

79.工作竊取算法的有缺點

優點:充分利用線程進行並行計算,減少了線程間的競爭。

缺點:有些情況下還是存在競爭,比如雙端隊列中只有一個任務。這樣就消耗了更多資源。

80.Java中原子操作更新基本類型,Atomic包提供了哪幾個類?

AtomicBoolean:原子更新布爾類型

AtomicInteger:原子更新整形

AtomicLong:原子更新長整形

81.Java中原子操作更新數組,Atomic包提供了哪幾個類?

AtomicIntegerArray: 原子更新整形數據裏的元素

AtomicLongArray: 原子更新長整形數組裏的元素

AtomicReferenceArray: 原子更新飲用類型數組裏的元素

AtomicIntegerArray: 主要提供原子方式更新數組裏的整形

82.Java中原子操作更新引用類型,Atomic包提供了哪幾個類?

如果原子需要更新多個變量,就需要用引用類型了。

AtomicReference : 原子更新引用類型

AtomicReferenceFieldUpdater: 原子更新引用類型裏的字段。

AtomicMarkableReference: 原子更新帶有標記位的引用類型。標記位用boolean類型表示,構造方法時AtomicMarkableReference(V initialRef,boolean initialMark)

83.Java中原子操作更新字段類,Atomic包提供了哪幾個類?

AtomiceIntegerFieldUpdater: 原子更新整形字段的更新器

AtomiceLongFieldUpdater: 原子更新長整形字段的更新器

AtomiceStampedFieldUpdater: 原子更新帶有版本號的引用類型,將整數值

84.JDK併發包中提供了哪幾個比較常見的處理併發的工具類?

提供併發控制手段: CountDownLatch、CyclicBarrier、Semaphore

線程間數據交換: Exchanger

85.CountDownLatch

允許一個或多個線程等待其他線程完成操作。

CountDownLatch的構造函數接受一個int類型的參數作爲計數器,你想等待n個點完成,就傳入n。

兩個重要的方法:

countDown() : 調用時,n會減1。

await() : 調用會阻塞當前線程,直到n變成0。

await(long time,TimeUnit unit) : 等待特定時間後,就不會繼續阻塞當前線程。

tips:計數器必須大於等於0,當爲0時,await就不會阻塞當前線程。

不提供重新初始化或修改內部計數器的值的功能。

86.CyclicBarrier

可循環使用的屏障。

讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。

CyclicBarrier默認構造放時CyclicBarrier(int parities) ,其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達屏障,然後當前線程被阻塞。

87.CountDownLatch與CyclicBarrier區別

CountDownLatch:

計數器:計數器只能使用一次。

等待: 一個線程或多個等待另外n個線程完成之後才能執行。

CyclicBarrier:

計數器:計數器可以重置(通過reset()方法)。

等待: n個線程相互等待,任何一個線程完成之前,所有的線程都必須等待。

88.Semaphore

用來控制同時訪問資源的線程數量,通過協調各個線程,來保證合理的公共資源的訪問。

應用場景:流量控制,特別是公共資源有限的應用場景,比如數據鏈接,限流等。

89.Exchanger

Exchanger是一個用於線程間協作的工具類,它提供一個同步點,在這個同步點上,兩個線程可以交換彼此的數據。比如第一個線程執行exchange()方法,它會一直等待第二個線程也執行exchange,當兩個線程都到同步點,就可以交換數據了。

一般來說爲了避免一直等待的情況,可以使用exchange(V x,long timeout,TimeUnit unit),設置最大等待時間。

Exchanger可以用於遺傳算法。

90.爲什麼使用線程池

幾乎所有需要異步或者併發執行任務的程序都可以使用線程池。合理使用會給我們帶來以下好處。

  • 降低系統消耗:重複利用已經創建的線程降低線程創建和銷燬造成的資源消耗。
  • 提高響應速度: 當任務到達時,任務不需要等到線程創建就可以立即執行。
  • 提供線程可以管理性: 可以通過設置合理分配、調優、監控。

91.線程池工作流程

1、判斷核心線程池裏的線程是否都有在執行任務,否->創建一個新工作線程來執行任務。是->走下個流程。

2、判斷工作隊列是否已滿,否->新任務存儲在這個工作隊列裏,是->走下個流程。

3、判斷線程池裏的線程是否都在工作狀態,否->創建一個新的工作線程來執行任務,

是->走下個流程。

4、按照設置的策略來處理無法執行的任務。

92.創建線程池參數有哪些,作用?

 public ThreadPoolExecutor(   int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

1.corePoolSize:核心線程池大小,當提交一個任務時,線程池會創建一個線程來執行任務,即使其他空閒的核心線程能夠執行新任務也會創建,等待需要執行的任務數大於線程核心大小就不會繼續創建。

2.maximumPoolSize:線程池最大數,允許創建的最大線程數,如果隊列滿了,並且已經創建的線程數小於最大線程數,則會創建新的線程執行任務。如果是無界隊列,這個參數基本沒用。

3.keepAliveTime: 線程保持活動時間,線程池工作線程空閒後,保持存活的時間,所以如果任務很多,並且每個任務執行時間較短,可以調大時間,提高線程利用率。

4.unit: 線程保持活動時間單位,天(DAYS)、小時(HOURS)、分鐘(MINUTES、毫秒MILLISECONDS)、微秒(MICROSECONDS)、納秒(NANOSECONDS)

5.workQueue: 任務隊列,保存等待執行的任務的阻塞隊列。

一般來說可以選擇如下阻塞隊列:

ArrayBlockingQueue:基於數組的有界阻塞隊列。

LinkedBlockingQueue:基於鏈表的阻塞隊列。

SynchronizedQueue:一個不存儲元素的阻塞隊列。

PriorityBlockingQueue:一個具有優先級的阻塞隊列。

6.threadFactory:設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。

  1. handler: 飽和策略也叫拒絕策略。當隊列和線程池都滿了,即達到飽和狀態。所以需要採取策略來處理新的任務。默認策略是AbortPolicy。

    AbortPolicy:直接拋出異常。

    CallerRunsPolicy: 調用者所在的線程來運行任務。

    DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。

    DiscardPolicy:不處理,直接丟掉。

    當然可以根據自己的應用場景,實現RejectedExecutionHandler接口自定義策略。

93.向線程池提交任務

可以使用execute()和submit() 兩種方式提交任務。

execute():無返回值,所以無法判斷任務是否被執行成功。

submit():用於提交需要有返回值的任務。線程池返回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,並且可以通過future的get()來獲取返回值,get()方法會阻塞當前線程知道任務完成。get(long timeout,TimeUnit unit)可以設置超市時間。

94.關閉線程池

可以通過shutdown()或shutdownNow()來關閉線程池。它們的原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt來中斷線程,所以無法響應終端的任務可以能永遠無法停止。

shutdownNow首先將線程池狀態設置成STOP,然後嘗試停止所有的正在執行或者暫停的線程,並返回等待執行任務的列表。

shutdown只是將線程池的狀態設置成shutdown狀態,然後中斷所有沒有正在執行任務的線程。

只要調用兩者之一,isShutdown就會返回true,當所有任務都已關閉,isTerminaed就會返回true。

一般來說調用shutdown方法來關閉線程池,如果任務不一定要執行完,可以直接調用shutdownNow方法。

95.線程池如何合理設置

配置線程池可以從以下幾個方面考慮。

  • 任務是cpu密集型、IO密集型或者混合型

  • 任務優先級,高中低。

  • 任務時間執行長短。

  • 任務依賴性:是否依賴其他系統資源。

    cpu密集型可以配置可能小的線程,比如 n + 1個線程。

    io密集型可以配置較多的線程,如 2n個線程。

    混合型可以拆成io密集型任務和cpu密集型任務,

    如果兩個任務執行時間相差大,否->分解後執行吞吐量將高於串行執行吞吐量。

    否->沒必要分解。

    可以通過Runtime.getRuntime().availableProcessors()來獲取cpu個數。

    建議使用有界隊列,增加系統的預警能力和穩定性。

96.Executor

從JDK5開始,把工作單元和執行機制分開。工作單元包括Runnable和Callable,而執行機制由Executor框架提供。

97.Executor框架的主要成員

ThreadPoolExecutor :可以通過工廠類Executors來創建。

可以創建3種類型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool、CachedThreadPool。

ScheduledThreadPoolExecutor :可以通過工廠類Executors來創建。

可以創建2中類型的ScheduledThreadPoolExecutor:ScheduledThreadPoolExecutor、SingleThreadScheduledExecutor

Future接口:Future和實現Future接口的FutureTask類來表示異步計算的結果。

Runnable和Callable:它們的接口實現類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。Runnable不能返回結果,Callable可以返回結果。

98.FixedThreadPool

可重用固定線程數的線程池。

查看源碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
   return new ThreadPoolExecutor(nThreads, nThreads,
                                 0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());}

corePoolSize 和maxPoolSize都被設置成我們設置的nThreads。

當線程池中的線程數大於corePoolSize ,keepAliveTime爲多餘的空閒線程等待新任務的最長時間,超過這個時間後多餘的線程將被終止,如果設爲0,表示多餘的空閒線程會立即終止。

工作流程:

1.當前線程少於corePoolSize,創建新線程執行任務。

2.當前運行線程等於corePoolSize,將任務加入LinkedBlockingQueue。

3.線程執行完1中的任務,會循環反覆從LinkedBlockingQueue獲取任務來執行。

LinkedBlockingQueue作爲線程池工作隊列(默認容量Integer.MAX_VALUE)。因此可能會造成如下贏下。

1.當線程數等於corePoolSize時,新任務將在隊列中等待,因爲線程池中的線程不會超過corePoolSize。

2.maxnumPoolSize等於說是一個無效參數。

3.keepAliveTime等於說也是一個無效參數。

4.運行中的FixedThreadPool(未執行shundown或shundownNow))則不會調用拒絕策略。

5.由於任務可以不停的加到隊列,當任務越來越多時很容易造成OOM。

99.SingleThreadExecutor

是使用單個worker線程的Executor。

查看源碼:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

corePoolSize和maxnumPoolSize被設置爲1。其他參數和FixedThreadPool相同。

執行流程以及造成的影響同FixedThreadPool.

100.CachedThreadPool

根據需要創建新線程的線程池。

查看源碼:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());

corePoolSize設置爲0,maxmumPoolSize爲Integer.MAX_VALUE。keepAliveTime爲60秒。

工作流程:

1.首先執行SynchronousQueue.offer (Runnable task)。如果當前maximumPool 中有空閒線程正在執行S ynchronousQueue.poll(keepAliveTIme,TimeUnit.NANOSECONDS),那麼主線程執行offer操作與空閒線程執行的poll操作配對成功,主線程把任務交給空閒線程執行,execute方 法執行完成;否則執行下面的步驟2。

  1. 當初始maximumPool爲空或者maximumPool中當前沒有空閒線程時,將沒有線程執行 SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,步驟 1將失 敗。此時CachedThreadPool會創建一個新線程執行任務,execute()方法執行完成。

3.在步驟2中新創建的線程將任務執行完後,會執行SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閒線程最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主線程提交了一個新任務(主線程執行步驟1),那麼這個空閒線程將執行主線程提交的新任務;否則,這個空閒線程將終止。由於空閒60秒的空閒線程會被終止,因此長時間保持空閒的CachedThreadPool不會使用任何資源。

一般來說它適合處理時間短、大量的任務。

參考:

  • 《Java多線程編程核心技術》

  • 《Java高併發編程詳解》

  • 《Java 併發編程的藝術》

    WechatIMG360

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