java面試題:異常&多線程線程

異常概念

1.1 異常概念

異常,就是不正常的意思。在生活中:醫生說,你的身體某個部位有異常,該部位和正常相比有點不同,該部位的功能將受影響.在程序中的意思就是:

  • 異常 :指的是程序在執行過程中,出現的非正常的情況,最終會導致JVM的非正常停止。

在Java等面向對象的編程語言中,異常本身是一個類,產生異常就是創建異常對象並拋出了一個異常對象。Java處理異常的方式是中斷處理。

異常指的並不是語法錯誤,語法錯了,編譯不通過,不會產生字節碼文件,根本不能運行.

1.2 異常體系

異常機制其實是幫助我們找到程序中的問題,異常的根類是java.lang.Throwable,其下有兩個子類:java.lang.Errorjava.lang.Exception,平常所說的異常指java.lang.Exception

Throwable體系:

  • Error:嚴重錯誤Error,無法通過處理的錯誤,只能事先避免,好比絕症。

  • Exception:表示異常,異常產生後程序員可以通過代碼的方式糾正,使程序繼續運行,是必須要處理的。好比感冒、闌尾炎。

  • Exception的分類:根據在編譯時期還是運行時期去檢查異常?

    • 編譯時期異常:checked異常。在編譯時期,就會檢查,如果沒有處理異常,則編譯失敗。(如日期格式化異常)
    • 運行時期異常:runtime異常。在運行時期,檢查異常.在編譯時期,運行異常不會編譯器檢測(不報錯)。(如數學異常)

    在這裏插入圖片描述

Throwable中的常用方法:

  • public void printStackTrace():打印異常的詳細信息。

    包含了異常的類型,異常的原因,還包括異常出現的位置,在開發和調試階段,都得使用printStackTrace。

  • public String getMessage():獲取發生異常的原因。

    提示給用戶的時候,就提示錯誤原因。

  • public String toString():獲取異常的類型和異常描述信息(不用)。

1.3 異常如何處理?

在這裏插入圖片描述

觀察控制檯內容:

紅線:異常出現的位置,從開始main方法到最後調用的方法所有位置

藍線:異常的類型

藍線:異常出現的原因

出現異常,不要緊張,把異常的簡單類名,拷貝到API中去查。

異常的處理

Java異常處理的五個關鍵字:try、catch、finally、throw、throws

2.1 拋出異常throw

在編寫程序時,我們必須要考慮程序出現問題的情況。比如,在定義方法時,方法需要接受參數。那麼,當調用方法使用接受到的參數時,首先需要先對參數數據進行合法的判斷,數據若不合法,就應該告訴調用者,傳遞合法的數據進來。這時需要使用拋出異常的方式來告訴調用者。

在java中,提供了一個throw關鍵字,它用來拋出一個指定的異常對象。那麼,拋出一個異常具體如何操作呢?

  1. 創建一個異常對象。封裝一些提示信息(信息可以自己編寫)。

  2. 需要將這個異常對象告知給調用者。怎麼告知呢?怎麼將這個異常對象傳遞到調用者處呢?通過關鍵字throw就可以完成。throw 異常對象。

    throw用在方法內,用來拋出一個異常對象,將這個異常對象傳遞到調用者處,並結束當前方法的執行。

使用格式:

throw new 異常類名(參數);

注意:如果產生了問題,我們就會throw將問題描述類即異常進行拋出,也就是將問題返回給該方法的調用者。

那麼對於調用者來說,該怎麼處理呢?一種是進行捕獲處理,另一種就是繼續講問題聲明出去,使用throws聲明處理。

2.2 Objects非空判斷

還記得我們學習過一個類Objects嗎,曾經提到過它由一些靜態的實用方法組成,這些方法是null-save(空指針安全的)或null-tolerant(容忍空指針的),那麼在它的源碼中,對對象爲null的值進行了拋出異常操作。

  • public static <T> T requireNonNull(T obj):查看指定引用對象不是null。

查看源碼發現這裏對爲null的進行了拋出異常操作:

public static <T> T requireNonNull(T obj) {
    if (obj == null)
      	throw new NullPointerException();
    return obj;
}

2.3 聲明異常throws

聲明異常:將問題標識出來,報告給調用者。如果方法內通過throw拋出了編譯時異常,而沒有捕獲處理(稍後講解該方式),那麼必須通過throws進行聲明,讓調用者去處理。

關鍵字throws運用於方法聲明之上,用於表示當前方法不處理異常,而是提醒該方法的調用者來處理異常(拋出異常).

聲明異常格式:

修飾符 返回值類型 方法名(參數) throws 異常類名1,異常類名2…{   }	

throws用於進行異常類的聲明,若該方法可能有多種異常情況產生,那麼在throws後面可以寫多個異常類,用逗號隔開。

2.4 捕獲異常try…catch

如果異常出現的話,會立刻終止程序,所以我們得處理異常:

  1. 該方法不處理,而是聲明拋出,由該方法的調用者來處理(throws)。
  2. 在方法中使用try-catch的語句塊來處理異常。

try-catch的方式就是捕獲異常。

  • 捕獲異常:Java中對異常有針對性的語句進行捕獲,可以對出現的異常進行指定方式的處理。

捕獲異常語法如下:

try{
     編寫可能會出現異常的代碼
}catch(異常類型  e){
     處理異常的代碼
     //記錄日誌/打印異常信息/繼續拋出異常
}

**try:**該代碼塊中編寫可能產生異常的代碼。

**catch:**用來進行某種異常的捕獲,實現對捕獲到的異常進行處理。

注意:try和catch都不能單獨使用,必須連用。

2.5 finally 代碼塊

finally:有一些特定的代碼無論異常是否發生,都需要執行。另外,因爲異常會引發程序跳轉,導致有些語句執行不到。而finally就是解決這個問題的,在finally代碼塊中存放的代碼都是一定會被執行的。

什麼時候的代碼必須最終執行?

當我們在try語句塊中打開了一些物理資源(磁盤文件/網絡連接/數據庫連接等),我們都得在使用完之後,最終關閉打開的資源。

finally的語法:

try…catch…finally:自身需要處理異常,最終還得關閉資源。

注意:finally不能單獨使用。

比如在我們之後學習的IO流中,當打開了一個關聯文件的資源,最後程序不管結果如何,都需要把這個資源關閉掉。

當只有在try或者catch中調用退出JVM的相關方法,此時finally纔不會執行,否則finally永遠會執行。

2.6 異常注意事項

  • 多個異常使用捕獲又該如何處理呢?

    1. 多個異常分別處理。
    2. 多個異常一次捕獲,多次處理。
    3. 多個異常一次捕獲一次處理。

    一般我們是使用一次捕獲多次處理方式,格式如下:

    try{
         編寫可能會出現異常的代碼
    }catch(異常類型A  e){try中出現A類型異常,就用該catch來捕獲.
         處理異常的代碼
         //記錄日誌/打印異常信息/繼續拋出異常
    }catch(異常類型B  e){try中出現B類型異常,就用該catch來捕獲.
         處理異常的代碼
         //記錄日誌/打印異常信息/繼續拋出異常
    }
    
    

    注意:這種異常處理方式,要求多個catch中的異常不能相同,並且若catch中的多個異常之間有子父類異常的關係,那麼子類異常要求在上面的catch處理,父類異常在下面的catch處理。

  • 運行時異常被拋出可以不處理。即不捕獲也不聲明拋出。

  • 如果finally有return語句,永遠返回finally中的結果,避免該情況.

  • 如果父類拋出了多個異常,子類重寫父類方法時,拋出和父類相同的異常或者是父類異常的子類或者不拋出異常。

  • 父類方法沒有拋出異常,子類重寫父類該方法時也不可拋出異常。此時子類產生該異常,只能捕獲處理,不能聲明拋出

自定義異常

3.1 概述

爲什麼需要自定義異常類:

我們說了Java中不同的異常類,分別表示着某一種具體的異常情況,那麼在開發中總是有些異常情況是SUN沒有定義好的,此時我們根據自己業務的異常情況來定義異常類。例如年齡負數問題,考試成績負數問題等等。

在上述代碼中,發現這些異常都是JDK內部定義好的,但是實際開發中也會出現很多異常,這些異常很可能在JDK中沒有定義過,例如年齡負數問題,考試成績負數問題.那麼能不能自己定義異常呢?

什麼是自定義異常類:

在開發中根據自己業務的異常情況來定義異常類.

自定義一個業務邏輯異常: RegisterException。一個註冊異常類。

異常類如何定義:

  1. 自定義一個編譯期異常: 自定義類 並繼承於java.lang.Exception
  2. 自定義一個運行時期的異常類:自定義類 並繼承於java.lang.RuntimeException

多線程

線程基本概念

4.1 併發與並行

  • 併發:指兩個或多個事件在同一個時間段內發生。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。

在操作系統中,安裝了多個程序,併發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感覺是同時運行,那是因爲分時交替運行的時間是非常短的。

而在多個 CPU 系統中,則這些可以併發執行的程序便可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每個處理器來處理一個可以併發執行的程序,這樣多個程序便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核 越多,並行處理的程序越多,能大大的提高電腦運行的效率。

注意:單核處理器的計算機肯定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同理,線程也是一樣的,從宏觀角度上理解線程是並行運行的,但是從微觀角度上分析卻是串行運行的,即一個線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,我們把這種情況稱之爲線程調度。

4.2 線程與進程

  • 進程:是指一個內存中運行的應用程序,每個進程都有一個獨立的內存空間,一個應用程序可以同時運行多個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序即是一個進程從創建、運行到消亡的過程。

  • 線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程中是可以有多個線程的,這個應用程序也可以稱之爲多線程程序。

    簡而言之:一個程序運行後至少有一個進程,一個進程中可以包含多個線程

線程調度:

  • 分時調度

    所有線程輪流使用 CPU 的使用權,平均分配每個線程佔用 CPU 的時間。

  • 搶佔式調度

    優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。

    • 搶佔式調度詳解

      大部分操作系統都支持多進程併發運行,現在的操作系統幾乎都支持同時運行多個程序。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟件,同時還開着畫圖板,dos窗口等軟件。此時,這些程序是在同時運行,”感覺這些軟件好像在同一時刻運行着“。

      實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。
      其實,多線程程序並不能提高程序的運行速度,但能夠提高程序運行效率,讓CPU的使用率更高。

4.3 創建線程類

Java使用java.lang.Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。每個線程的作用是完成一定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來代表這段程序流。Java中通過繼承Thread類來創建啓動多線程的步驟如下:

  1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務,因此把run()方法稱爲線程執行體。
  2. 創建Thread子類的實例,即創建了線程對象
  3. 調用線程對象的start()方法來啓動該線程

採用 java.lang.Runnable 也是非常常見的一種,我們只需要重寫run方法即可。
步驟如下:

  1. 定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
  2. 創建Runnable實現類的實例,並以此實例作爲Thread的target來創建Thread對象,該Thread對象纔是真正
    的線程對象。
  3. 調用線程對象的start()方法來啓動線程。

4.4線程如何執行?

多線程如下圖所示:調用start開啓一個新的線程
在這裏插入圖片描述

多線程執行時,在棧內存中,其實每一個執行線程都有一片自己所屬的棧內存空間。進行方法的壓棧和彈棧。
在這裏插入圖片描述

當執行線程的任務結束了,線程自動在棧內存中釋放了。但是當所有的執行線程都結束了,那麼進程就結束了。

4.5Thread類 API

在上一天內容中我們已經可以完成最基本的線程開啓,那麼在我們完成操作過程中用到了 java.lang.Thread 類,
API中該類中定義了有關線程的一些方法,具體如下:

構造方法:

public Thread() :分配一個新的線程對象。
public Thread(String name) :分配一個指定名字的新的線程對象。
public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象並指定名字。

常用方法:

public String getName() :獲取當前線程名稱。
public void start() :導致此線程開始執行; Java虛擬機調用此線程的run方法。
public void run() :此線程要執行的任務在此處定義代碼。
public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時停止執行)。
public static Thread currentThread() :返回對當前正在執行的線程對象的引用。

面試題1:Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。
總結:
實現Runnable接口比繼承Thread類所具有的優勢:

  1. 適合多個相同的程序代碼的線程去共享同一個資源。
  2. 可以避免java中的單繼承的侷限性。
  3. 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
  4. 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
    擴充:在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用
    java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進
    程。

線程安全

線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫
操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,
否則的話就可能影響線程安全。

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程併發訪問一個資源的安全性問題:,Java中提供了同步機制(synchronized)來解決。

  1. 同步代碼塊
  2. 同步方法。
  3. 鎖機制。

總結:同步中的線程,沒有執行完程序就不會釋放鎖,同步外的線程沒有鎖就進不去同步。

同步代碼塊

同步代碼塊: synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問
格式:

synchronized(同步鎖){
     需要同步操作的代碼
}

同步鎖:
對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.

  1. 鎖對象 可以是任意類型。
  2. 多個線程對象 要使用同一把鎖。
    注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着
    BLOCKED)。

synchronized(同步鎖){
需要同步操作的代碼

注意事項:

通過代碼塊中的鎖對象,可以使用任意的對象

但是必須保證多個線程使用的是同一個對象

鎖對象的作用:將同步代碼塊鎖住,只讓一個線程在同步代碼塊中執行。

同步方法:

同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外
等着。

public synchronized void method(){
    可能會產生線程安全問題的代碼
}

同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。

Lock鎖

java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作,
同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:
public void lock() :加同步鎖。
public void unlock() :釋放同步鎖。

先要在成員位置創建一個對象

Lock lock = new ReentrantLock();

線程狀態

在這裏插入圖片描述

線程狀態 導致狀態發生條件
NEW(新建) 線程剛被創建,但是並未啓動。還沒調用start方法。
Runnable(可運行) 線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操作系統處理器。
Blocked(鎖阻塞) 當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀態;當該線程持有鎖時,該線程將變成Runnable狀態。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。進入這個狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒
Timed Waiting(計時等待) 同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。這一狀態將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、Object.wait
Teminated(被終止) 因爲run方法正常退出而死亡,或者因爲沒有捕獲的異常終止了run方法而死亡

在這裏插入圖片描述

線程通信

概念:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。

爲什麼要處理線程間通信:

多個線程併發執行時, 在默認情況下CPU是隨機切換線程的,當我們需要多個線程來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一份數據。

如何保證線程間通信有效利用資源:

多個線程在處理同一個資源,並且任務不同時,需要線程通信來幫助解決線程之間對同一個變量的使用或操作。 就是多個線程在操作同一份數據時, 避免對同一共享變量的爭奪。也就是我們需要通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

什麼是等待喚醒機制

就是在一個線程進行了規定操作後,就進入等待狀態(wait()), 等待其他線程執行完他們的指定代碼過後 再將其喚醒(notify());在有多個線程進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待線程。

wait/notify 就是線程間的一種協作機制。

等待喚醒中的方法

等待喚醒機制就是用於解決線程間通信的問題的,使用到的3個方法的含義如下:

  1. wait:線程不再活動,不再參與調度,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態即是 WAITING。它還要等着別的線程執行一個特別的動作,也即是“通知(notify)”在這個對象上等待的線程從wait set 中釋放出來,重新進入到調度隊列(ready queue)中
  2. notify:則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
  3. notifyAll:則釋放所通知對象的 wait set 上的全部線程。

線程池:

爲什麼用線程池

如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間。

線程池是什麼:

線程池:其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。
在這裏插入圖片描述

合理利用線程池能夠帶來三個好處:

  1. 降低資源消耗。減少了創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
  2. 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  3. 提高線程的可管理性。可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因爲消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

如何使用線程池:

Java裏面線程池的頂級接口是java.util.concurrent.Executor,但是嚴格意義上講Executor並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是java.util.concurrent.ExecutorService

要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優的,因此在java.util.concurrent.Executors線程工廠類裏面提供了一些靜態工廠,生成一些常用的線程池。官方建議使用Executors工程類來創建線程池對象。

Executors類中有個創建線程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回線程池對象。(創建的是有界線程池,也就是池中的線程個數可以指定最大數量)

獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法如下:

  • public Future<?> submit(Runnable task):獲取線程池中的某一個線程對象,並執行

    Future接口:用來記錄線程任務執行完畢後產生的結果。線程池創建與使用。

使用線程池中線程對象的步驟:

  1. 創建線程池對象。
  2. 創建Runnable接口子類對象。(task)
  3. 提交Runnable接口子類對象。(take task)
  4. 關閉線程池(一般不做)。
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 創建線程池對象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
        // 創建Runnable實例對象
        MyRunnable r = new MyRunnable();

        //自己創建線程對象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 調用MyRunnable中的run()

        // 從線程池中獲取線程對象,然後調用MyRunnable中的run()
        service.submit(r);
        // 再獲取個線程對象,調用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法調用結束後,程序並不終止,是因爲線程池控制了線程的關閉。
        // 將使用完的線程又歸還到了線程池中
        // 關閉線程池
        //service.shutdown();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章