面試題總結-Java部分

LiuJian-Android發表於2019-03-12

1 集合

1.1 hashmap原理

HashMap是基於雜湊表實現的,每一個元素是一個key-value對,實現了Serializable、Cloneable介面,允許使用null值和null鍵。不保證對映的順序,內部通過單連結串列解決衝突問題,容量超過(容量*載入因子)時,會自動增長。(除了不同步和允許使用null之外,HashMap類與Hashtable大致相同)。HashMap不是執行緒安全的。

1.2 ConcurrentHashMap實現原理

首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段(Segment)資料的時候,其他段的資料也能被其他執行緒訪問。 有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。 Segment繼承了ReetrantLock,表示Segment是一個可重入鎖,因此ConcurrentHashMap通過可重入鎖對每個分段進行加鎖。

1.3 Hashmap hashtable區別

  • 繼承的父類不同
    Hashtable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但二者都實現了Map介面。
  • 執行緒安全性不同
    Hashtable 中的方法是Synchronize的,而HashMap中的方法在預設情況下是非Synchronize的。在多執行緒併發的環境下,可以直接使用Hashtable,不需要自己為它的方法實現同步,但使用HashMap時就必須要自己增加同步處理。
  • 是否提供contains方法
    HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因為contains方法容易讓人引起誤解。Hashtable則保留了contains,containsValue和containsKey三個方法,其中contains和containsValue功能相同。
  • key和value是否允許null值
    Hashtable中,key和value都不允許出現null值。HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。
  • 兩個遍歷方式的內部實現上不同
    Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式 。
  • hash值不同
    雜湊值的使用不同,HashTable直接使用物件的hashCode。而HashMap重新計算hash值。
  • 內部實現使用的陣列初始化和擴容方式不同
    HashTable在不指定容量的情況下的預設容量為11,而HashMap為16,Hashtable不要求底層陣列的容量一定要為2的整數次冪,而HashMap則要求一定為2的整數次冪。Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。

1.4 hashmap的resize

當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容之後,最消耗效能的點就出現了:原陣列中的資料必須重新計算其在新陣列中的位置,並放進去,這就是resize。
那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過陣列大小loadFactor時,就會進行陣列擴容,loadFactor的預設值為0.75,也就是說,預設情況下,陣列大小為16,那麼當hashmap中元素個數超過160.75=12的時候,就把陣列的大小擴充套件為216=32,即擴大一倍,然後重新計算每個元素在陣列中的位置,而這是一個非常消耗效能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的效能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設定為1024。 但是new HashMap(1024)還不是更合適的,因為0.751000 < 1000, 也就是說為了讓0.75 * size > 1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。

2 資料結構

2.1 連結串列和陣列記憶體中區別

  • 1.佔用的記憶體空間
    連結串列存放的記憶體空間可以是連續的,也可以是不連續的,陣列則是連續的一段記憶體空間。一般情況下存放相同多的資料陣列佔用較小的記憶體,而連結串列還需要存放其前驅和後繼的空間。
  • 2.長度的可變性
    連結串列的長度是按實際需要可以伸縮的,而陣列的長度是在定義時要給定的,如果存放的資料個數超過了陣列的初始大小,則會出現溢位現象。
  • 3.對資料的訪問
    連結串列方便資料的移動而訪問資料比較麻煩;陣列訪問資料很快捷而移動資料比較麻煩。 連結串列和陣列的差異決定了它們的不同使用場景,如果需要很多對資料的訪問,則適合使用陣列;如果需要對資料進行很多移位操作,則設和使用連結串列。

3 執行緒

3.1 synchronized使用

  • 修飾一個方法
  • 修飾一個程式碼塊
  • 修改一個靜態的方法
  • 修飾一個類

3.2 synchronized與Lock的區別

  • Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
  • synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
  • Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
  • 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
  • Lock可以提高多個執行緒進行讀操作的效率。
  • 在效能上來說,如果競爭資源不激烈,兩者的效能是差不多的,而當競爭資源非常激烈時(即有大量執行緒同時競爭),此時Lock的效能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇

3.3 volatile

volatile作為java中的關鍵詞之一,用以宣告變數的值可能隨時會別的執行緒修改,使用volatile修飾的變數會強制將修改的值立即寫入主存,主存中值的更新會使快取中的值失效(非volatile變數不具備這樣的特性,非volatile變數的值會被快取,執行緒A更新了這個值,執行緒B讀取這個變數的值時可能讀到的並不是是執行緒A更新後的值)。volatile會禁止指令重排。volatile具有可見性、有序性,不具備原子性。

3.4 執行緒等待

在Object.java中,定義了wait(), notify()和notifyAll()等介面。wait()的作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖。而notify()和notifyAll()的作用,則是喚醒當前物件上的等待執行緒;notify()是喚醒單個執行緒,而notifyAll()是喚醒所有的執行緒。

3.5 ThreadPoolExecutor執行順序

  • 當執行緒數小於核心執行緒數時,建立執行緒。
  • 當執行緒數大於等於核心執行緒數,且任務佇列未滿時,將任務放入任務佇列。
  • 當執行緒數大於等於核心執行緒數,且任務佇列已滿
    • 若執行緒數小於最大執行緒數,建立執行緒
    • 若執行緒數等於最大執行緒數,丟擲異常,拒絕任務

3.6 ThreadPoolExecutor引數預設值

  • corePoolSize=1
  • queueCapacity=Integer.MAX_VALUE
  • maxPoolSize=Integer.MAX_VALUE
  • keepAliveTime=60s
  • allowCoreThreadTimeout=false
  • rejectedExecutionHandler=AbortPolicy()

3.7 執行緒池型別:

  • newCachedThreadPool:

    • 底層:
      返回ThreadPoolExecutor例項,corePoolSize為0;maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為60L;unit為TimeUnit.SECONDS;workQueue為SynchronousQueue(同步佇列)
    • 通俗:
      當有新任務到來,則插入到SynchronousQueue中,由於SynchronousQueue是同步佇列,因此會在池中尋找可用執行緒來執行,若有可以執行緒則執行,若沒有可用執行緒則建立一個執行緒來執行該任務;若池中執行緒空閒時間超過指定大小,則該執行緒會被銷燬。
    • 適用:
      執行很多短期非同步的小程式或者負載較輕的伺服器
  • newFixedThreadPool:

    • 底層:
      返回ThreadPoolExecutor例項,接收引數為所設定執行緒數量nThread,corePoolSize為nThread,maximumPoolSize為nThread;keepAliveTime為0L(不限時);unit為:TimeUnit.MILLISECONDS;WorkQueue為:new LinkedBlockingQueue() 無解阻塞佇列
    • 通俗:
      建立可容納固定數量執行緒的池子,每隔執行緒的存活時間是無限的,當池子滿了就不在新增執行緒了;如果池中的所有執行緒均在繁忙狀態,對於新任務會進入阻塞佇列中(無界的阻塞佇列)
    • 適用:
      執行長期的任務,效能好很多
  • newSingleThreadExecutor:

    • 底層:
      FinalizableDelegatedExecutorService包裝的ThreadPoolExecutor例項,corePoolSize為1;maximumPoolSize為1;keepAliveTime為0L;unit為:TimeUnit.MILLISECONDS;workQueue為:new LinkedBlockingQueue() 無解阻塞佇列
    • 通俗:
      建立只有一個執行緒的執行緒池,且執行緒的存活時間是無限的;當該執行緒正繁忙時,對於新任務會進入阻塞佇列中(無界的阻塞佇列)
    • 適用:
      一個任務一個任務執行的場景
  • newScheduledThreadPool:

    • 底層:
      建立ScheduledThreadPoolExecutor例項,corePoolSize為傳遞來的引數,maximumPoolSize為Integer.MAX_VALUE;keepAliveTime為0;unit為:TimeUnit.NANOSECONDS;workQueue為:new DelayedWorkQueue() 一個按超時時間升序排序的佇列
    • 通俗:
      建立一個固定大小的執行緒池,執行緒池內執行緒存活時間無限制,執行緒池可以支援定時及週期性任務執行,如果所有執行緒均處於繁忙狀態,對於新任務會進入DelayedWorkQueue佇列中,這是一種按照超時時間排序的佇列結構
    • 適用:
      週期性執行任務的場景

4 Java虛擬機器

4.1 工作原理

從巨集觀上介紹一下Java虛擬機器的工作原理。首先Java原始檔經過前端編譯器(javac或ECJ)將.java檔案編譯為Java位元組碼檔案,然後JRE載入Java位元組碼檔案,載入系統分配給JVM的記憶體區,然後執行引擎解釋或編譯類檔案,再由即時編譯器將位元組碼轉化為機器碼。

面試題總結-Java部分

4.2 執行時資料區

面試題總結-Java部分

程式計數器:      
執行緒私有,用來指示當前執行緒所執行的位元組碼的行號,就是用來標記執行緒現在執行的程式碼的位置;
對Java方法,它儲存的是位元組碼指令的地址;對於Native方法,該計數器的值為空。
棧:      
執行緒私有,一個方法的執行和退出就是用一個棧幀的入棧和出棧表示的,通常我們不允許你使用遞迴就是因為,方法就是一個棧,太多的方法只執行而沒有退出就會導致棧溢位,不過可以通過尾遞迴優化。棧又分為虛擬機器棧和本地方法棧,一個對應Java方法,一個對應Native方法。
堆:      
用來給物件分配記憶體的,幾乎所有的物件例項(包括陣列)都在上面分配。它是垃圾收集器的主要管理區域,因此也叫GC堆。它實際上是一塊記憶體區域,由於一些收集演算法的原因,又將其細化分為新生代和老年代等。
方法區:      
方法區由多執行緒共享,用來儲存類資訊、常量、靜態變數、即使編譯後的程式碼等資料。執行時常量池是方法區的一部分,它用於存放編譯器生成的各種字面量和符號引用,比如字串常量等。
複製程式碼

4.3 是否回收判斷

  • 引用記數法
    給物件新增一個引用計數器,被引用時計數器加1,引用失效時減1。 這種方法不常用,因為它難以解決兩個變數相互引用的問題。
  • 可達性分析
    通過一系列GC Roots的物件作為起始點,從節點向下搜尋, 當一個物件沒有任何一條可到GC Roots的引用鏈,則該物件可回收。

4.4 垃圾回收演算法

  • 標記-清除演算法,這種演算法直接在記憶體中把需要回收的物件“摳”出來。 好好的記憶體被它搞成了馬蜂窩,所以效率不高,清除之後會產生內容碎片,造成記憶體不連續,當分配較大記憶體物件時可能會因記憶體不足而觸發垃圾收集動作。

  • 複製演算法:將記憶體分成兩塊,一次只在一塊記憶體中進行分配,垃圾回收一次之後, 就將該記憶體中的未被回收的物件移動到另一塊記憶體中,然後將該記憶體一次清理掉。 比如將記憶體分成A和B,先在A中分配,當垃圾回收的時候把A中需要回收的記憶體清理掉,然後把不需要清理的所有物件複製到B裡面。 複製演算法常被用來回收新生代,而且分配空間也不是1:1,而是較大的Eden空間和較小的Survivor空間。在HotSpot中,其比例是8:1。

    • 標記整理演算法:類似於標記-清除演算法,只是回收了之後,它要對記憶體空間進行整理,以使得剩餘的物件佔用連續的儲存空間。

    • 分代收集演算法:上面是三種基本的垃圾回收演算法,但實際上,我們通常根據物件存活週期的不同將記憶體劃分成幾塊,然後根據其特點採用不同的回收演算法。

4.5 記憶體分配與回收策略

物件記憶體分配,往大方向上講,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配,少數情況下可能直接分配在老年代。

  • 物件優先在Eden分配:
    大多數情況下,物件在新生代Eden區非中分配,當Eden區沒有足夠空間時,虛擬機器發起一次Minor GC。

  • 大物件直接進入老年代:
    大物件指需要大量連續記憶體空間的Java物件,比如很大的陣列或者字串。經常出現大物件會導致記憶體還有不少空間時就提前觸發垃圾收集來獲取足夠的連續空間來安置它們。 虛擬機器提供了-XX:PretenureSizeThreshold引數,當物件的大小大於它的值的時候將直接分配在老年代。這樣做是為了避免在Eden區和Survivor區之間發生大量的記憶體複製。

  • 長期存活物件將進入老年代:
    若物件出生在Eden區並經過一次Minor GC後仍然存活,並且能被Survivor容納,將被移動到Survivor空間中,並且物件年齡將加1。 在Survivor中,每熬過一次Minor GC,則年齡加1,當年齡達到一定程度時(預設15歲),就會被晉升到老年代。該年齡的閾值通過引數-XX:MaxTenuringThreshold設定。

5 HTTP

5.1 Http連線複用原理

當一個http請求完成後,tcp連線不會立即釋放,如果有新的http請求,並且host和上次一樣,那麼可以複用tcp連線,省去重新連線的過程。

5.2 Http中keep alive與TCP區別

  • HTTP Keep-Alive
    在HTTP 1.0以前,每個http請求都要求開啟一個TCP socket連線,並且使用一次之後就斷開這個TCP連線,這會導致頻繁地建立和銷燬TCP。HTTP 1.1通過使用keep-alive可以改善這種狀態,即在一次TCP連線中可以持續傳送多份資料而不會斷開連線。
  • TCP KEEPALIVE
    這是TCP協議棧為了檢測連線狀況的保活機制,當TCP空閒一定時間後會傳送心跳包給對方,如果對端回覆ACK後,就認為對端是存活的,重置定時器;如果對端回覆RST應答(對端崩潰或者其他原因,導致的復位),那就關閉該連線;如果對端無任何迴應,那就會出發超時重傳,直到達到重傳的次數,如果對端依然沒有回覆,那麼就關閉該連線。

HTTP位於網路協議棧的應用層,而TCP位於網路協議棧的傳輸層,兩者的KEEP-ALIVE雖然名稱相同,但是作用不同。HTTP是為了重用TCP,避免每次請求,都重複建立TCP;而TCP的KEEP-ALIVE是一種保活機制,檢測對端是否依然存活。

6 git

  • git cherry-pick
    將某一段commit貼上到另一個分支上
  • git revert
    通過反做建立一個新的版本,這個版本的內容與我們要回退到的目標版本一樣,但是HEAD指標是指向這個新生成的版本,而不是目標版本。 如果我們想恢復之前的某一版本(該版本不是merge型別),但是又想保留該目標版本後面的版本,記錄下這整個版本變動流程,就可以用這種方法。
  • git rebase
    合併多個commit為一個完整commit
  • git reset
    修改HEAD的位置,即將HEAD指向的位置改變為之前存在的某個版本。如果想恢復到之前某個提交的版本,且那個版本之後提交的版本我們都不要了,就可以用這種方法。

7 基本資料型別相關

7.1 replace() replaceAll() replaceFirst()

  • replace(CharSequence target, CharSequence replacement) 用replacement替換掉target。這兩個引數都是字串
  • replaceAll(String regex, String replacement) 用replacement所有regex匹配的字串。很明顯regex引數是個正則匹配式,replacement是個字串。
  • replaceFirst(String regex, String replacement),基本和replaceAll相同,區別是隻替換第一個匹配項

7.2 java中int與Integer用==比較詳解

①、無論如何,Integer與new Integer不會相等。不會經歷拆箱過程,因為它們存放記憶體的位置不一樣。(要看具體位置,可以看看這篇文章:點選開啟連結)

②、兩個都是非new出來的Integer,如果數在-128到127之間,則是true,否則為false。

③、兩個都是new出來的,則為false。

④、int和integer(new或非new)比較,都為true,因為會把Integer自動拆箱為int,其實就是相當於兩個int型別比較。

8 其他

8.1 xml解析都有哪些 區別

  • SAX
    sax是一個用於處理xml事件驅動的“推”模型;
    優點:解析速度快,佔用記憶體少,它需要哪些資料再載入和解析哪些內容。
    缺點:它不會記錄標籤的關係,而是需要應用程式自己處理,這樣就會增加程式的負擔。
  • DOM
    dom是一種文件物件模型;
    優點:dom可以以一種獨立於平臺和語言的方式訪問和修改一個文件的內容和結構,dom技術使得使用者頁面可以動態的變化,如動態顯示隱藏一個元素,改變它的屬性,增加一個元素等,dom可以使頁面的互動性大大增強。
    缺點:dom解析xml檔案時會將xml檔案的所有內容以文件樹方式存放在記憶體中。
  • PULL
    pull和sax很相似,區別在於:pull讀取xml檔案後觸發相應的事件呼叫方法返回的是數字,且pull可以在程式中控制,想解析到哪裡就可以停止解析。 (SAX解析器的工作方式是自動將事件推入事件處理器進行處理,因此你不能控制事件的處理主動結束;而Pull解析器的工作方式為允許你的應用程式程式碼主動從解析器中獲取事件,正因為是主動獲取事件,因此可以在滿足了需要的條件後不再獲取事件,結束解析。pull是一個while迴圈,隨時可以跳出,而sax不是,sax是隻要解析了,就必須解析完成。)

相關文章