Java面試整理大全

文章目錄

Java

基礎

基本數據類型

序號 數據類型 位數 默認值 取值範圍 舉例說明
1 byte(位) 8 0 -2^7 - 2^7-1 byte b = 10;
2 short(短整數) 16 0 -2^15 - 2^15-1 short s = 10;
3 int(整數) 32 0 -2^31 - 2^31-1 int i = 10;
4 long(長整數) 64 0 -2^63 - 2^63-1 long l = 10l;
5 float(單精度) 32 0.0 -2^31 - 2^31-1 float f = 10.0f;
6 double(雙精度) 64 0.0 -2^63 - 2^63-1 double d = 10.0d;
7 char(字符) 16 0 - 2^16-1 char c = ‘c’;
8 boolean(布爾值) 8 false true、false boolean b = true;

float和double區別

01.在內存中佔有的字節數不同

單精度浮點數在機內存佔4個字節

雙精度浮點數在機內存佔8個字節

02.有效數字位數不同

單精度浮點數有效數字8位

雙精度浮點數有效數字16位

03.數值取值範圍

單精度浮點數的表示範圍:-3.40E+38~3.40E+38

雙精度浮點數的表示範圍:-1.79E+308~-1.79E+308

04.在程序中處理速度不同

一般來說,CPU處理單精度浮點數的速度比處理雙精度浮點數快

如果不聲明,默認小數爲double類型,所以如果要用float的話,必須進行強轉

例如:float a=1.3; 會編譯報錯,正確的寫法 float a = (float)1.3;或者float a = 1.3f;(f或F都可以不區分大小寫)

注意:float是8位有效數字,第7位數字將會四捨五入,所以float會丟失精度

Object 常見方法

public final native Class<?> getClass()//native方法,用於返回當前運行時對象的Class對象,使用了 final關鍵字修飾,故不允許子類重寫。
public native int hashCode() //native方法,用於返回對象的哈希碼,主要使用在哈希表中,比如JDK中的 HashMap。
public boolean equals(Object obj)//用於比較2個對象的內存地址是否相等,String類對該方法進行了重寫用戶 比較字符串的值是否相等。
protected native Object clone() throws CloneNotSupportedException//naitive方法,用於創建並返回 當前對象的一份拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 爲true,x.clone().getClass() == x.getClass() 爲true。Object本身沒有實現Cloneable接口,所以不重寫clone方法並且進行調用的話會發生 CloneNotSupportedException異常。
public String toString()//返回類的名字@實例的哈希碼的16進制的字符串。建議Object所有的子類都重寫這個方 法。
public final native void notify()//native方法,並且不能重寫。喚醒一個在此對象監視器上等待的線程(監視 器相當於就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。
public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒 在此對象監視器上等待的所有線程,而不是一個線程。
public final native void wait(long timeout) throws InterruptedException//native方法,並且不能 重寫。暫停線程的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。
public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos參數, 這個參數表示額外時間(以毫微秒爲單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。
public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等 待,沒有超時時間這個概念
protected void finalize() throws Throwable { }//實例被垃圾回收器回收的時候觸發的操作

Java中數據結構

Java工具包提供了強大的數據結構。在Java中的數據結構主要包括以下幾種接口和類:

  • 枚舉(Enumeration)
  • 位集合(BitSet)
  • 向量(Vector)
  • 棧(Stack)
  • 字典(Dictionary)
  • 哈希表(Hashtable)
  • 屬性(Properties)

Java中異常處理

image-20200213082602822

在 Java 中,所有的異常都有一個共同的祖先java.lang包中的 Throwable類。Throwable: 有兩個重要的子類: Exception(異常) 和 Error(錯誤) ,二者都是 Java 異常處理的重要子類,各自都包含大量子類。

Error(錯誤):是程序無法處理的錯誤,表示運行應用程序中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無 關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,Java虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java虛擬機(JVM)一 般會選擇線程終止。

這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因爲它們在應用程序的控制和 處理能力之 外,而且絕大多數是程序運行時不允許出現的狀況。對於設計合理的應用程序來說,即使確實發生了錯 誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java中,錯誤通過Error的子類描述。

Exception(異常):是程序本身可以處理的異常。Exception 類有一個重要的子類 RuntimeException。 RuntimeException 異常由Java虛擬機拋出。NullPointerException(要訪問的變量沒有引用任何對象時,拋出該 異常)、ArithmeticException(算術運算異常,一個整數除以0時,拋出該異常)和 ArrayIndexOutOfBoundsException (下標越界異常)。

注意:異常和錯誤的區別:異常能被程序本身可以處理,錯誤是無法處理。 Throwable類常用方法

public string getMessage():返回異常發生時的詳細信息
public string toString():返回異常發生時的簡要描述
public string getLocalizedMessage():返回異常對象的本地化信息。使用Throwable的子類覆蓋這個方法,可 以聲稱本地化信息。如果子類沒有覆蓋該方法,則該方法返回的信息與getMessage()返回的結果相同 public void printStackTrace():在控制檯上打印Throwable對象封裝的異常信息

異常處理總結
try 塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。

catch 塊:用於處理try捕獲到的異常。
finally 塊:無論是否捕獲或處理異常,finally塊裏的語句都會被執行。當在try塊或catch塊中遇到return語句 時,finally語句塊將在方法返回之前被執行。

在以下4種特殊情況下,finally塊不會被執行:

  1. 在finally語句塊中發生了異常。
  2. 在前面的代碼中用了System.exit()退出程序。 3. 程序所在的線程死亡。
  3. 關閉CPU。

StackOverFlowError和**OutOfMemoryError

  • StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 異常。
  • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 異常。

訪問控制修飾符

Java中,可以使用訪問控制符來保護對類、變量、方法和構造方法的訪問。Java 支持 4 種不同的訪問權限。

  • default (即默認,什麼也不寫): 在同一包內可見,不使用任何修飾符。使用對象:類、接口、變量、方法。
  • private : 在同一類內可見。使用對象:變量、方法。 注意:不能修飾類(外部類)
  • public : 對所有類可見。使用對象:類、接口、變量、方法
  • protected : 對同一包內的類和所有子類可見。使用對象:變量、方法。 注意:不能修飾類(外部類)

== 與 equals區別

== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象。(基本數據類型比較的是 值,引用數據類型比較的是內存地址)

equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:

情況1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價於通過“==”比較這兩個對象。 情況2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來兩個對象的內容相等;若它們的內容相 等,則返回 true (即,認爲這兩個對象相等)。

舉個例子:

public class test1 {
    public static void main(String[] args) {
} }
String a = new String("ab"); // a 爲一個引用
String b = new String("ab"); // b爲另一個引用,對象的內容一樣 String aa = "ab"; // 放在常量池中
String bb = "ab"; // 從常量池中查找
if (aa == bb) // true
System.out.println("aa==bb"); if (a == b) // false,非同一對象
System.out.println("a==b"); if (a.equals(b)) // true
System.out.println("aEQb"); if (42 == 42.0) { // true
System.out.println("true"); }

String 中的 equals 方法是被重寫過的,因爲 object 的 equals 方法是比較的對象的內存地址,而 String 的 equals 方法比較的是對象的值。
當創建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經存在的值和要創建的值相同的對象,如果有 就把它賦給當前引用。如果沒有就在常量池中重新創建一個 String 對象。

重寫equals方法,爲什麼要重寫hashcode

  1. hashCode是不是重寫需要看業務,開放開發人員可以重寫這個方法,可能有這種情況,比如我們僅僅對比object的部分屬性,就認爲兩者相等,而不對比其其他屬性。
  2. 重寫java object hashCode方法,是爲了在一些算法中避免我們不想要的衝突和碰撞。比如其HashMap,HashSet的使用中。

final 關鍵字的一些總結

final關鍵字主要用在三個地方:變量、方法、類。

  1. 對於一個final變量,如果是基本數據類型的變量,則其數值一旦在初始化之後便不能更改;如果是引用類型的 變量,則在對其初始化之後便不能再讓其指向另一個對象。

  2. 當用final修飾一個類時,表明這個類不能被繼承。final類中的所有成員方法都會被隱式地指定爲final方法。

  3. 使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。

    在早期的Java實現版本中,會將final方法轉爲內嵌調用。但是如果方法過於龐大,可能看不到內嵌調用帶來的 任何性能提升(現在的Java版本已經不需要使用final方法進行這些優化了)。類中所有的private方法都隱式地 指定爲fianl。

String 和 StringBuffer、StringBuilder 的區別是什麼?String 爲什麼是不可變的?

可變性

簡單的來說:String 類中使用 final 關鍵字字符數組保存字符串, private final char value[] ,所以 String 對象是不可變的。而StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串 char[]value 但是沒有用 final 關鍵字修飾,所以這兩種對象都是可變的。

StringBuilder 與 StringBuffer 的構造方法都是調用父類構造方法也就是 AbstractStringBuilder 實現的,大家可以自 行查閱源碼。

AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count;
    AbstractStringBuilder() {
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

線程安全性

String 中的對象是不可變的,也就可以理解爲常量,線程安全。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類,定義了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共 方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 並沒有對 方法進行加同步鎖,所以是非線程安全的。

性能

每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然後將指針指向新的 String 對象。 StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象並改變對象引用。相同情況下使用 StirngBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。

對於三者使用的總結:

  1. 操作少量的數據 = String
  2. 單線程操作字符串緩衝區下操作大量數據 = StringBuilder 3. 多線程操作字符串緩衝區下操作大量數據 = StringBuffer

集合

集合框架底層數據結構

1. List

  • Arraylist: Object數組
  • Vector: Object數組
  • LinkedList: 雙向鏈表(JDK1.6之前爲循環鏈表,JDK1.7取消了循環)

2. Set

  • HashSet(無序,唯一): 基於 HashMap 實現的,底層採用 HashMap 來保存元素
  • LinkedHashSet: LinkedHashSet 繼承於 HashSet,並且其內部是通過 LinkedHashMap 來實現的。有點類似於我們之前說的LinkedHashMap 其內部是基於 HashMap 實現一樣,不過還是有一點點區別的
  • TreeSet(有序,唯一): 紅黑樹(自平衡的排序二叉樹)

3. Map

  • HashMap: JDK1.8之前HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的(“拉鍊法”解決衝突)。JDK1.8以後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間
  • LinkedHashMap: LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基於拉鍊式散列結構即由數組和鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向鏈表,使得上面的結構可以保持鍵值對的插入順序。同時通過對鏈表進行相應的操作,實現了訪問順序相關邏輯。
  • Hashtable: 數組+鏈表組成的,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的
  • TreeMap: 紅黑樹(自平衡的排序二叉樹)

線程安全容器

JDK 提供的這些容器大部分在 java.util.concurrent 包中。

  • **Vector **

  • HashTable

  • ConcurrentHashMap: 線程安全的 HashMap

  • CopyOnWriteArrayList: 線程安全的 List,在讀多寫少的場合性能非常好,遠遠好於 Vector.

  • ConcurrentLinkedQueue: 高效的併發隊列,使用鏈表實現。可以看做一個線程安全的 LinkedList,這是一個非阻塞隊列。

  • BlockingQueue: 這是一個接口,JDK 內部通過鏈表、數組等方式實現了這個接口。表示阻塞隊列,非常適合用於作爲數據共享的通道。

  • ConcurrentSkipListMap: 跳錶的實現。這是一個 Map,使用跳錶的數據結構進行快速查找。

ArrayList和LinkedList區別

  • 底層數據結構:ArrayList是對象數組,所以查詢效率很高,通過下標查詢;LinkedList採用的是雙向鏈表,添加刪除效率很高。兩者皆爲線程不安全。

ArrayList 與 Vector 區別呢?爲什麼要用Arraylist取代Vector呢

Vector類的所有方法都是同步的。可以由兩個線程安全地訪問一個Vector對象、但是一個線程訪問Vector的話代碼要在同步操作上耗費大量的時間。

Arraylist不是同步的,所以在不需要保證線程安全時建議使用Arraylist。

ArrayList擴容機制

  • 初始化時,有三種方式(無參,傳入容量,傳入集合),如果是無參的話,默認是大小爲10的容量
  • add()方法存在擴容機制,grow()方法裏面,每次擴容的大小爲之前的1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1)😉

HashMap 和 HashSet區別

HashSet 底層就是基於 HashMap 實現的。(HashSet 的源碼非常非常少,因爲除了 clone()writeObject()readObject()是 HashSet 自己不得不實現之外,其他方法都是直接調用 HashMap 中的方法。

HashMap HashSet
實現了Map接口 實現Set接口
存儲鍵值對 僅存儲對象
調用 put()向map中添加元素 調用 add()方法向Set中添加元素
HashMap使用鍵(Key)計算Hashcode HashSet使用成員對象來計算hashcode值,對於兩個對象來說hashcode可能相同,所以equals()方法用來判斷對象的相等性,

HashSet如何檢查重複

HashSet通過對象的hashcode值來判斷,如果沒有相同的hashcode,就添加進去。但是如果發現有相同hashcode值的對象,這時會調用equals()方法來檢查hashcode相等的對象是否真的相同。如果兩者相同,就無法添加。

HashMap底層實現

數組+鏈表,1.8

如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過鏈表解決衝突。

JDK1.8之後,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減少搜索時間。

JDK1.8 之前 HashMap 底層是 數組和鏈表 結合在一起使用也就是 鏈表散列。HashMap 通過 key 的 hashCode 經 過擾動函數處理過後得到 hash 值,然後通過 判斷當前元素存放的位置(這裏的 n 指的是數組的 長度),如果當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的 話,直接覆蓋,不相同就通過拉鍊法解決衝突。

所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是爲了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數之後可以減少碰撞。

HasMap擴容

當HashMap中的元素個數超過數組大小(數組總大小length,不是數組中個數size)loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,這是一個折中的取值。也就是說,默認情況下,數組大小爲16,那麼當HashMap中元素個數超過160.75=12(這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把數組的大小擴展爲 2*16=32,即擴大一倍,然後重新計算每個元素在數組中的位置。

0.75這個值成爲負載因子,那麼爲什麼負載因子爲0.75呢?這是通過大量實驗統計得出來的,如果過小,比如0.5,那麼當存放的元素超過一半時就進行擴容,會造成資源的浪費;如果過大,比如1,那麼當元素滿的時候才進行擴容,會使get,put操作的碰撞機率增加。

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果當前的數組長度已經達到最大值,則不在進行調整
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //根據傳入參數的長度定義新的數組
    Entry[] newTable = new Entry[newCapacity];
    //按照新的規則,將舊數組中的元素轉移到新數組中
    transfer(newTable);
    table = newTable;
    //更新臨界值
    threshold = (int)(newCapacity * loadFactor);
}
//舊數組中元素往新數組中遷移
void transfer(Entry[] newTable) {
    //舊數組
    Entry[] src = table;
    //新數組長度
    int newCapacity = newTable.length;
    //遍歷舊數組
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);//放在新數組中的index位置
                e.next = newTable[i];//實現鏈表結構,新加入的放在鏈頭,之前的的數據放在鏈尾
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

HashMap 的長度爲什麼是2的冪次方

爲了存取效率高,儘量較少碰撞,數據分配均勻。我們上面也講到了過了,Hash 值的範圍值-2147483648到2147483647,前後加起來大概40億的映射空間,只要哈希函數映射得比較均勻鬆散,一般應用是很難出現碰撞的。

HashMap中hash算法

(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)

HashMap 多線程操作導致死循環問題

併發下的Rehash 會造成元素之間會形成一個循環鏈表。不過,jdk 1.8 後解決了這個問題,但是還是不建議在多線程下使用 HashMap,因爲多線程下使用 HashMap 還是會存在其他問題比如數據丟失。併發環境下推薦使用 ConcurrentHashMap

HashMap和HashTable、ConcurrentMap區別

  • HashMap線程不安全
  • HashTable線程安全,添加了同步鎖
  • ConcurrentMap線程安全,ReentrantLock實現線程安全

ConcurrentHashMap 和 Hashtable 的區別

ConcurrentHashMap 和 Hashtable 的區別是線程安全實現不同。

  • 底層數據結構: JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構一樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 之前的 HashMap 的底層數據結構類似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
  • 實現線程安全的方式(重要):在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對數組進行了分割分段(Segment)處理,每個鎖,只鎖部分數據,多線程訪問不同分段段的數據,就不會存在鎖競爭,提高併發訪問率。 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操作。(JDK1.6以後 對 synchronized鎖做了很多優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率非常低下。當一個線程訪問同步方法時,其他線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另一個線程不能使用 put 添加元素,也不能使用 get,競爭會越來越激烈效率越低。

ConcurrentHashMap線程安全的具體實現方式/底層具體實

首先將數據分爲一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成

Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment的鎖。

Java併發

爲什麼使用多線程

  • 提高程序效率
  • 充分利用cpu資源

線程安全

多個線程訪問同一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他操作,調用這個對象的行爲都可以獲得正確的結果,那麼這個對象就是線程安全的。

或者說:一個類或者程序所提供的接口對於線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性,也就是說我們不用考慮同步的問題。

線程安全問題大多是由全局變量靜態變量引起的,局部變量逃逸也可能導致線程安全問題。

若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

類要成爲線程安全的,首先必須在單線程環境中有正確的行爲。如果一個類實現正確(這是說它符合規格說明的另一種方式),那麼沒有一種對這個類的對象的操作序列(讀或者寫公共字段以及調用公共方法)可以讓對象處於無效狀態,觀察到對象處於無效狀態、或者違反類的任何不可變量、前置條件或者後置條件的情況。

此外,一個類要成爲線程安全的,在被多個線程訪問時,不管運行時環境執行這些線程有什麼樣的時序安排或者交錯,它必須仍然有如上所述的正確行爲,並且在調用的代碼中沒有任何額外的同步。其效果就是,在所有線程看來,對於線程安全對象的操作是以固定的、全局一致的順序發生的。

正確性與線程安全性之間的關係非常類似於在描述 ACID(原子性、一致性、獨立性和持久性)事務時使用的一致性與獨立性之間的關係:從特定線程的角度看,由不同線程所執行的對象操作是先後(雖然順序不定)而不是並行執行的。

線程狀態

NEW          ---- 創建了線程對象但尚未調用start()方法時的狀態。
RUNNABLE     ---- 線程對象調用start()方法後,線程處於可運行狀態,此時線程等待獲取CPU執行權。
BLOCKED      ---- 線程等待獲取鎖時的狀態。
WAITING      ---- 線程處於等待狀態,處於該狀態標識當前線程需要等待其他線程做出一些特定的操作喚醒自己。
TIME_WAITING ---- 超時等待狀態,與WAITING不同,在等待指定的時間後會自行返回。
TERMINATED   ---- 終止狀態,表示當前線程已執行完畢。

image-20200210180346891

image-20200210180730589

1、當線程調用了自身的sleep()方法或其他線程的join()方法,就會進入阻塞狀態(該狀態既停止當前線程,但並不釋放所佔有的資源) 。當sleep()結束或join()結束後,該線程進入可運行狀態,繼續等待OS分配時間片;
2、線程調用了yield()方法,意思是放棄當前獲得的CPU時間片,回到可運行狀態 ,這時與其他進程處於同等競爭狀態,OS有可能會接着又讓這個進程進入運行狀態;
3、當線程剛進入可運行狀態(即就緒狀態),發現將要調用的資源被synchroniza(同步),獲取不到鎖標記,將會立即進入鎖池狀態,等待獲取鎖標記(這時的鎖池裏也許已經有了其他線程在等待獲取鎖標記,這時它們處於隊列狀態,既先到先得),一旦線程獲得鎖標記後,就轉入可運行狀態,等待 OS分配CPU時間片

​ Wait()方法和notify()方法:當一個線程執行到wait()方法時,它就進入到一個和該對象相關的等待池中,同時失去了對象的鎖。當它被一個notify()方法喚醒時,等待池中的線程就被放到了鎖池中。該線程從鎖池中獲得鎖,然後回到wait()前的中斷現場
4、當線程調用wait()方法後會進入等待隊列(進入這個狀態會釋放所佔有的所有資源,與阻塞狀態不同),進入這個狀態後,是不能自動喚醒的,必須依靠其他線程調用notify()或notifyAll()方法才能被喚醒 ( wait(1000)時可以自動喚醒 ) (由於notify()只是喚醒一個線程,但我們由不能確定具體喚醒的是哪一個線程,也許我們需要喚醒的線程不能夠被喚醒,因此在實際使用時,一般都用notifyAll()方法,喚醒有所線程),線程被喚醒後會進入鎖池,等待獲取鎖標記。

使用線程池的好處

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

Executor 框架

框架組成

  • 任務(Runnable /Callable)

執行任務需要實現的 Runnable 接口Callable接口Runnable 接口Callable 接口 實現類都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。

  • 任務的執行(Executor)

如下圖所示,包括任務執行機制的核心接口 Executor ,以及繼承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutorScheduledThreadPoolExecutor 這兩個關鍵類實現了 ExecutorService 接口

這裏提了很多底層的類關係,但是,實際上我們需要更多關注的是 ThreadPoolExecutor 這個類,這個類在我們實際使用線程池的過程中,使用頻率還是非常高的。

注意: 通過查看 ScheduledThreadPoolExecutor 源代碼我們發現 ScheduledThreadPoolExecutor 實際上是繼承了 ThreadPoolExecutor 並實現了 ScheduledExecutorService ,而 ScheduledExecutorService 又實現了 ExecutorService,正如我們下面給出的類關係圖顯示的一樣。

ThreadPoolExecutor 類描述:

//AbstractExecutorService實現了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor 類描述:

//ScheduledExecutorService實現了ExecutorService接口
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService
  • 異步計算的結果(Future)

Future 接口以及 Future 接口的實現類 FutureTask 類都可以代表異步計算的結果。

當我們把 Runnable接口Callable 接口 的實現類提交給 ThreadPoolExecutorScheduledThreadPoolExecutor 執行。(調用 submit() 方法時會返回一個 FutureTask 對象)

Executor 使用流程

  1. 主線程首先要創建實現 Runnable 或者 Callable 接口的任務對象。
  2. 把創建完成的實現 Runnable/Callable接口的 對象直接交給 ExecutorService 執行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 對象或Callable 對象提交給 ExecutorService 執行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable task))。
  3. 如果執行 ExecutorService.submit(…)ExecutorService 將返回一個實現Future接口的對象(我們剛剛也提到過了執行 execute()方法和 submit()方法的區別,submit()會返回一個 FutureTask 對象)。由於 FutureTask 實現了 Runnable,我們也可以創建 FutureTask,然後直接交給 ExecutorService 執行。
  4. 最後,主線程可以執行 FutureTask.get()方法來等待任務執行完成。主線程也可以執行 FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

ThreadPoolExecutor

ThreadPoolExecutor 類中提供的四個構造方法。我們來看最長的那個,其餘三個都是在這個構造方法的基礎上產生(其他幾個構造方法說白點都是給定某些默認參數的構造方法比如默認制定拒絕策略是什麼),這裏就不貼代碼講了,比較簡單。

    /**
     * 用給定的初始參數創建一個新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量
                              int maximumPoolSize,//線程池的最大線程數
                              long keepAliveTime,//當線程數大於核心線程數時,多餘的空閒線程存活的最長時間
                              TimeUnit unit,//時間單位
                              BlockingQueue<Runnable> workQueue,//任務隊列,用來儲存等待執行任務的隊列
                              ThreadFactory threadFactory,//線程工廠,用來創建線程,一般默認即可
                              RejectedExecutionHandler handler//拒絕策略,當提交的任務過多而不能及時處理時,我們可以定製策略來處理任務
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面這些對創建 非常重要,在後面使用線程池的過程中你一定會用到!所以,務必拿着小本本記清楚。

ThreadPoolExecutor 3 個最重要的參數:

  • corePoolSize : 核心線程數線程數定義了最小可以同時運行的線程數量。
  • maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變爲最大線程數。
  • workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,任務就會被存放在隊列中。

ThreadPoolExecutor其他常見參數:

  1. keepAliveTime:當線程池中的線程數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime纔會被回收銷燬;
  2. unit : keepAliveTime 參數的時間單位。
  3. threadFactory :executor 創建新線程的時候會用到。
  4. handler :飽和策略。關於飽和策略下面單獨介紹一下。

ThreadPoolExecutor 飽和策略定義:

如果當前同時運行的線程數量達到最大線程數量並且隊列也已經被放滿了任務時,ThreadPoolTaskExecutor 定義一些策略:

  • ThreadPoolExecutor.AbortPolicy:拋出 RejectedExecutionException異常。
  • ThreadPoolExecutor.CallerRunsPolicy:提交線程的任務,自己執行。調用執行自己的線程運行任務,也就是直接在調用execute方法的線程中運行(run)被拒絕的任務,如果執行程序已關閉,則會丟棄該任務。因此這種策略會降低對於新任務提交速度,影響程序的整體性能。另外,這個策略喜歡增加隊列容量。如果您的應用程序可以承受此延遲並且你不能任務丟棄任何一個任務請求的話,你可以選擇這個策略。
  • ThreadPoolExecutor.DiscardPolicy 不處理新任務,直接丟棄掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略將丟棄隊列中最靠前的任務,然後執行此任務。

ThreadPoolExecutor 使用示例

首先創建一個 Runnable 接口的實現類(當然也可以是 Callable 接口,我們上面也說了兩者的區別。)

MyRunnable.java
import java.util.Date;

/**
 * 這是一個簡單的Runnable類,需要大約5秒鐘來執行其任務。
 * @author shuang.kou
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

編寫測試程序,我們這裏以阿里巴巴推薦的使用 ThreadPoolExecutor 構造函數自定義參數的方式來創建線程池。

ThreadPoolExecutorDemo.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推薦的創建線程池的方式
        //通過ThreadPoolExecutor構造函數自定義參數創建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //創建WorkerThread對象(WorkerThread類實現了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //執行Runnable
            executor.execute(worker);
        }
        //終止線程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

可以看到我們上面的代碼指定了:

  1. corePoolSize: 核心線程數爲 5。
  2. maximumPoolSize :最大線程數 10
  3. keepAliveTime : 等待時間爲 1L。
  4. unit: 等待時間的單位爲 TimeUnit.SECONDS。
  5. workQueue:任務隊列爲 ArrayBlockingQueue,並且容量爲 100;
  6. handler:飽和策略爲 CallerRunsPolicy

Output:

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

Runnable vs Callable

Runnable 接口不會返回結果或拋出檢查異常,但是**Callable 接口**可以。所以,如果任務不需要返回結果或拋出異常推薦使用 Runnable 接口,這樣代碼看起來會更加簡潔。

工具類 Executors 可以實現 Runnable 對象和 Callable 對象之間的相互轉換。(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Object resule))。

Runnable.java
@FunctionalInterface
public interface Runnable {
   /**
    * 被線程執行,沒有返回值也無法拋出異常
    */
    public abstract void run();
}
Callable.java
@FunctionalInterface
public interface Callable<V> {
    /**
     * 計算結果,或在無法這樣做時拋出異常。
     * @return 計算得出的結果
     * @throws 如果無法計算結果,則拋出異常
     */
    V call() throws Exception;
}

execute() vs submit()

  1. execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;
  2. submit()方法用於提交需要返回值的任務。線程池會返回一個 Future 類型的對象,通過這個 Future 對象可以判斷任務是否執行成功,並且可以通過 Futureget()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

我們以**AbstractExecutorService**接口中的一個 submit 方法爲例子來看看源代碼:

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

上面方法調用的 newTaskFor 方法返回了一個 FutureTask 對象。

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

我們再來看看execute()方法:

    public void execute(Runnable command) {
      ...
    }

shutdown() vs shutdownNow()

  • shutdown() :關閉線程池,線程池的狀態變爲 SHUTDOWN。線程池不再接受新任務了,但是隊列裏的任務得執行完畢。
  • shutdownNow() :關閉線程池,線程的狀態變爲 STOP。線程池會終止當前正在運行的任務,並停止處理排隊的任務並返回正在等待執行的 List。

isTerminated() vs isShutdown()

  • isShutDown 當調用 shutdown() 方法後返回爲 true。
  • isTerminated 當調用 shutdown() 方法後,並且所有提交的任務完成後返回爲 true

爲什麼不推薦使用FixedThreadPool

FixedThreadPool 使用無界隊列 LinkedBlockingQueue(隊列的容量爲 Intger.MAX_VALUE)作爲線程池的工作隊列會對線程池帶來如下影響 :

  1. 當線程池中的線程數達到 corePoolSize 後,新任務將在無界隊列中等待,因此線程池中的線程數不會超過 corePoolSize;
  2. 由於使用無界隊列時 maximumPoolSize 將是一個無效參數,因爲不可能存在任務隊列滿的情況。所以,通過創建 FixedThreadPool的源碼可以看出創建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被設置爲同一個值。
  3. 由於 1 和 2,使用無界隊列時 keepAliveTime 將是一個無效參數;
  4. 運行中的 FixedThreadPool(未執行 shutdown()shutdownNow())不會拒絕任務,在任務比較多的時候會導致 OOM(內存溢出)。

線程池大小確定

有一個簡單並且適用面比較廣的公式:

  • CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數設置爲 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是爲了防止線程偶發的缺頁中斷,或者其它原因導致的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閒狀態,而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閒時間。
  • I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。

線程創建的方式

  • 繼承Thread類
  • 實現Runnable接口
  • Excutor創建一個線程的線程池

線程池的創建方式

  • newCachedThreadPool(可以自動擴展或者回收)
  • newFixedThreadPool (設定固定長度線程池,超出則會放在隊列鍾等待)
  • newScheduledThreadPool(創建一定長度線程,週期性執行任務)
  • newSingleThreadExecutor(創建單個線程的線程池)

java中有哪些鎖

公平鎖、非公平鎖、可重入鎖、自旋鎖、獨佔鎖、共享鎖

  • 公平鎖:是指多個線程按照申請的順序來獲取值

  • 非公平鎖:是值多個線程獲取值的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖,在高並 發的情況下,可能會造成優先級翻轉或者飢餓現象

    區別

    公平鎖:在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一 個就佔有鎖,否者就會加入到等待隊列中,以後會按照 FIFO 的規則獲取鎖

    非公平鎖:一上來就嘗試佔有鎖,如果失敗在進行排隊

  • 重入鎖(遞歸鎖):指的是同一個線程外層函數獲得鎖之後,內層仍然能獲取到該鎖的代碼,在同一個線程在外層方法獲取鎖的時候,在進入內層方法或會自動獲取該鎖。也就是說,線程可以進入任何一個他已經擁有的鎖的同步代碼塊。ReentrantLook / Synchronized就是一個典型的可重入鎖,作用:防止死鎖

  • synchronized

    1. 是一個關鍵字
    2. 程序執行完畢之後纔會釋放鎖
  • lock

    1. 是一個接口,其中實現就有 ReentrantLock
    2. 手動釋放鎖

synchronized關鍵字最主要的三種使用方式

  • 修飾實例方法,作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
  • 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖 。也就是給當前類加鎖,會作 用於類的所有對象實例,因爲靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態 資源,不管new了多少個對象,只有一份,所以對該類的所有對象都加了鎖)。所以如果一個線程A調用一個實 例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允 許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。 和 synchronized 方 法一樣,synchronized(this)代碼塊也是鎖定當前對象的。synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。這裏再提一下:synchronized關鍵字加到非 static 靜態 方法上是給對象實例上鎖。另外需要注意的是:儘量不要使用 synchronized(String a) 因爲JVM中,字符串常量 池具有緩衝功能!

synchronized和lock區別

1.首先synchronized是java內置關鍵字,在jvm層面,Lock是個java類;

2.synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;

3.synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;

4.用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;

5.synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)

6.Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。

原理區別

Lock鎖使用的是CAS和volatile來實現同步的,CAS使用硬件命令實現緩存一致性保證了原子性,volatile保證了可見性,所線程環境下所有的線程通過CAS進行競爭資源,只能有一個成功,其它的都會自旋

Volatile 變量

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,但是隻能應用於非常有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。因此,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具有與多個變量相關的不變式(Invariants)的類(例如 “start <=end”)。

  • 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。(實現可見性)
  • 禁止進行指令重排序。(實現有序性)
  • volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。

悲觀鎖與樂觀鎖

樂觀鎖對應於生活中樂觀的人總是想着事情往好的方向發展,悲觀鎖對應於生活中悲觀的人總是想着事情往壞的方向發展。。

悲觀鎖

總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完後再把資源轉讓給其它線程)。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronizedReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

兩種鎖的使用場景

從上面對兩種鎖的介紹,我們知道兩種鎖各有優缺點,不可認爲一種好於另一種,像樂觀鎖適用於寫比較少的情況下(多讀場景),即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果是多寫的情況,一般會經常產生衝突,這就會導致上層應用會不斷的進行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。

樂觀鎖常見的兩種實現方式

版本號機制

一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

CAS算法

compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數

  • 需要讀寫的內存值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試

CAS與synchronized的使用情景

簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)

  1. 對於資源競爭較少(線程衝突較輕)的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋機率較少,因此可以獲得更高的性能。
  2. 對於資源競爭嚴重(線程衝突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。

IO

同步與異步

  • 同步: 同步就是發起一個調用後,被調用者未處理完請求之前,調用不返回。
  • 異步: 異步就是發起一個調用後,立刻得到被調用者的迴應表示已接收到請求,但是被調用者並沒有返回結果,此時我們可以處理其他的請求,被調用者通常依靠事件,回調等機制來通知調用者其返回結果。

同步和異步的區別最大在於異步的話調用者不需要等待處理結果,被調用者會通過回調等機制來通知調用者其返回結果。

阻塞和非阻塞

  • 阻塞: 阻塞就是發起一個請求,調用者一直等待請求結果返回,也就是當前線程會被掛起,無法從事其他任務,只有當條件就緒才能繼續。
  • 非阻塞: 非阻塞就是發起一個請求,調用者不用一直等着結果返回,可以先去幹其他事情。

舉個生活中簡單的例子,你媽媽讓你燒水,小時候你比較笨啊,在那裏傻等着水開(同步阻塞)。等你稍微再長大一點,你知道每次燒水的空隙可以去幹點其他事,然後只需要時不時來看看水開了沒有(同步非阻塞)。後來,你們家用上了水開了會發出聲音的壺,這樣你就只需要聽到響聲後就知道水開了,在這期間你可以隨便幹自己的事情,你需要去倒水了(異步非阻塞)。

BIO (Blocking I/O)

同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。

示例:

客戶端

/**
 * 
 * @author 閃電俠
 * @date 2018年10月14日
 * @Description:客戶端
 */
public class IOClient {

  public static void main(String[] args) {
    // TODO 創建多個線程,模擬多個客戶端連接服務端
    new Thread(() -> {
      try {
        Socket socket = new Socket("127.0.0.1", 3333);
        while (true) {
          try {
            socket.getOutputStream().write((new Date() + ": hello world").getBytes());
            Thread.sleep(2000);
          } catch (Exception e) {
          }
        }
      } catch (IOException e) {
      }
    }).start();

  }

}

服務端

/**
 * @author 閃電俠
 * @date 2018年10月14日
 * @Description: 服務端
 */
public class IOServer {

  public static void main(String[] args) throws IOException {
    // TODO 服務端處理客戶端連接請求
    ServerSocket serverSocket = new ServerSocket(3333);

    // 接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理
    new Thread(() -> {
      while (true) {
        try {
          // 阻塞方法獲取新的連接
          Socket socket = serverSocket.accept();

          // 每一個新的連接都創建一個線程,負責讀取數據
          new Thread(() -> {
            try {
              int len;
              byte[] data = new byte[1024];
              InputStream inputStream = socket.getInputStream();
              // 按字節流方式讀取數據
              while ((len = inputStream.read(data)) != -1) {
                System.out.println(new String(data, 0, len));
              }
            } catch (IOException e) {
            }
          }).start();

        } catch (IOException e) {
        }

      }
    }).start();

  }

}

在活動連接數不是特別高(小於單機1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注於自己的 I/O 並且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩衝一些系統處理不了的連接或請求。但是,當面對十萬甚至百萬級連接的時候,傳統的 BIO 模型是無能爲力的。因此,我們需要一種更高效的 I/O 處理模型來應對更高的併發量。

NIO (New I/O)

NIO是一種同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,對應 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N可以理解爲Non-blocking,不單純是New。它支持面向緩衝的,基於通道的I/O操作方法。 NIO提供了與傳統BIO模型中的 SocketServerSocket 相對應的 SocketChannelServerSocketChannel 兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對於低負載、低併發的應用程序,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對於高負載、高併發的(網絡)應用,應使用 NIO 的非阻塞模式來開發。

代碼示例:

/**
 * 
 * @author 閃電俠
 * @date 2019年2月21日
 * @Description: NIO 改造後的服務端
 */
public class NIOServer {
  public static void main(String[] args) throws IOException {
    // 1. serverSelector負責輪詢是否有新的連接,服務端監測到新的連接之後,不再創建一個新的線程,
    // 而是直接將新連接綁定到clientSelector上,這樣就不用 IO 模型中 1w 個 while 循環在死等
    Selector serverSelector = Selector.open();
    // 2. clientSelector負責輪詢連接是否有數據可讀
    Selector clientSelector = Selector.open();

    new Thread(() -> {
      try {
        // 對應IO編程中服務端啓動
        ServerSocketChannel listenerChannel = ServerSocketChannel.open();
        listenerChannel.socket().bind(new InetSocketAddress(3333));
        listenerChannel.configureBlocking(false);
        listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

        while (true) {
          // 監測是否有新的連接,這裏的1指的是阻塞的時間爲 1ms
          if (serverSelector.select(1) > 0) {
            Set<SelectionKey> set = serverSelector.selectedKeys();
            Iterator<SelectionKey> keyIterator = set.iterator();

            while (keyIterator.hasNext()) {
              SelectionKey key = keyIterator.next();

              if (key.isAcceptable()) {
                try {
                  // (1) 每來一個新連接,不需要創建一個線程,而是直接註冊到clientSelector
                  SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                  clientChannel.configureBlocking(false);
                  clientChannel.register(clientSelector, SelectionKey.OP_READ);
                } finally {
                  keyIterator.remove();
                }
              }

            }
          }
        }
      } catch (IOException ignored) {
      }
    }).start();
    new Thread(() -> {
      try {
        while (true) {
          // (2) 批量輪詢是否有哪些連接有數據可讀,這裏的1指的是阻塞的時間爲 1ms
          if (clientSelector.select(1) > 0) {
            Set<SelectionKey> set = clientSelector.selectedKeys();
            Iterator<SelectionKey> keyIterator = set.iterator();

            while (keyIterator.hasNext()) {
              SelectionKey key = keyIterator.next();

              if (key.isReadable()) {
                try {
                  SocketChannel clientChannel = (SocketChannel) key.channel();
                  ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                  // (3) 面向 Buffer
                  clientChannel.read(byteBuffer);
                  byteBuffer.flip();
                  System.out.println(
                      Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                } finally {
                  keyIterator.remove();
                  key.interestOps(SelectionKey.OP_READ);
                }
              }

            }
          }
        }
      } catch (IOException ignored) {
      }
    }).start();

  }
}

NIO的特性/NIO與IO區別

1. IO流是阻塞的,NIO流是不阻塞的。

Java NIO使我們可以進行非阻塞IO操作。比如說,單線程中從通道讀取數據到buffer,同時可以繼續做別的事情,當數據讀取到buffer中後,線程再繼續處理數據。寫數據也是一樣的。另外,非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。

Java IO的各種流是阻塞的。這意味着,當一個線程調用 read()write() 時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了

2. Buffer(緩衝區)

IO 面向流(Stream oriented),而 NIO 面向緩衝區(Buffer oriented)。

Buffer是一個對象,它包含一些要寫入或者要讀出的數據。在NIO類庫中加入Buffer對象,體現了新庫與原I/O的一個重要區別。在面向流的I/O中·可以將數據直接寫入或者將數據直接讀到 Stream 對象中。雖然 Stream 中也有 Buffer 開頭的擴展類,但只是流的包裝類,還是從流讀到緩衝區,而 NIO 卻是直接讀到 Buffer 中進行操作。

在NIO厙中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的; 在寫入數據時,寫入到緩衝區中。任何時候訪問NIO中的數據,都是通過緩衝區進行操作。

最常用的緩衝區是 ByteBuffer,一個 ByteBuffer 提供了一組功能用於操作 byte 數組。除了ByteBuffer,還有其他的一些緩衝區,事實上,每一種Java基本類型(除了Boolean類型)都對應有一種緩衝區。

3. Channel (通道)

NIO 通過Channel(通道) 進行讀寫。

通道是雙向的,可讀也可寫,而流的讀寫是單向的。無論讀寫,通道只能和Buffer交互。因爲 Buffer,通道可以異步地讀寫。

4. Selector (選擇器)

NIO有選擇器,而IO沒有。

選擇器用於使用單個線程處理多個通道。因此,它需要較少的線程來處理這些通道。線程之間的切換對於操作系統來說是昂貴的。 因此,爲了提高系統效率選擇器是有用的。

NIO 讀數據和寫數據方式

通常來說NIO中的所有IO都是從 Channel(通道) 開始的。

  • 從通道進行數據讀取 :創建一個緩衝區,然後請求通道讀取數據。
  • 從通道進行數據寫入 :創建一個緩衝區,填充數據,並要求通道寫入數據。

AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的IO模型。異步 IO 是基於事件和回調機制實現的,也就是應用操作之後會直接返回,不會堵塞在那裏,當後臺處理完成,操作系統會通知相應的線程進行後續的操作。

AIO 是異步IO的縮寫,雖然 NIO 在網絡操作中,提供了非阻塞的方法,但是 NIO 的 IO 行爲還是同步的。對於 NIO 來說,我們的業務線程是在 IO 操作準備好時,得到通知,接着就由這個線程自行進行 IO 操作,IO操作本身是同步的。

Java註解

自定義註解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  • Target: 註解作用的位置,可以是方法,類,或者屬性上面

  • Retention: 註解的生命週期

  • Inherited: 是否允許子類繼承註解

    Java註解實現原理

​ 通過反射機制,和動態代理機制

Java動態代理

實現原理

通過攔截器和反射技術實現

  1. 需要實現InvocationHandler 接口
  2. Proxy來創建實例

cglib動態代理區別

cglib是通過修改字節碼生成子類處理

  1. JDK動態代理只提供接口的代理,不支持類的代理。核心InvocationHandler接口和Proxy類,InvocationHandler 通過invoke()方法反射來調用目標類中的代碼,動態地將橫切邏輯和業務編織在一起;接着,Proxy利用 InvocationHandler動態創建一個符合某一接口的的實例, 生成目標類的代理對象。

  2. 如果代理類沒有實現 InvocationHandler 接口,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個代碼生成的類庫,可以在運行時動態的生成指定類的一個子類對象,並覆蓋其中特定方法並添加增強代碼,從而實現AOP。CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記爲final,那麼它是無法使用CGLIB做動態代理的。

JVM

JVM內存模型

  • 方法區(Method Area): 靜態變量,常量,類,共享區域。

    與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。

    Java 內存可以粗糙的區分爲堆內存(Heap)和棧內存 (Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。 (實際上,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。)

    局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。

    Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 異常。
    • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 異常。

    Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨着線程的創建而創建,隨着線程的死亡而死亡。

  • 堆(Heap):存放實例,數組,垃圾回收。

    Java 虛擬機所管理的內存中最大的一塊,Java 堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這裏分配內存。

    Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以 Java 堆還可以細分爲:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

    上圖所示的 eden 區、s0 區、s1 區都屬於新生代,tentired 區屬於老年代。大部分情況,對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果對象還存活,則會進入 s0 或者 s1,並且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增加到一定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。

  • 棧(Stack): 每個線程一個棧區,用於基本類型,及對象的應用。

    與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是 Java 方法執行的內存模型,每次方法調用的數據都是通過棧傳遞的。

    Java 內存可以粗糙的區分爲堆內存(Heap)和棧內存 (Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分。 (實際上,Java 虛擬機棧是由一個個棧幀組成,而每個棧幀中都擁有:局部變量表、操作數棧、動態鏈接、方法出口信息。)

    局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)。

    Java 虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

    • StackOverFlowError: 若 Java 虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出 StackOverFlowError 異常。
    • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出 OutOfMemoryError 異常。

    Java 虛擬機棧也是線程私有的,每個線程都有各自的 Java 虛擬機棧,而且隨着線程的創建而創建,隨着線程的死亡而死亡。

    擴展:那麼方法/函數如何調用?

    Java 棧可用類比數據結構中棧,Java 棧中保存的主要內容是棧幀,每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束後,都會有一個棧幀被彈出。

    Java 方法有兩種返回方式:

    1. return 語句。
    2. 拋出異常。

    不管哪種返回方式都會導致棧幀被彈出。

  • 本地方法棧

    和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。

    本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用於存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。

    方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。

  • 程序寄存器(ProgramCountRegister): 記錄每個線程的位置

    程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。

    另外,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

    從上面的介紹中我們知道程序計數器主要有兩個作用:

    1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
    2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

    注意:程序計數器是唯一一個不會出現 OutOfMemoryError 的內存區域,它的生命週期隨着線程的創建而創建,隨着線程的結束而死亡。

常用參數

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小

-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen

相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入方法區後就“永久存在”了。

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

下面是一些常用參數:

-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小

與永久代很大的不同就是,如果不指定大小的話,隨着更多類的創建,虛擬機會耗盡所有可用的系統內存。

爲什麼要將永久代 (PermGen) 替換爲元空間 (MetaSpace)

永久代有一個 JVM 本身設置固定大小上限,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,並且永遠不會得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize 標誌設置最大元空間大小,默認值爲 unlimited,這意味着它只受系統內存的限制。-XX:MetaspaceSize 調整標誌定義元空間的初始大小如果未指定此標誌,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。

JVM如何加載類

類加載過程

Class 文件需要加載到虛擬機中之後才能運行和使用,那麼虛擬機是如何加載這些 Class 文件呢?

系統加載 Class 類型的文件主要三步:加載->連接->初始化。連接過程又可分爲三步:驗證->準備->解析

image-20200208121103340

加載

類加載過程的第一步,主要完成下面3件事情:

  1. 通過全類名獲取定義此類的二進制字節流
  2. 將字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構
  3. 在內存中生成一個代表該類的 Class 對象,作爲方法區這些數據的訪問入口

虛擬機規範多上面這3點並不具體,因此是非常靈活的。比如:“通過全類名獲取定義此類的二進制字節流” 並沒有指明具體從哪裏獲取、怎樣獲取。比如:比較常見的就是從 ZIP 包中讀取(日後出現的JAR、EAR、WAR格式的基礎)、其他文件生成(典型應用就是JSP)等等。

一個非數組類的加載階段(加載階段獲取類的二進制字節流的動作)是可控性最強的階段,這一步我們可以去完成還可以自定義類加載器去控制字節流的獲取方式(重寫一個類加載器的 loadClass() 方法)。數組類型不通過類加載器創建,它由 Java 虛擬機直接創建。

類加載器、雙親委派模型也是非常重要的知識點,這部分內容會在後面的文章中單獨介紹到。

加載階段和連接階段的部分內容是交叉進行的,加載階段尚未結束,連接階段可能就已經開始了。

驗證

image-20200208121252589

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:

  1. 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
  2. 這裏所設置的初始值"通常情況"下是數據類型默認的零值(如0、0L、null、false等),比如我們定義了public static int value=111 ,那麼 value 變量在準備階段的初始值就是 0 而不是111(初始化階段纔會賦值)。特殊情況:比如給 value 變量加上了 fianl 關鍵字public static final int value=111 ,那麼準備階段 value 的值就被賦值爲 111。

解析

解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符7類符號引用進行。

符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。在程序實際運行時,只有符號引用是不夠的,舉個例子:在程序執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機爲每個類都準備了一張方法表來存放類中所有的方法。當需要調用一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接調用該方法了。通過解析操作符號引用就可以直接轉變爲目標方法在類中方法表的位置,從而使得方法可以被調用。

綜上,解析階段是虛擬

初始化

初始化是類加載的最後一步,也是真正執行類中定義的 Java 程序代碼(字節碼),初始化階段是執行類構造器 ()方法的過程。

對於() 方法的調用,虛擬機會自己確保其在多線程環境中的安全性。因爲 () 方法是帶鎖線程安全,所以在多線程環境下進行類初始化的話可能會引起死鎖,並且這種死鎖很難被發現。

對於初始化階段,虛擬機嚴格規範了有且只有5種情況下,必須對類進行初始化:

  1. 當遇到 new 、 getstatic、putstatic或invokestatic 這4條直接碼指令時,比如 new 一個類,讀取一個靜態字段(未被 final 修飾)、或調用一個類的靜態方法時。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用時 ,如果類沒初始化,需要觸發其初始化。
  3. 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
  4. 當虛擬機啓動時,用戶需要定義一個要執行的主類 (包含 main 方法的那個類),虛擬機會先初始化這個類。
  5. 當使用 JDK1.7 的動態動態語言時,如果一個 MethodHandle 實例的最後解析結構爲 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,並且這個句柄沒有初始化,則需要先觸發器初始化。

類加載器

JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader

  • Bootstrap (加載核心庫,lib下面的所有jar)

    最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類

    • Extention (加載lib/ext下面的所有jar,及自定義目錄的jar)

      主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。

    • Appclass Loader (最後加載classpath下面所有文件)

      面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。

雙親委派模型

每一個類都有一個對應它的類加載器。系統中的 ClassLoder 在協同工作的時候會默認使用 雙親委派模型 。即在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則纔會嘗試加載。加載的時候,首先會把該請求委派該父類加載器的 loadClass() 處理,因此所有的請求最終都應該傳送到頂層的啓動類加載器 BootstrapClassLoader 中。當父類加載器無法處理時,才由自己來處理。當父類加載器爲null時,會使用啓動類加載器 BootstrapClassLoader 作爲父類加載器。

image-20200208122703744

作用

雙親委派模型保證了Java程序的穩定運行,可以避免類的重複加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱爲 java.lang.Object 類的話,那麼程序運行的時候,系統就會出現多個不同的 Object 類。

若不使用雙親委派模型

我們可以自己定義一個類加載器,然後重寫 loadClass() 即可。

自定義類加載器

除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader。如果我們要自定義自己的類加載器,很明顯需要繼承 ClassLoader

GC垃圾回收

GC介紹

Java 的自動內存管理主要是針對對象內存的回收和對象內存的分配。同時,Java 自動內存管理最核心的功能是 內存中對象的分配與回收。

Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC 堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以 Java 堆還可以細分爲:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

堆空間結構

eden 區、s0(“From”) 區、s1(“To”) 區都屬於新生代,tentired 區屬於老年代。

  1. 對象都會首先在 Eden 區域分配,在一次新生代垃圾回收後,如果對象還存活,則會進入 s1(“To”),並且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變爲 1),當它的年齡增加到一定程度(默認爲 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 來設置。
  2. 經過這次GC後,Eden區和"From"區已經被清空。這個時候,“From"和"To"會交換他們的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎樣,都會保證名爲To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,"To"區被填滿之後,會將所有對象移動到老年代中。

對象死亡判斷

  • 應用計數法

    給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器爲 0 的對象就是不可能再被使用的。

    這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。 所謂對象之間的相互引用問題,如下面代碼所示:除了對象 objA 和 objB 相互引用着對方之外,這兩個對象之間再無任何引用。但是他們因爲互相引用對方,導致它們的引用計數器都不爲 0,於是引用計數算法無法通知 GC 回收器回收他們。

  • 可達性分析算法

    這個算法的基本思想就是通過一系列的稱爲 “GC Roots” 的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。

  • 引用

    JDK1.2 以後,Java 對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

    1.強引用(StrongReference)

    以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java 虛擬機寧願拋出 OutOfMemoryError 錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

    2.軟引用(SoftReference)

    如果一個對象只具有軟引用,那就類似於可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

    軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

    3.弱引用(WeakReference)

    如果一個對象只具有弱引用,那就類似於可有可無的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。

    弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

    4.虛引用(PhantomReference)

    "虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

    虛引用主要用來跟蹤對象被垃圾回收的活動

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

    特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因爲軟引用可以加速 JVM 對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生

如何判斷幾個常量是廢棄的

運行時常量池主要回收的是廢棄的常量。那麼,我們如何判斷一個常量是廢棄常量呢?

假如在常量池中存在字符串 “abc”,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,“abc” 就會被系統清理出常量池。

JDK1.7 及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

如何判斷一個類是無用的

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類”

  • 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述 3 個條件的無用類進行回收,這裏說的僅僅是“可以”,而並不是和對象一樣不使用了就會必然被回收。

垃圾收集算法

  • 標記-清除法
  • 複製算法
  • 標記-整理算法
  • 分代收集算法

標記-清除算法

該算法分爲“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它是最基礎的收集算法,後續的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:

  1. 效率問題
  2. 空間問題(標記清除後會產生大量不連續的碎片)

image-20200208083012783

複製算法

爲了解決效率問題,“複製”收集算法出現了。它可以將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

image-20200208083127341

標記-整理法

根據老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的內存。

image-20200208083252571

分代收集算法

當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不同將內存分爲幾塊。一般將 java 堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

新生代:每次收集都會有大量對象死去,所以可以選擇複製算法,只需要付出少量對象的複製成本就可以完成每次垃圾收集。

老年代:對象存活機率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇標記-清除標記-整理算法進行垃圾收集。

延伸面試問題: HotSpot 爲什麼要分爲新生代和老年代?

根據上面的對分代收集算法的介紹回答。

垃圾收集器

如果說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

雖然我們對各個收集器進行比較,但並非要挑選出一個最好的收集器。因爲直到現在爲止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那麼我們的 HotSpot 虛擬機就不會實現那麼多不同的垃圾收集器了。

Serial收集器

Serial(串行)收集器是一個單線程收集器。它在進行垃圾收集工作的時候必須暫停其他所有的工作線程,直到它收集結束。

虛擬機的設計者們當然知道 Stop The World 帶來的不良用戶體驗,所以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是 Serial 收集器有沒有優於其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial 收集器由於沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對於運行在 Client 模式下的虛擬機來說是個不錯的選擇。

特點

  • 針對新生代的收集器;
  • 採用複製算法;
  • 單線程收集;
  • 進行垃圾收集時,必須暫停所有工作線程,直到完成;
    即會"Stop The World";

應用場景

依然是HotSpot在Client模式下默認的新生代收集器;
簡單高效(與其他收集器的單線程相比);
對於限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;
在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一百多MS),只要不頻繁發生,這是可以接受的

設置

添加該參數來顯式的使用串行垃圾收集器:
-XX:+UseSerialGC

ParNew 收集器

ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和 Serial 收集器完全一樣。

它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的併發收集器,後面會介紹到)配合工作。

並行和併發概念補充

  • 並行(Parallel) :指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個 CPU 上。

特點

  • 除了多線程外,其餘的行爲、特點和Serial收集器一樣;
  • 如Serial收集器可用控制參數、收集算法、Stop The World、內存分配規則、回收策略等;
  • Serial收集器共用了不少代碼;

應用場景

在Server模式下,ParNew收集器是一個非常重要的收集器,因爲除Serial外,目前只有它能與CMS收集器配合工作;
但在單個CPU環境中,不會比Serail收集器有更好的效果,因爲存在線程交互開銷。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是使用複製算法的多線程收集器,它看上去幾乎和ParNew都一樣。 那麼它有什麼特別之處呢?

-XX:+UseParallelGC 

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代並行

Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是 CPU 中用於運行用戶代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,手工優化存在困難的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。

特點

  • 新生代收集器;
  • 採用複製算法;
  • 多線程收集;
  • CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間;而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput)

####CMS(Concurrent Mark Sweep)收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它非常符合在注重用戶體驗的應用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。

從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:

  • 初始標記: 暫停所有的其他線程,並記錄下直接與 root 相連的對象,速度很快 ;
  • 併發標記: 同時開啓 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
  • 重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
  • 併發清除: 開啓用戶線程,同時 GC 線程開始對爲標記的區域做清掃。

從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:

  • 對 CPU 資源敏感;
  • 無法處理浮動垃圾;
  • 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
指定使用CMS收集器
"-XX:+UseConcMarkSweepGC"

特點

  • 針對老年代
  • 基於"標記-清除"算法(不進行壓縮操作,會產生內存碎片)
  • 以獲取最短回收停頓時間爲目標
  • 併發收集、低停頓
  • 需要更多的內存

G1 (Garbage-First)收集器

G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特徵.

被視爲 JDK1.7 中 HotSpot 虛擬機的一個重要進化特徵。它具備一下特點:

  • 並行與併發:G1 能充分利用 CPU、多核環境下的硬件優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執行的 GC 動作,G1 收集器仍然可以通過併發的方式讓 java 程序繼續執行。
  • 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
  • 空間整合:與 CMS 的“標記–清理”算法不同,G1 從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的。
  • 可預測的停頓:這是 G1 相對於 CMS 的另一個大優勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內。

G1 收集器的運作大致分爲以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收

G1 收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃分內存空間以及有優先級的區域回收方式,保證了 GF 收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)。

GC調優

顯示指定堆內存-Xms和-Xmx

與性能有關的最常見實踐之一是根據應用程序要求初始化堆內存。如果我們需要指定最小和最大堆大小(推薦顯示指定大小),以下參數可以幫助你實現:

-Xms<heap size>[unit] 
-Xmx<heap size>[unit]
  • heap size 表示要初始化內存的具體大小。
  • unit 表示要初始化內存的單位。單位爲***“ g”*** (GB) 、*“ m”*(MB)、*“ k”*(KB)。

如果我們要爲JVM分配最小2 GB和最大5 GB的堆內存大小,我們的參數應該這樣來寫:

-Xms2G -Xmx5G

顯示新生代內存

根據Oracle官方文檔,在堆總可用內存配置完成之後,第二大影響因素是爲 Young Generation 在堆內存所佔的比例。默認情況下,YG 的最小大小爲 1310 MB,最大大小爲無限制

一共有兩種指定 新生代內存(Young Ceneration)大小的方法:

1.通過-XX:NewSize-XX:MaxNewSize指定

-XX:NewSize=<young size>[unit] 
-XX:MaxNewSize=<young size>[unit]

舉個栗子🌰,如果我們要爲 新生代分配 最小256m 的內存,最大 1024m的內存我們的參數應該這樣來寫:

-XX:NewSize=256m
-XX:MaxNewSize=1024m

2.通過-Xmn[unit]指定

舉個栗子🌰,如果我們要爲 新生代分配256m的內存(NewSize與MaxNewSize設爲一致),我們的參數應該這樣來寫:

-Xmn256m 

GC 調優策略中很重要的一條經驗總結是這樣說的:

將新對象預留在新生代,由於 Full GC 的成本遠高於 Minor GC,因此儘可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日誌分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。

另外,你還可以通過**-XX:NewRatio=**來設置新生代和老年代內存的比值。

比如下面的參數就是設置新生代(包括Eden和兩個Survivor區)與老年代的比值爲1。也就是說:新生代與老年代所佔比值爲1:1,新生代佔整個堆棧的 1/2。

-XX:NewRatio=1

顯示指定永久代/元空間大小

從Java 8開始,如果我們沒有指定 Metaspace 的大小,隨着更多類的創建,虛擬機會耗盡所有可用的系統內存(永久代並不會出現這種情況)。

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小

-XX:PermSize=N //方法區 (永久代) 初始大小
-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen

相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入方法區後就“永久存在”了。

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

下面是一些常用參數:

-XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //設置 Metaspace 的最大大小,如果不指定大小的話,隨着更多類的創建,虛擬機會耗盡所有可用

GC調優策略

**策略 1:**將新對象預留在新生代,由於 Full GC 的成本遠高於 Minor GC,因此儘可能將對象分配在新生代是明智的做法,實際項目中根據 GC 日誌分析新生代空間大小分配是否合理,適當通過“-Xmn”命令調節新生代大小,最大限度降低新對象直接進入老年代的情況。

**策略 2:**大對象進入老年代,雖然大部分情況下,將對象分配在新生代是合理的。但是對於大對象這種做法卻值得商榷,大對象如果首次在新生代分配可能會出現空間不足導致很多年齡不夠的小對象被分配的老年代,破壞新生代的對象結構,可能會出現頻繁的 full gc。因此,對於大對象,可以設置直接進入老年代(當然短命的大對象對於垃圾回收來說簡直就是噩夢)。-XX:PretenureSizeThreshold 可以設置直接進入老年代的對象大小。

**策略 3:**合理設置進入老年代對象的年齡,-XX:MaxTenuringThreshold 設置對象進入老年代的年齡大小,減少老年代的內存佔用,降低 full gc 發生的頻率。

**策略 4:**設置穩定的堆大小,堆大小設置有兩個參數:-Xms 初始化堆大小,-Xmx 最大堆大小。

**策略5:**注意: 如果滿足下面的指標,則一般不需要進行 GC 優化:

MinorGC 執行時間不到50ms; Minor GC 執行不頻繁,約10秒一次; Full GC 執行時間不到1s; Full GC 執行頻率不算頻繁,不低於10分鐘1次。

JVM配置參數

堆參數

image-20200208125107719

回收器參數

image-20200208125141975

如上表所示,目前主要有串行、並行和併發三種,對於大內存的應用而言,串行的性能太低,因此使用到的主要是並行和併發兩種。並行和併發 GC 的策略通過 UseParallelGCUseConcMarkSweepGC 來指定,還有一些細節的配置參數用來配置策略的執行方式。例如:XX:ParallelGCThreadsXX:CMSInitiatingOccupancyFraction 等。 通常:Young 區對象回收只可選擇並行(耗時間),Old 區選擇併發(耗 CPU)。

JDK監控工具

這些命令在 JDK 安裝目錄下的 bin 目錄下:

  • jps (JVM Process Status): 類似 UNIX 的 ps 命令。用戶查看所有 Java 進程的啓動類、傳入參數和 Java 虛擬機參數等信息;
  • jstat( JVM Statistics Monitoring Tool): 用於收集 HotSpot 虛擬機各方面的運行數據;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,顯示虛擬機配置信息;
  • jmap (Memory Map for Java) :生成堆轉儲快照;
  • jhat (JVM Heap Dump Browser ) : 用於分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在瀏覽器上查看分析結果;
  • jstack (Stack Trace for Java):生成虛擬機當前時刻的線程快照,線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合。

jps (JVM Process Status)

查看Java進程

jps:顯示虛擬機執行主類名稱以及這些進程的本地虛擬機唯一 ID(Local Virtual Machine Identifier,LVMID)。

jps -q :只輸出進程的本地虛擬機唯一 ID。

C:\Users\SnailClimb>jps
7360 NettyClient2
17396
7972 Launcher
16504 Jps
17340 NettyServer

jps -l:輸出主類的全名,如果進程執行的是 Jar 包,輸出 Jar 路徑。

C:\Users\SnailClimb>jps -l
7360 firstNettyDemo.NettyClient2
17396
7972 org.jetbrains.jps.cmdline.Launcher
16492 sun.tools.jps.Jps
17340 firstNettyDemo.NettyServer

jps -v:輸出虛擬機進程啓動時 JVM 參數。

jps -m:輸出傳遞給 Java 進程 main() 函數的參數。

jstat(JVM Statistics Monitoring Tool)

jstat(JVM Statistics Monitoring Tool) 使用於監視虛擬機各種運行狀態信息的命令行工具。 它可以顯示本地或者遠程(需要遠程主機提供 RMI 支持)虛擬機進程中的類信息、內存、垃圾收集、JIT 編譯等運行數據,在沒有 GUI,只提供了純文本控制檯環境的服務器上,它將是運行期間定位虛擬機性能問題的首選工具。

jstat 命令使用格式:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

比如 jstat -gc -h3 31736 1000 10表示分析進程 id 爲 31736 的 gc 情況,每隔 1000ms 打印一次記錄,打印 10 次停止,每 3 行後打印指標頭部。

常見的 option 如下:

  • jstat -class vmid :顯示 ClassLoader 的相關信息;
  • jstat -compiler vmid :顯示 JIT 編譯的相關信息;
  • jstat -gc vmid :顯示與 GC 相關的堆信息;
  • jstat -gccapacity vmid :顯示各個代的容量及使用情況;
  • jstat -gcnew vmid :顯示新生代信息;
  • jstat -gcnewcapcacity vmid :顯示新生代大小與使用情況;
  • jstat -gcold vmid :顯示老年代和永久代的信息;
  • jstat -gcoldcapacity vmid :顯示老年代的大小;
  • jstat -gcpermcapacity vmid :顯示永久代大小;
  • jstat -gcutil vmid :顯示垃圾收集信息;

另外,加上 -t參數可以在輸出信息上加一個 Timestamp 列,顯示程序的運行時間。

jinfo

實時查看虛擬機參數

jinfo vmid :輸出當前 jvm 進程的全部參數和系統屬性 (第一部分是系統的屬性,第二部分是 JVM 的參數)。

jinfo -flag name vmid :輸出對應名稱的參數的具體值。比如輸出 MaxHeapSize、查看當前 jvm 進程是否開啓打印 GC 日誌 ( -XX:PrintGCDetails :詳細 GC 日誌模式,這兩個都是默認關閉的)。

C:\Users\SnailClimb>jinfo  -flag MaxHeapSize 17340
-XX:MaxHeapSize=2124414976
C:\Users\SnailClimb>jinfo  -flag PrintGC 17340
-XX:-PrintGC

使用 jinfo 可以在不重啓虛擬機的情況下,可以動態的修改 jvm 的參數。尤其在線上的環境特別有用,請看下面的例子:

jinfo -flag [+|-]name vmid 開啓或者關閉對應名稱的參數。

C:\Users\SnailClimb>jinfo  -flag  PrintGC 17340
-XX:-PrintGC

C:\Users\SnailClimb>jinfo  -flag  +PrintGC 17340

C:\Users\SnailClimb>jinfo  -flag  PrintGC 17340
-XX:+PrintGC

jmap

生成堆轉儲快照

jmap(Memory Map for Java)命令用於生成堆轉儲快照。 如果不使用 jmap 命令,要想獲取 Java 堆轉儲,可以使用 “-XX:+HeapDumpOnOutOfMemoryError” 參數,可以讓虛擬機在 OOM 異常出現之後自動生成 dump 文件,Linux 命令下可以通過 kill -3 發送進程退出信號也能拿到 dump 文件。

jmap 的作用並不僅僅是爲了獲取 dump 文件,它還可以查詢 finalizer 執行隊列、Java 堆和永久代的詳細信息,如空間使用率、當前使用的是哪種收集器等。和jinfo一樣,jmap有不少功能在 Windows 平臺下也是受限制的。

示例:將指定應用程序的堆快照輸出到桌面。後面,可以通過 jhat、Visual VM 等工具分析該堆文件。

C:\Users\SnailClimb>jmap -dump:format=b,file=C:\Users\SnailClimb\Desktop\heap.hprof 17340
Dumping heap to C:\Users\SnailClimb\Desktop\heap.hprof ...
Heap dump file created

jhat

jhat 用於分析 heapdump 文件,它會建立一個 HTTP/HTML 服務器,讓用戶可以在瀏覽器上查看分析結果。

C:\Users\SnailClimb>jhat C:\Users\SnailClimb\Desktop\heap.hprof
Reading from C:\Users\SnailClimb\Desktop\heap.hprof...
Dump file created Sat May 04 12:30:31 CST 2019
Snapshot read, resolving...
Resolving 131419 objects...
Chasing references, expect 26 dots..........................
Eliminating duplicate references..........................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

訪問 http://localhost:7000/

jstack

jstack(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合.

生成線程快照的目的主要是定位線程長時間出現停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等都是導致線程長時間停頓的原因。線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在後臺做些什麼事情,或者在等待些什麼資源。

下面是一個線程死鎖的代碼。我們下面會通過 jstack 命令進行死鎖檢查,輸出死鎖信息,找到發生死鎖的線程。

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}

Output

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過Thread.sleep(1000);讓線程 A 休眠 1s 爲的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。

通過 jstack 命令分析:

C:\Users\SnailClimb>jps
13792 KotlinCompileDaemon
7360 NettyClient2
17396
7972 Launcher
8932 Launcher
9256 DeadLockDemo
10764 Jps
17340 NettyServer

C:\Users\SnailClimb>jstack 9256

輸出的部分內容如下:

Found one Java-level deadlock:
=============================
"線程 2":
  waiting to lock monitor 0x000000000333e668 (object 0x00000000d5efe1c0, a java.lang.Object),
  which is held by "線程 1"
"線程 1":
  waiting to lock monitor 0x000000000333be88 (object 0x00000000d5efe1d0, a java.lang.Object),
  which is held by "線程 2"

Java stack information for the threads listed above:
===================================================
"線程 2":
        at DeadLockDemo.lambda$main$1(DeadLockDemo.java:31)
        - waiting to lock <0x00000000d5efe1c0> (a java.lang.Object)
        - locked <0x00000000d5efe1d0> (a java.lang.Object)
        at DeadLockDemo$$Lambda$2/1078694789.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"線程 1":
        at DeadLockDemo.lambda$main$0(DeadLockDemo.java:16)
        - waiting to lock <0x00000000d5efe1d0> (a java.lang.Object)
        - locked <0x00000000d5efe1c0> (a java.lang.Object)
        at DeadLockDemo$$Lambda$1/1324119927.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

可以看到 jstack 命令已經幫我們找到發生死鎖的線程的具體信息。

JDK監控可視化工具

  • JConsole
  • JProfile

JDK1.8特性

  • lambda表達式
  • 關鍵字default: 可以在接口中添加方法的實現,比如Iterator迭代器,裏面的foreach
  • 函數式接口@FunctionalInterface
  • Api的添加,Stream,方便集合的操作

Servlet

Servlet簡介

Java Servlet 是運行在 Web 服務器或應用服務器上的程序,它是作爲來自 Web 瀏覽器或其他 HTTP 客戶端的請求和 HTTP 服務器上的數據庫或應用程序之間的中間層。

Servlet生命週期

Servlet有良好的生存期的定義,包括加載和實例化、初始化、處理請求以及服務結束。這個生存期由javax.servlet.Servlet接口的init,service和destroy方法表達。 Servlet被服務器實例化後,容器運行其init方法,請求到達時運行其service方法,service方法自動派遣運行與請求對應的doXXX方法(doGet,doPost)等,當服務器決定將實例銷燬的時候調用其destroy方法。

  1. 初始化:Web容器加載servlet,調用init()方法
  2. 處理請求:當請求到達時,運行其service()方法。service()自動派遣運行與請求相對應的doXXX(doGet或者doPost)方法。
  3. 銷燬:服務結束,web容器會調用servlet的distroy()方法銷燬servlet。

get提交和post提交有何區別

  • get一般用於從服務器上獲取數據,post一般用於向服務器傳送數據
  • 請求的時候參數的位置有區別,get的參數是拼接在url後面,用戶在瀏覽器地址欄可以看到。post是放在http包的包體中。比如說用戶註冊,你不能把用戶提交的註冊信息用get的方式吧,那不是說把用戶的註冊信息都顯示在Url上了嗎,是不安全的。
  • 能提交的數據有區別,get方式能提交的數據只能是文本,且大小不超過1024個字節,而post不僅可以提交文本還有二進制文件。所以說想上傳文件的話,那我們就需要使用post請求方式
  • servlet在處理請求的時候分別對應使用doGet和doPost方式進行處理請求

doGet與doPost方法的兩個參數是什麼

HttpServletRequest:封裝了與請求相關的信息

HttpServletResponse:封裝了與響應相關的信息

forward和redirect的區別

轉發與重定向

(1)從地址欄顯示來說

forward是服務器請求資源,服務器直接訪問目標地址的URL,把那個URL的響應內容讀取過來,然後把這些內容再發給瀏覽器.瀏覽器根本不知道服務器發送的內容從哪裏來的,所以它的地址欄還是原來的地址

redirect是服務端根據邏輯,發送一個狀態碼,告訴瀏覽器重新去請求那個地址.所以地址欄顯示的是

新的URL.

(2)從數據共享來說

forward:轉發頁面和轉發到的頁面可以共享request裏面的數據.

redirect:不能共享數據.

(3)從運用地方來說

forward:一般用於用戶登陸的時候,根據角色轉發到相應的模塊.

redirect:一般用於用戶註銷登陸時返回主頁面和跳轉到其它的網站等.

(4)從效率來說

forward:高.

redirect:低.

四種會話跟蹤技術作用域

(1)page:一個頁面

(2)request::一次請求

(3)session:一次會話

(4)application:服務器從啓動到停止。

request.getAttribute()和request.getParameter

(1)有setAttribute,沒有setParameter方法

(2)getParameter獲取到的值只能是字符串,不可以是對象,而getAttribute獲取到的值是Object類型的。

(3)通過form表單或者url來向另一個頁面或者servlet傳遞參數的時候需要用getParameter獲取值;getAttribute只能獲取setAttribute的值

(4)setAttribute是應用服務器把這個對象放到該頁面所對應的一塊內存當中,當你的頁面服務器重定向到另一個頁面的時候,應用服務器

會把這塊內存拷貝到另一個頁面對應的內存當中。通過getAttribute可以取得你存下的值,當然這種方法可以用來傳對象。

用session也是一樣的道理,這是說request和session的生命週期不一樣而已。

JDBC

JDBC的全稱是Java DataBase Connection,也就是Java數據庫連接,我們可以用它來操作關係型數據庫。JDBC接口及相關類在java.sql包和javax.sql包裏。我們可以用它來連接數據庫,執行SQL查詢,存儲過程,並處理返回的結果。

JDBC接口讓Java程序和JDBC驅動實現了松耦合,使得切換不同的數據庫變得更加簡單。

JDBC連接數據庫的步驟

 public static void main(String[] args) throws Exception {
        //1.加載驅動程序
        Class.forName("com.mysql.jdbc.Driver");
        //2. 獲得數據庫連接
        Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
        //3.操作數據庫,實現增刪改查
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT user_name, age FROM imooc_goddess");
        //如果有數據,rs.next()返回true
        while(rs.next()){
            System.out.println(rs.getString("user_name")+" 年齡:"+rs.getInt("age"));
        }
    }
  1. 註冊驅動
  2. 獲取連接
  3. 創建一個Statement語句對象
  4. 執行SQL語句
  5. 處理結果集
  6. 關閉資源

有哪些不同類型的JDBC驅動

image-20200211093403376

A JDBC-ODBC Bridge plus ODBC Driver(類型1):它使用ODBC驅動連接數據庫。需要安裝ODBC以便連接數據庫,正因爲這樣,這種方式現在已經基本淘汰了。

B Native API partly Java technology-enabled driver(類型2):這種驅動把JDBC調用適配成數據庫的本地接口的調用。

C Pure Java Driver for Database Middleware(類型3):這個驅動把JDBC調用轉發給中間件服務器,由它去和不同的數據庫進行連接。用這種類型的驅動需要部署中間件服務器。這種方式增加了額外的網絡調用,導致性能變差,因此很少使用。

D Direct-to-Database Pure Java Driver(類型4):這個驅動把JDBC轉化成數據庫使用的網絡協議。這種方案最簡單,也適合通過網絡連接數據庫。不過使用這種方式的話,需要根據不同數據庫選用特定的驅動程序,比如OJDBC是Oracle開發的Oracle數據庫的驅動,而MySQL Connector/J是MySQL數據庫的驅動。

JDBC是如何實現Java程序和JDBC驅動的松耦合的

DBC API使用Java的反射機制來實現Java程序和JDBC驅動的松耦合。隨便看一個簡單的JDBC示例,你會發現所有操作都是通過JDBC接口完成的,而驅動只有在通過Class.forName反射機制來加載的時候纔會出現。

我覺得這是Java核心庫裏反射機制的最佳實踐之一,它使得應用程序和驅動程序之間進行了隔離,讓遷移數據庫的工作變得更簡單。

JDBC的DriverManager是用來做什麼的

JDBC的DriverManager是一個工廠類,我們通過它來創建數據庫連接。當JDBC的Driver類被加載進來時,它會自己註冊到DriverManager類裏面,你可以看下JDBC Driver類的源碼來了解一下。

然後我們會把數據庫配置信息傳成DriverManager.getConnection()方法,DriverManager會使用註冊到它裏面的驅動來獲取數據庫連接,並返回給調用的程序。

JDBC的Statement是什麼

Statement是JDBC中用來執行數據庫SQL查詢語句的接口。通過調用連接對象的getStatement()方法我們可以生成一個Statement對象。我們可以通過調用它的execute(),executeQuery(),executeUpdate()方法來執行靜態SQL查詢。

由於SQL語句是程序中傳入的,如果沒有對用戶輸入進行校驗的話可能會引起SQL注入的問題,默認情況下,一個Statement同時只能打開一個ResultSet。如果想操作多個ResultSet對象的話,需要創建多個Statement。Statement接口的所有execute方法開始執行時都默認會關閉當前打開的ResultSet。

execute,executeQuery,executeUpdate的區別

Statement的execute(String query)方法用來執行任意的SQL查詢,如果查詢的結果是一個ResultSet,這個方法就返回true。如果結果不是ResultSet,比如insert或者update查詢,它就會返回false。我們可以通過它的getResultSet方法來獲取ResultSet,或者通過getUpdateCount()方法來獲取更新的記錄條數。

Statement的executeQuery(String query)接口用來執行select查詢,並且返回ResultSet。即使查詢不到記錄返回的ResultSet也不會爲null。我們通常使用executeQuery來執行查詢語句,這樣的話如果傳進來的是insert或者update語句的話,它會拋出錯誤信息爲 “executeQuery method can not be used for update”的java.util.SQLException。

Statement的executeUpdate(String query)方法用來執行insert或者update/delete(DML)語句,或者 什麼也不返回DDL語句。返回值是int類型,如果是DML語句的話,它就是更新的條數,如果是DDL的話,就返回0。

只有當你不確定是什麼語句的時候才應該使用execute()方法,否則應該使用executeQuery或者executeUpdate方法。

JDBC的ResultSet是什麼

在查詢數據庫後會返回一個ResultSet,它就像是查詢結果集的一張數據表。

ResultSet對象維護了一個遊標,指向當前的數據行。開始的時候這個遊標指向的是第一行。如果調用了ResultSet的next()方法遊標會下移一行,如果沒有更多的數據了,next()方法會返回false。可以在for循環中用它來遍歷數據集。

默認的ResultSet是不能更新的,遊標也只能往下移。也就是說你只能從第一行到最後一行遍歷一遍。不過也可以創建可以回滾或者可更新的ResultSet,像下面這樣。

Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);

當生成ResultSet的Statement對象要關閉或者重新執行或是獲取下一個ResultSet的時候,ResultSet對象也會自動關閉。

可以通過ResultSet的getter方法,傳入列名或者從1開始的序號來獲取列數據。

有哪些不同的ResultSet

根據創建Statement時輸入參數的不同,會對應不同類型的ResultSet。如果你看下Connection的方法,你會發現createStatement和prepareStatement方法重載了,以支持不同的ResultSet和併發類型。

一共有三種ResultSet對象。

  • ResultSet.TYPE_FORWARD_ONLY:這是默認的類型,它的遊標只能往下移。
  • ResultSet.TYPE_SCROLL_INSENSITIVE:遊標可以上下移動,一旦它創建後,數據庫裏的數據再發生修改,對它來說是透明的。
  • ResultSet.TYPE_SCROLL_SENSITIVE:遊標可以上下移動,如果生成後數據庫還發生了修改操作,它是能夠感知到的。

ResultSet有兩種併發類型。

  • ResultSet.CONCUR_READ_ONLY:ResultSet是隻讀的,這是默認類型。
  • ResultSet.CONCUR_UPDATABLE:我們可以使用ResultSet的更新方法來更新裏面的數據。

JDBC中的Statement 和PreparedStatement的區別

  • PreparedStatement 繼承於 Statemen
  • Statement 一般用於執行固定的沒有參數的SQL
  • PreparedStatement 一般用於執行有?參數預編譯的SQL語句。
  • PreparedStatement支持?操作參數,相對於Statement更加靈活。
  • PreparedStatement可以防止SQL注入,安全性高於Statement。

JDBC的事務管理是什麼

JDBC接口提供了一個setAutoCommit(boolean flag)方法,我們可以用它來關閉連接自動提交的特性。我們應該在需要手動提交時才關閉這個特性,不然的話事務不會自動提交,每次都得手動提交。數據庫通過表鎖來管理事務,這個操作非常消耗資源。因此我們應當完成操作後儘快的提交事務。

如何回滾事務

通過Connection對象的rollback方法可以回滾事務。它會回滾這次事務中的所有修改操作,並釋放當前連接所持有的數據庫鎖。

JDBC的保存點(Savepoint)是什麼,如何使用?

有時候事務包含了一組語句,而我們希望回滾到這個事務的某個特定的點。JDBC的保存點可以用來生成事務的一個檢查點,使得事務可以回滾到這個檢查點。

一旦事務提交或者回滾了,它生成的任何保存點都會自動釋放並失效。回滾事務到某個特定的保存點後,這個保存點後所有其它的保存點會自動釋放並且失效。

Spring

什麼是 Spring 框架

Spring 是一種輕量級開發框架,旨在提高開發人員的開發效率以及系統的可維護性。Spring 官網:https://spring.io/。

我們一般說 Spring 框架指的都是 Spring Framework,它是很多模塊的集合,使用這些模塊可以很方便地協助我們進行開發。這些模塊是:核心容器、數據訪問/集成,、Web、AOP(面向切面編程)、工具、消息和測試模塊。比如:Core Container 中的 Core 組件是Spring 所有組件的核心,Beans 組件和 Context 組件是實現IOC和依賴注入的基礎,AOP組件用來實現面向切面編程。

Spring 官網列出的 Spring 的 6 個特徵:

  • 核心技術 :依賴注入(DI),AOP,事件(events),資源,i18n,驗證,數據綁定,類型轉換,SpEL。
  • 測試 :模擬對象,TestContext框架,Spring MVC 測試,WebTestClient。
  • 數據訪問 :事務,DAO支持,JDBC,ORM,編組XML。
  • Web支持 : Spring MVC和Spring WebFlux Web框架。
  • 集成 :遠程處理,JMS,JCA,JMX,電子郵件,任務,調度,緩存。
  • 語言 :Kotlin,Groovy,動態語言。

Spring模塊

  • Spring Core: 基礎,可以說 Spring 其他所有的功能都需要依賴於該類庫。主要提供 IoC 依賴注入功能。
  • Spring Aspects : 該模塊爲與AspectJ的集成提供支持。
  • Spring AOP :提供了面向切面的編程實現。
  • Spring JDBC : Java數據庫連接。
  • Spring JMS :Java消息服務。
  • Spring ORM : 用於支持Hibernate等ORM工具。
  • Spring Web : 爲創建Web應用程序提供支持。
  • Spring Test : 提供了對 JUnit 和 TestNG 測試的支持。

image-20200208150822841

Spring的啓動過程

  • 通過SpringbootApplication註解啓動
    • @SpringBootConfiguration(加載所有配置,@Configuration定義配置類,@Bean註解定義的方法,加載到容器中,實例名爲方法名)
    • @EnableAutoConfiguration(Spring框架自動配置,自動配置包,自動配置組件)打開自動配置的功能
    • @ComponentScan(組件掃描,自動裝配)

Spring Boot能根據當前類路徑下的類、jar包來自動配置bean,如添加一個spring-boot-starter-web啓動器就能擁有web的功能,無需其他配置。

Springboot自動配置原理

註解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自動配置的核心,首先它得是一個配置文件,其次根據類路徑下是否有這個類去自動配置

SpringBoot 自動配置主要通過 @EnableAutoConfiguration, @Conditional, @EnableConfigurationProperties 或者 @ConfigurationProperties 等幾個註解來進行自動配置完成的。
@EnableAutoConfiguration 開啓自動配置,主要作用就是調用 Spring-Core 包裏的 loadFactoryNames(),將 autoconfig 包裏的已經寫好的自動配置加載進來。
@Conditional 條件註解,通過判斷類路徑下有沒有相應配置的 jar 包來確定是否加載和自動配置這個類。
@EnableConfigurationProperties的作用就是,給自動配置提供具體的配置參數,只需要寫在application.properties 中,就可以通過映射寫入配置類的 POJO 屬性中。

1. new Tomcat() ,設置相關屬性值 。
 2. 寫一個 WebApplicationInitializer 接口的實現類(Servlet規範會自動加載指定接口的所有實現類,WebApplicationInitializer就是其中一個接口)。WebApplicationInitializer可以看做是Web.xml的替代。通過實現WebApplicationInitializer,在其中可以添加servlet,listener等,在加載Web項目的時候會加載這個接口實現類,從而起到web.xml相同的作用。
 3. 加載實例化 ApplicationContext , 從而創建管理Bean (Bean是Spring管理的基本單位,在基於Spring的Java EE應用中,所有的組件都被當成Bean處理)。
 4. 創建初始化 DispatcherServlet 。

Spring依賴注入方式

  1. 構造器注入

    public class UserService implements IUserService {
    
        private IUserDao userDao;
    
        public UserService(IUserDao userDao) {
            this.userDao = userDao;
        }
    
        public void loginUser() {
            userDao.loginUser();
        }
    
    }
    
  2. Setter注入

    public class UserService implements IUserService {
    
        private IUserDao userDao1;
    
        public void setUserDao(IUserDao userDao1) {
            this.userDao1 = userDao1;
        }
    
        public void loginUser() {
            userDao1.loginUser();
        }
    
    }
    
  3. 註解注入

    @Autowired
    @Qualifier("userDaoJdbc")
    
    private IUserDao userDao;
    

Spring事務的傳播性

事務傳播行爲類型 說明
PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
PROPAGATION_SUPPORTS 支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則拋出異常。
PROPAGATION_NESTED 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。

Spring中的隔離級別

① ISOLATION_DEFAULT:這是個 PlatfromTransactionManager 默認的隔離級別,使用數據庫默認的事務隔離級別。

② ISOLATION_READ_UNCOMMITTED:讀未提交,允許另外一個事務可以看到這個事務未提交的數據。

③ ISOLATION_READ_COMMITTED:讀已提交,保證一個事務修改的數據提交後才能被另一事務讀取,而且能看到該事務對已有記錄的更新。

④ ISOLATION_REPEATABLE_READ:可重複讀,保證一個事務修改的數據提交後才能被另一事務讀取,但是不能看到該事務對已有記錄的更新。

⑤ ISOLATION_SERIALIZABLE:一個事務在執行的過程中完全看不到其他事務對數據庫所做的更新。

Spring通知類型

(1)前置通知(Before advice):在某連接點(join point)之前執行的通知,但這個通知不能阻止連接點前的執行(除非它拋出一個異常)。

(2)返回後通知(After returning advice):在某連接點(join point)正常完成後執行的通知:例如,一個方法沒有拋出任何異常,正常返回。

(3)拋出異常後通知(After throwing advice):在方法拋出異常退出時執行的通知。

(4)後通知(After (finally) advice):當某連接點退出的時候執行的通知(不論是正常返回還是異常退出)。

(5)環繞通知(Around Advice):包圍一個連接點(join point)的通知,如方法調用。這是最強大的一種通知類型。 環繞通知可以在方法調用前後完成自定義的行爲。它也會選擇是否繼續執行連接點或直接返回它們自己的返回值或拋出異常來結束執行。 環繞通知是最常用的一種通知類型。大部分基於攔截的AOP框架

SpringMVC原理

客戶端發送請求-> 前端控制器 DispatcherServlet 接受客戶端請求 -> 找到處理器映射 HandlerMapping 解析請求對應的 Handler-> HandlerAdapter 會根據 Handler 來調用真正的處理器開處理請求,並處理相應的業務邏輯 -> 處理器返回一個模型視圖 ModelAndView -> 視圖解析器進行解析 -> 返回一個視圖對象->前端控制器 DispatcherServlet 渲染數據(Moder)->將得到視圖對象返回給用戶

Spring AOP

AOP思想的實現一般都是基於 代理模式 ,在JAVA中一般採用JDK動態代理模式,但是我們都知道,JDK動態代理模式只能代理接口而不能代理類。因此,Spring AOP 會這樣子來進行切換,因爲Spring AOP 同時支持 CGLIB、ASPECTJ、JDK動態代理。

AOP,一般稱爲面向切面,作爲面向對象的一種補充,用於將那些與業務無關,但卻對多個對象產生影響的公共行爲和邏輯,抽取並封裝爲一個可重用的模塊,這個模塊被命名爲“切面”(Aspect),減少系統中的重複代碼,降低了模塊間的耦合度,同時提高了系統的可維護性。可用於權限認證、日誌、事務處理。

  • 如果目標對象的實現類實現了接口,Spring AOP 將會採用 JDK 動態代理來生成 AOP 代理類;

    • 如果目標對象的實現類沒有實現接口,Spring AOP 將會採用 CGLIB 來生成 AOP 代理類——不過這個選擇過程對開發者完全透明、開發者也無需關心。

應用場景

  1. 日誌記錄
  2. 事務管理
  3. 權限監控
  4. 方法監控

實現原理

  1. JDK動態代理
  2. Cglib動態代理

Spring IOC

IoC(Inverse of Control:控制反轉)是一種設計思想,就是 將原本在程序中手動創建對象的控制權,交由Spring框架來管理。 IoC 在其他語言中也有應用,並非 Spirng 特有。 IoC 容器是 Spring 用來實現 IoC 的載體, IoC 容器實際上就是個Map(key,value),Map 中存放的是各種對象。

將對象之間的相互依賴關係交給 IoC 容器來管理,並由 IoC 容器完成對象的注入。這樣可以很大程度上簡化應用的開發,把應用從複雜的依賴關係中解放出來。 IoC 容器就像是一個工廠一樣,當我們需要創建一個對象的時候,只需要配置好配置文件/註解即可,完全不用考慮對象是如何被創建出來的。 在實際項目中一個 Service 類可能有幾百甚至上千個類作爲它的底層,假如我們需要實例化這個 Service,你可能要每次都要搞清這個 Service 所有底層類的構造函數,這可能會把人逼瘋。如果利用 IoC 的話,你只需要配置好,然後在需要的地方引用就行了,這大大增加了項目的可維護性且降低了開發難度。

Spring 時代我們一般通過 XML 文件來配置 Bean,後來開發人員覺得 XML 文件來配置不太好,於是 SpringBoot 註解配置就慢慢開始流行起來。

實現原理

1.讀取bean的XML配置文件

2.使用beanId查找bean配置,並獲取配置文件中class地址。

3.使用Java反射技術實例化對象

4.獲取屬性配置,使用反射技術進行賦值。

詳細流程

1.利用傳入的參數獲取xml文件的流,並且利用dom4j解析成Document對象

2.對於Document對象獲取根元素對象後對下面的標籤進行遍歷,判斷是否有符合的id.

3.如果找到對應的id,相當於找到了一個Element元素,開始創建對象,先獲取class屬性,根據屬性值利用反射建立對象.

4.遍歷標籤下的property標籤,並對屬性賦值.注意,需要單獨處理int,float類型的屬性.因爲在xml配置中這些屬性都是以字符串的形式來配置的,因此需要額外處理.

5.如果屬性property標籤有ref屬性,說明某個屬性的值是一個對象,那麼根據id(ref屬性的值)去獲取ref對應的對象,再給屬性賦值.

6.返回建立的對象,如果沒有對應的id,或者下沒有子標籤都會返回nul

  • 通過容器初始化實例和配置
  • 如何設計
    1. 容器工廠
    2. 應用上下文

SpringBean作用域

  • singleton : 唯一 bean 實例,Spring 中的 bean 默認都是單例的。
  • prototype : 每次請求都會創建一個新的 bean 實例。
  • request : 每一次HTTP請求都會產生一個新的bean,該bean僅在當前HTTP request內有效。
  • session : 每一次HTTP請求都會產生一個新的 bean,該bean僅在當前 HTTP session 內有效。
  • global-session: 全局session作用域,僅僅在基於portlet的web應用中才有意義,Spring5已經沒有了。Portlet是能夠生成語義代碼(例如:HTML)片段的小型Java Web插件。它們基於portlet容器,可以像servlet一樣處理HTTP請求。但是,與 servlet 不同,每個 portlet 都有不同的會話

##SpringBean生命週期

Servlet的生命週期:實例化,初始init,接收請求service,銷燬destroy;

image-20200208152246580

  • Bean 容器找到配置文件中 Spring Bean 的定義。
  • Bean 容器利用 Java Reflection API 創建一個Bean的實例。
  • 如果涉及到一些屬性值 利用 set()方法設置一些屬性值。
  • 如果 Bean 實現了 BeanNameAware 接口,調用 setBeanName()方法,傳入Bean的名字。
  • 如果 Bean 實現了 BeanClassLoaderAware 接口,調用 setBeanClassLoader()方法,傳入 ClassLoader對象的實例。
  • 與上面的類似,如果實現了其他 *.Aware接口,就調用相應的方法。
  • 如果有和加載這個 Bean 的 Spring 容器相關的 BeanPostProcessor 對象,執行postProcessBeforeInitialization() 方法。
  • 如果Bean實現了InitializingBean接口,執行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定義包含 init-method 屬性,執行指定的方法。
  • 如果有和加載這個 Bean的 Spring 容器相關的 BeanPostProcessor 對象,執行postProcessAfterInitialization() 方法
  • 當要銷燬 Bean 的時候,如果 Bean 實現了 DisposableBean 接口,執行 destroy() 方法。
  • 當要銷燬 Bean 的時候,如果 Bean 在配置文件中的定義包含 destroy-method 屬性,執行指定的方法。

(1)實例化Bean:

對於BeanFactory容器,當客戶向容器請求一個尚未初始化的bean時,或初始化bean的時候需要注入另一個尚未初始化的依賴時,容器就會調用createBean進行實例化。對於ApplicationContext容器,當容器啓動結束後,通過獲取BeanDefinition對象中的信息,實例化所有的bean。

(2)設置對象屬性(依賴注入):

實例化後的對象被封裝在BeanWrapper對象中,緊接着,Spring根據BeanDefinition中的信息 以及 通過BeanWrapper提供的設置屬性的接口完成依賴注入。

(3)處理Aware接口:

接着,Spring會檢測該對象是否實現了xxxAware接口,並將相關的xxxAware實例注入給Bean:

①如果這個Bean已經實現了BeanNameAware接口,會調用它實現的setBeanName(String beanId)方法,此處傳遞的就是Spring配置文件中Bean的id值;

②如果這個Bean已經實現了BeanFactoryAware接口,會調用它實現的setBeanFactory()方法,傳遞的是Spring工廠自身。

③如果這個Bean已經實現了ApplicationContextAware接口,會調用setApplicationContext(ApplicationContext)方法,傳入Spring上下文;

(4)BeanPostProcessor:

如果想對Bean進行一些自定義的處理,那麼可以讓Bean實現了BeanPostProcessor接口,那將會調用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean 與 init-method:

如果Bean在Spring配置文件中配置了 init-method 屬性,則會自動調用其配置的初始化方法。

(6)如果這個Bean實現了BeanPostProcessor接口,將會調用postProcessAfterInitialization(Object obj, String s)方法;由於這個方法是在Bean初始化結束時調用的,所以可以被應用於內存或緩存技術;

以上幾個步驟完成後,Bean就已經被正確創建了,之後就可以使用這個Bean了。

(7)DisposableBean:

當Bean不再需要時,會經過清理階段,如果Bean實現了DisposableBean這個接口,會調用其實現的destroy()方法;

(8)destroy-method:

最後,如果這個Bean的Spring配置中配置了destroy-method屬性,會自動調用其配置的銷燬方法。

Spring支持Bean的作用域

Spring容器中的bean可以分爲5個範圍:

(1)singleton:默認,每個容器中只有一個bean的實例,單例的模式由BeanFactory自身來維護。

(2)prototype:爲每一個bean請求提供一個實例。

(3)request:爲每一個網絡請求創建一個實例,在請求完成以後,bean會失效並被垃圾回收器回收。

(4)session:與request範圍類似,確保每個session中有一個bean的實例,在session過期後,bean會隨之失效。

(5)global-session:全局作用域,global-session和Portlet應用相關。當你的應用部署在Portlet容器中工作時,它包含很多portlet。如果你想要聲明讓所有的portlet共用全局的存儲變量的話,那麼這全局變量需要存儲在global-session中。全局作用域與Servlet中的session作用域效果相同。

Spring框架中的單例Beans線程安全

Spring框架並沒有對單例bean進行任何多線程的封裝處理。關於單例bean的線程安全和併發問題需要開發者自行去搞定。但實際上,大部分的Spring bean並沒有可變的狀態(比如Serview類和DAO類),所以在某種程度上說Spring的單例bean是線程安全的。如果你的bean有多種狀態的話,就需要自行保證線程安全。最淺顯的解決辦法就是將多態bean的作用域由“singleton”變更爲“prototype”。

Spring如何處理線程併發問題

在一般情況下,只有無狀態的Bean纔可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明爲singleton作用域,因爲Spring對一些Bean中非線程安全狀態採用ThreadLocal進行處理,解決線程安全問題。

ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。同步機制採用了“時間換空間”的方式,僅提供一份變量,不同的線程在訪問前需要獲取鎖,沒獲得鎖的線程則需要排隊。而ThreadLocal採用了“空間換時間”的方式。

ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

Spring自動裝配

在spring中,對象無需自己查找或創建與其關聯的其他對象,由容器負責把需要相互協作的對象引用賦予各個對象,使用autowire來配置自動裝載模式。

在Spring框架xml配置中共有5種自動裝配:

(1)no:默認的方式是不進行自動裝配的,通過手工設置ref屬性來進行裝配bean。

(2)byName:通過bean的名稱進行自動裝配,如果一個bean的 property 與另一bean 的name 相同,就進行自動裝配。

(3)byType:通過參數的數據類型進行自動裝配。

(4)constructor:利用構造函數進行裝配,並且構造函數的參數通過byType進行裝配。

(5)autodetect:自動探測,如果有構造方法,通過 construct的方式自動裝配,否則使用 byType的方式自動裝配。

基於註解的方式:

使用@Autowired註解來自動裝配指定的bean。在使用@Autowired註解之前需要在Spring配置文件進行配置,<context:annotation-config />。在啓動spring IoC時,容器自動裝載了一個AutowiredAnnotationBeanPostProcessor後置處理器,當容器掃描到@Autowied、@Resource或@Inject時,就會在IoC容器自動查找需要的bean,並裝配給該對象的屬性。在使用@Autowired時,首先在容器中查詢對應類型的bean:

如果查詢結果剛好爲一個,就將該bean裝配給@Autowired指定的數據;

如果查詢的結果不止一個,那麼@Autowired會根據名稱來查找;

如果上述查找的結果爲空,那麼會拋出異常。解決方法時,使用required=false。

@Autowired可用於:構造函數、成員變量、Setter方法

注:@Autowired和@Resource之間的區別

(1) @Autowired默認是按照類型裝配注入的,默認情況下它要求依賴對象必須存在(可以設置它required屬性爲false)。

(2) @Resource默認是按照名稱來裝配注入的,只有當找不到與名稱匹配的bean纔會按照類型來裝配注入。

spring控制bean加載順序

Bean上使用@Order註解,如@Order(2)。數值越小表示優先級越高。默認優先級最低。

##Spring註解@Resource和@Autowired區別

@Resource和@Autowired都是做bean的注入時使用,其實@Resource並不是Spring的註解,它的包是javax.annotation.Resource,需要導入,但是Spring支持該註解的注入

1、共同點

兩者都可以寫在字段和setter方法上。兩者如果都寫在字段上,那麼就不需要再寫setter方法。

2、 @Autowired默認按類型裝配(這個註解是屬業spring的),默認情況下必須要求依賴對象必須存在,如果要允許null值,可以設置它的required屬性爲false,如:@Autowired(required=false) ,如果我們想使用名稱裝配可以結合@Qualifier註解進行使用,如下:

@Autowired()
@Qualifier(“baseDao”)
private BaseDao baseDao;

3、@Resource(這個註解屬於J2EE的),默認安裝名稱進行裝配,名稱可以通過name屬性進行指定,如果沒有指定name屬性,當註解寫在字段上時,默認取字段名進行安裝名稱查找,如果註解寫在setter方法上默認取屬性名進行裝配。當找不到與名稱匹配的bean時才按照類型進行裝配。但是需要注意的是,如果name屬性一旦指定,就只會按照名稱進行裝配。

@Resource(name=”baseDao”)
private BaseDao baseDao;

Spring@Component和@Bean區別

  1. 作用對象不同: @Component 註解作用於類,而@Bean註解作用於方法。
  2. @Component通常是通過類路徑掃描來自動偵測以及自動裝配到Spring容器中(我們可以使用 @ComponentScan 註解定義要掃描的路徑從中找出標識了需要裝配的類自動裝配到 Spring 的 bean 容器中)。@Bean 註解通常是我們在標有該註解的方法中定義產生這個 bean,@Bean告訴了Spring這是某個類的示例,當我需要用它的時候還給我。
  3. @Bean 註解比 Component 註解的自定義性更強,而且很多地方我們只能通過 @Bean 註解來註冊bean。比如當我們引用第三方庫中的類需要裝配到 Spring容器時,則只能通過 @Bean來實現。

@Bean註解使用示例:

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

上面的代碼相當於下面的 xml 配置

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

Spring中BeanFactory和FactoryBean區別

1、 BeanFactory

BeanFactory定義了IOC容器的最基本形式,並提供了IOC容器應遵守的的最基本的接口,也就是Spring IOC所遵守的最底層和最基本的編程規範。

它的職責包括:實例化、定位、配置應用程序中的對象及建立這些對象間的依賴。

在Spring代碼中,BeanFactory只是個接口,並不是IOC容器的具體實現,

​ 但是Spring容器給出了很多種實現,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,都是附加了某種功能的實現。

2、FactoryBean
一般情況下,Spring通過反射機制利用的class屬性指定實現類實例化Bean,在某些情況下,實例化Bean過程比較複雜,

如果按照傳統的方式,則需要在中提供大量的配置信息。配置方式的靈活性是受限的,這時採用編碼的方式可能會得到一個簡單的方案。

Spring爲此提供了一個org.springframework.bean.factory.FactoryBean的工廠類接口,用戶可以通過實現該接口定製實例化Bean的邏輯。
FactoryBean接口對於Spring框架來說佔用重要的地位,Spring自身就提供了70多個FactoryBean的實現。

BeanFactory和ApplicationContext有什麼區別?

BeanFactory和ApplicationContext是Spring的兩大核心接口,都可以當做Spring的容器。其中ApplicationContext是BeanFactory的子接口。

(1)BeanFactory:是Spring裏面最底層的接口,包含了各種Bean的定義,讀取bean配置文檔,管理bean的加載、實例化,控制bean的生命週期,維護bean之間的依賴關係。ApplicationContext接口作爲BeanFactory的派生,除了提供BeanFactory所具有的功能外,還提供了更完整的框架功能:

①繼承MessageSource,因此支持國際化。

②統一的資源文件訪問方式。

③提供在監聽器中註冊bean的事件。

④同時加載多個配置文件。

⑤載入多個(有繼承關係)上下文 ,使得每一個上下文都專注於一個特定的層次,比如應用的web層。

(2)①BeanFactroy採用的是延遲加載形式來注入Bean的,即只有在使用到某個Bean時(調用getBean()),纔對該Bean進行加載實例化。這樣,我們就不能發現一些存在的Spring的配置問題。如果Bean的某一個屬性沒有注入,BeanFacotry加載後,直至第一次使用調用getBean方法纔會拋出異常。

    ②ApplicationContext,它是在容器啓動時,一次性創建了所有的Bean。這樣,在容器啓動時,我們就可以發現Spring中存在的配置錯誤,這樣有利於檢查所依賴屬性是否注入。 ApplicationContext啓動後預載入所有的單實例Bean,通過預載入單實例bean ,確保當你需要的時候,你就不用等待,因爲它們已經創建好了。

    ③相對於基本的BeanFactory,ApplicationContext 唯一的不足是佔用內存空間。當應用程序配置Bean較多時,程序啓動較慢。

(3)BeanFactory通常以編程的方式被創建,ApplicationContext還能以聲明的方式創建,如使用ContextLoader。

(4)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但兩者之間的區別是:BeanFactory需要手動註冊,而ApplicationContext則是自動註冊

MySQL

什麼是MySQL

MySQL 是一種關係型數據庫,在Java企業級開發中非常常用,因爲 MySQL 是開源免費的,並且方便擴展。阿里巴巴數據庫系統也大量用到了 MySQL,因此它的穩定性是有保障的。MySQL是開放源代碼的,因此任何人都可以在 GPL(General Public License) 的許可下下載並根據個性化的需要對其進行修改。MySQL的默認端口號是3306

存儲引擎

image-20200209085146084

InnoDB

  • InnoDB存儲引擎支持事務,回滾以及系統崩潰修復能力和多版本迸發控制的事務的安全。
  • InnoDB支持自增長列(auto_increment),支持外鍵(foreignkey)
  • InnoDB支持mvcc的行級鎖
  • InnoDB存儲引擎索引使用的是B+Tree,聚集索引

MyISAM

  • MyISAM 這種存儲引擎不支持事務,不支持行級鎖,只支持併發插入的表鎖,主要用於高負載的select。
  • MyISAM 類型的表支持三種不同的存儲結構:靜態型(表列大小固定)、動態型(空間少,但碎片造成性能降低)、壓縮型(只讀表,減少佔用空間)。
  • MyISAM也使用B+tree索引但和Innodb的在具體實現上有些不同,是非聚集索引。

MEMORY

  • 基於memory存儲引擎的表實際對應一個磁盤文件,該文件只存儲表的結構,而其數據文件都是存儲在內存中,這樣有利於對數據的快速處理,提高整個表的處理能力。
  • memory存儲引擎文件數據都存儲在內存中,如果mysqld進程發生異常,重啓或關閉機器這些數據都會消失。所以memory存儲引擎中的表的生命週期很短,一般只使用一次。
  • memory存儲引擎默認使用哈希(HASH)索引,其速度比使用B-+Tree型要快,如果讀者希望使用B樹型,則在創建的時候可以引用。

Mysql事務

  • 原子性

    事務是最小的執行單位,不允許分割。事務的原子性確保動作要麼全部完成,要麼完全不起作用;

  • 一致性

    執行事務前後,數據保持一致,多個事務對同一個數據讀取的結果是相同的;

  • 隔離性

    ​ 併發訪問數據庫時,一個用戶的事務不被其他事務所幹擾,各併發事務之間數據庫是獨立的;

    • 隔離級別

      1. 未提交讀 read-uncommitted 存在髒讀,不可重複讀,幻讀

        最低的隔離級別,允許讀取尚未提交的數據變更,可能會導致髒讀、幻讀或不可重複讀

      2. 已提交讀 read-committed 存在不可重複讀

        允許讀取併發事務已經提交的數據,可以阻止髒讀,但是幻讀或不可重複讀仍有可能發生

      3. 可重複讀 repeatable read 存在幻讀, MySQL默認級別

        對同一字段的多次讀取結果都是一致的,除非數據是被本身事務自己所修改,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生

      4. 串行化 Serializable

        最高的隔離級別,完全服從ACID的隔離級別。所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止髒讀、不可重複讀以及幻讀

  • 持久性

    ​ 一個事務被提交之後。它對數據庫中數據的改變是持久的,即使數據庫發生故障也不應該對其有任何影響。

可能出現問題

  • 髒讀:指一個線程中的事務讀取到了另外一個線程中未提交的數據。
  • 不可重複讀(虛讀):指一個線程中的事務讀取到了另外一個線程中提交的update的數據。
  • 幻讀:指一個線程中的事務讀取到了另外一個線程中提交的insert的數據。

鎖機制和InnoDB鎖算法

MyISAM和InnoDB存儲引擎使用的鎖:

  • MyISAM採用表級鎖(table-level locking)。
  • InnoDB支持行級鎖(row-level locking)和表級鎖,默認爲行級鎖

表級鎖和行級鎖對比:

  • 表級鎖: MySQL中鎖定 粒度最大 的一種鎖,對當前操作的整張表加鎖,實現簡單,資源消耗也比較少,加鎖快,不會出現死鎖。其鎖定粒度最大,觸發鎖衝突的概率最高,併發度最低,MyISAM和 InnoDB引擎都支持表級鎖。
  • 行級鎖: MySQL中鎖定 粒度最小 的一種鎖,只針對當前操作的行進行加鎖。 行級鎖能大大減少數據庫操作的衝突。其加鎖粒度最小,併發度高,但加鎖的開銷也最大,加鎖慢,會出現死鎖。

InnoDB存儲引擎的鎖的算法有三種:

  • Record lock:單個行記錄上的鎖
  • Gap lock:間隙鎖,鎖定一個範圍,不包括記錄本身
  • Next-key lock:record+gap 鎖定一個範圍,包含記錄本身

相關知識點:

  1. innodb對於行的查詢使用next-key lock
  2. Next-locking keying爲了解決Phantom Problem幻讀問題
  3. 當查詢的索引含有唯一屬性時,將next-key lock降級爲record key
  4. Gap鎖設計的目的是爲了阻止多個事務將記錄插入到同一範圍內,而這會導致幻讀問題的產生
  5. 有兩種方式顯式關閉gap鎖:(除了外鍵約束和唯一性檢查外,其餘情況僅使用record lock) A. 將事務隔離級別設置爲RC B. 將參數innodb_locks_unsafe_for_binlog設置爲1

索引

MySQL索引使用的數據結構主要有BTree索引哈希索引 。對於哈希索引來說,底層的數據結構就是哈希表,因此在絕大多數需求爲單條記錄查詢的時候,可以選擇哈希索引,查詢性能最快;其餘大部分場景,建議選擇BTree索引。

MySQL的BTree索引使用的是B樹中的B+Tree,但對於主要的兩種存儲引擎的實現方式是不同的。

  • MyISAM: B+Tree葉節點的data域存放的是數據記錄的地址。在索引檢索的時候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其 data 域的值,然後以 data 域的值爲地址讀取相應的數據記錄。這被稱爲“非聚簇索引”。

  • InnoDB: 其數據文件本身就是索引文件。相比MyISAM,索引文件和數據文件是分離的,其表數據文件本身就是按B+Tree組織的一個索引結構,樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。這被稱爲“聚簇索引(或聚集索引)”。而其餘的索引都作爲輔助索引,輔助索引的data域存儲相應記錄主鍵的值而不是地址,這也是和MyISAM不同的地方。在根據主索引搜索時,直接找到key所在的節點即可取出數據;在根據輔助索引查找時,則需要先取出主鍵的值,再走一遍主索引。 因此,在設計表的時候,不建議使用過長的字段作爲主鍵,也不建議使用非單調的字段作爲主鍵,這樣會造成主索引頻繁分裂。 PS:整理自《Java工程師修煉之道》

索引的類型

  1. 普通索引(Normal)
  2. 唯一索引(Unique)
  3. 全文索引(Fulltext)
  4. 組合索引(Spatail)

索引方法

  1. Btree
  2. Hash

索引爲什麼有效

如果沒有索引我們查詢數據是需要遍歷雙向鏈表來定位對應的page,現在通過索引創建的“目錄”就可以很快定位對應頁上了!

其實底層實現的結構就是B+樹,B+樹作爲樹的一種實現能夠讓我們很快查找出對應內容。

B樹和B+樹不同點

  • 所有葉子節點中包含了全部關鍵字的信息。
  • 各葉子節點用指針進行連接。
  • 非葉子節點上只存儲 key 的信息,這樣相對 B 樹,可以增加每一頁中存儲 key 的數量。
  • B 樹是縱向擴展,最終變成一個“瘦高個”,而 B+ 樹是橫向擴展的,最終會變成一個“矮胖子”。在 B+ 樹中,所有記錄節點都是按鍵值的大小順序存放在同一層的葉子節點上。B+ 樹中的 B 不是代表二叉(binary) 而是代表(balance),B+ 樹並不是一個二叉樹。

B+與B樹最大的區別就是:B+樹它的鍵一定會出現在葉子節點上,同時也有可能在非葉子節點中重複出現。而 B 樹中同一鍵值不會出現多次。

覆蓋索引和聯合索引

覆蓋索引

select的數據列僅從索引中就能夠取得並且不必從數據表中讀取,換句話說查詢列要被所使用的索引覆蓋。
例子:如以country表a(表示城市)、b(表示省)兩列做複合索引,select a from country where b=‘浙江省’

覆蓋索引就是從輔助索引中就能直接得到查詢結果,而不需要回表到聚簇索引中進行再次查詢,所以可以減少搜索次數(不需要從輔助索引樹回表到聚簇索引樹),或者說減少IO操作(通過輔助索引樹可以一次性從磁盤載入更多節點),從而提升性能。

聯合索引

聯合索引是指對錶上的多個列進行索引。下面以一個例子進行說明。假設有下面這樣一張表,有這樣一個需求,我們需要查詢某個用戶的購物情況,並按照時間進行排序,取出某用戶近幾次的購物情況。

聯合索引(a, b)是根據a, b進行排序(先根據a排序,如果a相同則根據b排序)。因此,下列語句可以直接使用聯合索引得到結果(事實上,也就是用到了最左前綴原則)

最左前綴原則

對於有很多字段的一張表,查詢的方式是多樣的,難道要爲了每一種可能的查詢都定義索引嗎?這樣豈不是很浪費空間,畢竟建索引也是需要一些空間的。事實上,B+ 樹這種索引結構,可以利用索引的“最左前綴”原則來定位記錄,避免重複定義索引。

索引項是按照索引定義裏面出現的字段順序排序的

只要滿足最左前綴,就可以利用索引來加速檢索。這個最左前綴可以是聯合索引的最左 N 個字段,也可以是字符串索引的最左 M 個字符。

image-20200211084225418

Explian

explain這個命令來查看一個這些SQL語句的執行計劃,查看該SQL語句有沒有使用上了索引,有沒有做全表掃描,這都可以通過explain命令來查看。所以我們深入瞭解MySQL的基於開銷的優化器,還可以獲得很多可能被優化器考慮到的訪問策略的細節,以及當運行SQL語句時哪種策略預計會被優化器採用。

概要描述:
id:選擇標識符
select_type:表示查詢的類型。
table:輸出結果集的表
partitions:匹配的分區
type:表示表的連接類型
possible_keys:表示查詢時,可能使用的索引
key:表示實際使用的索引
key_len:索引字段的長度
ref:列與索引的比較
rows:掃描出的行數(估算的行數)
filtered:按表條件過濾的行百分比
Extra:執行情況的描述和說明

一、id

SELECT識別符。這是SELECT的查詢序列號

我的理解是SQL執行的順序的標識,SQL從大到小的執行

SELECT識別符。這是SELECT的查詢序列號

我的理解是SQL執行的順序的標識,SQL從大到小的執行

  1. id相同時,執行順序由上至下

  2. 如果是子查詢,id的序號會遞增,id值越大優先級越高,越先被執行

  3. id如果相同,可以認爲是一組,從上往下順序執行;在所有組中,id值越大,優先級越高,越先執行

二、select_type**

示查詢中每個select子句的類型

(1) SIMPLE(簡單SELECT,不使用UNION或子查詢等)

(2) PRIMARY(子查詢中最外層查詢,查詢中若包含任何複雜的子部分,最外層的select被標記爲PRIMARY)

(3) UNION(UNION中的第二個或後面的SELECT語句)

(4) DEPENDENT UNION(UNION中的第二個或後面的SELECT語句,取決於外面的查詢)

(5) UNION RESULT(UNION的結果,union語句中第二個select開始後面所有select)

(6) SUBQUERY(子查詢中的第一個SELECT,結果不依賴於外部查詢)

(7) DEPENDENT SUBQUERY(子查詢中的第一個SELECT,依賴於外部查詢)

(8) DERIVED(派生表的SELECT, FROM子句的子查詢)

(9) UNCACHEABLE SUBQUERY(一個子查詢的結果不能被緩存,必須重新評估外鏈接的第一行)

三、table

顯示這一步所訪問數據庫中表名稱(顯示這一行的數據是關於哪張表的),有時不是真實的表名字,可能是簡稱,例如上面的e,d,也可能是第幾步執行的結果的簡稱

四、type

對錶訪問方式,表示MySQL在表中找到所需行的方式,又稱“訪問類型”。

常用的類型有: **ALL、index、range、 ref、eq_ref、const、system、**NULL(從左到右,性能從差到好)

ALL:Full Table Scan, MySQL將遍歷全表以找到匹配的行

index: Full Index Scan,index與ALL區別爲index類型只遍歷索引樹

range:只檢索給定範圍的行,使用一個索引來選擇行

ref: 表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值

eq_ref: 類似ref,區別就在使用的索引是唯一索引,對於每個索引鍵值,表中只有一條記錄匹配,簡單來說,就是多表連接中使用primary key或者 unique key作爲關聯條件

const、system: 當MySQL對查詢某部分進行優化,並轉換爲一個常量時,使用這些類型訪問。如將主鍵置於where列表中,MySQL就能將該查詢轉換爲一個常量,system是const類型的特例,當查詢的表只有一行的情況下,使用system

NULL: MySQL在優化過程中分解語句,執行時甚至不用訪問表或索引,例如從一個索引列裏選取最小值可以通過單獨索引查找完成。

五、possible_keys

指出MySQL能使用哪個索引在表中找到記錄,查詢涉及到的字段上若存在索引,則該索引將被列出,但不一定被查詢使用(該查詢可以利用的索引,如果沒有任何索引顯示 null)

該列完全獨立於EXPLAIN輸出所示的表的次序。這意味着在possible_keys中的某些鍵實際上不能按生成的表次序使用。
如果該列是NULL,則沒有相關的索引。在這種情況下,可以通過檢查WHERE子句看是否它引用某些列或適合索引的列來提高你的查詢性能。如果是這樣,創造一個適當的索引並且再次用EXPLAIN檢查查詢

六、Key

key列顯示MySQL實際決定使用的鍵(索引),必然包含在possible_keys中

如果沒有選擇索引,鍵是NULL。要想強制MySQL使用或忽視possible_keys列中的索引,在查詢中使用FORCE INDEX、USE INDEX或者IGNORE INDEX。

七、key_len

表示索引中使用的字節數,可通過該列計算查詢中使用的索引的長度(key_len顯示的值爲索引字段的最大可能長度,並非實際使用長度,即key_len是根據表定義計算而得,不是通過表內檢索出的)

不損失精確性的情況下,長度越短越好

八、ref

列與索引的比較,表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值

九、rows

估算出結果集行數,表示MySQL根據表統計信息及索引選用情況,估算的找到所需的記錄所需要讀取的行數

十、Extra

該列包含MySQL解決查詢的詳細信息,有以下幾種情況:

Using where:不用讀取表中所有信息,僅通過索引就可以獲取所需數據,這發生在對錶的全部的請求列都是同一個索引的部分的時候,表示mysql服務器將在存儲引擎檢索行後再進行過濾

Using temporary:表示MySQL需要使用臨時表來存儲結果集,常見於排序和分組查詢,常見 group by ; order by

Using filesort:當Query中包含 order by 操作,而且無法利用索引完成的排序操作稱爲“文件排序”

-- 測試Extra的filesort
explain select * from emp order by name;

Using join buffer:改值強調了在獲取連接條件時沒有使用索引,並且需要連接緩衝區來存儲中間結果。如果出現了這個值,那應該注意,根據查詢的具體情況可能需要添加索引來改進能。

Impossible where:這個值強調了where語句會導致沒有符合條件的行(通過收集統計信息不可能存在結果)。

Select tables optimized away:這個值意味着僅通過使用索引,優化器可能僅從聚合函數結果中返回一行

No tables used:Query語句中使用from dual 或不含任何from子句

• EXPLAIN不會告訴你關於觸發器、存儲過程的信息或用戶自定義函數對查詢的影響情況
• EXPLAIN不考慮各種Cache
• EXPLAIN不能顯示MySQL在執行查詢時所作的優化工作
• 部分統計信息是估算的,並非精確值
• EXPALIN只能解釋SELECT操作,其他操作要重寫爲SELECT後查看執行計劃。

大表優化

  1. 查詢時通過條件查詢

  2. 讀寫分離

    經典的數據庫拆分方案,主庫負責寫,從庫負責讀;

  3. 垂直分區

    根據數據庫裏面數據表的相關性進行拆分。 例如,用戶表中既有用戶的登錄信息又有用戶的基本信息,可以將用戶表拆分成兩個單獨的表,甚至放到單獨的庫做分庫。

    簡單來說垂直拆分是指數據表列的拆分,把一張列比較多的表拆分爲多張表。 如下圖所示,這樣來說大家應該就更容易理解了。

    • 垂直拆分的優點: 可以使得列數據變小,在查詢時減少讀取的Block數,減少I/O次數。此外,垂直分區可以簡化表的結構,易於維護。
    • 垂直拆分的缺點: 主鍵會出現冗餘,需要管理冗餘列,並會引起Join操作,可以通過在應用層進行Join來解決。此外,垂直分區會讓事務變得更加複雜;
  4. 水平分區

    保持數據表結構不變,通過某種策略存儲數據分片。這樣每一片數據分散到不同的表或者庫中,達到了分佈式的目的。 水平拆分可以支撐非常大的數據量。

    水平拆分是指數據錶行的拆分,表的行數超過200萬行時,就會變慢,這時可以把一張的表的數據拆成多張表來存放。舉個例子:我們可以將用戶信息表拆分成多個用戶信息表,這樣就可以避免單一表數據量過大對性能造成影響。

**分庫分表ID處理

  • UUID:不適合作爲主鍵,因爲太長了,並且無序不可讀,查詢效率低。比較適合用於生成唯一的名字的標示比如文件的名字。
  • 數據庫自增 id : 兩臺數據庫分別設置不同步長,生成不重複ID的策略來實現高可用。這種方式生成的 id 有序,但是需要獨立部署數據庫實例,成本高,還會有性能瓶頸。
  • 利用 redis 生成 id : 性能比較好,靈活方便,不依賴於數據庫。但是,引入了新的組件造成系統更加複雜,可用性降低,編碼更加複雜,增加了系統成本。
  • Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
  • 美團的Leaf分佈式ID生成系統 :Leaf 是美團開源的分佈式ID生成器,能保證全局唯一性、趨勢遞增、單調遞增、信息安全,裏面也提到了幾種分佈式方案的對比,但也需要依賴關係數據庫、Zookeeper等中間件。感覺還不錯。美團技術團隊的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。

SQL優化

  1. 對查詢進行優化,應儘量避免全表掃描,首先應考慮在 where 及 order by 涉及的列上建立索引
  2. 應儘量避免在 where 子句中對字段進行 null 值判斷,否則將導致引擎放棄使用索引而進行全表掃描,如:
    select id from t where num is null
    可以在num上設置默認值0,確保表中num列沒有null值,然後這樣查詢:
    select id from t where num=0
  3. in 和 not in 也要慎用,否則會導致全表掃描, exists 代替 in 是一個好的選擇,如:
    select id from t where num in(1,2,3)
    對於連續的數值,能用 between 就不要用 in 了:
    select id from t where num between 1 and 3
  4. 數據多,可以條件查詢
  5. 不能使用select*from查詢
  6. 儘可能使用varchar代替char,因爲首先變長字段存儲空間小,可以節省存儲空間,且搜索效率高
  7. union代替or,or操作會全表掃描
  8. Not運算,無法使用索引
  9. 表連接
    • 連接的表越多,性能越差
    • 可能的話,將連接拆分成若干個過程逐一執行
    • 優先執行可顯著減少數據量的連接,既降低了複雜度,也能夠容易按照預期執行
    • 如果不可避免多表連接,很可能是設計缺陷
    • 外鏈接效果差,因爲必須對左右表進行表掃描
    • 儘量使用inner join查詢

索引失效情況

1.如果條件中有or,即使其中有條件帶索引也不會使用(這也是爲什麼儘量少用or的原因)

要想使用or,又想讓索引生效,只能將or條件中的每個列都加上索引

2.對於多列索引,不是使用的第一部分,則不會使用索引

3.like查詢以%開頭

4.如果列類型是字符串,那一定要在條件中將數據使用引號引用起來,否則不使用索引

5.如果mysql估計使用全表掃描要比使用索引快,則不使用索引

6.不在索引列上做任何操作(計算,函數,(自動或者手動)類型裝換),會導致索引失效而導致全表掃描。

7. is null或者is not null 也會導致無法使用索引

8. 字符串不加單引號索引失效。

Redis

Redis簡介

redis 就是一個數據庫,不過與傳統數據庫不同的是 redis 的數據是存在內存中的,所以讀寫速度非常快,因此 redis 被廣泛應用於緩存方向。另外,redis 也經常用來做分佈式鎖。redis 提供了多種數據類型來支持不同的業務場景。除此之外,redis 支持事務 、持久化、LUA腳本、LRU驅動事件、多種集羣方案。

Redis作用

高性能:

假如用戶第一次訪問數據庫中的某些數據。這個過程會比較慢,因爲是從硬盤上讀取的。將該用戶訪問的數據存在緩存中,這樣下一次再訪問這些數據的時候就可以直接從緩存中獲取了。操作緩存就是直接操作內存,所以速度相當快。如果數據庫中的對應數據改變的之後,同步改變緩存中相應的數據即可!

image-20200209105526252

高併發:

直接操作緩存能夠承受的請求是遠遠大於直接訪問數據庫的,所以我們可以考慮把數據庫中的部分數據轉移到緩存中去,這樣用戶的一部分請求會直接到緩存這裏而不用經過數據庫。

image-20200209105539571

Redis線程模型

edis 內部使用文件事件處理器 file event handler,這個文件事件處理器是單線程的,所以 redis 才叫做單線程的模型。它採用 IO 多路複用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器進行處理。

文件事件處理器的結構包含 4 個部分:

  • 多個 socket
  • IO 多路複用程序
  • 文件事件分派器
  • 事件處理器(連接應答處理器、命令請求處理器、命令回覆處理器)

多個 socket 可能會併發產生不同的操作,每個操作對應不同的文件事件,但是 IO 多路複用程序會監聽多個 socket,會將 socket 產生的事件放入隊列中排隊,事件分派器每次從隊列中取出一個事件,把該事件交給對應的事件處理器進行處理。

Redis常見數據結構及使用場景

1.String

常用命令: set,get,decr,incr,mget 等。

String數據結構是簡單的key-value類型,value其實不僅可以是String,也可以是數字。 常規key-value緩存應用; 常規計數:微博數,粉絲數等。

2.Hash

常用命令: hget,hset,hgetall 等。

hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用於存儲對象,後續操作的時候,你可以直接僅僅修改這個對象中的某個字段的值。 比如我們可以 hash 數據結構來存儲用戶信息,商品信息等等。比如下面我就用 hash 類型存放了我本人的一些信息:

key=JavaUser293847
value={
  “id”: 1,
  “name”: “SnailClimb”,
  “age”: 22,
  “location”: “Wuhan, Hubei”
}

3.List

常用命令: lpush,rpush,lpop,rpop,lrange等

list 就是鏈表,Redis list 的應用場景非常多,也是Redis最重要的數據結構之一,比如微博的關注列表,粉絲列表,消息列表等功能都可以用Redis的 list 結構來實現。

Redis list 的實現爲一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷。

另外可以通過 lrange 命令,就是從某個元素開始讀取多少個元素,可以基於 list 實現分頁查詢,這個很棒的一個功能,基於 redis 實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西(一頁一頁的往下走),性能高。

4.Set

常用命令: sadd,spop,smembers,sunion 等

set 對外提供的功能與list類似是一個列表的功能,特殊之處在於 set 是可以自動排重的。

當你需要存儲一個列表數據,又不希望出現重複數據時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要接口,這個也是list所不能提供的。可以基於 set 輕易實現交集、並集、差集的操作。

比如:在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能。這個過程也就是求交集的過程,具體命令如下:

sinterstore key1 key2 key3     將交集存在key1內

5.Sorted Set

常用命令: zadd,zrange,zrem,zcard等

和set相比,sorted set增加了一個權重參數score,使得集合中的元素能夠按score進行有序排列。

舉例: 在直播系統中,實時排行信息包含直播間在線用戶列表,各種禮物排行榜,彈幕消息(可以理解爲按消息維度的消息排行榜)等信息,適合使用 Redis 中的 Sorted Set 結構進行存儲。

Redis設置過期時間

Redis中有個設置時間過期的功能,即對存儲在 redis 數據庫中的值可以設置一個過期時間。作爲一個緩存數據庫,這是非常實用的。如我們一般項目中的 token 或者一些登錄信息,尤其是短信驗證碼都是有時間限制的,按照傳統的數據庫處理方式,一般都是自己判斷過期,這樣無疑會嚴重影響項目性能。

我們 set key 的時候,都可以給一個 expire time,就是過期時間,通過過期時間我們可以指定這個 key 可以存活的時間。

如果假設你設置了一批 key 只能存活1個小時,那麼接下來1小時後,redis是怎麼對這批key進行刪除的?

定期刪除+惰性刪除。

通過名字大概就能猜出這兩個刪除方式的意思了。

  • 定期刪除:redis默認是每隔 100ms 就隨機抽取一些設置了過期時間的key,檢查其是否過期,如果過期就刪除。注意這裏是隨機抽取的。爲什麼要隨機呢?你想一想假如 redis 存了幾十萬個 key ,每隔100ms就遍歷所有的設置過期時間的 key 的話,就會給 CPU 帶來很大的負載!
  • 惰性刪除 :定期刪除可能會導致很多過期 key 到了時間並沒有被刪除掉。所以就有了惰性刪除。假如你的過期 key,靠定期刪除沒有被刪除掉,還停留在內存裏,除非你的系統去查一下那個 key,纔會被redis給刪除掉。這就是所謂的惰性刪除,也是夠懶的哈!

但是僅僅通過設置過期時間還是有問題的。我們想一下:如果定期刪除漏掉了很多過期 key,然後你也沒及時去查,也就沒走惰性刪除,此時會怎麼樣?如果大量過期key堆積在內存裏,導致redis內存塊耗盡了。怎麼解決這個問題呢? redis 內存淘汰機制。

Redis內存淘汰機制

redis 提供 6種數據淘汰策略:

  1. volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰
  2. volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰
  3. volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰
  4. allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key(這個是最常用的)
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
  6. no-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!

4.0版本後增加以下兩種:

  1. volatile-lfu:從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
  2. allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的key

Redis持久化機制

Redis支持持久化,而且支持兩種不同的持久化操作。Redis的一種持久化方式叫快照(snapshotting,RDB),另一種方式是隻追加文件(append-only file,AOF)。這兩種方法各有千秋,下面我會詳細這兩種持久化方法是什麼,怎麼用,如何選擇適合自己的持久化方法。

快照(snapshotting)持久化(RDB)

Redis可以通過創建快照來獲得存儲在內存裏面的數據在某個時間點上的副本。Redis創建快照之後,可以對快照進行備份,可以將快照複製到其他服務器從而創建具有相同數據的服務器副本(Redis主從結構,主要用來提高Redis性能),還可以將快照留在原地以便重啓服務器的時候使用。

快照持久化是Redis默認採用的持久化方式,在redis.conf配置文件中默認有此下配置:

save 900 1           #在900秒(15分鐘)之後,如果至少有1個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。

save 300 10          #在300秒(5分鐘)之後,如果至少有10個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。

save 60 10000        #在60秒(1分鐘)之後,如果至少有10000個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。

AOF(append-only file)持久化

與快照持久化相比,AOF持久化 的實時性更好,因此已成爲主流的持久化方案。默認情況下Redis沒有開啓AOF(append only file)方式的持久化,可以通過appendonly參數開啓:

appendonly yes

開啓AOF持久化後每執行一條會更改Redis中的數據的命令,Redis就會將該命令寫入硬盤中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通過dir參數設置的,默認的文件名是appendonly.aof。

在Redis的配置文件中存在三種不同的 AOF 持久化方式,它們分別是:

appendfsync always    #每次有數據修改發生時都會寫入AOF文件,這樣會嚴重降低Redis的速度
appendfsync everysec  #每秒鐘同步一次,顯示地將多個寫命令同步到硬盤
appendfsync no        #讓操作系統決定何時進行同步

爲了兼顧數據和寫入性能,用戶可以考慮 appendfsync everysec選項 ,讓Redis每秒同步一次AOF文件,Redis性能幾乎沒受到任何影響。而且這樣即使出現系統崩潰,用戶最多隻會丟失一秒之內產生的數據。當硬盤忙於執行寫入操作的時候,Redis還會優雅的放慢自己的速度以便適應硬盤的最大寫入速度。

Redis 4.0 對於持久化機制的優化

Redis 4.0 開始支持 RDB 和 AOF 的混合持久化(默認關閉,可以通過配置項 aof-use-rdb-preamble 開啓)。

如果把混合持久化打開,AOF 重寫的時候就直接把 RDB 的內容寫到 AOF 文件開頭。這樣做的好處是可以結合 RDB 和 AOF 的優點, 快速加載同時避免丟失過多的數據。當然缺點也是有的, AOF 裏面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。

補充內容:AOF 重寫

AOF重寫可以產生一個新的AOF文件,這個新的AOF文件和原有的AOF文件所保存的數據庫狀態一樣,但體積更小。

AOF重寫是一個有歧義的名字,該功能是通過讀取數據庫中的鍵值對來實現的,程序無須對現有AOF文件進行任何讀入、分析或者寫入操作。

在執行 BGREWRITEAOF 命令時,Redis 服務器會維護一個 AOF 重寫緩衝區,該緩衝區會在子進程創建新AOF文件期間,記錄服務器執行的所有寫命令。當子進程完成創建新AOF文件的工作之後,服務器會將重寫緩衝區中的所有內容追加到新AOF文件的末尾,使得新舊兩個AOF文件所保存的數據庫狀態一致。最後,服務器用新的AOF文件替換舊的AOF文件,以此來完成AOF文件重寫操作

Redis事務

Redis 通過 MULTI、EXEC、WATCH 等命令來實現事務(transaction)功能。事務提供了一種將多個命令請求打包,然後一次性、按順序地執行多個命令的機制,並且在事務執行期間,服務器不會中斷事務而改去執行其他客戶端的命令請求,它會將事務中的所有命令都執行完畢,然後纔去處理其他客戶端的命令請求。

在傳統的關係式數據庫中,常常用 ACID 性質來檢驗事務功能的可靠性和安全性。在 Redis 中,事務總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),並且當 Redis 運行在某種特定的持久化模式下時,事務也具有持久性(Durability)。

##Redis實現分佈式鎖

利用Redis的setnx命令。此命令同樣是原子性操作,只有在key不存在的情況下,才能set成功。(setnx命令並不完善,後續會介紹替代方案)

1.加鎖

最簡單的方法是使用setnx命令。key是鎖的唯一標識,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名爲 “lock_sale_商品ID” 。而value設置成什麼呢?鎖的value值爲一個隨機生成的UUID。我們可以姑且設置成1。加鎖的僞代碼如下:

setnx(key,1)
當一個線程執行setnx返回1,說明key原本不存在,該線程成功得到了鎖;當一個線程執行setnx返回0,說明key已經存在,該線程搶鎖失敗。

2.解鎖

有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行del指令,僞代碼如下:

del(key)
釋放鎖之後,其他線程就可以繼續執行setnx命令來獲得鎖。

3.鎖超時

鎖超時是什麼意思呢?如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的線程再也別想進來。

所以,setnx的key必須設置一個超時時間,單位爲second,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放,避免死鎖。setnx不支持超時參數,所以需要額外的指令,僞代碼如下:

expire(key, 30)

緩存雪崩和緩存穿透問題處理

緩存雪崩我們可以簡單的理解爲:由於原有緩存失效,新緩存未到期間
(例如:我們設置緩存時採用了相同的過期時間,在同一時刻出現大面積的緩存過期),所有原本應該訪問緩存的請求都去查詢數據庫了,而對數據庫CPU和內存造成巨大壓力,嚴重的會造成數據庫宕機。從而形成一系列連鎖反應,造成整個系統崩潰。
解決辦法:
大多數系統設計者考慮用加鎖( 最多的解決方案)或者隊列的方式保證來保證不會有大量的線程對數據庫一次性進行讀寫,從而避免失效時大量的併發請求落到底層存儲系統上。還有一個簡單方案就時講緩存失效時間分散開。

緩存雪崩

什麼是緩存雪崩?

簡介:緩存同一時間大面積的失效,所以,後面的請求都會落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。

有哪些解決辦法?

(中華石杉老師在他的視頻中提到過,視頻地址在最後一個問題中有提到):

  • 事前:儘量保證整個 redis 集羣的高可用性,發現機器宕機儘快補上。選擇合適的內存淘汰策略。
  • 事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL崩掉
  • 事後:利用 redis 持久化機制保存的數據儘快恢復緩存

緩存穿透

什麼是緩存穿透?

緩存穿透說簡單點就是大量請求的 key 根本不存在於緩存中,導致請求直接到了數據庫上,根本沒有經過緩存這一層。舉個例子:某個黑客故意製造我們緩存中不存在的 key 發起大量請求,導致大量請求落到數據庫。下面用圖片展示一下(這兩張圖片不是我畫的,爲了省事直接在網上找的,這裏說明一下):

image-20200209112703287

image-20200209112725189

一般MySQL 默認的最大連接數在 150 左右,這個可以通過 show variables like '%max_connections%';命令來查看。最大連接數一個還只是一個指標,cpu,內存,磁盤,網絡等無力條件都是其運行指標,這些指標都會限制其併發能力!所以,一般 3000 個併發請求就能打死大部分數據庫了。

解決方法

最基本的就是首先做好參數校驗,一些不合法的參數請求直接拋出異常信息返回給客戶端。比如查詢的數據庫 id 不能小於 0、傳入的郵箱格式不對的時候直接返回錯誤消息給客戶端等等。

緩存無效 key : 如果緩存和數據庫都查不到某個 key 的數據就寫一個到 redis 中去並設置過期時間,具體命令如下:SET key value EX 10086。這種方式可以解決請求的 key 變化不頻繁的情況,如何黑客惡意攻擊,每次構建的不同的請求key,會導致 redis 中緩存大量無效的 key 。很明顯,這種方案並不能從根本上解決此問題。如果非要用這種方式來解決穿透問題的話,儘量將無效的 key 的過期時間設置短一點比如 1 分鐘。

另外,這裏多說一嘴,一般情況下我們是這樣設計 key 的: 表名:列名:主鍵名:主鍵值

**布隆過濾器:**布隆過濾器是一個非常神奇的數據結構,通過它我們可以非常方便地判斷一個給定數據是否存在與海量數據中。我們需要的就是判斷 key 是否合法,有沒有感覺布隆過濾器就是我們想要找的那個“人”。具體是這樣做的:把所有可能存在的請求的值都存放在布隆過濾器中,當用戶請求過來,我會先判斷用戶發來的請求的值是否存在於布隆過濾器中。不存在的話,直接返回請求參數錯誤信息給客戶端,存在的話纔會走下面的流程

布隆過濾器(推薦)

就是引入了k(k>1)k(k>1)個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。
它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。
Bloom-Filter算法的核心思想就是利用多個不同的Hash函數來解決“衝突”。
Hash存在一個衝突(碰撞)的問題,用同一個Hash得到的兩個URL的值有可能相同。爲了減少衝突,我們可以多引入幾個Hash,如果通過其中的一個Hash值我們得出某元素不在集合中,那麼該元素肯定不在集合中。只有在所有的Hash函數告訴我們該元素在集合中時,才能確定該元素存在於集合中。這便是Bloom-Filter的基本思想。
Bloom-Filter一般用於在大數據量的集合中判定某元素是否存在。

如何解決Redis併發競爭Key問題

所謂 Redis 的併發競爭 Key 的問題也就是多個系統同時對一個 key 進行操作,但是最後執行的順序和我們期望的順序不同,這樣也就導致了結果的不同!

推薦一種方案:分佈式鎖(zookeeper 和 redis 都可以實現分佈式鎖)。(如果不存在 Redis 的併發競爭 Key 問題,不要使用分佈式鎖,這樣會影響性能)

基於zookeeper臨時有序節點可以實現的分佈式鎖。大致思想爲:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。完成業務流程後,刪除對應的子節點釋放鎖。

如何保持緩存與數據庫雙寫一致性

一般情況下我們都是這樣使用緩存的:先讀緩存,緩存沒有的話,就讀數據庫,然後取出數據後放入緩存,同時返回響應。這種方式很明顯會存在緩存和數據庫的數據不一致的情況。

你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性的問題,那麼你如何解決一致性問題?

一般來說,就是如果你的系統不是嚴格要求緩存+數據庫必須一致性的話,緩存可以稍微的跟數據庫偶爾有不一致的情況,最好不要做這個方案,讀請求和寫請求串行化,串到一個內存隊列裏去,這樣就可以保證一定不會出現不一致的情況

串行化之後,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。

Mybatis

Mybatis簡述

  • Mybatis是一個半ORM(對象關係映射)框架,它內部封裝了JDBC,開發時只需要關注SQL語句本身,不需要花費精力去處理加載驅動、創建連接、創建statement等繁雜的過程。程序員直接編寫原生態sql,可以嚴格控制sql執行性能,靈活度高。

  • MyBatis 可以使用 XML 或註解來配置和映射原生信息,將 POJO映射成數據庫中的記錄,避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集。

  • 通過xml 文件或註解的方式將要執行的各種 statement 配置起來,並通過java對象和 statement中sql的動態參數進行映射生成最終執行的sql語句,最後由mybatis框架執行sql並將結果映射爲java對象並返回。(從執行sql到返回result的過程)。

#{}和${}的區別是什麼

${}是字符串替換,#{}是預編譯處理。

Mybatis在處理#{}時,會將sql中的#{}替換爲?號,調用PreparedStatement的set方法來賦值;

Mybatis在處理${}時,就是把${}替換成變量的值。

使用#{}可以有效的防止SQL注入,提高系統安全性。

Xml和Mapper接口對應實現原理

Mapper 接口的工作原理是JDK動態代理,Mybatis運行時會使用JDK動態代理爲Mapper接口生成代理對象proxy,代理對象會攔截接口方法,轉而執行MapperStatement所代表的sql,然後將sql執行結果返回。

Mybatis 一二級緩存

  1. 一級緩存: 基於 PerpetualCache 的 HashMap 本地緩存,其存儲作用域爲 Session,當 Session flush 或 close 之後,該 Session 中的所有 Cache 就將清空,默認打開一級緩存。
  2. 二級緩存與一級緩存其機制相同,默認也是採用 PerpetualCache,HashMap 存儲,不同在於其存儲作用域爲 Mapper(Namespace),並且可自定義存儲源,如 Ehcache。默認不打開二級緩存,要開啓二級緩存,使用二級緩存屬性類需要實現Serializable序列化接口(可用來保存對象的狀態),可在它的映射文件中配置 ;
  3. 對於緩存數據更新機制,當某一個作用域(一級緩存 Session/二級緩存Namespaces)的進行了C/U/D 操作後,默認該作用域下所有 select 中的緩存將被 clear 掉並重新更新,如果開啓了二級緩存,則只根據配置判斷是否刷新。

微服務

SpringlCloud組件及作用

  • Eureka:各個服務啓動時,Eureka Client都會將服務註冊到Eureka Server,並且Eureka Client還可以反過來從Eureka Server拉取註冊表,從而知道其他服務在哪裏

  • Ribbon:服務間發起請求的時候,基於Ribbon做負載均衡,從一個服務的多臺機器中選擇一臺

  • Feign:基於Feign的動態代理機制,根據註解和選擇的機器,拼接請求URL地址,發起請求

  • Hystrix:發起請求是通過Hystrix的線程池來走的,不同的服務走不同的線程池,實現了不同服務調用的隔離,避免了服務雪崩的問題

  • Zuul:如果前端、移動端要調用後端系統,統一從Zuul網關進入,由Zuul網關轉發請求給對應的服務

  • Sleuth和Zipkin :鏈式追蹤排查服務

  • Oauth2 授權協議

image-20200208211355004

實現分佈式鎖的方式

  • 基於DB的唯一索引。
  • 基於ZK的臨時有序節點。
  • 基於Redis的NX、EX參數。

分佈式事務解決方案

tx-lcn

LCN分佈式事務框架其本身並不創建事務,而是基於對本地事務的協調從而達到事務一致性的效果

實現步驟

  1. 創建事務組
    是指在事務發起方開始執行業務代碼之前先調用TxManager創建事務組對象,然後拿到事務標示GroupId的過程。
  2. 添加事務組
    添加事務組是指參與方在執行完業務方法以後,將該模塊的事務信息添加通知給TxManager的操作。
  3. 關閉事務組
    是指在發起方執行完業務代碼以後,將發起方執行結果狀態通知給TxManager的動作。當執行完關閉事務組的方法以後,TxManager將根據事務組信息來通知相應的參與模塊提交或回滾事務。

image-20200211085402807

TxClient的代理連接池實現了javax.sql.DataSource接口,並重寫了close方法,事務模塊在提交關閉以後TxClient連接池將執行"假關閉"操作,等待TxManager協調完成事務以後在關閉連接。

事務補償機制

爲什麼需要事務補償?

事務補償是指在執行某個業務方法時,本應該執行成功的操作卻因爲服務器掛機或者網絡抖動等問題導致事務沒有正常提交,此種場景就需要通過補償來完成事務,從而達到事務的一致性。

補償機制的觸發條件?

當執行關閉事務組步驟時,若發起方接受到失敗的狀態後將會把該次事務識別爲待補償事務,然後發起方將該次事務數據異步通知給TxManager。TxManager接受到補償事務以後先通知補償回調地址,然後再根據是否開啓自動補償事務狀態來補償或保存該次切面事務數據。

補償事務機制?
LCN的補償事務原理是模擬上次失敗事務的請求,然後傳遞給TxClient模塊然後再次執行該次請求事務。

Activemq消息隊列

實現原理

ActiveMQ 在 queue 中存儲 Message 時,採用先進先出順序(FIFO)存儲。同一時間一個消息被分派給單個消費者,且只有當 Message 被消費並確認時,它才能從存儲中刪除。

image-20200213162218600

對於持久化訂閱者來說,每個消費者獲得 Message 的副本。爲了節省存儲空間,Provider 僅存儲消息的一個副本。持久化訂閱者維護了指向下一個 Message 的指針,並將其副本分派給消費者。以這種方式實現消息存儲,因爲每個持久化訂閱者可能以不同的速率消費 Message,或者它們可能不是全部同時運行。此外,因每個 Message 可能存在多個消費者,所以在它被成功地傳遞給所有持久化訂閱者之前,不能從存儲中刪除。
image-20200213162233045

DispatchPolcicy:轉發策略

  1. RoundRobinDispatchPolicy: “輪詢”,消息將依次發送給每個“訂閱者”。“訂閱者”列表默認按照訂閱的先後順序排列,在轉發消息時,對於匹配消息的第一個訂閱者,將會被移動到“訂閱者”列表的尾部,這也意味着“下一條”消息,將會較晚的轉發給它。

  2. StrictOrderDispatchPolicy: 嚴格有序,消息依次發送給每個訂閱者,按照“訂閱者”訂閱的時間先後。它和RoundRobin最大的區別是,沒有移動“訂閱者”順序的操作。

  3. PriorityDispatchPolicy: 基於“property”權重對“訂閱者”排序。它要求開發者首先需要對每個訂閱者指定priority,默認每個consumer的權重都一樣。

  4. SimpleDispatchPolicy: 默認值,按照當前“訂閱者”列表的順序。其中PriorityDispatchPolicy是其子類。

SubscriptionRecoveryPolicy: 恢復策略(Topic)

在非耐久訂閱者“失效”期間或一個新的Topic,broker可以保留的可追溯的消息量。前提是Topic必須是“retroactive”,我們可以在distination地址中指定此屬性,例如:“order.topic?consumer.retroactive=true”。默認情況下,訂閱者只能獲取“訂閱”開始之後的消息,如果Retroactive=true,那麼訂閱者就可以獲取其創建之前的消息列表。此Policy就是用來控制“retroactive”的消息量。

  1. FixedSizedSubscriptionRecoveryPolicy: 保存一定size的消息,broker將爲此Topic開闢定額的RAM用來保存最新的消息。
2) FixedCountSubscriptionRecoveryPolicy: 保存一定條數的消息。
3) LastImageSubscriptionRecoveryPolicy: 只保留最新的一條數據

4) QueryBasedSubscriptionRecoveryPolicy: 符合置頂selector的消息都將被保存,具體能夠“恢復”多少消息,由底層存儲機制決定;比如對於非持久化消息,只要內存中還存在,則都可以恢復。

5) TimedSubscriptionRecoveryPolicy: 保留最近一段時間的消息。
6) NoSubscriptionRecoveryPolicy: 關閉“恢復機制”。默認值。

ActiveMQ 常用的存儲方式

1.KahaDB

ActiveMQ 5.3 版本起的默認存儲方式。KahaDB存儲是一個基於文件的快速存儲消息,設計目標是易於使用且儘可能快。它使用基於文件的消息數據庫意味着沒有第三方數據庫的先決條件。

要啓用 KahaDB 存儲,需要在 activemq.xml 中進行以下配置:

2.AMQ

與 KahaDB 存儲一樣,AMQ存儲使用戶能夠快速啓動和運行,因爲它不依賴於第三方數據庫。AMQ 消息存儲庫是可靠持久性和高性能索引的事務日誌組合,當消息吞吐量是應用程序的主要需求時,該存儲是最佳選擇。但因爲它爲每個索引使用兩個分開的文件,並且每個 Destination 都有一個索引,所以當你打算在代理中使用數千個隊列的時候,不應該使用它。

3.JDBC

選擇關係型數據庫,通常的原因是企業已經具備了管理關係型數據的專長,但是它在性能上絕對不優於上述消息存儲實現。事實是,許多企業使用關係數據庫作爲存儲,是因爲他們更願意充分利用這些數據庫資源。

4.內存存儲

內存消息存儲器將所有持久消息保存在內存中。在僅存儲有限數量 Message 的情況下,內存消息存儲會很有用,因爲 Message 通常會被快速消耗。在 activema.xml 中將 broker 元素上的 persistent 屬性設置爲 false 即可。

其他框架

mybatis

tkmapper

xxljob 任務調度

logback 日誌管理

tx-lcn 事務框架

redis 緩存,分佈式鎖

冪等性問題

什麼是冪等性

一個請求,不管重複來多少次,結果是不會改變的。就是管多少次掉用接口的,最終接口返回的結果是一致的。

解決方案

  1. 全局唯一ID

    如果使用全局唯一ID,就是根據業務的操作和內容生成一個全局ID,在執行操作前先根據這個全局唯一ID是否存在,來判斷這個操作是否已經執行。如果不存在則把全局ID,存儲到存儲系統中,比如數據庫、Redis等。如果存在則表示該方法已經執行。

    使用全局唯一ID是一個通用方案,可以支持插入、更新、刪除業務操作。但是這個方案看起來很美但是實現起來比較麻煩,下面的方案適用於特定的場景,但是實現起來比較簡單。

  2. 多版本控制

    這種方法適合在更新的場景中,比如我們要更新商品的名字,這時我們就可以在更新的接口中增加一個版本號

微服保護機制(熔斷)

熔斷

當下遊的服務因爲某種原因突然變得不可用或響應過慢,上游服務爲了保證自己整體服務的可用性,不再繼續調用目標服務,直接返回,快速釋放資源。如果目標服務情況好轉則恢復調用(hytrix自動測試請求,請求在閾值內恢復調用)。

服務降級

當下遊的服務因爲某種原因響應過慢,下游服務主動停掉一些不太重要的業務,釋放出服務器資源,增加響應速度!
當下遊的服務因爲某種原因不可用,上游主動調用本地的一些降級邏輯,避免卡頓,迅速返回給用戶!

服務降級有很多種降級方式!如開關降級、限流降級、熔斷降級!

當整個微服務架構整體的負載超出了預設的上限閾值或即將到來的流量預計將會超過預設的閾值時,爲了保證重要或基本的服務能正常運行,我們可以將一些 不重要不緊急 的服務或任務進行服務的 延遲使用暫停使用

數據結構

數據結構概述

什麼是數據結構

數據結構是指相互之間存在着一種或多種關係的數據元素的集合和該集合中數據元素之間的關係組成。

數據存儲結構

順序存儲結構(數組)

是把數據元素存放在地址連續的存儲單元裏,其數據間的邏輯關係和物理關係是一致的。數組就是順序存儲結構的典型代表。

鏈式存儲(鏈表)

是把數據元素存放在內存中的任意存儲單元裏,也就是可以把數據存放在內存的各個位置。這些數據在內存中的地址可以是連續的,也可以是不連續的。

數據邏輯結構

集合結構

集合結構中的數據元素同屬於一個集合,他們之間是並列的關係,除此之外沒有其他關係。

image-20200210122612871

線性結構

線性結構中的元素存在一對一的相互關係。

image-20200210122655694

樹結構

樹形結構中的元素存在一對多的相互關係。

image-20200210122715263

圖形結構

圖形結構中的元素存在多對多的相互關係。

image-20200210122755459

加密

散列算法

  • MD5(Message-Digest Algorithm)

    用於確保信息傳輸完整一致。是計算機廣泛使用的雜湊算法之一(又譯摘要算法、哈希算法),主流編程語言普遍已有MD5實現。將數 據(如漢字)運算爲另一固定長度值,是雜湊算法的基礎原理,MD5的前身有MD2、MD3和 MD4。廣泛用於加密和解密技術,常用於文件校驗。校驗?不管文件多大,經過MD5後都能生成唯一的MD5值。好比現在的ISO校驗,都是MD5校驗。怎 麼用?當然是把ISO經過MD5後產生MD5的值。一般下載linux-ISO的朋友都見過下載鏈接旁邊放着MD5的串。就是用來驗證文件是否一致的。

  • SHA(Secure Hash Algorithm)

    主要適用於數字簽名標準(Digital Signature Standard DSS)裏面定義的數字簽名算法(Digital Signature Algorithm DSA)。對於長度小於2^64位的消息,SHA1會產生一個160位的消息摘要。該算法經過加密專家多年來的發展和改進已日益完善,並被廣泛使用。該算 法的思想是接收一段明文,然後以一種不可逆的方式將它轉換成一段(通常更小)密文,也可以簡單的理解爲取一串輸入碼(稱爲預映射或信息),並把它們轉化爲 長度較短、位數固定的輸出序列即散列值(也稱爲信息摘要或信息認證代碼)的過程。散列函數值可以說是對明文的一種“指紋”或是“摘要”所以對散列值的數字 簽名就可以視爲對此明文的數字簽名。

對稱加密

  • DES

    DES(Data Encryption Standard)是一種對稱加密算法,所謂對稱加密就是加密和解密都是使用同一個密鑰

    DES 使用一個 56 位的密鑰以及附加的 8 位奇偶校驗位,產生最大 64 位的分組大小。這是一個迭代的分組密碼,使用稱爲 Feistel 的技術,其中將加密的文本塊分成兩半。使用子密鑰對其中一半應用循環功能,然後將輸出與另一半進行"異或"運算;接着交換這兩半,這一過程會繼續下去,但最後一個循環不交換。DES 使用 16 個循環,使用異或,置換,代換,移位操作四種基本運算。

  • 3DES

    3DES是三重數據加密,且可以逆推的一種算法方案。但由於3DES的算法是公開的,所以算法本身沒有密鑰可言,主要依靠唯一密鑰來確保數據加解密的安全,其具體實現如下:設Ek()和Dk()代表DES算法的加密和解密過程,K代表DES算法使用的密鑰,M代表明文,C代表密文:

    3DES加密過程爲:C=Ek3(Dk2(Ek1(M)))

    3DES解密過程爲:M=Dk1(EK2(Dk3©))

    SecretKeyFactory.getInstance(“DESede”)

    DESede 相比 DES ,多出的ede,正好是encrypt - decrypt -encrypt,使用3條56位的密鑰對數據進行三次加密

  • AES

    AES加密屬於對稱加密算法,可以使用相同的密碼反向解密出來。另外,AES加密屬於典型的塊加密算法,其中常用的塊加密的工作模式包含:

    ECB模式:又稱電碼本(ECB,Electronic Codebook Book)模式。這是最簡單的塊密碼加密模式,加密前根據加密塊大小(如AES爲128位)分成若干塊,之後將每塊使用相同的密鑰單獨加密,解密同理。
    CBC模式:又稱密碼分組鏈接(CBC,Cipher-block chaining)模式。在這種加密模式中,每個密文塊都依賴於它前面的所有明文塊。同時,爲了保證每條消息的唯一性,在第一個塊中需要使用初始化向量IV。

非對稱加密

  • RSA

    1.RSA是基於大數因子分解難題。目前各種主流計算機語言都支持RSA算法的實現
    2.java6支持RSA算法
    3.RSA算法可以用於數據加密和數字簽名
    4.RSA算法相對於DES/AES等對稱加密算法,他的速度要慢的多
    5.總原則:公鑰加密,私鑰解密 / 私鑰加密,公鑰解密

    RSA算法構建密鑰對簡單的很,這裏我們還是以甲乙雙方發送數據爲模型
    1.甲方在本地構建密鑰對(公鑰+私鑰),並將公鑰公佈給乙方
    2.甲方將數據用私鑰進行加密,發送給乙方

  • ECC

Base64

Base64是一種能將任意Binary資料用64種字元組合成字串的方法,而這個Binary資料和字串資料彼此之間是可以互相轉換的,十分方便。在實際應用上,Base64除了能將Binary資料可視化之外,也常用來表示字串加密過後的內容。如果要使用Java 程式語言來實作Base64的編碼與解碼功能

Base64是網絡上最常見的用於傳輸8Bit字節碼的編碼方式之一,Base64就是一種基於64個可打印字符來標識二進制數據的方法。
Base64是一種可逆的編碼方式,是一種用64個Ascii字符來表示任意二進制數據的方法。
主要用於將不可打印字符轉換爲可打印字符,或者簡單的說將二進制數據編碼爲Ascii字符

  • **基本:**輸出被映射到一組字符A-Za-z0-9+/,編碼不添加任何行標,輸出的解碼僅支持A-Za-z0-9+/。
  • **URL:**輸出映射到一組字符A-Za-z0-9+_,輸出是URL和文件。
  • **MIME:**輸出隱射到MIME友好格式。輸出每行不超過76字符,並且使用’\r’並跟隨’\n’作爲分割。編碼輸出最後沒有行分割。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章