Java基礎中的面試題(二),你能接幾招

Java基礎中的面試題(二),你能接幾招

  1. 線程池的構造方法中有哪些參數,代表什麼含義

    我們在使用線程池的時候經常會通過 Executors.newFixedThreadPool(), 或者 Executors.newCachedThreadPool() 這樣的方法, 通過查看源碼的方式,可以看出底層都是調用 new ThreadPoolExecutor(), 來實現的,並且在阿里巴巴的代碼規範中,要求我們創建線程池的時候,要通過ThreadPoolExecutor的構造方法來實現,那麼我們接下來就來看下他的構造方法

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

    以上給出了源碼中該類的方法描述。接下來逐個介紹一下:

    corePoolSize: 核心線程數,當線程池中的線程數目達到corePoolSize後,就會把達到的任務放到緩存隊列中

    maxmumPoolSize: 線程池中允許的最大線程數

    keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。

    unit: 參數keepAliveTime的時間單位,有七種。 天,小時,分鐘,秒,毫秒,微秒,納秒

    workQueue: 一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程差生重大影響。一般來說這裏的阻塞隊列有幾種選擇:ArrayBlockingQueue, LinkedBlockingQueue,SynchronousQueue

    threadFactory: 線程工廠,主要用來創建線程,自帶一個默認的線程工廠

    handler:拒絕策略,有以下四種取值:

    ​ ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
    ​ ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
    ​ ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
    ​ ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

    任務緩存隊列及任務排隊策略:

    前面我們講到當線程池超過corePoolSize時就會放入到緩存隊列中那麼下面我們就來說說任務的緩存隊列。

    1、ArrayBlockingQueue:基於數組的先進先出,創建時必須指定大小,超出直接corePoolSize個任務,則加入到該隊列中,只能加該queue設置的大小,其餘的任務則創建線程,直到(corePoolSize+新建線程)> maximumPoolSize。

    2、LinkedBlockingQueue:基於鏈表的先進先出,無界隊列。超出直接corePoolSize個任務,則加入到該隊列中,直到資源耗盡。

    3、synchronousQueue:這個隊列比較特殊,它不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。

  2. 如何實現有返回值的多線程

    一般我們使用多線程的時候就習慣使用Runnable, 實現Runnable接口後,要求實現裏邊 public void run(){}方法,這個方法明顯是沒有返回值的, 所以無法實現有返回值的多線程。要想實現又返回值的多線程我們應該使用Callable接口。下面通過代碼給出一個簡單的實現

    public class Test{
        public static void main(String[] args){
            // 使用線程池調用Callable並獲取返回值
            ExecutorService pool = Executors.newFixedThreadPool(10);
            Future f = pool.submit(new Task());
            String result = f.get();
            
            
        }
    }
    // 有返回值的多線程任務
    class Task<String> implements Callable<String>{
        public String call(){
            // do sth
            return "success";     
        }    
    }
    
    
  3. 如何對List進行排序

    可以通過Collections.sort(List); list.sort(); 此種排序要求List中的元素必須實現Comparable接口,使其具備排序能力。重寫compareTo方法,定義排序規則。

    還有一種方式Collections.sort(Comparator c), list.sort(Comparator); 通過傳入一個外部比較器,覆蓋原來的排序內容,主要用於類已經無法繼承Comparable, 或者想換一種實現方式,對於Comparable裏邊的實現方式不滿意

    接下來我們還是通過案例,來舉例。 分別 用兩種方式實現對Person集合中的元素按年齡升序和降序

    public class Test{
        public static void main(String[] args){
            List<Person> list = new ArrayList<>();
            list.add(new Person(15,"zhangsan"));
            list.add(new Person(18,"lisi"));
            list.add(new Person(14,"wangwu"));
            list.add(new Person(12,"zhaoliu"));
       
            // 方式1, 讓Person類實現Comparable接口,重寫compareTo方法,使用Collections.sort()排序
            // 需改變類結構,實現一個接口
            Collectios.sort(list);// 該方法只能對list排序
            // 或者使用list調用
            list.sort();
            
            // 方式二,通過Comparator接口,一般用於不改變類結構,類可以不用實現Comparable接口
            Collections.sort(list, new Comparator<Person>(){
                public int compare(Person p1, Person p2){
                    return p2.age - p1.age;
                }
            });
            
            // 或者list直接調用
            list.sort(new Comparator<Person>(){
                public int compare(Person p1, Person p2){
                    return p2.age - p1.age;
                }
                
            });
            
        }
    }
    
    @Data
    class Person implements Comparable<Person>{
        
        private int age;
        
        private String name;
        
        public Person(int age, String name){
            this.age = age;
            this.name = name;
        }
    
        public int compareTo(Person p){
            // 升序: 降序用p.age-this.age;
            return this.age - p.age;
        }
    }
    
  4. JVM如何加載一個類並介紹雙親委派模型

    類加載的過程:加載,驗證(驗證階段作用是保證Class文件的字節流包含的信息符合JVM規範,不會給JVM造成危害),準備(準備階段是爲變量分配內存並設置類變量的初始化), 解析(解析過程是將常量池內的符號引用替換成直接引用)、初始化。

    雙親委派模型: 雙親委派是如果一個類加載器收到了一個類加載的請求,不會自己先嚐試加載,先去找父類加載器去完成,當頂層啓動類加載器表示無法加載這個類的時候,子類纔會嘗試自己去加載。當回到最初的發起者加載器還無法加載時,並不會向下找,而是拋出ClassNotFound異常。

  5. JVM中的結構

    jvm中包含棧,堆,方法區,程序計數器,本地方法棧

    • 棧:

      java棧中只保存基本數據類型和自定義對象的引用,注意只是對象的引用,而不是對象本身,對象本身保存在堆中。像String,Integer,等包裝類是存放於堆中的。棧是先進後出類型的,棧內的數據在超出其作用域後,會被自動釋放掉,不由JVM GC管理。每一個線程都包含一個棧區,每個棧中的數據都是私有的,其他棧不能訪問。每個線程都會創建一個操作棧,每個棧又包含了若干個棧幀,每個棧幀對應着每個方法的每次調用,每個棧幀包含了三個部分:

    ​ 局部變量區(方法內基本類型變量、變量對象指針),操作數棧區(存放方法執行過程中產生的中間結果),運行環境區(動態連接、正確的方法返回相關信息、異常捕捉)

    • 本地方法棧

      本地方法棧的功能和jvm棧得非常類似,用於存儲本地方法的局部變量表,本地方法的操作數棧等信息。本地方法棧是在程序調用或jvm調用本地方法接口(native)時候啓用。本地方法都不是使用java語言編寫的,比如使用C語言編寫的本地方法,本地方法也不由jvm去運行,所有本地方法的運行不受jvm的管理。hotspot vm將本地方法棧和jvm棧合併了。

    • 線程共享。所有的對象實例以及數組都要在堆上分配。回收器主要管理的對象。從結構上來分,可以分爲新生代和老年代。而新生代又可以分爲Eden 空間、From Survivor 空間(s0)、To Survivor 空間(s1)。 所有新生成的對象首先都是放在新生代的。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來的對象,和從前一個Survivor複製過來的對象,而複製到老年代的只有從第一個Survivor區過來的對象。而且,Survivor區總有一個是空的。

      當java創建一個類的對象或者數組時,都在堆中爲新的對象分配內存,虛擬機中只有一個堆,程序中所有的線程都共享它。堆佔用的控件是最多的,堆的存取類型爲管道類型,先進先出。在程序運行中,可以動態分配堆內存的大小。

      -Xms設置堆的最小空間大小。-Xmx設置堆的最大空間大小。-XX:NewSize設置新生代最小空間大小。-XX:MaxNewSize設置新生代最小空間大小。

    • 程序計數器

      在jvm概念模型裏,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支,循環,跳轉 執行的是一個native方法,那麼程序計數器的值則爲空(undefined). 程序計數器是唯一一個在jvm規範中沒有規定任何oom的區域。

    • 方法區

      在jvm中,類型信息和類靜態變量都保存在方法區中,類型信息是由類加載器在類加載的過程中從類文件中提取出來的,需要注意一點的是,常量池也存放於方法區中。

      ​ 程序中所有的線程共享一個方法區,所以訪問方法區的信息必須確保線程是安全的。如果有兩個線程同時去加載一個類,那麼只能有一個線程被允許去加載這個類,另一個必須等待。

      ​ 方法區也是可以被垃圾回收,但是條件肺炎嚴苛,必須在該類沒有任何引用的情況下。

      ​ 類型信息包括:類型全名,類型的父類型全名,接口還是類,類型修飾符,父接口全名列表,類型的字段信息,類型的方法信息,所有的靜態變量,指向類加載器的引用,指向Class的引用,基本類型常量池

      1.8以後叫元空間。

  6. 簡述java中的強引用,弱引用,軟引用和虛引用

    從jdk1.2開始,對象的引用別劃分爲4中級別,從而使程序能夠更加靈活地控制對象的生命週期,這4中級別從高到底分別爲: 強引用,軟引用,弱引用和虛引用

    強引用:之前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。比如下面這段代碼中的object和str都是強引用。如果一個對象具有強引用,那就類似於必不可少的物品,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。

    比如: Object o = new Object();

    軟引用:軟引用是用來描述一些有用但並不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。對於軟引用關聯着的對象,只有在內存不足的時候JVM纔會回收該對象。因此,這一點可以很好地用來解決OOM的問題,並且這個特性很適合用來實現緩存:比如網頁緩存、圖片緩存等。
    比如: SoftReference sr = new SoftReference(obj);

    弱引用:弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。

    弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。所以被軟引用關聯的對象只有在內存不足時纔會被回收,而被弱引用關聯的對象在JVM進行垃圾回收時總會被回收。
    比如: WeakReference sr = new WeakReference(new String(“hello world”));

    虛引用: 虛引用和前面的軟引用、弱引用不同,它並不影響對象的生命週期。在java中用java.lang.ref.PhantomReference類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收的活動

    虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

    ​ ReferenceQueue queue = new ReferenceQueue();
    ​ PhantomReference pr = new PhantomReference(new String(“hello”), queue);
    ​ System.out.println(pr.get());

  7. 什麼情況下會產生死鎖

    死鎖發生的四個必要條件是: 1.資源互斥使用。 2.多個進程保持一定的資源,但又請求新的資源。 3.資源不可被剝奪。 4.多個進程循環等待。 一般死鎖的應對策略有: 1.死鎖預防。如進程需要的所有資源,在一開始就全部申請好得到之後再開始執行。 2.死鎖避免。如進程每次申請申請資源的時候,根據一定的算法,去看該請求可能不可能造成死鎖,如果可能,就不給它分配該資源。 3.死鎖處理。破壞四個必要條件的其中一個,比如kill掉一個進程。 4.死鎖忽略。不管死鎖,由用戶自行處理,比如重啓電腦。一般的系統其實都採取這種策略。

  8. 同步和異步的區別

同步和異步最大的區別就在於。一個需要等待,一個不需要等待。同步可以避免出現死鎖,讀髒數據的發生,一般共享某一資源的時候用,如果每個人都有修改權限,同時修改一個文件,有可能使一個人讀取另一個人已經刪除的內容,就會出錯,同步就會按順序來修改。

同步:從時間上強調處理事情的結果,強調結果意味着對結果的迫不及待,不管結果如何,反正你要立即給我一個結果響應,一直處於等待狀態。

異步:調用者發起一個調用後,立刻得到被調用者的迴應表示已接收到請求,但是被調用者並沒有返回結果,此時調用者在等待結果過程中浪費時間是極其難受的,這個時候我們可以處理其他的請求,被調用者通常依靠事件、回調等機制來通知調用者其返回結果。

  1. 介紹UDP協議和TCP協議的區別

    TCP(Transmission Control Protocol,傳輸控制協議)是面向連接的協議,也就是說,在收發數據前,必須和對方建立可靠的連接。 一個TCP連接必須要經過三次“對話”才能建立起來,其中的過程非常複雜, 只簡單的描述下這三次對話的簡單過程:

    1)主機A向主機B發出連接請求數據包:“我想給你發數據,可以嗎?”,這是第一次對話;

    2)主機B向主機A發送同意連接和要求同步 (同步就是兩臺主機一個在發送,一個在接收,協調工作)的數據包 :“可以,你什麼時候發?”,這是第二次對話;

    3)主機A再發出一個數據包確認主機B的要求同步:“我現在就發,你接着吧!”, 這是第三次對話。

    三次“對話”的目的是使數據包的發送和接收同步, 經過三次“對話”之後,主機A才向主機B正式發送數據。

​ UDP(User Data Protocol,用戶數據報協議)

1、UDP是一個非連接的協議,傳輸數據之前源端和終端不建立連接, 當它想傳送時就簡單地去抓取來自應用程序的數據,並儘可能快地把它扔到網絡上。 在發送端,UDP傳送數據的速度僅僅是受應用程序生成數據的速度、 計算機的能力和傳輸帶寬的限制; 在接收端,UDP把每個消息段放在隊列中,應用程序每次從隊列中讀一個消息段。

2、 由於傳輸數據不建立連接,因此也就不需要維護連接狀態,包括收發狀態等, 因此一臺服務機可同時向多個客戶機傳輸相同的消息。

3、UDP信息包的標題很短,只有8個字節,相對於TCP的20個字節信息包的額外開銷很小。

4、吞吐量不受擁擠控制算法的調節,只受應用軟件生成數據的速率、傳輸帶寬、 源端和終端主機性能的限制。

5、UDP使用盡最大努力交付,即不保證可靠交付, 因此主機不需要維持複雜的鏈接狀態表(這裏面有許多參數)。

6、UDP是面向報文的。發送方的UDP對應用程序交下來的報文, 在添加首部後就向下交付給IP層。既不拆分,也不合並,而是保留這些報文的邊界, 因此,應用程序需要選擇合適的報文大小。

總結:

​ 1、基於連接與無連接;

​ 2、對系統資源的要求(TCP較多,UDP少);

​ 3、UDP程序結構較簡單;

​ 4、流模式與數據報模式 ;

​ 5、TCP保證數據正確性,UDP可能丟包;

​ 6、TCP保證數據順序,UDP不保證。

  1. BIO, NIO 的區別

BIO (同步阻塞I/O模式)

數據的讀取寫入必須阻塞在一個線程內等待其完成。

這裏使用那個經典的燒開水例子,這裏假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,纔去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什麼都沒有做。

NIO(同步非阻塞)

同時支持阻塞與非阻塞模式,但這裏我們以其同步非阻塞I/O模式來說明,那麼什麼叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。

AIO (異步非阻塞I/O模型)

異步非阻塞與同步非阻塞的區別在哪裏?異步非阻塞無需一個線程去輪詢所有IO操作的狀態改變,在相應的狀態改變後,系統會通知對應的線程來處理。對應到燒開水中就是,爲每個水壺上面裝了一個開關,水燒開之後,水壺會自動通知我水燒開了。

  1. & 和 && 的區別

& 可以作爲邏輯運算符和位運算符使用,作爲邏輯運算符是,前後連接布爾值,或最終結果爲布爾值的表達式,該運算符不會短路,就是即使 & 前面的表達式結果已經爲false,& 後面的表達式依舊會執行。

& 作爲位運算符,一般兩個數字,計算的時候需要將兩個數組轉爲二進制,逐位進行運算,兩個都是1結果是1, 有一個是0結果爲0, 最後再將計算後的二進制結果轉成10進制。

&&:邏輯運算符, 短路, 和& 唯一的差別就是,如果前面的結果已經可以確定最後的結果,&&後面的表達式不再執行。 需注意 && 不能做爲位運算符連接數字

  1. short s1 = 1; s1 = s1 + 1;有什麼錯? short s1 = 1; s1 += 1;有什麼錯

這是一道關於類型轉換的經典面試題。 第一個寫法是錯誤的, 分析 等號右邊 s1+ 1 一個short類型和一個int類型相加,結果是int類型,把一個int類型賦值給等號左邊的short類型是會報錯誤的。 因爲short有可能裝不下。 第二種方式是正確的。 S1 += 1; 這個+= 自帶了一個強制類型轉換的功能。相當於 s1 = (short)(s1 +1);

  1. error和 exception的區別

Error 和 Exception都是 Throwable的子類.

Error(錯誤)是系統中的錯誤,程序員是不能改變的和處理的,是在程序編譯時出現的錯誤,只能通過修改程序才能修正。一般是指與虛擬機相關的問題,如系統崩潰,虛擬機錯誤,內存空間不足,方法調用棧溢等。對於這類錯誤的導致的應用程序中斷,僅靠程序本身無法恢復和和預防,遇到這樣的錯誤,建議讓程序終止。

Exception(異常)表示程序可以處理的異常,可以捕獲且可能恢復。遇到這類異常,應該儘可能處理異常,使程序恢復運行,而不應該隨意終止異常。

Exception又分爲兩類

CheckedException:(編譯時異常) 需要用try——catch顯示的捕獲,對於可恢復的異常使用CheckedException。

UnCheckedException(RuntimeException):(運行時異常)不需要捕獲,對於程序錯誤(不可恢復)的異常使用RuntimeException。

  1. 介紹常見的垃圾回收算法

  2. 根搜索算法(可達性分析): 從GCROOT開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢後,剩餘的節點則被認爲是沒有被引用到的節點,及無用的節點。目前java中可以作爲GCroot的對象有: 虛擬機棧中引用的對象(本地變量表),方法區中靜態屬性引用的對象,方法區中常量引用的對象,本地方法棧中引用的對象(native)

  3. 標記-清除算法:

    標記-清除算法採用從根集合進行掃描,對存活的對象進行標記,標記完畢後,在掃描整個空間中未標記的對象進行直接回收。標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活的對象比較多的情況下極爲高效,但是由於標記-清除算法直接回收不存活的對象,並沒有對存活的對象進行整理,因此會導致內存碎片。

  4. 複製算法:

    複製算法將內存劃分爲兩個區間,使用此算法時,所有的動態分配的對象都只能分配在其中一個區間,而另一個區間是閒置的。複製算法採用從根集合掃描,將存活對象複製到空閒區間,當掃描完畢活動區間後,會將活動區間一次性全部回收,此時原本的空閒區間變成了活動區間,下次gc的時候會重複剛纔的操作,以此循環。複製算法在存活對象較少的時候,極爲高效,但是帶來的成本是犧牲一半的內存空間用於對象的移動,所以複製算法使用的場景,必須是對象的存活率非常低纔行。

  5. 標記-整理算法:

    標記-整理算法採用和標記-清除算法一樣的方式進行對象的標記,清除,但是在回收不存活對象佔用的空間後,會見給所有的存活的對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法之上,又進行了對象的移動排序整理,因此成本更高,但卻解決了內存碎片的問題,

JVM 爲了優化內存得回收,是用來分代回收的方式,對於新生代的內存回收,主要採用複製算法,而對於老年代的回收,大多采用標記整理算法。

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