【面試】網易遊戲面試題目整理及答案(2)

網易遊戲面試題目整理及題目(2)

Java部分

  1. 多線程安全問題
    答:首先,瞭解什麼情況下會產生線程安全問題?同時滿足以下兩個條件時:
    ①多個線程在操作共享的數據
    ②操作共享數據的線程代碼有多條
    當一個線程在執行操作共享數據的多條代碼過程中,其他線程參與了運算,就會導致線程安全問題的產生。
    解決思路:將多條操作共享數據的線程代碼封裝起來,當有線程在執行這些代碼的時候,其他線程不可以參與運算。當前線程把這些代碼都執行完畢後,其他線程纔可以參與運算。在java中,用同步代碼塊就可以解決這個問題
    同步代碼塊的格式:
synchronized(對象)
{
	需要被同步的代碼 ;
}

這個對象一般稱爲同步鎖。同步的前提:同步中必須有多個線程並使用同一個鎖同步的好處:解決了線程的安全問題
同步的弊端:相對降低了效率,因爲同步外的線程的都會判斷同步鎖
然後,需要知道同步鎖是什麼?同步函數使用的鎖是 this靜態的同步函數使用的鎖是該函數所屬字節碼文件對象,可以用 getClass()方法獲取,也可以用當前類名.class表示。
同步函數和同步代碼塊的區別:
①同步函數的鎖是固定的this。
②同步代碼塊的鎖是任意的對象。
③建議使用同步代碼塊。
同步嵌套時,兩個線程你拿了我的鎖,我拿了你的鎖,都不釋放,造成死鎖。示例代碼如下:

class MyLock {
    public static final Object locka = new Object();
    public static final Object lockb = new Object();
}

public class Testa implements Runnable {
    private boolean flag;

    Testa(boolean flag) {
        this.flag = flag;
    }

    public void run() {
        if (flag) {
            while (true) {
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName() + "..if locka...");
                    synchronized (MyLock.lockb) {
                        System.out.println(Thread.currentThread().getName() + "...if lockb...");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName() + "...if lockb...");
                    synchronized (MyLock.locka) {
                        System.out.println(Thread.currentThread().getName() + "...if locka...");
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Testa a = new Testa(true);
        Testa b = new Testa(false);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);
        t1.start();
        t2.start();
    }
}

單例模式中的線程安全問題

/**
 * 餓漢式 單例模式
 */
public class ESingle {
    private static final ESingle s = new ESingle();
    private ESingle(){}
    public static ESingle getInstance(){
        return s;
    }
}
/**
 * 懶漢式 單例模式
 */
public class LSingle {
    private static LSingle s = null;

    private LSingle() {
    }

    public static LSingle getInstance() {
        if (s == null) {
            synchronized (LSingle.class) {
                if (s == null)
                    s = new LSingle();
            }
        }
        return s;
    }
}

開發用餓漢式,沒有線程安全問題。——餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以後不再改變,所以天生是線程安全的
懶漢式的特點:延遲加載,不足:當多線程訪問時會出現安全問題。可以採用加同步的方式解決。

  1. 併發和並行的區別
    答:併發和並行從宏觀上來講都是同時處理多路請求的概念。但併發和並行又有區別,並行是指兩個或者多個事件在同一時刻發生;而併發是指兩個或多個事件在同一時間間隔內發生。在操作系統中,併發是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。①程序與計算不再一一對應,一個程序副本可以有多個計算。②併發程序之間有相互制約關係,直接制約體現爲一個程序需要另一個程序的計算結果,間接制約體現爲多個程序競爭某一資源,如處理機、緩衝區等。③併發程序在執行中是走走停停,斷續推進的。

  2. Java的併發安全機制(答了synchronized、ReentrantLock、CAS)
    答:首先弄清楚什麼是進程?什麼是線程?進程是正在運行的一個程序。線程在進程裏邊,也可以說是程序內部的一條執行路徑。若同一個進程同一時間並執行多個線程,就是支持多線程的。線程作爲調度和執行的單位,每個線程擁有獨立的運行棧和程序計數器。單核CPU:其實是一種假的多線程,因爲在一個時間單位內,也只能執行一個線程的任務。多核CPU:能夠更好的發揮多線程效率。一個Java應用程序java.exe其實至少有三個線程:main()主線程,gc()垃圾回收機制,異常處理線程
    並行:多個CPU同時執行多個任務。
    併發:一個CPU同時執行多個任務。
    何時需要多線程?①當一個程序需要同時執行兩個或多個任務;②程序需要實現一些需要等待的任務時,如用戶輸入、文件讀寫等;③需要一些後臺運行的程序時。
    創建多線程的方式一:繼承Thread類,步驟如下:①創建一個繼承於Thread類的子類;②重寫Thread類的run()方法;③創建該子類的對象;④通過此對象調用start()方法。注意:如果線程對象直接調用run(),即m.run(),此時只是普通的對象調用方法,並未啓動線程。
    Thread類的方法:
    1.start():啓動當前線程,調用當前線程的run();
    2.run():通常需要重寫Thread類中的此方法,將創建線程要執行的操作聲明在此方法中;
    3.currentThread():靜態方法,返回執行當前代碼的線程;
    4.getName():獲取當前線程名字;
    5.setName():設置當前線程名;
    6.yield():釋放當前CPU的執行權;
    7.join():在線程A中調用線程B的join(),此時線程A就進入阻塞狀態,直到線程B完全執行完以後,線程A才結束阻塞狀態;
    8.stop():已過時。強制結束當前線程;
    9.sleep(long millitime):單位爲毫秒,讓當前線程“睡眠”指定的millitime毫秒。在此時間之內,當前線程是阻塞狀態;
    10.isAlive():判斷當前線程是否存活,返回ture或false
    線程的調度:①同優先級線程組成先進先出隊列;②對高優先級,會搶佔CPU的執行權;
    線程的優先級: ①線程的優先等級分爲3個:MAX_PRIORITY:10 ,MIN_PRIORITY:1,NORM_PRIORITY:5;
    如何獲取和設置當前線程的優先級?getPriority():獲取線程的優先級;setPriority():設置線程的優先級
    注意:高優先級的線程會搶佔CPU的執行權,但這只是從概率上講,並不意味着只有高優先級的線程執行完後才執行低優先級的線程
    創建多線程的方式二:實現Runnable接口,步驟如下:①定義類實現Runnable接口;②覆蓋Runnable接口中的run方法,將線程要運行的代碼存放在該run方法中;③通過Thread類建立線程對象;④將Runnable接口的子類對象作爲實際參數傳遞給Thread類的構造函數;⑤調用Thread類的start方法開啓線程並調用Runnable接口子類的run方法。
    這裏有兩個問題:1. 爲什麼要將Runnable接口的子類對象傳遞給Thread的構造函數?因爲,自定義的run方法所屬的對象是Runnable接口的子類對象。所以要讓線程去指定對象的run方法,就必須明確該run方法所屬對象。2.實現方式和繼承方式創建線程有什麼區別?實現方式的好處是避免了單繼承的侷限性。在定義線程時,建議使用實現方式。兩種方式的區別:繼承Thread:線程代碼存放Thread子類run方法中;實現Runnable:線程代碼存在接口的子類的run方法。
    線程的生命週期:
    線程的生命週期
    新建:使用new關鍵字創建Thread類或及其子類對象後,該線程就處於新建狀態。此時,通過對象調用start()方法後,線程進入就緒狀態;
    就緒:此時線程已經具備了運行條件,但是還沒有分配到CPU的執行權,處於線程就緒隊列,等待系統爲其分配CPU。一旦獲得了CPU的執行權,那麼線程就進入運行狀態,並自動調用自己的run()方法。
    運行:此時線程執行自己的run()方法,直到調用其他方法而終止,或等待某資源而阻塞或完成任務而死亡。
    阻塞:處於運行狀態的線程在某種情況下,如執行了sleep()方法後,此時將讓出CPU的執行權並停止自己的運行。只有當引起阻塞的原因消除時,如睡眠時間已到,此時線程便轉入就緒狀態,再次等待CPU的執行權。其實阻塞狀態時線程具備運行資格,但沒有CPU執行權。
    死亡:死亡狀態是線程生命週期的最後一個階段。線程死亡有兩個原因:一是正常運行的線程完成了自己的工作,而是一個線程被強制終止,如通過stop()。
    然後我們要了解一下
    內存模型的相關內容
    java的內存模型中有主內存和線程的工作內存之分主內存上存放的是線程共享的變量(實例字段,靜態字段和構成數組的元素),線程的工作內存是線程私有的空間,存放的是線程私有的變量(方法參數與局部變量)線程在工作的時候如果要操作主內存上的共享變量,爲了獲得更好的執行性能並不是直接去修改主內存而是會在線程私有的工作內存中創建一份變量的拷貝(緩存),在工作內存上對變量的拷貝修改之後再把修改的值刷回到主內存的變量中去,JVM提供了8中原子操作來完成這一過程:lock, unlock, read, load, use, assign, store, write。深入理解java虛擬機-jvm最高特性與實踐這本書中有一個圖很好的表示了線程,主內存和工作內存之間的關係:
    Java的主內存和工作內存之間的區別
    如果只有一個線程當然不會有什麼問題,但是如果有多個線程同時在操作主內存中的變量,因爲
    8種操作的非連續性和線程搶佔cpu執行的機制就會帶來衝突的問題,也就是多線程的安全問題
    線程安全的定義就是:如果線程執行過程中不會產生共享資源的衝突就是線程安全的
    Java一般用一下幾種機制保證線程安全
    1)互斥同步鎖(悲觀鎖)
    ①Synchronized
    ②ReentrantLock
    互斥同步鎖也叫做阻塞同步鎖,特徵是會對沒有獲取鎖的線程進行阻塞。要理解互斥同步鎖,首選要明白什麼是互斥什麼是同步。簡單的說互斥就是非你即我,同步就是順序訪問。互斥同步鎖就是以互斥的手段達到順序訪問的目的。操作系統提供了很多互斥機制比如信號量,互斥量,臨界區資源等來控制在某一個時刻只能有一個或者一組線程訪問同一個資源
    Java裏面的互斥同步鎖就是Synchronized和ReentrantLock前者是由語言級別實現的互斥同步鎖,理解和寫法簡單但是機制笨拙,在JDK6之後性能優化大幅提升,即使在競爭激烈的情況下也能保持一個和ReentrantLock相差不多的性能,所以JDK6之後的程序選擇不應該再因爲性能問題而放棄synchorized。ReentrantLock是API層面的互斥同步鎖,需要程序自己打開並在finally中關閉鎖,和synchorized相比更加的靈活,體現在三個方面**:等待可中斷,公平鎖以及綁定多個條件**。但是如果開發人員對ReentrantLock理解不夠深刻,或者忘記釋放lock,那麼不僅不會提升性能反而會帶來額外的問題。另外Synchronized是JVM實現的,可以通過監控工具來監控鎖的狀態,遇到異常JVM會自動釋放掉鎖。而ReentrantLock必須由程序主動的釋放鎖
    互斥同步鎖都是可重入鎖,好處是可以保證不會死鎖。但是因爲涉及到核心態和用戶態的切換,因此比較消耗性能。JVM開發團隊在JDK5-JDK6升級過程中採用了很多鎖優化機制來優化同步無競爭情況下鎖的性能。比如:自旋鎖和適應性自旋鎖,輕量級鎖,偏向鎖,鎖粗化和鎖消除。
    2)非阻塞同步鎖
    1.原子類(CAS)
    非阻塞同步鎖也叫樂觀鎖
    ,相比悲觀鎖來說,它會先進行資源在工作內存中的更新,然後根據與主存中舊值的對比來確定在此期間是否有其他線程對共享資源進行了更新,如果舊值與期望值相同,就認爲沒有更新,可以把新值寫回內存,否則就一直重試直到成功。它的實現方式依賴於處理器的機器指令:CAS(Compare And Swap)
    JUC中提供了幾個Automic類以及每個類上的原子操作就是樂觀鎖機制。不激烈情況下,性能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的性能會優於ReentrantLock一倍左右。但是其有一個缺點,就是隻能同步一個值,一段代碼中只能出現一個Atomic的變量,多於一個同步無效。因爲它不能在多個Atomic之間同步。 非阻塞鎖是不可重入的,否則會造成死鎖
    3)無同步方案
    1.可重入代碼:在執行的任何時刻都可以中斷-重入執行而不會產生衝突。特點就是不會依賴堆上的共享資源
    2.ThreadLocal/Volaitile:線程本地的變量,每個線程獲取一份共享變量的拷貝,單獨進行處理。
    3.線程本地存儲:如果一個共享資源一定要被多線程共享,可以儘量讓一個線程完成所有的處理操作,比如生產者消費者模式中,一般會讓一個消費者完成對隊列上資源的消費。典型的應用是基於請求-應答模式的web服務器的設計

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
補充知識:
4. 樂觀鎖/悲觀鎖
1)樂觀鎖:樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。
java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
2)悲觀鎖:悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試CAS樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。
5. 公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖
非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。
對於Java ReentrantLock而言,通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
對於synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。
6. 獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個線程所持有;共享鎖是指該鎖可被多個線程所持有。
對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫、寫讀 、寫寫的過程是互斥的。獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。對於synchronized而言,當然是獨享鎖
7. 互斥鎖/讀寫鎖
上面說到的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。互斥鎖在Java中的具體實現就是ReentrantLock讀寫鎖在Java中的具體實現就是ReadWriteLock
8. 可重入鎖
可重入鎖又名遞歸鎖,是指同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。對於Java ReentrantLock而言, 其名字是Reentrant Lock即是重新進入鎖。對於synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖
9. 分段鎖
分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。當需要put元素的時候,並不是對整個HashMap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。但是,在統計size的時候,可就是獲取HashMap全局信息的時候,就需要獲取所有的分段鎖才能統計
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。
10. 自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU
11. 偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,並且是針對synchronized。在Java 5通過引入鎖升級的機制來實現高效synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段來表明的
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價
輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能
重量級鎖是指當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低
Java SE 1.6爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬於樂觀鎖。後面將會對這四種狀態進行詳細說明。

補充知識:markword/AQS/CAS

  1. markword
    markword是java對象數據結構中的一部分,這裏只做markword的詳細介紹,因爲對象的markword和java各種類型的鎖密切相關;
    markword數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,如下表所示:
    markword的鎖狀態標誌位與存儲內容之間的關係
    32位虛擬機在不同狀態下markword結構如下圖所示:
    32位虛擬機在不同狀態下markword結構圖
  2. AQS
    AbstractQueuedSynchronized 抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch…
    抽象的隊列式的同步器
    AQS維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。state的訪問方式有三種:getState()、setState()、compareAndSetState()
    AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。
  3. CAS
    CAS(Compare and Swap 比較並交換)是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。   
    CAS操作中包含三個操作數——需要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。如果內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B,否則處理器不做任何操作。無論哪種情況,它都會在CAS 指令之前返回該位置的值(在CAS的一些特殊情況下將僅返回CAS是否成功,而不提取當前值)。CAS有效地說明了“ 我認爲位置V應該包含值A;如果包含該值,則將 B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可”。這其實和樂觀鎖的衝突檢查 + 數據更新的原理是一樣的。
    補充知識:Synchronized的執行過程
    1.檢測MarkWord裏面是不是當前線程的ID,如果是,表示當前線程處於偏向鎖
    2.如果不是,則使用CAS將當前線程的ID替換Mard Word,如果成功則表示當前線程獲得偏向鎖,置偏向標誌位1
    3.如果失敗,則說明發生競爭,撤銷偏向鎖,進而升級爲輕量級鎖。
    4.當前線程使用CAS將對象頭的Mark Word替換爲鎖記錄指針,如果成功,當前線程獲得鎖
    5.如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
    6.如果自旋成功則依然處於輕量級狀態。
    7.如果自旋失敗,則升級爲重量級鎖。
    鎖的優缺點:
    鎖的優缺點
    補充知識:Synchronized與Lock的區別
    1.synchronized是關鍵字,是JVM層面的底層啥都幫我們做了,而Lock是一個接口,是JDK層面的有豐富的API。
    2.synchronized會自動釋放鎖,而Lock必須手動釋放鎖。
    3.synchronized是不可中斷的,Lock可以中斷也可以不中斷。
    4.通過Lock可以知道線程有沒有拿到鎖,而synchronized不能。
    5.synchronized能鎖住方法和代碼塊,而Lock只能鎖住代碼塊。
    6.Lock可以使用讀鎖提高多線程讀效率。
    7.synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。
    兩者一個是JDK層面的一個是JVM層面的,最大的區別其實在,是否需要豐富的api,還有一個是我們的場景。

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

  1. String、StringBuilder、StringBuffer的區別和應用場景
    答:String、StringBuffer與StringBuilder的區別是:①String內容不可變,是值不可變的變量,且使用了final修飾符,是不可繼承的,StringBuffer和StringBuilder內容可變;②StringBuilder非線程安全(單線程使用),String與StringBuffer線程安全(多線程使用);③如果程序不是多線程的,那麼使用StringBuilder效率高於StringBuffer。
    String與StringBuffer區別:String在修改時不會改變對象自身,在每次對 String 類型進行改變的時候其實都等同於生成了一個新的 String 對象,然後將指針指向新的 String 對象,所以經常改變內容的字符串最好不要用 String 。StringBuffer在修改時會改變對象自身,每次結果都會對 StringBuffer 對象本身進行操作,而不是生成新的對象,再改變對象引用。所以在一般情況下我們推薦使用 StringBuffer ,特別是字符串對象經常改變的情況下。StringBuffer 上的主要操作是 append 和 insert 方法。StringBuffer類中的方法主要偏重於對於字符串的變化,例如追加、插入和刪除等,常用方法有:append方法、insert方法、deleteCharAt方法、reverse方法等。
    ①如果要操作少量的數據用 String;②(多線程下)經常需要對一個字符串進行修改,例如追加、插入和刪除等操作,使用StringBuffer要更加適合一些。
    StringBuffer與StringBuilder區別:StringBuilder是可變的對象,用在字符串緩衝區被單個線程使用的時候(這種情況很普遍)。StringBuffer:線程安全的; StringBuilder:線程非安全的。
    String,StringBuffer與StringBuilder速度區別:在大部分情況下,StringBuilder > StringBuffer > String;特殊情況, String > StringBuffer
    (1)如果要操作少量的數據用 String;
    (2)多線程操作字符串緩衝區下操作大量數據 StringBuffer;
    (3)單線程操作字符串緩衝區下操作大量數據 StringBuilder。

  2. String a=“abc” String b=“abc” ,a等於b嗎?常量池位於JVM的哪裏?String提供了什麼方法使用常量池?(intern)
    答:首先注意和equals()的區別:對於對象而言,==是判斷兩個對象是否指向同一地址,而equals()纔是判斷兩個對象的內容是否相同(所以說自定義對象需要重寫hashcode()以及equals()方法)。String類型屬於不可變(immutable)對象,也就是說String變量的內容一旦確定就不能修改。當使用String a = “abc”;語句時實際上是在字符串常量池中建立了一個"abc"字符串,String a = "bcd"同理,所以兩個語句只是改變了a變量指向內存池中的位置,其內容並沒有改變。當你使用String a = “abc”; String b = “abc”;時,實際上是在Java常量池中新建了一個"abc"字符串,而a和b都是指向內存池中的這個"abc"的,兩者的地址也是相同的,所以ab返回true。
    JVM的結構圖如下:
    JVM的結構圖
    Java6和6之前,常量池是存放在方法區(永久代)中的。Java7,將常量池是存放到了堆中。Java8之後,取消了整個永久代區域,取而代之的是元空間。運行時常量池和靜態常量池存放在元空間中,而字符串常量池依然存放在堆中
    jdk1.6下字符串常量池是在永久區中,是與堆完全獨立的兩個空間。intern()方法能使一個位於堆中的字符串在運行期間動態地加入到字符串常量池中(字符串常量池的內容是程序啓動的時候就已經加載好了),如果字符串常量池中有該對象對應的字面量,則返回該字面量在字符串常量池中的引用,否則,創建複製一份該字面量到字符串常量池並返回它的引用
    jdk1.7,1.8下字符串常量池已經轉移到堆中了,是堆中的一部分內容,jvm設計人員對intern()進行了一些修改,當執行intern()時,jvm不再把字符串對應的字面量複製一份到字符串常量池中,而是在字符串常量池中存儲一份字符串的引用,這個引用指向堆中的字面量,當運行到String s = "hellohello"時,發現字符串常量池已經存在一個指向堆中該字面量的引用,則返回這個引用。

  3. String爲什麼不會變?
    答:String類是不可變類,一個String對象被創建以後,包含這個對象中的字符串序列是不可改變的。與其問String爲什麼是不可變的,還不如問String類是如何實現其對象不可變的。
    當使用final修飾基本類型變量時,不能對基本類型變量重新賦值,因此基本類型變量不能被改變。但對於引用類型變量而言,它保存的僅僅是一個引用,final只保證這個引用變量所引用的地址不會改變,即一直引用同一個對象,但這個對象完全可以發生改變
    String對象真的不可變嗎?雖然value是final修飾的,只是說明value不能再重新指向其他的引用。但是value指向的數組可以改變,一般情況下我們是沒有辦法訪問到這個value指向的數組的元素。但是,反射可以。可以反射出String對象中的value屬性, 進而改變通過獲得的value引用改變數組的結構

public static void main(String[] args) throws Exception {
        String str = "Hello World";
        System.out.println("修改前的str:" + str);
        System.out.println("修改前的str的內存地址" + System.identityHashCode(str));
        // 獲取String類中的value字段
        Field valueField = String.class.getDeclaredField("value");
        // 改變value屬性的訪問權限
        valueField.setAccessible(true);
        // 獲取str對象上value屬性的值
        char[] value = (char[]) valueField.get(str);
        // 改變value所引用的數組中的字符
        value[3] = '?';
        System.out.println("修改後的str:" + str);
        System.out.println("修改前的str的內存地址" + System.identityHashCode(str));
    }
  1. collection與collections區別
    答:java.util.Collection 是一個集合框架的父接口。它提供了對集合對象進行基本操作的通用接口方法。Collection接口在Java 類庫中有很多具體的實現。Collection接口的意義是爲各種具體的集合提供了最大化的統一操作方式。
    java.util.Collections 是一個包裝類。它包含有各種有關集合操作的靜態多態方法。此類不能實例化,就像一個工具類,服務於Java的Collection框架。它提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。排序(Sort)、混排(Shuffling)、反轉(Reverse)、替換所以的元素(Fill)、拷貝(Copy)、返回Collections中最小元素(min)、返回Collections中最小元素(max)、lastIndexOfSubList、IndexOfSubList、Rotate等

數據庫部分

  1. MySQL架構
    答:和其它數據庫相比,MySQL有點與衆不同,它的架構可以在多種不同場景中應用併發揮良好作用。主要體現在存儲引擎的架構上,插件式的存儲引擎架構將查詢處理和其它的系統任務以及數據的存儲提取相分離。這種架構可以根據業務的需求和實際需要選擇合適的存儲引擎。
    MySQL架構圖
    1.**連接層:最上層是一些客戶端和連接服務。**主要完成一些類似於連接處理、授權認證、及相關的安全方案。在該層上引入了線程池的概念,爲通過認證安全接入的客戶端提供線程。同樣在該層上可以實現基於SSL的安全鏈接。服務器也會爲安全接入的每個客戶端驗證它所具有的操作權限。
    2.服務層:第二層服務層,主要完成大部分的核心服務功能, 包括查詢解析、分析、優化、緩存、以及所有的內置函數,所有跨存儲引擎的功能也都在這一層實現,包括觸發器、存儲過程、視圖等
    3.引擎層:第三層存儲引擎層,存儲引擎真正的負責了MySQL中數據的存儲和提取,服務器通過API與存儲引擎進行通信。不同的存儲引擎具有的功能不同,這樣我們可以根據自己的實際需要進行選取
    4.存儲層:第四層爲數據存儲層,主要是將數據存儲在運行於該設備的文件系統之上,並完成與存儲引擎的交互

  2. 一條SQL語句在MySQL中如何執行的?
    答:客戶端請求 —> 連接器(驗證用戶身份,給予權限) —> 查詢緩存(存在緩存則直接返回,不存在則執行後續操作) —> 分析器(對SQL進行詞法分析和語法分析操作) —> 優化器(主要對執行的sql優化選擇最優的執行方案方法) —> 執行器(執行時會先看用戶是否有執行權限,有才去使用這個引擎提供的接口) —> 去引擎層獲取數據返回(如果開啓查詢緩存則會緩存查詢結果)
    MySQL的查詢流程

  3. MySQL有哪些存儲引擎?都有哪些區別?
    答:存儲引擎是MySQL的組件,用於處理不同表類型的SQL操作。不同的存儲引擎提供不同的存儲機制、索引技巧、鎖定水平等功能,使用不同的存儲引擎,還可以獲得特定的功能。
    使用哪一種引擎可以靈活選擇,一個數據庫中多個表可以使用不同引擎以滿足各種性能和實際需求,使用合適的存儲引擎,將會提高整個數據庫的性能 。
    MySQL服務器使用可插拔的存儲引擎體系結構,可以從運行中的 MySQL 服務器加載或卸載存儲引擎
    查看存儲引擎:

SHOW ENGINES; #查看支持的存儲引擎
SHOW VARIABLES LIKE 'storage_engine'#查看默認存儲引擎
SHOW table status like 'tablename'
SHOW table status from database where name="tablename" # 準確查看某個數據庫的某一個表所使用的存儲引擎

設置存儲引擎:

# 建表時指定存儲引擎
CREATE TBALE t1 (i INT) ENGINE = INNODB;
CREATE TABLE t2 (i INT) ENGINE = MEMORY; 
# 修改存儲引擎
ALTER TABLE t_ ENGINE = InnoDB# 修改默認存儲引擎
SET default_storage_engine = NDBCLUSTER;

存儲引擎對比:
常見的存儲引擎就 InnoDB、MyISAM、Memory、NDB
InnoDB 現在是 MySQL 默認的存儲引擎,支持事務行級鎖定外鍵
文件存儲結構對比:
MySQL中建立任何一張數據表,在其數據目錄對應的數據庫目錄下都有對應表的 .frm 文件.frm 文件是用來保存每個數據表的元數據(meta)信息,包括表結構的定義等,與數據庫存儲引擎無關,也就是任何存儲引擎的數據表都必須有.frm文件,命名方式爲 數據表名.frm,如user.frm。查看MySQL 數據保存在哪裏:

show variables like 'data%';

1)MyISAM 物理文件結構爲:

  • .frm文件:與表相關的元數據信息都存放在frm文件,包括表結構的定義信息等
  • .MYD (MYData) 文件:MyISAM 存儲引擎專用,用於存儲MyISAM 表的數據
  • .MYI (MYIndex)文件:MyISAM 存儲引擎專用,用於存儲MyISAM 表的索引相關信息
    2)InnoDB 物理文件結構爲:
  • .frm 文件:與表相關的元數據信息都存放在frm文件,包括表結構的定義信息等
  • .ibd 文件或 .ibdata 文件:這兩種文件都是存放 InnoDB 數據的文件,之所以有兩種文件形式存放 InnoDB 的數據,是因爲 InnoDB 的數據存儲方式能夠通過配置來決定是使用共享表空間存放存儲數據,還是用獨享表空間存放存儲數據獨享表空間存儲方式使用.ibd文件,並且每個表一個.ibd文件;共享表空間存儲方式使用.ibdata文件,所有表共同使用一個.ibdata文件(或多個,可自己配置)
    MyISAM與InnoDB的對比:
  1. InnoDB 支持事務,MyISAM 不支持事務。這是 MySQL 將默認存儲引擎從 MyISAM 變成 InnoDB 的重要原因之一;
  2. InnoDB 支持外鍵,而 MyISAM 不支持。對一個包含外鍵的 InnoDB 錶轉爲 MYISAM 會失敗;
  3. InnoDB 是聚簇索引,MyISAM 是非聚簇索引聚簇索引的文件存放在主鍵索引的葉子節點上,因此 InnoDB 必須要有主鍵,通過主鍵索引效率很高。但是輔助索引需要兩次查詢,先查詢到主鍵,然後再通過主鍵查詢到數據。因此,主鍵不應該過大,因爲主鍵太大,其他索引也都會很大。而 MyISAM 是非聚集索引,數據文件是分離的,索引保存的是數據文件的指針。主鍵索引和輔助索引是獨立的
  4. InnoDB 不保存表的具體行數,執行select count(*) from table 時需要全表掃描。而 MyISAM 用一個變量保存了整個表的行數,執行上述語句時只需要讀出該變量即可,速度很快;
  5. InnoDB 最小的鎖粒度是行鎖MyISAM 最小的鎖粒度是表鎖。一個更新語句會鎖住整張表,導致其他查詢和更新都會被阻塞,因此併發訪問受限。這也是 MySQL 將默認存儲引擎從 MyISAM 變成 InnoDB 的重要原因之一;

補充問題:
問題1:一張表,裏面有ID自增主鍵,當insert了17條記錄之後,刪除了第15,16,17條記錄,再把Mysql重啓,再insert一條記錄,這條記錄的ID是18還是15 ?
答:如果表的類型是MyISAM,那麼是18。因爲MyISAM表會把自增主鍵的最大ID 記錄到數據文件中,重啓MySQL自增主鍵的最大ID也不會丟失;如果表的類型是InnoDB,那麼是15。因爲InnoDB 表只是把自增主鍵的最大ID記錄到內存中,所以重啓數據庫或對錶進行OPTION操作,都會導致最大ID丟失。
問題2:哪個存儲引擎執行 select count(*) 更快,爲什麼?
答:MyISAM更快,因爲MyISAM內部維護了一個計數器,可以直接調取。在 MyISAM 存儲引擎中,把表的總行數存儲在磁盤上,當執行 select count(*) from t 時,直接返回總數據。
在 InnoDB 存儲引擎中,跟 MyISAM 不一樣,沒有將總行數存儲在磁盤上,當執行 select count(*) from t 時,會先把數據讀出來,一行一行的累加,最後返回總數量。InnoDB 中 count(*) 語句是在執行的時候,全表掃描統計總數量,所以當數據越來越大時,語句就越來越耗時了,爲什麼 InnoDB 引擎不像 MyISAM 引擎一樣,將總行數存儲到磁盤上?這跟 InnoDB 的事務特性有關,由於多版本併發控制(MVCC)的原因,InnoDB 表“應該返回多少行”也是不確定的。

  1. MySQL中的數據類型
    答:主要包括以下五大類:
  • 整數類型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT
  • 浮點數類型:FLOAT、DOUBLE、DECIMAL
  • 字符串類型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB
  • 日期類型:Date、DateTime、TimeStamp、Time、Year
  • 其他數據類型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等
    補充問題:
    問題1:CHAR 和 VARCHAR 的區別?
    答:char是固定長度,varchar長度可變:char(n) 和 varchar(n) 中括號中 n 代表字符的個數,並不代表字節個數,比如 CHAR(30) 就可以存儲 30 個字符。存儲時,前者不管實際存儲數據的長度,直接按 char 規定的長度分配存儲空間;而後者會根據實際存儲的數據分配最終的存儲空間
    相同點:
  • char(n),varchar(n)中的n都代表字符的個數
  • 超過char,varchar最大長度n的限制後,字符串會被截斷。
    不同點:
  • char不論實際存儲的字符數都會佔用n個字符的空間,而varchar只會佔用實際字符應該佔用的字節空間加1(實際長度length,0<=length<255)或加2(length>255)。因爲varchar保存數據時除了要保存字符串之外還會加一個字節來記錄長度(如果列聲明長度大於255則使用兩個字節來保存長度)。
  • 能存儲的最大空間限制不一樣:char的存儲上限爲255字節
  • char在存儲時會截斷尾部的空格,而varchar不會。
    總之,char是適合存儲很短的、一般固定長度的字符串。例如,char非常適合存儲密碼的MD5值,因爲這是一個定長的值。對於非常短的列,char比varchar在存儲空間上也更有效率。
    問題2:列的字符串類型可以是什麼?
    答:字符串類型是:SET、BLOB、ENUM、CHAR、CHAR、TEXT、VARCHAR
    問題3:BLOB和TEXT有什麼區別?
    答:BLOB是一個二進制對象,可以容納可變數量的數據。有四種類型的BLOB:TINYBLOB、BLOB、MEDIUMBLO和 LONGBLOB;TEXT是一個不區分大小寫的BLOB。四種TEXT類型:TINYTEXT、TEXT、MEDIUMTEXT 和 LONGTEXT。BLOB 保存二進制數據,TEXT 保存字符數據
  1. 對MySQL索引的理解
    答:MYSQL官方對索引的定義爲:索引(Index)是幫助MySQL高效獲取數據的數據結構,所以說索引的本質是:數據結構
    ①索引的目的在於提高查詢效率,可以類比字典、 火車站的車次表、圖書的目錄等。
    除數據本身之外,數據庫還維護者一個滿足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實現高級查找算法。這種數據結構,就是索引
    ③索引本身也很大,不可能全部存儲在內存中,一般以索引文件的形式存儲在磁盤上
    ④平常說的索引,沒有特別指明的話,就是B+樹(多路搜索樹,不一定是二叉樹)結構組織的索引。其中聚集索引,次要索引,覆蓋索引,符合索引,前綴索引,唯一索引默認都是使用B+樹索引,統稱索引。此外還有哈希索引等。
    創建索引:CREATE [UNIQUE] INDEX indexName ON mytable(username(length));如果是CHAR,VARCHAR類型,length可以小於字段實際長度;如果是BLOB和TEXT類型,必須指定 length。
    修改表結構(添加索引):ALTER table tableName ADD [UNIQUE] INDEX indexName(columnName);
    刪除索引:DROP INDEX [indexName] ON mytable;
    查看索引:SHOW INDEX FROM table_name\G; --可以通過添加 \G 來格式化輸出信息。
    修改索引(使用ALTER命令):
ALTER TABLE tbl_name ADD PRIMARY KEY (column_list)#該語句添加一個主鍵,這意味着索引值必須是唯一的,且不能爲NULL。
ALTER TABLE tbl_name ADD UNIQUE index_name (column_list); #這條語句創建索引的值必須是唯一的(除了NULL外,NULL可能會出現多次)。
ALTER TABLE tbl_name ADD INDEX index_name (column_list); #添加普通索引,索引值可出現多次。
ALTER TABLE tbl_name ADD FULLTEXT index_name (column_list); #該語句指定了索引爲 FULLTEXT ,用於全文索引。

索引的優勢:

  • 提高數據檢索效率,降低數據庫IO成本
  • 降低數據排序的成本,降低CPU的消耗
    索引的劣勢:
  • 索引也是一張表,保存了主鍵和索引字段,並指向實體表的記錄,所以也需要佔用內存
  • 雖然索引大大提高了查詢速度,同時卻會降低更新表的速度,如對錶進行INSERT、UPDATE和DELETE。因爲更新表時,MySQL不僅要保存數據,還要保存一下索引文件每次更新添加了索引列的字段, 都會調整因爲更新所帶來的鍵值變化後的索引信息
  1. MySQL索引分類
    答:從三個角度對MySQL索引進行分類:
    1)從數據結構角度
  • B+樹索引
  • Hash索引
  • Full-Text全文索引
  • R-Tree索引
    2)從物理存儲角度
  • 聚集索引(clustered index)
  • 非聚集索引(non-clustered index),也叫輔助索引(secondary index)
  • 聚集索引和非聚集索引都是B+樹結構
    3)從邏輯角度
  • 主鍵索引主鍵索引是一種特殊的唯一索引,不允許有空值
  • 普通索引或者單列索引:每個索引只包含單個列,一個表可以有多個單列索引
  • 多列索引(複合索引、聯合索引)複合索引指多個字段上創建的索引,只有在查詢條件中使用了創建索引時的第一個字段,索引纔會被使用。使用複合索引時遵循最左前綴集合
  • 唯一索引或者非唯一索引
  • 空間索引:空間索引是對空間數據類型的字段建立的索引,MYSQL中的空間數據類型有4種,分別是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL關鍵字進行擴展,使得能夠用於創建正規索引類型的語法創建空間索引。創建空間索引的列,必須將其聲明爲NOT NULL,空間索引只能在存儲引擎爲MYISAM的表中創建
  1. 爲什麼MySQL索引中用B+tree,不用B-tree 或者其他樹,爲什麼不用 Hash 索引?聚簇索引/非聚簇索引,MySQL 索引底層實現,葉子結點存放的是數據還是指向數據的內存地址,使用索引需要注意的幾個地方?使用索引查詢一定能提高查詢的性能嗎?爲什麼?
    答:首先要明白索引(index)是在存儲引擎(storage engine)層面實現的,而不是server層面。不是所有的存儲引擎都支持所有的索引類型。即使多個存儲引擎支持某一索引類型,它們的實現和行爲也可能有所差別。
    MyISAM 和 InnoDB 存儲引擎,都使用 B+Tree的數據結構,它相對與 B-Tree結構,所有的"數據都存放在葉子節點上",且把葉子節點通過指針連接到一起,形成了一條數據鏈表,以加快相鄰數據的檢索效率
    B-Tree 和 B+Tree 的區別:
    1. B-Tree是爲磁盤等外存儲設備設計的一種平衡查找樹
    這裏需要了解系統如何從磁盤中讀取數據到內容中?系統從磁盤讀取數據到內存時是以磁盤塊(block)爲基本單位的,位於同一個磁盤塊中的數據會被一次性讀取出來,而不是需要什麼取什麼。InnoDB 存儲引擎中有頁(Page)的概念,頁是其磁盤管理的最小單位InnoDB 存儲引擎中默認每個頁的大小爲16KB,可通過參數 innodb_page_size 將頁的大小設置爲 4K、8K、16K,在 MySQL 中可通過如下命令查看頁的大小:show variables like 'innodb_page_size';而系統一個磁盤塊的存儲空間往往沒有這麼大,因此InnoDB 每次申請磁盤空間時都會是若干地址連續磁盤塊來達到頁的大小 16KB。InnoDB 在把磁盤數據讀入到磁盤時會以頁爲基本單位,在查詢數據時如果一個頁中的每條數據都能有助於定位數據記錄的位置,這將會減少磁盤I/O次數,提高查詢效率。
    B-Tree 結構的數據可以讓系統高效的找到數據所在的磁盤塊。爲了描述 B-Tree,首先定義一條記錄爲一個二元組[key, data] ,key爲記錄的鍵值,對應表中的主鍵值,data 爲一行記錄中除主鍵外的數據。對於不同的記錄,key值互不相同。
    一棵m階的B-Tree有如下特性:
    1 每個節點最多有m個孩子
    2 除了根節點和葉子節點外,其它每個節點至少有Ceil(m/2)個孩子。
    3 若根節點不是葉子節點,則至少有2個孩子
    4 所有葉子節點都在同一層,且不包含其它關鍵字信息
    5 每個非終端節點包含n個關鍵字信息(P0,P1,…Pn, k1,…kn)
    6 關鍵字的個數n滿足:ceil(m/2)-1 <= n <= m-1
    7 ki(i=1,…n)爲關鍵字,且關鍵字升序排序
    8 Pi(i=1,…n)爲指向子樹根節點的指針。P(i-1)指向的子樹的所有節點關鍵字均小於ki,但都大於k(i-1)
    B-Tree 中的每個節點根據實際情況可以包含大量的關鍵字信息和分支,如下圖所示爲一個 3 階的 B-Tree:
    一個3階B-Tree
    每個節點佔用一個盤塊的磁盤空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指針,指針存儲的是子節點所在磁盤塊的地址。兩個關鍵詞劃分成的三個範圍域對應三個指針指向的子樹的數據的範圍域。以根節點爲例,關鍵字爲17和35,P1指針指向的子樹的數據範圍爲小於17,P2指針指向的子樹的數據範圍爲17~35,P3指針指向的子樹的數據範圍爲大於35。
    模擬查找關鍵字29的過程:
    1 根據根節點找到磁盤塊1,讀入內存。【磁盤I/O操作第1次】
    2 比較關鍵字29在區間(17,35),找到磁盤塊1的指針P2。
    3 根據P2指針找到磁盤塊3,讀入內存。【磁盤I/O操作第2次】
    4 比較關鍵字29在區間(26,30),找到磁盤塊3的指針P2。
    5 根據P2指針找到磁盤塊8,讀入內存。【磁盤I/O操作第3次】
    6 在磁盤塊8中的關鍵字列表中找到關鍵字29。
    分析上面過程,發現需要3次磁盤I/O操作,和3次內存查找操作。由於內存中的關鍵字是一個有序表結構,可以利用二分法查找提高效率。而3次磁盤I/O操作是影響整個B-Tree查找效率的決定因素。B-Tree相對於AVLTree縮減了節點個數,使每次磁盤I/O取到內存的數據都發揮了作用,從而提高了查詢效率。
    2. B+Tree 是在 B-Tree 基礎上的一種優化,使其更適合實現外存儲索引結構,InnoDB 存儲引擎就是用 B+Tree 實現其索引結構。
    從上一節中的B-Tree結構圖中可以看到每個節點中不僅包含數據的key值,還有data值。而每一個頁的存儲空間是有限的,如果data數據較大時將會導致每個節點(即一個頁)能存儲的key的數量很小,當存儲的數據量很大時同樣會導致B-Tree的深度較大,增大查詢時的磁盤I/O次數,進而影響查詢效率。在B+Tree中,所有數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣可以大大加大每個節點存儲的key值數量,降低B+Tree的高度。
    B+Tree相對於B-Tree有幾點不同:
    1 非葉子節點只存儲鍵值信息;
    2 所有葉子節點之間都有一個鏈指針;
    3 數據記錄都存放在葉子節點中
    將上一節中的B-Tree優化,由於B+Tree的非葉子節點只存儲鍵值信息,假設每個磁盤塊能存儲4個鍵值及指針信息,則變成B+Tree後其結構如下圖所示:
    B+Tree
    通常在B+Tree上有兩個頭指針,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即數據節點)之間是一種鏈式環結構。因此可以對B+Tree進行兩種查找運算:一種是對於主鍵的範圍查找和分頁查找,另一種是從根節點開始,進行隨機查找。
    可能上面例子中只有22條數據記錄,看不出B+Tree的優點,下面做一個推算:
    InnoDB存儲引擎中頁的大小爲16KB,一般表的主鍵類型爲INT(佔用4個字節)或BIGINT(佔用8個字節),指針類型也一般爲4或8個字節,也就是說一個頁(B+Tree中的一個節點)中大概存儲16KB/(8B+8B)=1K個鍵值(因爲是估值,爲方便計算,這裏的K取值爲10^3)。也就是說一個深度爲3的B+Tree索引可以維護10^3 * 10^3 * 10^3 = 10億 條記錄。
    實際情況中每個節點可能不能填充滿,因此在數據庫中,B+Tree的高度一般都在2-4層。MySQL的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多隻需要1~3次磁盤I/O操作
    B+Tree性質
    1)通過上面的分析,我們知道IO次數取決於b+數的高度h假設當前數據表的數據爲N,每個磁盤塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁盤塊的大小 / 數據項的大小,磁盤塊的大小也就是一個數據頁的大小,是固定的,如果數據項佔的空間越小,數據項的數量越多,樹的高度越低。這就是爲什麼每個數據項,即索引字段要儘量的小,比如int佔4字節,要比bigint8字節少一半。這也是爲什麼b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。
    2)當b+樹的數據項是複合的數據結構,比如(name,age,sex)的時候,b+樹是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因爲建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪裏查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等於張三的數據都找到,然後再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性
    MyISAM主鍵索引與輔助索引的結構
    MyISAM引擎的索引文件和數據文件是分離的。MyISAM引擎索引結構的葉子節點的數據域,存放的並不是實際的數據記錄,而是數據記錄的地址索引文件與數據文件分離,這樣的索引稱爲"非聚簇索引"。MyISAM的主索引與輔助索引區別並不大,只是主鍵索引不能有重複的關鍵字。
    MYISAM的結構
    在MyISAM中,索引(含葉子節點)存放在單獨的.myi文件中,葉子節點存放的是數據的物理地址偏移量(通過偏移量訪問就是隨機訪問,速度很快)。主索引是指主鍵索引,鍵值不可能重複;輔助索引則是普通索引,鍵值可能重複。通過索引查找數據的流程:先從索引文件中查找到索引節點,從中拿到數據的文件指針,再到數據文件中通過文件指針定位了具體的數據。輔助索引類似。
    InnoDB主鍵索引與輔助索引的結構
    InnoDB引擎索引結構的葉子節點的數據域,存放的就是實際的數據記錄(對於主索引,此處會存放表中所有的數據記錄;對於輔助索引此處會引用主鍵,檢索的時候通過主鍵到主鍵索引中找到對應數據行),或者說,InnoDB的數據文件本身就是主鍵索引文件,這樣的索引被稱爲“聚簇索引”,一個表只能有一個聚簇索引。
    1)主鍵索引:我們知道InnoDB索引是聚集索引,它的索引和數據是存入同一個.idb文件中的,因此它的索引結構是在同一個樹節點中同時存放索引和數據,如下圖中最底層的葉子節點有三行數據,對應於數據表中的id、stu_id、name數據項。
    InnoDB索引結構
    在Innodb中,索引分葉子節點和非葉子節點,非葉子節點就像新華字典的目錄,單獨存放在索引段中,葉子節點則是順序排列的,在數據段中。Innodb的數據文件可以按照表來切分(只需要開啓innodb_file_per_table),切分後存放在xxx.ibd中,默認不切分,存放在xxx.ibdata中。
    輔助(非主鍵)索引:
    這次我們以示例中學生表中的name列建立輔助索引,它的索引結構跟主鍵索引的結構有很大差別,在最底層的葉子結點有兩行數據,第一行的字符串是輔助索引,按照ASCII碼進行排序,第二行的整數是主鍵的值。這就意味着,對name列進行條件搜索,需要兩個步驟:
    ① 在輔助索引上檢索name,到達其葉子節點獲取對應的主鍵;
    ② 使用主鍵在主索引上再進行對應的檢索操作
    這也就是所謂的“回表查詢
    InnoDB輔助索引結構
    InnoDB 索引結構需要注意的點
    1 數據文件本身就是索引文件
    2 表數據文件本身就是按 B+Tree 組織的一個索引結構文件
    3 聚集索引中葉節點包含了完整的數據記錄
    4 InnoDB 表必須要有主鍵,並且推薦使用整型自增主鍵
    正如我們上面介紹 InnoDB 存儲結構,索引與數據是共同存儲的,不管是主鍵索引還是輔助索引,在查找時都是通過先查找到索引節點才能拿到相對應的數據,如果我們
    在設計表結構時沒有顯式指定索引列的話,MySQL 會從表中選擇數據不重複的列建立索引,如果沒有符合的列,則 MySQL 自動爲 InnoDB 表生成一個隱含字段作爲主鍵,並且這個字段長度爲6個字節,類型爲整型。

    Hash索引
    主要就是通過Hash算法(常見的Hash算法有直接定址法、平方取中法、摺疊法、除數取餘法、隨機數法),將數據庫字段數據轉換成定長的Hash值,與這條數據的行指針一併存入Hash表的對應位置;如果發生Hash碰撞(兩個不同關鍵字的Hash值相同),則在對應Hash鍵下以鏈表形式存儲
    ①檢索算法:在檢索查詢時,就再次對待查關鍵字再次執行相同的Hash算法,得到Hash值,到對應Hash表對應位置取出數據即可,如果發生Hash碰撞,則需要在取值時進行篩選。目前使用Hash索引的數據庫並不多,主要有Memory等。
    MySQL目前有Memory引擎和NDB引擎支持Hash索引
    full-text全文索引
    全文索引也是MyISAM的一種特殊索引類型,主要用於全文索引,InnoDB從MYSQL5.6版本提供對全文索引的支持。它用於替代效率較低的LIKE模糊匹配操作,而且可以通過多字段組合的全文索引一次性全模糊匹配多個字段。同樣使用B-Tree存放索引數據,但使用的是特定的算法,將字段數據分割後再進行索引(一般每4個字節一次分割),索引文件存儲的是分割前的索引字符串集合,與分割後的索引信息,對應Btree結構的節點存儲的是分割後的詞信息以及它在分割前的索引字符串集合中的位置
    R-Tree空間索引
    空間索引是MyISAM的一種特殊索引類型,主要用於地理空間數據類型
    問題1:爲什麼Mysql索引要用B+樹不是B樹?
    答:用B+樹不用B樹考慮的是IO對性能的影響,B樹的每個節點都存儲數據,而B+樹只有葉子節點才存儲數據,所以查找相同數據量的情況下,B樹的高度更高,IO更頻繁。數據庫索引是存儲在磁盤上的,當數據量大時,就不能把整個索引全部加載到內存了,只能逐一加載每一個磁盤頁(對應索引樹的節點)。其中在MySQL底層對B+樹進行進一步優化:在葉子節點中是雙向鏈表,且在鏈表的頭結點和尾節點也是循環指向的。
    問題2:爲何不採用Hash方式?
    答:因爲Hash索引底層是哈希表,哈希表是一種以key-value存儲數據的結構,所以多個數據在存儲關係上是完全沒有任何順序關係的,所以,對於區間查詢是無法直接通過索引查詢的,就需要全表掃描。所以,哈希索引只適用於等值查詢的場景。而B+ Tree是一種多路平衡查詢樹,所以他的節點是天然有序的(左子節點小於父節點、父節點小於右子節點),所以對於範圍查詢的時候不需要做全表掃描。哈希索引不支持多列聯合索引的最左匹配規則,如果有大量重複鍵值得情況下,哈希索引的效率會很低,因爲存在哈希碰撞問題

  2. 哪些情況需要創建索引
    答:1 主鍵自動建立唯一索引
    2 頻繁作爲查詢條件的字段
    3 查詢中與其他表關聯的字段,外鍵關係建立索引
    4 單鍵/組合索引的選擇問題,高併發下傾向創建組合索引
    5 查詢中排序的字段,排序字段通過索引訪問大幅提高排序速度
    6 查詢中統計或分組字段
    補充問題:哪些情況不要創建索引?
    1 表記錄太少
    2 經常增刪改的表
    3 數據重複且分佈均勻的表字段,只應該爲最經常查詢和最經常排序的數據列建立索引(如果某個數據類包含太多的重複數據,建立索引沒有太大意義)
    4 頻繁更新的字段不適合創建索引(會加重IO負擔)
    5 where條件裏用不到的字段不創建索引

  3. MySQL覆蓋索引
    答:覆蓋索引(Covering Index),或者叫索引覆蓋, 也就是平時所說的不需要回表操作;就是select的數據列只用從索引中就能夠取得,不必讀取數據行,MySQL可以利用索引返回select列表中的字段,而不必根據索引再次讀取數據文件,換句話說查詢列要被所建的索引覆蓋。索引是高效找到行的一個方法,但是一般數據庫也能使用索引找到一個列的數據,因此它不必讀取整個行。畢竟索引葉子節點存儲了它們索引的數據,當能通過讀取索引就可以得到想要的數據,那就不需要讀取行了。一個索引包含(覆蓋)滿足查詢結果的數據就叫做覆蓋索引
    判斷標準:使用explain,可以通過輸出的extra列來判斷,對於一個索引覆蓋查詢,顯示爲using index,MySQL查詢優化器在執行查詢前會決定是否有索引覆蓋查詢

  4. MySQL中 in和 exists 的區別?
    答:exists:exists對外表用loop逐條查詢,每次查詢都會查看exists的條件語句,當exists裏的條件語句能夠返回記錄行時(無論記錄行是的多少,只要能返回),條件就爲真,返回當前loop到的這條記錄;反之,如果exists裏的條件語句不能返回記錄行,則當前loop到的這條記錄被丟棄,exists的條件就像一個bool條件,當能返回結果集則爲true,不能返回結果集則爲false
    in:in查詢相當於多個or條件的疊加

SELECT * FROM A WHERE A.id IN (SELECT id FROM B);
SELECT * FROM A WHERE EXISTS (SELECT * from B WHERE B.id = A.id);

如果查詢的兩個表大小相當,那麼用in和exists差別不大。如果兩個表中一個較小,一個是大表,則子查詢表大的用exists,子查詢表小的用in:

  1. UNION和UNION ALL的區別?
    答:UNION和UNION ALL都是將兩個結果集合併爲一個,兩個要聯合的SQL語句 字段個數必須一樣,而且字段類型要“相容”(一致);UNION在進行表連接後會篩選掉重複的數據記錄(效率較低),而UNION ALL則不會去掉重複的數據記錄UNION會按照字段的順序進行排序,而UNION ALL只是簡單的將兩個結果合併就返回

  2. SQL執行順序
    答:SQL的執行順序圖如下:SQL執行順序

  3. mysql 的內連接、左連接、右連接有什麼區別?什麼是內連接、外連接、交叉連接、笛卡爾積呢?
    答:MySQL的JOIN圖 MYSQL的JOIN圖

參考資料

  1. https://www.cnblogs.com/kkkky/p/7754383.html
  2. https://blog.csdn.net/jackieeecheng/article/details/69779824
  3. https://blog.csdn.net/JianNingGao/article/details/80351010
  4. https://www.zhihu.com/question/57697842
  5. https://www.cnblogs.com/tongkey/p/8587060.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章