Android 高頻面試之必考Java基礎

若是你們去面Android客戶端崗位,那麼必問Java基礎和Kotlin基礎,因此,我打算花3,4篇文章的樣子來給你們總結下Android面試中會問到的一些Java基礎知識。java

1,面向對象和麪向過程的區別

面向過程:面向過程性能比面向對象高。由於對象調用須要實例化,開銷比較大,較消耗資源,因此當性能是最重要的考量因素的時候,好比單片機、嵌入式開發、Linux/Unix 等,通常採用面向過程開發。可是,面向過程沒有面向對象易維護、易複用、易擴展。
面向對象:面向對象易維護、易複用、易擴展。由於面向對象有封裝、繼承、多態性的特性,因此可設計出低耦合的系統,使得系統更加靈活、更加易於維護。面試

那爲何,面向過程性能比面向對象高呢?
面向過程也須要分配內存,計算內存偏移量,Java 性能差的主要緣由並非由於它是面嚮對象語言,而是由於 Java 是半編譯語言,最終的執行代碼並非能夠直接被 CPU 執行的二進制機器碼。而面向過程語言大多都是直接編譯成機器碼在電腦上執行,而且其它一些面向過程的腳本語言性能也並不必定比 Java 好。算法

2,面向對象的特徵有哪些

  • 封裝:一般認爲封裝是把數據和操做數據的方法綁定起來,對數據的訪問只能經過已定義的接口。
  • 繼承:繼承是從已有類獲得繼承信息建立新類的過程。提供繼承信息的類被稱爲父類(超類、基類);獲得繼承信息的類被稱爲子類(派生類)。
  • 抽象:抽象是將一類對象的共同特徵總結出來構造類的過程,包括數據抽象和行爲抽象兩方面。抽象只關注對象有哪些屬性和行爲,並不關注這些行爲的細節是什麼。
  • 多態性:多態性是指容許不一樣子類型的對象對同一消息做出不一樣的響應。即同一消息能夠根據發送對象的不一樣而採起不一樣的行爲方式。

3,解釋下Java的編譯與解釋並存的現象

當 .class 字節碼文件經過 JVM 轉爲機器能夠執行的二進制機器碼時,JVM 類加載器首先加載字節碼文件,而後經過解釋器逐行進行解釋執行,這種方式的執行速度相對比較慢。並且有些方法和代碼塊是反覆被調用的(也就是所謂的熱點代碼),因此後面引進了 JIT 編譯器,而 JIT 屬於運行時編譯。當 JIT 編譯器完成一次編譯後,會將字節碼對應的機器碼保存下來,下次能夠直接調用。這也解釋了咱們爲何常常會說 Java 是編譯與解釋共存的語言。數據庫

4,簡單介紹下JVM的內存模型

Java虛擬機所管理的內存包含程序計數器、Java虛擬機棧、本地方法棧、Java堆和方法區5個部分,模型圖以下圖所示。
在這裏插入圖片描述編程

4.1 程序計數器

因爲Java虛擬機的多線程是經過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器只會執行一條線程中的指令。爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各個線程之間的計數器互不影響,獨立存儲,這類內存區域爲【線程私有】的內存。數組

程序計數器具備以下的特色:緩存

  • 是一塊較小的內存空間。
  • 線程私有,每條線程都有本身的程序計數器。
  • 生命週期方面,隨着線程的建立而建立,隨着線程的結束而銷燬。
  • 是惟一一個不會出現OutOfMemoryError的內存區域。

4.2 Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命週期與線程的生命週期同步,虛擬機棧描述的是Java方法執行的線程內存模型。每一個方法被執行的時候,Java虛擬機都會同步建立一個內存塊,用於存儲在該方法運行過程當中的信息,每一個方法被調用的過程都對應着一個棧幀在虛擬機中從入棧到出棧的過程。
在這裏插入圖片描述安全

Java虛擬機棧有以下的特色:服務器

  • 局部變量表所需的內存空間在編譯期間完成分配,進入一個方法時,這個方法須要在棧幀中分配的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。
  • Java虛擬機棧會出現兩種異常:StackOverflowError 和 OutOfMemoryError。

4.3 本地方法棧

本地方法棧與虛擬機所發揮的做用很類似,區別在於虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則是爲虛擬機使用到的本地方法服務。數據結構

4.4 Java堆

Java堆是虛擬機所管理的內存中最大的一塊,Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。
此內存區域的惟一目的就是存放對象實例,java中「幾乎」全部的對象實例都在這裏分配內存。這裏使用「幾乎」是由於java語言的發展,及時編譯的技術發展,逃逸分析技術的日漸強大,棧上分配、標量替換等優化手段,使java對象實例都分配在堆上變得不那麼絕對。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法(G1以後開始變得不同,引入了region,可是依舊採用了分代思想),Java堆中還能夠細分爲:新生代和老年代。再細緻一點的有Eden空間、From Survivor空間、ToSurvivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,簡寫TLAB)。

OOM異常
Java堆的大小既能夠固定也能夠擴展,可是主流的虛擬機,堆的大小都是支持擴展的。若是須要線程請求分配內存,但堆已滿且內存已沒法再擴展時,就拋出 OutOfMemoryError 異常。好比:

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOMTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        List<Integer[]> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Integer[] ints = new Integer[2 * _1MB];
            list.add(ints);
        }
    }
}

4.5 方法區

方法區和Java堆同樣,是各個線程共享的內存區域,他用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

在 HotSpot JVM 中,永久代(永久代實現方法區)中用於存放類和方法的元數據以及常量池,好比Class和Method。每當一個類初次被加載的時候,它的元數據都會放到永久代中。永久代是有大小限制的,所以若是加載的類太多,頗有可能致使永久代內存溢出,爲此咱們不得不對虛擬機作調優。

後來HotSpot放棄永久代(PermGen),jdk1.7版本中,HotSpot已經把本來放在永久代的字符串常量池、靜態變量等移出,到了jdk1.8,徹底廢棄了永久代,方法區移至元空間(Metaspace)。好比類元信息、字段、靜態屬性、方法、常量等都移動到元空間區。元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。

經常使用的JVM調參以下表:

參數 做用描述
-XX:MetaspaceSize 分配給Metaspace(以字節計)的初始大小。若是不設置的話,默認是20.79M,這個初始大小是觸發首次 Metaspace Full GC 的閾值,例如 -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize 分配給Metaspace 的最大值,超過此值就會觸發Full GC,此值默認沒有限制,但應取決於系統內存的大小。JVM會動態地改變此值。可是線上環境建議設置,例如-XX:MaxMetaspaceSize=256M
-XX:MinMetaspaceFreeRatio 最小空閒比,當 Metaspace 發生 GC 後,會計算 Metaspace 的空閒比,若是空閒比(空閒空間/當前 Metaspace 大小)小於此值,就會觸發 Metaspace 擴容。默認值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio 最大空閒比,當 Metaspace 發生 GC 後,會計算 Metaspace 的空閒比,若是空閒比(空閒空間/當前 Metaspace 大小)大於此值,就會觸發 Metaspace 釋放空間。默認值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70

運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表,用於存放編譯期間生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。
方法區中存放:類信息、常量、靜態變量、即時編譯器編譯後的代碼。常量就存放在運行時常量池中。
當類被 Java 虛擬機加載後, .class文件中的常量就存放在方法區的運行時常量池中。並且在運行期間,能夠向常量池中添加新的常量。如String類的intern()方法就能在運行期間向常量池中添加字符串常量。

4.6 直接內存

直接內存並非虛擬機運行時數據區的組成部分,在 NIO 中引入了一種基於通道和緩衝的 IO 方式。它能夠經過調用本地方法直接分配Java虛擬機以外的內存,而後經過一個存儲在堆中的DirectByteBuffer對象直接操做該內存,而無須先將外部內存中的數據複製到堆中再進行操做,從而提升了數據操做的效率。

因爲直接內存並不是Java虛擬機的組成部分,所以直接內存的大小不受 Java 虛擬機控制,但既然是內存,若是內存不足時仍是會拋出OutOfMemoryError異常。

下面是直接內存與堆內存的一些異同點:

  • 直接內存申請空間耗費更高的性能;
  • 直接內存讀取 IO 的性能要優於普通的堆內存。
  • 直接內存做用鏈: 本地 IO -> 直接內存 -> 本地 IO
  • 堆內存做用鏈:本地 IO -> 直接內存 -> 非直接內存 -> 直接內存 -> 本地 IO

服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制,從而致使動態擴展時出現OutOfMemoryError異常。

5,簡單介紹下Java的類加載器

Java的類加載器能夠分爲BootstrapClassLoader、ExtClassLoader和AppClassLoader,它們的做用以下。

  • BootstrapClassLoader:Bootstrap 類加載器負責加載 rt.jar 中的 JDK 類文件,它是全部類加載器的父加載器。Bootstrap 類加載器沒有任何父類加載器,若是調用String.class.getClassLoader(),會返回 null,任何基於此的代碼會拋出 NUllPointerException 異常,所以Bootstrap 加載器又被稱爲初始類加載器。
  • ExtClassLoader:Extension 將加載類的請求先委託給它的父加載器,也就是Bootstrap,若是沒有成功加載的話,再從 jre/lib/ext 目錄下或者 java.ext.dirs 系統屬性定義的目錄下加載類。Extension 加載器由 sun.misc.Launcher$ExtClassLoader 實現。
  • AppClassLoader:Java默認的加載器就是 System 類加載器,又叫做 Application 類加載器。它負責從 classpath 環境變量中加載某些應用相關的類,classpath 環境變量一般由 -classpath 或 -cp 命令行選項來定義,或者是 JAR 中的 Manifest 的 classpath 屬性,Application 類加載器是 Extension 類加載器的子加載器。

類加載會涉及一些加載機制。

  • 委託機制:加載任務委託交給父類加載器,若是不行就向下傳遞委託任務,由其子類加載器加載,保證Java核心庫的安全性。
  • 可見性機制:子類加載器能夠看到父類加載器加載的類,而反之則不行。
  • 單一性原則:父加載器加載過的類不能被子加載器加載第二次。

6,談一下Java的垃圾回收,以及經常使用的垃圾回收算法。

Java的內存管理主要涉及三個部分:堆 ( Java代碼可及的 Java堆 和 JVM自身使用的方法區)、棧 ( 服務Java方法的虛擬機棧 和 服務Native方法的本地方法棧 ) 和 保證程序在多線程環境下可以連續執行的程序計數器。
Java堆是進行垃圾回收的主要區域,故其也被稱爲GC堆;而方法區的垃圾回收主要針對的是新生代和中生代。總的來講,堆 (包括Java堆 和 方法區)是 垃圾回收的主要對象,特別是Java堆。

6.1 垃圾回收算法

6.1.1 對象存活判斷

引用計數

每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收。此方法雖然簡單,但沒法解決對象相互循環引用的問題。

可達性分析

從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。在Java中,GC Roots包括:

  • 虛擬機棧中引用的對象。
  • 方法區中類靜態屬性實體引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中 JNI 引用的對象。

    6.2 垃圾收集算法

    標記清除法

如它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其缺點進行改進而獲得的。
標記複雜算法有兩個主要的缺點:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

複製算法

複製的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
它的優勢是每次只須要對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。而缺點也是顯而易見的,內存縮小爲原來的一半,持續複製長生存期的對象則致使效率下降。

標記整理法

複製收集算法在對象存活率較高時就要執行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

分代收集算法
分代收集算法,就是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收。

7,成員變量和局部變量的區別

  • 從語法形式上看:成員變量是屬於類的,而局部變量是在方法中定義的變量或是方法的參數;成員變量能夠被 public、private、static 等修飾符所修飾,而局部變量不能被這些修飾符所修飾;可是它們均可以被 final 所修飾。
  • 從變量在內存中的存儲方式來看:若是成員變量被 static 所修飾,那麼這個成員變量屬於類,若是沒有被 static 修飾,則該成員變量屬於對象實例。對象存在於堆內存,局部變量存在於棧內存(具體是Java虛擬機棧)。
  • 從變量在內存中的生存時間來看:成員變量是對象的一部分,它隨着對象的建立而存在,而局部變量隨着方法的調用結束而自動消失。
  • 成員變量若是沒有賦初始值,則會自動以類型的默認值而賦值(例外:被 final 修飾的成員變量必須在初始化時賦值),局部變量則不會自動賦值。

8,Java 中的方法重寫(Overriding)和方法重載(Overload)的含義

方法重寫
在Java程序中,類的繼承關係能夠產生一個子類,子類繼承父類,它具有了父類全部的特徵,繼承了父類全部的方法和變量。子類能夠定義新的特徵,當子類須要修改父類的一些方法進行擴展,增大功能,程序設計者經常把這樣的一種操做方法稱爲重寫,也叫稱爲覆寫或覆蓋。

方法重寫有以下一些特色:

  • 方法名,參數列表必須相同,返回類型能夠相同也能夠是原類型的子類型
  • 重寫方法不能比原方法訪問性差(即訪問權限不容許縮小)。
  • 重寫方法不能比原方法拋出更多的異常。
  • 重寫發生在子類和父類之間。
  • 重寫實現運行時的多態性。

方法重載
方法重載是讓類以統一的方式處理不一樣類型數據的一種手段。調用方法時經過傳遞給它們的不一樣個數和類型的參數來決定具體使用哪一個方法,這就是多態性。所謂方法重載是指在一個類中,多個方法的方法名相同,可是參數列表不一樣。參數列表不一樣指的是參數個數、參數類型或者參數的順序不一樣。

  • 方法名必須相同,參數列表必須不一樣(個數不一樣、或類型不一樣、參數類型排列順序不一樣等)。
  • 方法的返回類型能夠相同也能夠不相同。
  • 重載發生在同一類中。
  • 重載實現編譯時的多態性。

9,簡單介紹下傳遞和引用傳遞

按值傳遞:值傳遞是指在調用函數時將實際參數複製一份傳遞到函數中,這樣在函數中若是對參數進行修改,將不會影響到實際參數。簡單來講就是直接複製了一份數據過去,由於是直接複製,因此這種方式在傳遞時若是數據量很是大的話,運行效率天然就變低了,因此Java在傳遞數據量很小的數據是值傳遞,好比Java中的各類基本類型:int、float、double、boolean等類型。

引用傳遞:引用傳遞其實就彌補了上面說的不足,若是每次傳參數的時候都複製一份的話,若是這個參數佔用的內存空間太大的話,運行效率會很底下,因此引用傳遞就是直接把內存地址傳過去,也就是說引用傳遞時,操做的其實都是源數據,這樣的話修改有時候會衝突,記得用邏輯彌補下就行了,具體的數據類型就比較多了,好比Object,二維數組,List,Map等除了基本類型的參數都是引用傳遞。

10,爲何重寫 equals 時必須重寫 hashCode 方法

下面是使用hashCode()與equals()的相關規定:

  • 若是兩個對象相等(即用 equals 比較返回 true),則 hashcode 必定也是相同的;
  • 兩個對象有相同的 hashcode 值,它們也不必定是相等的(不一樣的對象也可能產生相同的 hashcode,機率性問題);
  • equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋。

爲何必需要重寫 hashcode 方法?其實就是爲了保證同一個對象,保證在 equals 相同的狀況下 hashcode 值一定相同,若是重寫了 equals 而未重寫 hashcode 方法,可能就會出現兩個沒有關係的對象 equals 相同的(由於 equals 都是根據對象的特徵進行重寫的),但 hashcode 確實不相同的。

11,接口和抽象類的區別和相同點是什麼

相同點

  • 接口是絕對抽象的,不能夠被實例化,抽象類也不能夠被實例化。
  • 類能夠不實現抽象類和接口聲明的全部方法,固然,在這種狀況下,類也必須得聲明成是抽象的。

異同點:

  • 從設計層面來講,抽象是對類的抽象,是一種模板設計,接口是行爲的抽象,是一種行爲的規範。
  • 定義接口的關鍵字是 interface ,抽象類的關鍵字是 abstract class
  • 接口中全部的方法隱含的都是抽象的。而抽象類則能夠同時包含抽象和非抽象的方法。
  • 類能夠實現不少個接口,可是隻能繼承一個抽象類,接口能夠繼承多個接口
  • Java 接口中聲明的變量默認都是 public static final 的。抽象類能夠包含非 final 的變量。
  • 在JDK1.8以前,接口中不能有靜態方法,抽象類中能夠有普通方法和靜態方法;在 JDK1.8後,接口中能夠有默認方法和靜態方法,而且有方法體。
  • 抽象類能夠有構造方法,可是不能直接被 new 關鍵字實例化。
  • 在 JDK1.8 前,抽象類的抽象方法默認訪問權限爲 protected,1.8默認訪問權限爲 default,共有 default,protected 、 public 三種修飾符,非抽象方法可使用四種修飾符;在 JDK1.8 前,接口方法默認爲 public,1.8時默認爲 public,此時可使用 public 和 default,1.9時接口方法還支持 private。

12,簡述下HashMap

HashMap底層採用了數組+鏈表的數據結構,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的。

若是定位到的數組位置不含鏈表,那麼執行查找、添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

HashMap有4個構造器,其餘構造器若是用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值initialCapacity默認爲16,loadFactory默認爲0.75。

public HashMap(int initialCapacity, float loadFactor) {
     //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     
        init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
    }

加載因子存在的緣由,仍是由於減緩哈希衝突,若是初始桶爲16,等到滿16個元素才擴容,某些桶裏可能就有不止一個元素了。因此加載因子默認爲0.75,也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。

Put過程

  • 判斷當前數組是否要初始化。
  • 若是key爲空,則put一個空值進去。
  • 根據key計算出hashcode。
  • 根據hsahcode定位出在桶內的位置。
  • 若是桶是鏈表,則須要遍歷判斷hashcode,若是key和原來的key是否相等,相等則進行覆蓋,返回原來的值。
  • 若是桶是空的,說明當前位置沒有數據存入,新增一個 Entry 對象寫入當前位置.當調用 addEntry 寫入 Entry 時須要判斷是否須要擴容。若是須要就進行兩倍擴充,並將當前的 key 從新 hash 並定位。而在 createEntry中會將當前位置的桶傳入到新建的桶中,若是當前桶有值就會在位置造成鏈表。

Get過程

  • 根據key計算出hashcode,並定位到桶內的位置。
  • 判斷是否是鏈表,若是是則須要根據遍歷直到 key 及 hashcode 相等時候就返回值,若是不是就根據 key、key 的 hashcode 是否相等來返回值。
  • 若是啥也沒取到就返回null。

JDK 1.8的HashMap底層採用的是鏈表+紅黑樹,增長一個閾值進行判斷是否將鏈表轉紅黑樹,HashEntry 修改成 Node,目的是解決hash衝突形成的鏈表愈來愈長、查詢慢的問題。

Get過程

  • 判斷當前桶是否是空,空就須要初始化;
  • 根據key,計算出hashcode,根據hashcode,定位到具體的桶中,並判斷當前桶是否是爲空,爲空代表沒有hsah衝突建立一個新桶便可;
  • 若是有hash衝突,那麼就要比較當前桶中的 key、key 的 hashcode 與寫入的 key 是否相等,相等就賦值給 e,在第 8 步的時候會統一進行賦值及返回;
  • 若是當前位置是紅黑樹,就按照紅黑樹的方式寫入數據;
  • 若是當前位置是鏈表,則須要把key,value封裝一個新的節點,添加到當前的桶後面(尾插法),造成鏈表;
  • 接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹;
  • 若是在遍歷過程當中找到 key 相同時直接退出遍歷;
  • 若是 e != null 就至關於存在相同的 key,那就須要將值覆蓋;
  • 最後判斷是否須要進行擴容;

Get過程

  • 首先將 key hash 以後取得所定位的桶。
  • 若是桶爲空則直接返回 null 。
  • 不然判斷桶的第一個位置(有多是鏈表、紅黑樹)的 key 是否爲查詢的 key,是就直接返回 value。
  • 若是第一個不匹配,則判斷它的下一個是紅黑樹仍是鏈表。
  • 紅黑樹就按照樹的查找方式返回值。
  • 否則就按照鏈表的方式遍歷匹配返回值。

13, CurrentHashMap

JDK8中ConcurrentHashMap參考了JDK8 HashMap的實現,採用了數組+鏈表+紅黑樹的實現方式來設計,內部大量採用CAS操做,那什麼是CAS。

CAS是compare and swap的縮寫,中文稱爲【比較交換】。CAS是一種基於鎖的操做,並且是樂觀鎖。在Java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,性能較悲觀鎖有很大的提升。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和A的值是同樣的,那麼就將內存裏面的值更新成B。CAS是經過無限循環來獲取數據的,若是在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程須要自旋,到下次循環纔有可能機會執行。

14,介紹下什麼是樂觀鎖、悲觀鎖

Java 按照鎖的實現分爲樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖並非一種真實存在的鎖,而是一種設計思想。

悲觀鎖
悲觀鎖是一種悲觀思想,它總認爲最壞的狀況可能會出現,它認爲數據極可能會被其餘人所修改,因此悲觀鎖在持有數據的時候總會把資源 或者 數據 鎖住,這樣其餘線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放爲止。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。悲觀鎖的實現每每依靠數據庫自己的鎖功能實現。

Java 中的 Synchronized 和 ReentrantLock 等獨佔鎖(排他鎖)也是一種悲觀鎖思想的實現,由於 Synchronzied 和 ReetrantLock 無論是否持有資源,它都會嘗試去加鎖,生怕本身心愛的寶貝被別人拿走。

樂觀鎖
樂觀鎖的思想與悲觀鎖的思想相反,它總認爲資源和數據不會被別人所修改,因此讀取不會上鎖,可是樂觀鎖在進行寫入操做的時候會判斷當前數據是否被修改過(具體如何判斷咱們下面再說)。樂觀鎖的實現方案通常來講有兩種: 版本號機制 和 CAS實現 。樂觀鎖多適用於多度的應用類型,這樣能夠提升吞吐量。

在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

15,談談對Java線程的理解

線程是進程中可獨立執行的最小單位,也是 CPU 資源(時間片)分配的基本單位,同一個進程中的線程能夠共享進程中的資源,如內存空間和文件句柄。線程有一些基本的屬性,如id、name、以及priority。
id:線程 id 用於標識不一樣的線程,編號可能被後續建立的線程使用,編號是隻讀屬性,不能修改。
name:線程的名稱,默認值是 Thread-(id)
daemon:分爲守護線程和用戶線程,咱們能夠經過 setDaemon(true) 把線程設置爲守護線程。守護線程一般用於執行不重要的任務,好比監控其餘線程的運行狀況,GC 線程就是一個守護線程。setDaemon() 要在線程啓動前設置,不然 JVM 會拋出非法線程狀態異常,可被繼承。
priority:線程調度器會根據這個值來決定優先運行哪一個線程(不保證),優先級的取值範圍爲 1~10,默認值是 5,可被繼承。Thread 中定義了下面三個優先級常量:

  • 最低優先級:MIN_PRIORITY = 1
  • 默認優先級:NORM_PRIORITY = 5
  • 最高優先級:MAX_PRIORITY = 10

一個線程被建立後,會經歷從建立到消亡的狀態,下圖是線程狀態的變動過程。
在這裏插入圖片描述
下表是展現了線程的生命週期狀態變化:

狀態 說明
New 新建立了一個線程對象,但尚未調用start()方法。
Runnable Ready 狀態 線程對象建立後,其餘線程(好比 main 線程)調用了該對象的 start() 方法。該狀態的線程位於可運行線程池中,等待被線程調度選中 獲取 cpu 的使用權。Running 緒狀態的線程在得到 CPU 時間片後變爲運行中狀態(running)。
Blocked 線程由於某種緣由放棄了cpu 使用權(等待鎖),暫時中止運行。
Waiting 線程進入等待狀態由於如下幾個方法: Object#wait()、 Thread#join()、 LockSupport#park()
Terminated 該線程已經執行完畢。

16, Synchronized、volatile、Lock併發

線程同步和併發一般會問到Synchronized、volatile、Lock的做用。其中,Lock是一個類,而其他兩個則是Java關鍵字。

Synchronized

Synchronized是Java的關鍵字,也是Java的內置特性,在JVM層面實現了對臨界資源的同步互斥訪問,經過對對象的頭文件來操做,從而達到加鎖和釋放鎖的目的。使用Synchronized修飾的代碼或方法,一般有以下特性:

  • Synchronized在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生。
  • 不能響應中斷。
  • 同一時刻無論是讀仍是寫都只能有一個線程對共享資源操做,其餘線程只能等待,性能不高。

正是由於上面的特性,因此Synchronized的缺點也是顯而易見的:即若是一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其餘線程便只能一直等待,所以效率很低。

volatile
保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。而且volatile是禁止進行指令重排序。

所謂指令重排序,指的是處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。

volatile爲了保證原子性,必須具有如下條件:

  • 對變量的寫操做不依賴於當前值
  • 該變量沒有包含在具備其餘變量的不變式中

17,鎖

按照做用的不一樣,Java的鎖能夠分爲以下:
在這裏插入圖片描述

悲觀鎖、樂觀鎖

悲觀鎖認爲本身在使用數據的時候必定有別的線程來修改數據,所以在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java 中,synchronized 關鍵字和 Lock 的實現類都是悲觀鎖。悲觀鎖適合寫操做多的場景,先加鎖能夠保證寫操做時數據正確。

而樂觀鎖認爲本身在使用數據時不會有別的線程修改數據,因此不會添加鎖,只是在更新數據的時候去判斷以前有沒有別的線程更新了這個數據。若是這個數據沒有被更新,當前線程將本身修改的數據成功寫入。若是數據已經被其餘線程更新,則根據不一樣的實現方式執行不一樣的操做(例如報錯或者自動重試)。樂觀鎖在 Java 中是經過使用無鎖編程來實現,最常採用的是 CAS 算法,Java 原子類中的遞增操做就經過 CAS 自旋實現。樂觀鎖適合讀操做多的場景,不加鎖的特色可以使其讀操做的性能大幅提高。

這裏說到了CAS算法,那麼什麼是CAS算法呢?

CAS算法

一個線程失敗或掛起並不會致使其餘線程也失敗或掛起,那麼這種算法就被稱爲非阻塞算法。而CAS就是一種非阻塞算法實現,也是一種樂觀鎖技術,它能在不使用鎖的狀況下實現多線程安全,所以是一種無鎖算法。

CAS算法的定義:CAS的主要做用是不使用加鎖就能夠實現線程安全,CAS 算法又稱爲比較交換算法,是一種實現併發算法時經常使用到的技術,Java併發包中的不少類都使用了CAS技術。CAS具體包括三個參數:當前內存值V、舊的預期值A、即將更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作,並返回false。

原子更新的基本操做包括:

  • AtomicBoolean:原子更新布爾變量;
  • AtomicInteger:原子更新整型變量;
  • AtomicLong:原子更新長整型變量;

以AtomicInteger爲例,代碼以下:

public class AtomicInteger extends Number implements java.io.Serializable {
     //返回當前的值
     public final int get() {
         return value;
     }
     //原子更新爲新值並返回舊值
     public final int getAndSet(int newValue) {
         return unsafe.getAndSetInt(this, valueOffset, newValue);
     }
     //最終會設置成新值
     public final void lazySet(int newValue) {
         unsafe.putOrderedInt(this, valueOffset, newValue);
     }
     //若是輸入的值等於預期值,則以原子方式更新爲新值
     public final boolean compareAndSet(int expect, int update) {
         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
     //原子自增
     public final int getAndIncrement() {
         return unsafe.getAndAddInt(this, valueOffset, 1);
     }
     //原子方式將當前值與輸入值相加並返回結果
     public final int getAndAdd(int delta) {
         return unsafe.getAndAddInt(this, valueOffset, delta);
     }
 }

再如,下面是使用多線程對一個int值進行自增操做的代碼,以下所示。

public class AtomicIntegerDemo {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args){
        for (int i = 0; i < 5; i++){
            new Thread(new Runnable() {
                public void run() {
                    //調用AtomicInteger的getAndIncement返回的是增長以前的值
                     System.out.println(atomicInteger.getAndIncrement());
                }
            }).start();
        }
        System.out.println(atomicInteger.get());
    }
}

自旋鎖、適應性自旋鎖

阻塞或喚醒一個 Java 線程須要操做系統切換 CPU 狀態來完成,這種狀態轉換須要耗費處理器時間。在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。若是物理機器有多個處理器,可以讓兩個或以上的線程同時並行執行,咱們就可讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。

而爲了讓當前線程【稍等一下】,咱們需讓當前線程進行自旋,若是在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就能夠沒必要阻塞而是直接獲取同步資源,從而避免切換線程的開銷,這就是自旋鎖。

死鎖

當前線程擁有其餘線程須要的資源,當前線程等待其餘線程已擁有的資源,都不放棄本身擁有的資源。

18,談談你對Java 反射的理解

所謂反射,指的是在運行狀態中,對於任意一個類,都可以獲取這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性,而這種動態獲取的信息以及動態調用對象的方法的功能就被稱爲Java語言的反射機制。

使用反射前須要事先獲取到的字節碼,在Java中,獲取字節碼的方式有三種:

  1. Class.forName(className)
  2. 類名.class
  3. this.getClass()

19, 註解

Java 語言中的類、方法、變量、參數和包等均可以被標註。和 Javadoc 不一樣,Java 標註能夠經過反射獲取標註內容。根據做用時機的不一樣,Java的註解能夠分爲三種:

  • SOURCE:註解將被編譯器丟棄(該類型的註解信息只會保留在源碼裏,源碼通過編譯後,註解信息會被丟棄,不會保留在編譯好的class文件裏),如 @Override。
  • CLASS:註解在class文件中可用,但會被 VM 丟棄(該類型的註解信息會保留在源碼裏和 class 文件裏,在執行的時候,不會加載到虛擬機中),請注意,當註解未定義 Retention 值時,默認值是 CLASS。
  • RUNTIME:註解信息將在運行期 (JVM) 也保留,所以能夠經過反射機制讀取註解的信息(源碼、class 文件和執行的時候都有註解的信息),如 @Deprecated。

20,單例

爲了保證只有一個對象存在,可使用單例模式,網上有,單例模式的七種寫法。咱們介紹一下常見的幾種:

懶漢式
懶漢式使用的是static關鍵字,所以是線程不安全的。

public class Singleton {  
     private static Singleton instance;  
     private Singleton (){}   
     public static Singleton getInstance() {  
     if (instance == null) {  
        instance = new Singleton();  
     }  
    return instance;  
     }  
 }

若是要線程安全,那麼須要使用synchronized關鍵字。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
       instance = new Singleton();  
     }  
   return instance;  
     }  
 }

不過,使用synchronized鎖住以後,運行效率明顯下降。

靜態內部類
靜態內部類利用了classloder的機制來保證初始化instance時只有一個線程。

public class Singleton {  
   private static class SingletonHolder {  
   private static final Singleton INSTANCE = new Singleton();  
    }  
   private Singleton (){}
   public static final Singleton getInstance() {  
         return SingletonHolder.INSTANCE;  
      }  
 }

雙重校驗鎖

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}   
    public static Singleton getSingleton() {  
    if (singleton == null) {  
       synchronized (Singleton.class) {  
        if (singleton == null) {  
             singleton = new Singleton();  
        }  
       }  
     }  
    return singleton;  
    }  
 }

雙重檢查鎖定是synchronized的升級的寫法,那爲何要使用volatile關鍵字呢,是爲了禁止初始化實例時的重排序。咱們知道,初始化一個實例在java字節碼中會有4個步驟:

  1. 申請內存空間
  2. 初始化默認值(區別於構造器方法的初始化)
  3. 執行構造器方法
  4. 鏈接引用和實例

然後兩步是有可能會重排序,而使用volatile能夠禁止指令重排序。

相關文章
相關標籤/搜索