【面試】多線程知識(shí)點(diǎn)

一、創(chuàng)建多線程得四種方式

1、繼承Thread類的方式:

1.創(chuàng)建一個(gè)繼承于Thread類的子類
2.重寫Thread類的run() --> 將此線程執(zhí)行的操作聲明在run()中
3.創(chuàng)建Thread類的子類的對(duì)象
4.通過(guò)此對(duì)象調(diào)用start():①啟動(dòng)當(dāng)前線程 ②調(diào)用當(dāng)前線程的run()

class MyThread extends Thread {
    // 線程執(zhí)行體
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

class TestThread {

    public static void main(String[] args) {
        MyThread thread1 = new MyThread();// 創(chuàng)建一個(gè)新的線程thread1 此線程進(jìn)入新建狀態(tài)
        MyThread thread2 = new MyThread();
        thread1.start(); // 調(diào)用start()方法,使線程進(jìn)入就緒狀態(tài)
        thread2.start();
    }
}

問(wèn)題:

  1. 我們啟動(dòng)一個(gè)線程,必須調(diào)用start(),不能調(diào)用run()的方式啟動(dòng)線程
  2. .如果再啟動(dòng)一個(gè)線程,必須重新創(chuàng)建一個(gè)Thread子類的對(duì)象,調(diào)用此對(duì)象的start()

2、實(shí)現(xiàn)Runnable接口的方式:

1.創(chuàng)建一個(gè)實(shí)現(xiàn)了Runnable接口的類
2.實(shí)現(xiàn)類去實(shí)現(xiàn)Runnable中的抽象方法:run()
3.創(chuàng)建實(shí)現(xiàn)類的對(duì)象
4.將此對(duì)象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread類的對(duì)象
5.通過(guò)Thread類的對(duì)象調(diào)用start()

class MyThread implements Runnable{

    // 線程執(zhí)行體
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

class TestThread {

    public static void main(String[] args) {
        MyThread myRunnable = new MyThread();// 創(chuàng)建一個(gè)Runnable實(shí)現(xiàn)類的對(duì)象
        Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target創(chuàng)建新的線程
        Thread thread2 = new Thread(myRunnable);
        thread1.start(); // 調(diào)用start()方法,使線程進(jìn)入就緒狀態(tài)
        thread2.start();
    }
}

兩種方式的對(duì)比:
開發(fā)中優(yōu)先選擇:實(shí)現(xiàn)Runnable接口的方式
原因:1. 實(shí)現(xiàn)的方式?jīng)]類的單繼承性的局限性;2. 實(shí)現(xiàn)的方式更適合來(lái)處理多個(gè)線程共享數(shù)據(jù)的情況
聯(lián)系:public class Thread implements Runnable
相同點(diǎn):1. 兩種方式都需要重寫run(),將線程要執(zhí)行的邏輯聲明再run();2. 兩種方式要想啟動(dòng)線程,都是調(diào)用Thread類中的start()

3、實(shí)現(xiàn)Callable接口的方式

1.創(chuàng)建一個(gè)實(shí)現(xiàn)Callable的實(shí)現(xiàn)類
2.實(shí)現(xiàn)call方法,將此線程需要執(zhí)行的操作聲明在call()中
3.創(chuàng)建Callable接口實(shí)現(xiàn)類的對(duì)象
4.將此Callable接口實(shí)現(xiàn)類的對(duì)象作為參數(shù)傳遞到FutureTask構(gòu)造器中,創(chuàng)建FutureTask的對(duì)象
5.將FutureTask的對(duì)象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread對(duì)象,并調(diào)用start()
6.獲取Callable中call方法的返回值

public class ThreadTest {
    public static void main(String[] args) {

        // 創(chuàng)建CallableThread對(duì)象
        Callable<Integer> myCallable = new CallableThread();
        //使用FutureTask來(lái)包裝CallableThread對(duì)象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                // FutureTask對(duì)象作為Thread對(duì)象的target創(chuàng)建新的線程
                Thread thread = new Thread(ft);
                thread.start();// 線程進(jìn)入到就緒狀態(tài)
            }
        }
        System.out.println("主線程for循環(huán)執(zhí)行完畢..");
        try {
            int sum = ft.get();
            System.out.println("子線程的返回值:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class CallableThread implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }
}

如何理解實(shí)現(xiàn)Callable接口的方式創(chuàng)建多線程比實(shí)現(xiàn)Runnable接口創(chuàng)建多線程方式強(qiáng)大?
1.call()可以有返回值的。
2.call()可以拋出異常,被外面的操作捕獲,獲取異常的信息
3.Callable是支持泛型的

4、使用線程池的方式

使用線程池的好處:

  1. 提高響應(yīng)速度(減少了創(chuàng)建新線程的時(shí)間)
  2. 降低資源消耗(重復(fù)利用線程池中線程,不需要每次都創(chuàng)建)
  3. 便于線程管理

線程池屬性:
corePoolSize: 核心線程數(shù)量
maxmumPoolSize: 最大線程數(shù)
keepAliveTime: 線程沒(méi)有任務(wù)時(shí)最多保持多長(zhǎng)時(shí)間后會(huì)終止
TimeUnit:線程活動(dòng)保持時(shí)間的單位

創(chuàng)建線程池:ExecutorService 和 Executors
Executors.newFixedThreadPool(n); 創(chuàng)建一個(gè)可重用固定線程數(shù)的線程池

public class ThreadTest {
    public static void main(String[] args) {
        // 創(chuàng)建固定大小線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            MyRunnable myRunnable = new MyRunnable();
            // 執(zhí)行任務(wù)并獲取Future對(duì)象
            threadPool.execute(myRunnable);
        }
        //關(guān)閉線程池
        threadPool.shutdown();
        System.out.println("主線程for循環(huán)執(zhí)行完畢..");
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("通過(guò)線程池方式創(chuàng)建的線程:" + Thread.currentThread().getName() + " ");
    }
}

void execute(Runnable command) :執(zhí)行任務(wù)/命令,沒(méi)有返回值,一般用來(lái)執(zhí)行Runnable
Future submit(Callable task):執(zhí)行任務(wù),有返回值,一般又來(lái)執(zhí)行Callable
void shutdown():關(guān)閉連接池

二、Thread類中常用的方法

1.start():啟動(dòng)當(dāng)前線程,執(zhí)行當(dāng)前線程的run()
2.run():通常需要重寫Thread類中的此方法,將創(chuàng)建的線程要執(zhí)行的操作聲明在此方法中
3.currentThread(): 靜態(tài)方法,返回當(dāng)前代碼執(zhí)行的線程
4.getName():獲取當(dāng)前線程的名字
5.setName():設(shè)置當(dāng)前線程的名字
6.yield():釋放當(dāng)前CPU的執(zhí)行權(quán)
7.join():【在線程a中調(diào)用線程b的join(),此時(shí)線程a就進(jìn)入阻塞狀態(tài),直到線程b完全執(zhí)行完以后,線程a才結(jié)束阻塞狀態(tài)】。
8.stop():已過(guò)時(shí)。當(dāng)執(zhí)行此方法時(shí),強(qiáng)制結(jié)束當(dāng)前線程。
9.sleep(long millitime):讓當(dāng)前線程“睡眠”指定時(shí)間的millitime毫秒)。在指定的millitime毫秒時(shí)間內(nèi),當(dāng)前線程是阻塞狀態(tài)的。
10.isAlive():返回boolean,判斷線程是否還活著
11.setPriority(int p):設(shè)置線程的優(yōu)先級(jí)
11.getPriority():獲取線程的優(yōu)先級(jí)

1、停止線程

終止線程有三種方式:

(1)使用退出標(biāo)志,run()執(zhí)行完以后退出【拋出異?;蛘遰eturn】

(2)使用stop強(qiáng)行停止線程,不推薦,會(huì)導(dǎo)致當(dāng)前任務(wù)執(zhí)行到一半突然中斷,出現(xiàn)不可預(yù)料的問(wèn)題;而且stop和suspend以及resume一樣是過(guò)期作廢的方法

(3)使用interrupt中斷線程

interrupt()方法不會(huì)真的停止線程,而是會(huì)記錄一個(gè)標(biāo)志,這個(gè)標(biāo)志,可以由下面的兩個(gè)方法檢測(cè)到。

Thread.interrupted( ):測(cè)試當(dāng)前線程是否停止,但是它具有清除線程中斷狀態(tài)功能,如第一次返回true,第二次調(diào)用會(huì)返回false;
Thread.isInterrupted( ):僅返回結(jié)果,不清除狀態(tài)。重復(fù)調(diào)用會(huì)結(jié)果一致

基于上面的邏輯,可以根據(jù)標(biāo)志來(lái)判斷在run()里面狀態(tài),然后再使用interrupt()來(lái)使代碼停止,停止代碼可以使用拋出異常的方式。

如果在sleep里面拋出異常停止線程,會(huì)進(jìn)入catch,并清除停止?fàn)顟B(tài),使之變成false;

2、暫停線程與恢復(fù)線程

suspend()暫停,resume()恢復(fù),已經(jīng)被棄用,

缺點(diǎn):

  • 獨(dú)占,使用不當(dāng)很容易讓公共的同步對(duì)象獨(dú)占,使得其他線程無(wú)法訪問(wèn)。
  • 不同步:線程暫停容易導(dǎo)致不同步。

三、線程的同步

1、解決線程安全問(wèn)題

方式一:同步代碼塊(synchronized)

    synchronized(同步監(jiān)視器){
        // 需要被同步的代碼
    }    

說(shuō)明:
1.操作共享數(shù)據(jù)的代碼,即為需要被同步的代碼。
2.共享數(shù)據(jù):多個(gè)線程共同操作的變量。
3.同步監(jiān)視器:俗稱:鎖。任何一個(gè)類的對(duì)象,都可以充當(dāng)鎖。

要求:多個(gè)線程必須要共用同一把鎖。(同步監(jiān)視器必須唯一)
補(bǔ)充:在實(shí)現(xiàn)Runnable接口創(chuàng)建多線程的方式中,我們可以考慮使用this充當(dāng)同步監(jiān)視器

在繼承Thread類創(chuàng)建多線程的方式中,慎用this充當(dāng)同步監(jiān)視器,考慮使用當(dāng)前類充當(dāng)同步監(jiān)視器

方式二:同步方法(synchronized)

如果操作共享數(shù)據(jù)的代碼完整地聲明在一個(gè)方法中,我們不妨將此方法聲明同步的。

  1. 同步方法仍然涉及到同步監(jiān)視器(鎖),只是不需要我們顯示的聲明。
  2. 非靜態(tài)同步方法,同步監(jiān)視器是:this (對(duì)象)
    靜態(tài)同步方法,同步監(jiān)視器是:當(dāng)前類本身

同步的方式,解決了線程安全問(wèn)題 --- 好處
操作同步代碼時(shí),只能有一個(gè)線程參與,其他線程等待。相當(dāng)于一個(gè)單線程的過(guò)程,效率低。 --- 局限性

方式三:Lock鎖(jdk5.0新增) 使用子類ReentrantLock;

1. 實(shí)例化reentrantLock  
2. 調(diào)用鎖定方法:lock()  
3. 調(diào)用解鎖方法:unlock()  

使用的優(yōu)先順序:
            Lock -> 同步代碼塊(已經(jīng)進(jìn)入方法體,分配了相應(yīng)資源) -> 同步方法(在方法體之外)

【面試題】
1. synchronized 與 Lock 的異同?
相同點(diǎn):解決了線程安全問(wèn)題
不同點(diǎn):synchronized機(jī)制在執(zhí)行完相應(yīng)的同步代碼以后,自動(dòng)的釋放同步監(jiān)視器
Lock需要手動(dòng)的啟動(dòng)同步(lock()),同時(shí)結(jié)束同步也需要手動(dòng)的實(shí)現(xiàn)(unlock())
2. 如何解決線程安全問(wèn)題?有幾種方式?(回答以上)

2、線程安全的單例模式之懶漢式

/**
 * 使用同步機(jī)制將單例模式中的懶漢式改寫為線程安全的
 */
public class BankTest {
}
class Bank{

    private Bank(){}

    private static Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率稍差
        //快捷鍵:Alt+Shift+Z
//        synchronized (Bank.class) {
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }

        //方式二:效率較高
        if(instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

3、死鎖問(wèn)題

死鎖:不同的線程分別占用對(duì)方需要的同步資源不放棄,都在等待對(duì)方放棄自己相應(yīng)的同步資源,就形成了線程的死鎖。

說(shuō)明:

  1. 出現(xiàn)死鎖后,不會(huì)出現(xiàn)異常,不會(huì)出現(xiàn)提示,只是所有的線程都處于阻塞狀態(tài),無(wú)法繼續(xù)。
  2. 我們使用同步時(shí),要避免出現(xiàn)死鎖。

解決方法
專門的算法、原則
盡量減少同步資源的定義
盡量避免嵌套同步

四、線程的通信

涉及到的三個(gè)方法:
wait(): 一旦執(zhí)行此方法,當(dāng)前線程就進(jìn)入阻塞狀態(tài),并釋放同步監(jiān)視器。
notify(): 一旦執(zhí)行此方法,就會(huì)喚醒被wait的一個(gè)線程。如果有多個(gè)線程被wait,就喚醒優(yōu)先級(jí)高的那個(gè)。
notifyAll(): 一旦執(zhí)行此方法,就會(huì)喚醒所有被wait的線程。

說(shuō)明:
1. wait(),notify(),notifyAll()三個(gè)方法必須使用在同步代碼塊或同步方法中。
2. wait(),notify(),notifyAll()三個(gè)方法的調(diào)用者必須是同步代碼塊或同步方法中的同步監(jiān)視器。否則,會(huì)出現(xiàn)IllegalMonitorStateException異常
3. wait(),notify(),notifyAll()三個(gè)方法是定義在java.lang.Object類中。

線程通信:wait() / notify() / notifyAll() :此三個(gè)方法定義在Object類中
【調(diào)用wait方法可以讓當(dāng)前線程進(jìn)入等待喚醒狀態(tài),該線程會(huì)處于等待喚醒狀態(tài)直到另一個(gè)線程調(diào)用了object對(duì)象的notify方法或者notifyAll方法?!?/p>

面試題:sleep() 和 wait() 的異同?
1.相同點(diǎn):一旦執(zhí)行方法,都可以使得當(dāng)前線程進(jìn)入阻塞狀態(tài)。
2.不同點(diǎn):

  1. 兩個(gè)方法聲明的位置不同:Thread類中聲明sleep(),Object類中聲明wait()
  2. 調(diào)用的要求不同:sleep()可以在任何需要的場(chǎng)景下調(diào)用。wait()必須使用在同步代碼塊或同步方法中。
  3. 關(guān)于是否釋放同步監(jiān)視器:如果兩個(gè)方法都使用在同步代碼塊或同步方法中,sleep()不會(huì)釋放鎖,wait()會(huì)釋放鎖。

五、線程的生命周期

image.png

新建:當(dāng)一個(gè)Thread類或其子類的對(duì)象被聲明并創(chuàng)建時(shí),新生的線程對(duì)象處于新建狀態(tài)
就緒:處于新建狀態(tài)的線程被start()后,將進(jìn)入線程隊(duì)列等待CPU時(shí)間片,此時(shí)它已具備了運(yùn)行的條件,只是沒(méi)分配到CPU資源
運(yùn)行:當(dāng)就緒的線程被調(diào)度并獲得CPU資源時(shí),便進(jìn)入運(yùn)行狀態(tài),run()方法定義了線程的操作和功能
阻塞:在某種特殊情況下,被人為掛起或執(zhí)行輸入輸出操作時(shí),讓出CPU并臨時(shí)中止自己的執(zhí)行,進(jìn)入阻塞狀態(tài)
死亡:線程完成了它的全部工作或線程被提前強(qiáng)制性地中止或出現(xiàn)異常導(dǎo)致結(jié)束

六、volatile 關(guān)鍵字

一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile 修飾之后,那么就具備了兩層語(yǔ)義:

1)保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性,即一個(gè)線程修改了某個(gè)變量的值,這新值對(duì)其他線程來(lái)說(shuō)是立即可見(jiàn)的。

2)禁止進(jìn)行指令重排序。

1、與 synchronized 對(duì)比

volatile是線程同步的輕量實(shí)現(xiàn),只能修飾變量,性能高于synchronized

volatile保證可見(jiàn)性,不保證原子性【一旦其修飾的變量改變,其余的線程都能發(fā)現(xiàn),因?yàn)闀?huì)強(qiáng)制從公共堆棧取值】,synchronized保證原子性,間接保證可見(jiàn)性,因?yàn)樗麜?huì)將私有內(nèi)存和公共內(nèi)存的值同步

例如:i++操作,實(shí)際上不是原子操作,他有3步:

(1).從內(nèi)存取i值

(2).計(jì)算i的值

(3).將i的新值寫到內(nèi)存

多個(gè)線程執(zhí)行時(shí),使用volatile,可能導(dǎo)致數(shù)據(jù)臟讀,進(jìn)而出現(xiàn)錯(cuò)誤。

多線程訪問(wèn)volatile不會(huì)阻塞,而synchronized會(huì)

volatile是解決變量在多個(gè)線程之間的可見(jiàn)性,synchronized是保證多個(gè)線程之間資源的同步性。

2、volatile 的應(yīng)用場(chǎng)景

synchronized關(guān)鍵字是防止多個(gè)線程同時(shí)執(zhí)行一段代碼,那么就會(huì)很影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,但是要注意volatile關(guān)鍵字是無(wú)法替代synchronized關(guān)鍵字的,因?yàn)関olatile關(guān)鍵字無(wú)法保證操作的原子性。

通常來(lái)說(shuō),使用volatile必須具備以下2個(gè)條件:

1)對(duì)變量的寫操作不依賴于當(dāng)前值

2)該變量沒(méi)有包含在具有其他變量的不變式中

下面列舉幾個(gè)Java中使用volatile的幾個(gè)場(chǎng)景:

①狀態(tài)標(biāo)記量

volatile booleanflag = false;

//線程1
while(!flag){
  doSomething();
}

//線程2
public voidsetFlag() {
  flag = true;
}

根據(jù)狀態(tài)標(biāo)記,終止線程。

②單例模式中的doublecheck

class Singleton {
    
 private volatile static Singleton instance= null;

 private Singleton() {}

 public static Singleton getInstance() {

        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;

    }
}

為什么要使用volatile 修飾instance?

主要在于instance= new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:

1.給 instance 分配內(nèi)存

2.調(diào)用Singleton 的構(gòu)造函數(shù)來(lái)初始化成員變量

3.將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance就為非 null 了)。

但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說(shuō)上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance已經(jīng)是非 null 了(但卻沒(méi)有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。

創(chuàng)作不易,關(guān)注、點(diǎn)贊就是對(duì)作者最大的鼓勵(lì),歡迎在下方評(píng)論留言
歡迎關(guān)注微信公眾號(hào):鍵指JAVA,定期分享Java知識(shí),一起學(xué)習(xí),共同成長(zhǎng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容