[面試]Java常見面試題,持續更新...

一.List集合類

ArrayList和LinkedList的區別

從底層數據結構以及訪問、添加、刪除的效率分別說明
注意: ArrayList當數組大小不知足時須要增長存儲能力,就要將已經有數組的數據複製到新的存儲空間java

Vector

Vector也是經過數組實現的,但它支持線程同步,避免同時寫引發的不一致性,實現同步須要很高的開銷,所以它的訪問比ArrayList慢算法

二.Set 集合類

Set排序子類、HashSet、hashCode()、equals的關係
  • HashSet:依靠HashCode和equals方法判斷重複,首先判斷哈希值,若是哈希值相同再判斷equals;相同哈希值但equals不一樣的,放在一個bucket裏,;它是無序的
  • TreeSet:二叉樹原理;有序的,須要實現Comparable接口;
  • LinkedHashSet:繼承了HashSet,方法接口與HashSet相同;底層使用LinkedHashMap 來保存全部元素;屬於有序,增長順序爲保存順序

三.Map類

HashMap的底層實現原理

jdk1.7中的HashMap
image.png
HashMap是一個數組,數組的每個元素是一個Entry的鏈表
每個Entry對象包括了Keyvaluehash值和指向下一個元素的next數組

HashMap包括兩個構造參數:
1.capacity:當前數組的容量,可擴容,擴容後的大小爲當前數組大小的兩倍
2.loadFactor:負載因子,默認0.75緩存

HashMap(int initialCapacity, float loadFactor)安全

threshold:擴容的閾值,等於capacity * loadFactor數據結構

JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換爲紅黑樹,在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。多線程

(1). HashMap 容許有一個null的key,容許多個value爲null
(2). HashMap 是線程不安全的,可經過Collections的synchronizedMap 方法使HashMap 具備線程安全的能力,或者使用ConcurrentHashMap
ConcurrentHashMap
  • ConcurrentHashMap支持併發操做,線程安全
  • 由一個個Segment組成,是一個Segment數組,Segment經過繼承ReentrantLock進行加鎖,每次須要加鎖的操做鎖住的是一個 segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全.
  • 一個Segment至關於一個HashMap

構造參數:
concurrencyLevel:並行數,也即segment的個數,默認16,初始化後不可更改或擴容;一個segment同時只容許一個線程操做, 16個segment容許16個線程在各類不一樣的segment上併發寫;併發

HashMap和HashTable的區別

HashTable是上古版本的遺留類,如今不多使用,app

  • 繼承自Dictionary類
  • HashTable線程安全, 但併發性不如ConcurrentHashMap, 由於後者引入了分段鎖。 而HashMap非線程安全。
  • HashTable不容許key爲null
  • HashMap把Hashtable的contains方法去掉了,改爲containsValue和containsKey,
  • 初始容量和擴容:HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。 Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍
HashSet

HashSet 底層是由HashMap實現, 因此也是非線程安全,
其實HashSet也是<K,V> 只不過對每一個元素都是同一個object對象做爲Vjvm

TreeMap(可排序)
  • 實現SortedMap 接口,可以把它保存的記錄根據鍵排序
  • key 必須實現Comparable 接口或者在構造TreeMap 傳入自定義的Comparator
LinkHashMap(記錄插入順序)

它繼承HashMap、底層使用哈希表與雙向鏈表來保存全部元素
Entry元素比HashMap多了:
Entry<K, V> before
Entry<K, V> after
before、After是用於維護Entry插入的前後順序的

大概的圖:
image.png


多線程相關

四.線程池

爲何使用線程池

1.減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
2.能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止消耗過多的內存

newCachedThreadPool
  • 建立一個可根據須要建立新線程的線程池, 線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小
  • 調用 execute 將重用之前構造的線程(若是線程可用)
  • 若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程
newFixedThreadPool
  • 建立一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程
  • 建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小
  • 線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程
newScheduledThreadPool

建立一個線程池,它可安排在給定延遲後運行命令或者按期地執行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
    @Override
    public void run() {
    System.out.println("延遲三秒");
    }}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
    @Override
    public void run() {
    System.out.println("延遲1 秒後每三秒執行一次");
    }},1,3,TimeUnit.SECONDS);
newSingleThreadExecutor
  • 建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務
  • 若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。
  • 此線程池保證全部任務的執行順序按照任務的提交順序執行

五.java線程的生命週期

在線程的生命週期中,它要通過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5 種狀態

新建狀態 new

當程序使用new 關鍵字建立了一個線程以後,該線程就處於新建狀態,此時僅由JVM 爲其分配內存,並初始化其成員變量的值

就緒狀態 Runnable

當線程對象調用了start()方法以後,該線程處於就緒狀態。Java 虛擬機會爲其建立方法調用棧和程序計數器,等待調度運行。

運行狀態 Running

若是處於就緒狀態的線程得到了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。

阻塞狀態 Blocked

阻塞狀態是指線程由於某種緣由放棄了cpu 使用權,暫時中止運行
阻塞的狀況分三種:

  1. 等待阻塞: o.wait->等待隊列

運行(running)的線程執行o.wait()方法,JVM 會把該線程放入等待隊列(waitting queue)
中。

  1. 同步阻塞(lock->lockPool)

運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM 會把該線程放入鎖池(lock pool)中。

  1. 其餘阻塞(sleep/join)

運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O 請求時,JVM 會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O
處理完畢時,線程從新轉入可運行(runnable)狀態。

死亡狀態 dead

正常結束

  1. run()或call()方法執行完成,線程正常結束。

異常結束

  1. 線程拋出一個未捕獲的Exception 或Error。

調用 stop

  1. 直接調用該線程的stop()方法來結束該線程—該方法一般容易致使死鎖,不推薦使用。

六.Volatile 關鍵字

可見性

在訪問volatile 變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile 變量是一種比sychronized 關鍵字更輕量級的同步機制。volatile 適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值

image.png
簡單原理:
當對非 volatile 變量進行讀寫的時候,每一個線程先從內存拷貝變量到CPU 緩存中。若是計算機有
多個CPU,每一個線程可能在不一樣的CPU 上被處理,這意味着每一個線程能夠拷貝到不一樣的 CPU
cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache這一步。

屏蔽指令重排序

指令重排序是編譯器和處理器爲了高效對程序進行優化的手段,它只能保證程序執行的結果是正確的,可是沒法保證程序的操做順序與代碼順序一致。這在單線程中不會構成問題,可是在多線程中就會出現問題。很是經典的例子是在單例方法中同時對字段加入voliate,就是爲了防止指令重排序。

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) { // 1
            synchronized(Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(); // 2
                }
            }
        }
        return singleton;
    }
}

實際上當程序執行到2處的時候,若是咱們沒有使用volatile關鍵字修飾變量singleton,就可能會形成錯誤。這是由於使用new關鍵字初始化一個對象的過程並非一個原子的操做,它分紅下面三個步驟進行:

a. 給 singleton 分配內存
b. 調用 Singleton 的構造函數來初始化成員變量
c. 將 singleton 對象指向分配的內存空間(執行完這步 singleton 就爲非 null 了)

若是虛擬機存在指令重排序優化,則步驟b和c的順序是沒法肯定的。若是A線程率先進入同步代碼塊並先執行了c而沒有執行b,此時由於singleton已經非null。這時候線程B到了1處,判斷singleton非null並將其返回使用,由於此時Singleton實際上還未初始化,天然就會出錯。synchronized能夠解決內存可見性,可是不能解決重排序問題。

不能保證原子性

舉例說明:
以i++爲例,其包括讀取、操做、賦值三個操做,下面是兩個線程的操做順序
image.png

假如說線程A在作了i+1,但未賦值的時候,線程B就開始讀取i,那麼當線程A賦值i=1,並回寫到主內存,而此時線程B已經再也不須要i的值了,而是直接交給處理器去作+1的操做,因而當線程B執行完並回寫到主內存,i的值仍然是1,而不是預期的2。也就是說,volatile縮短了普通變量在不一樣線程之間執行的時間差,但仍然存有漏洞,依然不能保證原子性。

七.java的可見性、有序性、原子性

參考文章 : java的可見性、有序性和原子性

八.什麼是CAS 樂觀鎖

參考資料: Java:CAS(樂觀鎖)

sychronized 實現同步的問題

在JDK 5以前Java語言是靠synchronized關鍵字保證同步的,該機制存在如下問題:

(1)在多線程競爭下,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,引發性能問題

(2)一個線程持有鎖會致使其它全部須要此鎖的線程掛起

(3)若是一個優先級高的線程等待一個優先級低的線程釋放鎖會致使優先級倒置,引發性能風險。

悲觀鎖和樂觀鎖

獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。
而另外一個更加有效的鎖就是樂觀鎖。
所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止
樂觀鎖用到的機制就是CAS,Compare and Swap。

CAS 原理:

CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。

更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。

CAS存在的問題(瞭解,說個大概)
  1. ABA問題。由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
  1. 循環時間長開銷大自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。 當須要修改共享變量的線程增多時,狀況會更爲嚴重;
  2. 只能保證一個共享變量的原子操做。當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

九.內存模型相關

說說jvm內存模型

參加我的文章 java內存模型

垃圾回收的算法,各自優缺點

同上

Java 中的對象必定在堆上分配嗎?
  • 棧上分配

JVM在Server模式下的逃逸分析能夠分析出某個對象是否永遠只在某個方法、線程的範圍內,並無「逃逸」出這個範圍,逃逸分析的一個結果就是對於某些未逃逸對象能夠直接在棧上分配,因爲該對象必定是局部的,因此棧上分配不會有問題。


其餘java題目

一.java 多態的原理

假設有一個Person類,以及兩個子類Boy和Girl
當這三個類加載到虛擬機後,方法區就包含了這三個類的信息,包括各自的方法表
Girl 和 Boy 在方法區中的方法表可表示以下:

方法表中的條目指向的具體的方法地址
如 Girl 繼承自 Object 的方法中,只有 toString() 指向本身的實現(Girl 的方法代碼,被重寫),其他皆指向 Object 的方法代碼;其繼承自於 Person 的方法 eat() [未重寫]和 speak() [被重寫]分別指向 Person 的方法實現和自己的實現。

那麼父類方法表和子類方法的方法表有什麼關係呢?

若是子類改寫了父類的方法,那麼子類和父類的那些同名的方法仍然共享一個方法表項。(能夠理解爲這個方法在方法表裏的叫法/直接引用仍是不變的)
所以,方法在方法表中的偏移量老是固定的,全部繼承父類的子類的方法表中,其父類所定義的方法的偏移量也老是一個定值。(能夠理解爲即便被重寫,子類還能夠經過和父類的一樣直接引用找到該方法)
這樣 JVM 在調用實例方法其實只須要指定調用方法表中的第幾個方法便可

知道了方法表是怎樣,那怎麼經過方法表調用方法的過程是怎樣呢?

假設代碼以下:

class Party {
    void happyHour() {
        Person girl = new Girl();
        girl.speak();
    }
}

(1)在常量池中找到方法調用的符號引用
(2)查看Person的方法表,獲得speak方法在該方法表的偏移量(假設爲15),這樣就獲得該方法的直接引用。 
(3)根據this指針獲得具體的對象(即 girl 所指向的位於堆中的對象)。
(4)根據對象獲得該對象對應的方法表根據偏移量15查看有無重寫(override)該方法,若是重寫,則能夠直接調用(Girl的方法表的speak項指向自身的方法而非父類);若是沒有重寫,則須要拿到按照繼承關系從下往上的基類(這裏是Person類)的方法表,一樣按照這個偏移量15查看有無該方法。

類的加載過程?

下面代碼的執行結果是?

class Person{
    {
        System.out.println("P1");
    }
    static {
        System.out.println("P2");
    }
    public Person(){
        System.out.println("P3");
    }
}
class Student extends Person{
    static {
        System.out.println("S1");
    }

    {
        System.out.println("S2");
    }

    public Student(){
        System.out.println("S3");
    }
}

public class Main {

    public static void main(String[] args) {
        Student s = new Student();
    }
}

答案:P2,S1,P1,P3,S2,S3
參考 java中類加載與靜態變量、靜態方法與靜態代碼塊詳解與初始化順序

相關文章
相關標籤/搜索