一些常見問題

1、常用設計模式

單例模式:懶漢式、餓漢式、雙重校驗鎖、靜態加載,內部類加載、枚舉類加載。保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

單例模式是一種常用的軟件設計模式,其定義是單例對象的類只能允許一個實例存在;一般介紹單例模式的書籍都會提到 餓漢式 和 懶漢式 這兩種實現方式。但是除了這兩種方式,本文還會介紹其他幾種實現單例的方式;

基本的實現思路

單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例的實現主要是通過以下兩個步驟:

  1. 將該類的構造方法定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
  2. 在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不爲空就返回這個引用,如果類保持的引用爲空就創建該類的實例並將實例的引用賦予該類保持的引用。

注意事項

單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那麼它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。 解決這個問題的辦法是爲指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會降低效率)。

代理模式:動態代理和靜態代理,什麼時候使用動態代理。

適配器模式:將一個類的接口轉換成客戶希望的另外一個接口。適配器模式使得原本由於接口不兼容而不能一起工作的那些類可以一起工作。

單例模式的八種寫法

1、餓漢式(靜態常量)[可用]

public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

2、餓漢式(靜態代碼塊)[可用]

public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

這種方式和上面的方式其實類似,只不過將類實例化的過程放在了靜態代碼塊中,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。優缺點和上面是一樣的。

3、懶漢式(線程不安全)[不可用]

 

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

 

這種寫法起到了Lazy Loading的效果,但是只能在單線程下使用。如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。

4、懶漢式(線程安全,同步方法)[不推薦用]

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

解決上面第三種實現方式的線程不安全問題,做個線程同步就可以了,於是就對getInstance()方法進行了線程同步。

缺點:效率太低了,每個線程在想獲得類的實例時候,執行getInstance()方法都要進行同步。而其實這個方法只執行一次實例化代碼就夠了,後面的想獲得該類實例,直接return就行了。方法進行同步效率太低要改進。

5、懶漢式(線程安全,同步代碼塊)[不可用]

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

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

 

由於第四種實現方式同步效率太低,所以摒棄同步方法,改爲同步產生實例化的的代碼塊。但是這種同步並不能起到線程同步的作用。跟第3種實現方式遇到的情形一致,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。

6、雙重檢查[推薦用]

 

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {}

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

 

Double-Check概念對於多線程開發者來說不會陌生,如代碼中所示,我們進行了兩次if (singleton == null)檢查,這樣就可以保證線程安全了。這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (singleton == null),直接return實例化對象。

優點:線程安全;延遲加載;效率較高。

7、靜態內部類[推薦用]

 

public class Singleton {

    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

 

這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不同的地方在餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。

類的靜態屬性只會在第一次加載類的時候初始化,所以在這裏,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。

優點:避免了線程不安全,延遲加載,效率高。

8、枚舉[推薦用]

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {

    }
}

藉助JDK1.5中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。可能是因爲枚舉在JDK1.5中才添加,所以在實際項目開發中,很少見人這麼寫過。

優點

系統內存中該類只存在一個對象,節省了系統資源,對於一些需要頻繁創建銷燬的對象,使用單例模式可以提高系統性能。

缺點

當想實例化一個單例類的時候,必須要記住使用相應的獲取對象的方法,而不是使用new,可能會給其他開發人員造成困擾,特別是看不到源碼的時候。

適用場合

  • 需要頻繁的進行創建和銷燬的對象;
  • 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
  • 工具類對象;
  • 頻繁訪問數據庫或文件的對象。

 

裝飾者模式:動態給類加功能。

 

意圖: 動態地給一個對象添加一些額外的職責。就增加功能來說, Decorator模式相比生成子類更爲靈活。該模式以對客 戶端透明的方式擴展對象的功能。

2、適用環境

(1)在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職責。

(2)處理那些可以撤消的職責。

(3)當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,爲支持每一種組合將產生大量的 子類,使得子類數目呈爆炸性增長。另一種情況可能是因爲類定義被隱藏,或類定義不能用於生成子類。

3、參與者

    1.Component(被裝飾對象的基類)

      定義一個對象接口,可以給這些對象動態地添加職責。

    2.ConcreteComponent(具體被裝飾對象)

      定義一個對象,可以給這個對象添加一些職責。

    3.Decorator(裝飾者抽象類)

      維持一個指向Component實例的引用,並定義一個與Component接口一致的接口。

    4.ConcreteDecorator(具體裝飾者)

      具體的裝飾對象,給內部持有的具體被裝飾對象,增加具體的職責。

4、類圖

 

 

5、涉及角色

(1)抽象組件:定義一個抽象接口,來規範準備附加功能的類

(2)具體組件:將要被附加功能的類,實現抽象構件角色接口

(3)抽象裝飾者:持有對具體構件角色的引用並定義與抽象構件角色一致的接口

(4)具體裝飾:實現抽象裝飾者角色,負責對具體構件添加額外功能。

6、代碼 

Component 

public interface Person {

    void eat();
}

 

ConcreteComponent 

public class Man implements Person {

    public void eat() {
        System.out.println("男人在吃");
    }
}

Decorator

public abstract class Decorator implements Person {

    protected Person person;
    
    public void setPerson(Person person) {
        this.person = person;
    }
    
    public void eat() {
        person.eat();
    }
}

ConcreteDectrator

public class ManDecoratorA extends Decorator {

    public void eat() {
        super.eat();
        reEat();
        System.out.println("ManDecoratorA類");
    }

    public void reEat() {
        System.out.println("再吃一頓飯");
    }
}
public class ManDecoratorB extends Decorator {
    
    public void eat() {
        super.eat();
        System.out.println("===============");
        System.out.println("ManDecoratorB類");
    }
}

Test 

public class Test {

    public static void main(String[] args) {
        Man man = new Man();
        ManDecoratorA md1 = new ManDecoratorA();
        ManDecoratorB md2 = new ManDecoratorB();
        
        md1.setPerson(man);
        md2.setPerson(md1);
        md2.eat();
    }
}

7、裝飾者模式小結:
OO原則:動態地將責任附加到對象上。想要擴展功能, 裝飾者提供有別於繼承的另一種選擇。

8、要點:
1、繼承屬於擴展形式之一,但不見得是達到彈性設計的最佳方案。
2、在我們的設計中,應該允許行爲可以被擴展,而不須修改現有的代碼。
3、組合和委託可用於在運行時動態地加上新的行爲。
4、除了繼承,裝飾者模式也可以讓我們擴展行爲。
5、裝飾者模式意味着一羣裝飾者類, 這些類用來包裝具體組件。
6、裝飾者類反映出被裝飾的組件類型(實際上,他們具有相同的類型,都經過接口或繼承實現)。
7、裝飾者可以在被裝飾者的行爲前面與/或後面加上自己的行爲,甚至將被裝飾者的行爲整個取代掉,而達到特定的目的。
8、你可以有無數個裝飾者包裝一個組件。
9、 裝飾者一般對組建的客戶是透明的,除非客戶程序依賴於組件的具體類型。

 

觀察者模式:有時被稱作發佈/訂閱模式,觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。

 

比較概念的解釋是,目標和觀察者是基類,目標提供維護觀察者的一系列方法,觀察者提供更新接口。具體觀察者和具體目標繼承各自的基類,然後具體觀察者把自己註冊到具體目標裏,在具體目標發生變化時候,調度觀察者的更新方法。

比如有個“天氣中心”的具體目標A,專門監聽天氣變化,而有個顯示天氣的界面的觀察者B,B就把自己註冊到A裏,當A觸發天氣變化,就調度B的更新方法,並帶上自己的上下文。

 

發佈/訂閱模式

比較概念的解釋是,訂閱者把自己想訂閱的事件註冊到調度中心,當該事件觸發時候,發佈者發佈該事件到調度中心(順帶上下文),由調度中心統一調度訂閱者註冊到調度中心的處理代碼。

比如有個界面是實時顯示天氣,它就訂閱天氣事件(註冊到調度中心,包括處理程序),當天氣變化時(定時獲取數據),就作爲發佈者發佈天氣信息到調度中心,調度中心就調度訂閱者的天氣處理程序。

 

總結

1. 從兩張圖片可以看到,最大的區別是調度的地方。

雖然兩種模式都存在訂閱者和發佈者(具體觀察者可認爲是訂閱者、具體目標可認爲是發佈者),但是觀察者模式是由具體目標調度的,而發佈/訂閱模式是統一由調度中心調的,所以觀察者模式的訂閱者與發佈者之間是存在依賴的,而發佈/訂閱模式則不會。

2. 兩種模式都可以用於鬆散耦合,改進代碼管理和潛在的複用。

附錄

觀察者模式實現代碼(JavaScript版):

//觀察者列表
function ObserverList(){
  this.observerList = [];
}
ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};
ObserverList.prototype.count = function(){
  return this.observerList.length;
};
ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};
ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;
  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }
  return -1;
};
ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

//目標
function Subject(){
  this.observers = new ObserverList();
}
Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};
Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};
Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
    this.observers.get(i).update( context );
  }
};

//觀察者
function Observer(){
  this.update = function(){
    // ...
  };
}

發佈/訂閱模式實現代碼(JavaScript經典版):

 

var pubsub = {};
(function(myObject) {
    // Storage for topics that can be broadcast
    // or listened to
    var topics = {};
    // An topic identifier
    var subUid = -1;
    // Publish or broadcast events of interest
    // with a specific topic name and arguments
    // such as the data to pass along
    myObject.publish = function( topic, args ) {
        if ( !topics[topic] ) {
            return false;
        }
        var subscribers = topics[topic],
            len = subscribers ? subscribers.length : 0;
        while (len--) {
            subscribers[len].func( topic, args );
        }
        return this;
    };
    // Subscribe to events of interest
    // with a specific topic name and a
    // callback function, to be executed
    // when the topic/event is observed
    myObject.subscribe = function( topic, func ) {
        if (!topics[topic]) {
            topics[topic] = [];
        }
        var token = ( ++subUid ).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    // Unsubscribe from a specific
    // topic, based on a tokenized reference
    // to the subscription
    myObject.unsubscribe = function( token ) {
        for ( var m in topics ) {
            if ( topics[m] ) {
                for ( var i = 0, j = topics[m].length; i < j; i++ ) {
                    if ( topics[m][i].token === token ) {
                        topics[m].splice( i, 1 );
                        return token;
                    }
                }
            }
        }
        return this;
    };
}( pubsub ));

 

策略模式:定義一系列的算法,把它們一個個封裝起來, 並且使它們可相互替換。

外觀模式:爲子系統中的一組接口提供一個一致的界面,外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。

命令模式:將一個請求封裝成一個對象,從而使您可以用不同的請求對客戶進行參數化。

創建者模式:將一個複雜的構建與其表示相分離,使得同樣的構建過程可以創建不同的表示。

抽象工廠模式:提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類。

一、簡單工廠模式:
實例化對象的時候不再使用 new Object()形式,可以根據用戶的選擇條件來實例化相關的類。對於客戶端來說,去除了具體的類的依賴。只需要給出具體實例的描述給工廠,工廠就會自動返回具體的實例對象。

具體實現如下:

1. 定義一個操作接口:

public interface Operation {
 
    public double getResult(double numberA,double numberB) throws Exception;
 
}
2. 定義具體的操作類:

public class Add implements Operation{
 
    // 加法計算
    public double getResult(double numberA, double numberB) {
 
        return numberA + numberB;
    }
}
 
 
public class Sub implements Operation{
 
    // 減法計算
    public double getResult(double numberA, double numberB) {
        return numberA-numberB;
    }
}
 
 
public class Mul implements Operation{
 
    // 乘法計算
    public double getResult(double numberA, double numberB) {
        return numberA * numberB;
    }
}
 
 
public class Div implements Operation {
 
    // 除法計算
    public double getResult(double numberA, double numberB) throws Exception {
        if (numberB == 0) {
            throw new Exception("除數不能爲0!");
        }
        return numberA / numberB;
    }
}
3. 定義簡單工廠類:

public class EasyFactory {
 
    // 簡單工廠,根據字符串創建相應的對象
    public static Operation createOperation(String name) {
        Operation operationObj = null;
        switch (name) {
            case "+":
                operationObj = new Add();
                break;
            case "-":
                operationObj = new Sub();
                break;
            case "*":
                operationObj = new Mul();
                break;
            case "/":
                operationObj = new Div();
                break;
        }
        return operationObj;
    }
}
4. 用戶端代碼:

public class Client {
 
    public static void main(String[] args) throws Exception {
 
        Operation add = EasyFactory.createOperation("+");
        Operation sub = EasyFactory.createOperation("-");
        Operation mul = EasyFactory.createOperation("*");
        Operation div = EasyFactory.createOperation("/");
 
        System.out.println(add.getResult(1, 1));
        System.out.println(sub.getResult(1, 1));
        System.out.println(mul.getResult(1, 1));
        System.out.println(div.getResult(1, 1));
    }
}
Result:

2.0
0.0
1.0
1.0
我們無需提供具體的子類類名,只需要提供一個字符串即可得到相應的實例對象。這樣的話,當子類的類名更換或者增加子類時我們都無需修改客戶端代碼,只需要在簡單工廠類上增加一個分支判斷代碼即可。

使用這種模式,我們在生成工廠的時候可以加一些業務代碼,如日誌、判斷業務等,這時候可以直接在switch case中加上去就行了,如下:

public class EasyFactory {
 
    private static Operation operationObj = null;
 
    private static Operation add(){
        System.out.println("加法運算");
        return new Add();
    }
    private static Operation sub(){
        System.out.println("減法運算");
        return new Sub();
    }
    private static Operation mul(){
        System.out.println("乘法運算");
        return new Mul();
    }
    private static Operation div(){
        System.out.println("除法運算");
        return new Div();
    }
 
    // 簡單工廠,根據字符串創建相應的對象
    public static Operation createOperation(String name) {
 
        switch (name) {
            case "+":
                operationObj = add();
                break;
            case "-":
                operationObj = sub();
                break;
            case "*":
                operationObj = mul();
                break;
            case "/":
                operationObj = div();
                break;
        }
        return operationObj;
    }
}
這樣做的優點:我們可以對創建的對象進行一些 “加工” ,而且客戶端並不知道,因爲工廠隱藏了這些細節。如果,沒有工廠的話,那我們是不是就得自己在客戶端上寫這些代碼,這就好比本來可以在工廠裏生產的東西,拿來自己手工製作,不僅麻煩以後還不好維護。


但是缺點也很明顯:如果需要在方法裏寫很多與對象創建有關的業務代碼,而且需要的創建的對象還不少的話,我們要在這個簡單工廠類裏編寫很多個方法,每個方法裏都得寫很多相應的業務代碼,而每次增加子類或者刪除子類對象的創建都需要打開這簡單工廠類來進行修改。這會導致這個簡單工廠類很龐大臃腫、耦合性高,而且增加、刪除某個子類對象的創建都需要打開簡單工廠類來進行修改代碼也違反了開-閉原則。


二、工廠模式
這時候就需要使用工廠模式了。工廠方法模式是對簡單工廠模式進一步的解耦,因爲在工廠方法模式中是一個子類對應一個工廠類,而這些工廠類都實現於一個抽象接口。這相當於是把原本會因爲業務代碼而龐大的簡單工廠類,拆分成了一個個的工廠類,這樣代碼就不會都耦合在同一個類裏了。

這時上面那個例子的結構圖爲:

1. 首先定義一個工廠接口:

import org.zero01.operation.Operation;
 
public interface Factory {
 
    public Operation createOperation() ;
 
}
2. 然後是具體的工廠類:

// 加法類工廠
public class AddFactory implements Factory{
 
    public Operation createOperation() {
        System.out.println("加法運算");
        return new Add();
    }
}
 
// 減法類工廠
public class SubFactory implements Factory{
 
    public Operation createOperation() {
        System.out.println("減法運算");
        return new Sub();
    }
}
........
3. 運算類跟簡單工廠一樣。

4. 客戶端代碼:

public class Client {
 
    public static void main(String[] args) throws Exception {
 
        // 使用反射機制實例化工廠對象,因爲字符串是可以通過變量改變的
        Factory addFactory = (Factory) Class.forName("org.zero01.factory.AddFactory").newInstance();
        Factory subFactory=(Factory) Class.forName("org.zero01.factory.SubFactory").newInstance();
 
        // 通過工廠對象創建相應的實例對象
        Operation add = addFactory.createOperation();
        Operation sub = subFactory.createOperation();
 
        System.out.println(add.getResult(1, 1));
        System.out.println(sub.getResult(1, 1));
    }
}
比較:

工廠模式中,要增加產品類時也要相應地增加工廠類,客戶端的代碼也增加了不少。工廠方法把簡單工廠的內部邏輯判斷轉移到了客戶端代碼來進行。

你想要加功能,本來是改工廠類的,而現在是修改客戶端。而且各個不同功能的實例對象的創建代碼,也沒有耦合在同一個工廠類裏,這也是工廠方法模式對簡單工廠模式解耦的一個體現。工廠方法模式克服了簡單工廠會違背開-閉原則的缺點,又保持了封裝對象創建過程的優點。

但工廠方法模式的缺點是每增加一個產品類,就需要增加一個對應的工廠類,增加了額外的開發量。

3. 抽象工廠模式:
場景:對數據庫中的表進行修改

此時,使用工廠模式結構圖如下:

1. 我們現在要對mysql/oracle數據庫中的User表進行操作,User表定義如下:

public class User {
    private int uid;
    private String uname;
 
    public int getUid() {
        return uid;
    }
 
    public void setUid(int uid) {
        this.uid = uid;
    }
 
    public String getUname() {
        return uname;
    }
 
    public void setUname(String uname) {
        this.uname = uname;
    }
}
2. 接下來我們定義一個對User進行操作的接口:

public interface IUser {
    public void insert(User user);
    public User getUser(int uid);
}
3. 實現一個對mysql中User進行操作的類:

public class mysqlUser implements IUser{
 
    public void insert(User user){
        System.out.println("在mysql中的user表中插入一條元素");
    }
 
    public User getUser(int id){
        System.out.println("在mysql中的user表得到id爲"+id+"的一條數據");
        return null;
    }
}
  實現對oracle中User進行操作的類:

public class oracleUser implements IUser{
 
    @Override
    public void insert(User user) {
        System.out.println("在oracle中的user表中插入一條元素");
    }
 
    @Override
    public User getUser(int uid) {
        System.out.println("在oracle中的user表得到id爲"+uid+"的一條數據");
        return null;
    }
}
4. 接下來定義一個工廠接口,用於生產訪問User表的對象:

public interface sqlFactory {
    public IUser createUser();     //用於訪問User表的對象
}
5. 生產mysqlUser對象的mysql工廠類:

public class mysqlFactory implements sqlFactory {
    @Override
    public IUser createUser() {
        return new mysqlUser();  //訪問mysql中User表的對象
    }
}
生成oracleUser對象的oracle工廠類:

public class oracleFactory implements sqlFactory {
    @Override
    public IUser createUser() {
        return new oracleUser();   //訪問oracle中User表的對象
    }
}
6. 最後用戶測試類如下:

public class test_abstractFactory {
    public static void main(String[] args) {
        sqlFactory factory1 = new mysqlFactory();
        IUser userOperator = factory1.createUser();
        userOperator.getUser(1);
        userOperator.insert(new User());
    }
}
結果爲:
在mysql中的user表得到id爲1的一條數據
在mysql中的user表中插入一條元素


到此爲止,工廠模式都可以很好的解決,由於多態的關係,sqlFactory在聲明對象之前都不知道在訪問哪個數據庫,卻可以在運行時很好的完成任務,這就是業務邏輯與數據訪問的解耦。

但是,當數據庫中不止一個表的時候該怎麼解決問題呢,此時就可以引入抽象工廠模式了,結構圖如下:

1. 比如說現在增加了一個Login類,用於記錄登陸信息:

package DesignPattern.abstractFactory;
 
import java.util.Date;
 
public class Login {
    private int id;
    private Date date;
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public Date getDate() {
        return date;
    }
 
    public void setDate(Date date) {
        this.date = date;
    }
}
2. 此時就要相應地添加 對login表操作的Ilogin接口,mysqlLogin類,oracleLogin類:

public interface ILogin {
 
    public void insert(Login login);
    public Login getLogin(int id);
 
}
public class MysqlLogin implements ILogin{
 
    public void insert(Login login) {
        System.out.println("對 MySQL 裏的 Login 表插入了一條數據");
    }
 
    public Login getLogin(int id) {
        System.out.println("通過 uid 在 MySQL 裏的 Login 表得到了一條數據");
        return null;
    }
}
public class OracleLogin implements ILogin{
 
    public void insert(Login login) {
        System.out.println("對 Oracle 裏的 Login 表插入了一條數據");
    }
 
    public Login getLogin(int id) {
        System.out.println("通過 uid 在 Oracle 裏的 Login 表得到了一條數據");
        return null;
    }
}
3. 修改Factory接口及Factory實現類的內容:

IFactory,定義一個抽象的工廠接口,該工廠用於生產訪問User表以及Login表的對象:

public interface IFactory {
 
    public IUser createUser();
    public ILogin createLogin();
}
public class MysqlFactory implements IFactory{
 
    public IUser createUser() {
        return new MysqlUser();
    }
 
    public ILogin createLogin() {
        return new MysqlLogin();
    }
}
public class OracleFactory implements IFactory{
 
    public IUser createUser() {
        return new OracleUser();
    }
 
    public ILogin createLogin() {
        return new OracleLogin();
    }
}
4. 客戶端代碼:
public class Client {
 
    public static void main(String[] args){
 
        User user=new User();
        Login login = new Login();
 
        // 只需要確定實例化哪一個數據庫訪問對象給factory
        // IFactory factory=new MysqlFactory();
        IFactory factory=new OracleFactory();
 
        // 已與具體的數據庫訪問解除了耦合
        IUser userOperation=factory.createUser();
 
        userOperation.getUser(1);
        userOperation.insert(user);
 
        // 已與具體的數據庫訪問解除了耦合
        ILogin loginOperation=factory.createLogin();
 
        loginOperation.insert(login);
        loginOperation.getLogin(1);
 
    }
}
結果:

通過 uid 在 Oracle 裏的 User 表得到了一條數據
對 Oracle 裏的 User 表插入了一條數據
對 Oracle 裏的 Login 表插入了一條數據
通過 uid 在 Oracle 裏的 Login 表得到了一條數據
所以抽象工廠與工廠方法模式的區別在於:抽象工廠是可以生產多個產品的,例如 MysqlFactory 裏可以生產 MysqlUser 以及 MysqlLogin 兩個產品,而這兩個產品又是屬於一個系列的,因爲它們都是屬於MySQL數據庫的表。而工廠方法模式則只能生產一個產品,例如之前的 MysqlFactory 裏就只可以生產一個 MysqlUser 產品。

抽象工廠模式的優缺點:

優點:

1. 抽象工廠模式最大的好處是易於交換產品系列,由於具體工廠類,例如 IFactory factory=new OracleFactory(); 在一個應用中只需要在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它只需要改變具體工廠即可使用不同的產品配置。不管是任何人的設計都無法去完全防止需求的更改,或者項目的維護,那麼我們的理想便是讓改動變得最小、最容易,例如我現在要更改以上代碼的數據庫訪問時,只需要更改具體的工廠即可。

2. 抽象工廠模式的另一個好處就是它讓具體的創建實例過程與客戶端分離,客戶端是通過它們的抽象接口操作實例,產品實現類的具體類名也被具體的工廠實現類分離,不會出現在客戶端代碼中。就像我們上面的例子,客戶端只認識IUser和ILogin,至於它是MySQl裏的表還是Oracle裏的表就不知道了。

缺點:

1. 如果你的需求來自增加功能,比如增加Login表,就有點太煩了。首先需要增加 ILogin,mysqlLogin,oracleLogin。 然後我們還要去修改工廠類: sqlFactory, mysqlFactory, oracleFactory 纔可以實現,需要修改三個類,實在是有點麻煩。

2. 還有就是,客戶端程序肯定不止一個,每次都需要聲明sqlFactory factory=new MysqlFactory(), 如果有100個調用數據庫的類,就需要更改100次sqlFactory factory=new oracleFactory()。

4. 抽象工廠模式的改進1。(簡單工廠+抽象工廠)
我們將IFactory、MySQLFactory以及OracleFactory三個工廠類都拋棄掉,取而代之的是一個簡單工廠類EasyFactory,如下:

EasyFactory類,簡單工廠:

public class EasyFactory {
 
    // 數據庫名稱
    private static String db="MySQL";
    // private static String db="Oracle";
 
    public static IUser createUser(){
 
        IUser user=null;
        switch (db){
            case "MySQL":
                user=new MysqlUser();
                break;
 
            case "Oracle":
                user=new OracleUser();
                break;
        }
        return user;
    }
 
    public static ILogin createLogin(){
 
        ILogin login=null;
        switch (db){
            case "MySQL":
                login=new MysqlLogin();
                break;
 
            case "Oracle":
                login=new OracleLogin();
                break;
        }
        return login;
    }
}
客戶端代碼:

public class Client {
 
    public static void main(String[] args){
 
        User user=new User();
        Login login = new Login();
 
        // 直接得到實際的數據庫訪問實例,而不存在任何依賴
        IUser userOperation= EasyFactory.createUser();
 
        userOperation.getUser(1);
        userOperation.insert(user);
 
        // 直接得到實際的數據庫訪問實例,而不存在任何依賴
        ILogin loginOperation=EasyFactory.createLogin();
 
        loginOperation.insert(login);
        loginOperation.getLogin(1);
 
    }
}
由於事先在簡單工廠類裏設置好了db的值,所以簡單工廠的方法都不需要由客戶端來輸入參數,這樣在客戶端就只需要使用 EasyFactory.createUser(); 和 EasyFactory.createLogin(); 方法來獲得具體的數據庫訪問類的實例,客戶端代碼上沒有出現任何一個 MySQL 或 Oracle 的字樣,達到了解耦的目的,客戶端已經不再受改動數據庫訪問的影響了。

5. 抽象工廠的改進2(反射+簡單工廠)
使用反射的話,我們就可以不需要使用switch,因爲使用switch的話,我添加一個sql server數據庫的話,又要switch的話又需要添加case條件。

我們可以根據 選擇的數據庫名稱,如 “mysql”, 利用反射技術自動的獲得所需要的實例:

public class easyFactory1 {
 
    private static String packName = "DesignPattern.abstractFactory";
    private static String sqlName = "mysql";
 
    public static IUser createUser() throws Exception{
        String className = packName+"."+sqlName+"User";
        return (IUser)Class.forName(className).newInstance();
    }
 
    public static ILogin createLogin() throws Exception{
        String className = packName+"."+sqlName+"Login";
        return (ILogin)Class.forName(className).newInstance();
    }
}
以上我們使用簡單工廠模式設計的代碼中,是用一個字符串類型的db變量來存儲數據庫名稱的,所以變量的值到底是 MySQL 還是 Oracle ,完全可以由事先設置的那個db變量來決定,而我們又可以通過反射來去獲取實例,這樣就可以去除switch語句了。


6. 抽象工廠的改進3(反射+配置文件+簡單工廠)
在使用反射之後,我們還是需要進easyFactory中修改數據庫類型,還不是完全符合開-閉原則。

我們可以通過配置文件來達到目的,每次通過讀取配置文件來知道我們應該使用哪種數據庫。

如下是一個json類型的配置文件,也可以使用xml類型的配置文件:

{
  "packName": "DesignPattern.abstractFactory",
  "DB": "Mysql"
}
之後就可以通過這個配置文件去找需要加載的類是哪一個。

我們通過反射機制+配置文件+簡單工廠模式解決了數據庫訪問時的可維護、可擴展的問題。
 

2、基礎知識

Java基本類型哪些,所佔字節和範圍

 

1)四種整數類型(byte、short、int、long):    byte:8 位,用於表示最小數據單位,如文件中數據,-128~127    short:16 位,很少用,-32768 ~ 32767    int:32 位、最常用,-2^31-1~2^31  (21 億)    long:64 位、次常用    注意事項:    int i=5; // 5 叫直接量(或字面量),即 直接寫出的常數。    整數字面量默認都爲 int 類型,所以在定義的 long 型數據後面加 L或 l。    小於 32 位數的變量,都按 int 結果計算。    強轉符比數學運算符優先級高。見常量與變量中的例子。

 

2)兩種浮點數類型(float、double):    float:32 位,後綴 F 或 f,1 位符號位,8 位指數,23 位有效尾數。    double:64 位,最常用,後綴 D 或 d,1 位符號位,11 位指數,52 位有效尾    注意事項:    二 進 制 浮 點 數 : 1010100010=101010001.0*2=10101000.10*2^10(2次方)=1010100.010*2^11(3次方)= . 1010100010*2^1010(10次方)    尾數:  . 1010100010   指數:1010   基數:2    浮點數字面量默認都爲 double 類型,所以在定義的 float 型數據後面加F 或 f;double 類型可不寫後綴,但在小數計算中一定要寫 D 或 X.X    float  的精度沒有 long 高,有效位數(尾數)短。    float  的範圍大於 long  指數可以很大。    浮點數是不精確的,不能對浮點數進行精確比較。

 

3)一種字符類型(char):    char:16 位,是整數類型,用單引號括起來的 1 個字符(可以是一箇中文字符),使用 Unicode 碼代表字符,0~2^16-1(65535) 。    注意事項:    不能爲 0個字符。    轉義字符:\n  換行  \r  回車  \t Tab 字符  \" 雙引號  \\ 表示一個\    兩字符 char 中間用“+”連接,內部先把字符轉成 int 類型,再進行加法運算,char 本質就是個數!二進制的,顯示的時候,經過“處理”顯示爲字符。

 

4)一種布爾類型(boolean):true 真  和 false 假。

 

5)類型轉換:       char-->    自動轉換:byte-->short-->int-->long-->float-->double                   強制轉換:①會損失精度,產生誤差,小數點以後的數字全部捨棄。②容易超過取值範圍。

 

6)記憶:8位:Byte(字節型)          16位:short(短整型)、char(字符型)          32位:int(整型)、float(單精度型/浮點型)          64位:long(長整型)、double(雙精度型)          最後一個:boolean(布爾類型

 

Set、List、Map的區別和聯繫

什麼時候使用Hashmap

 

哈希表(hash table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也常常出現在各類的面試題中,重要性可見一斑。本文會對java集合框架中的對應實現HashMap的實現原理進行講解,然後會對JDK7的HashMap源碼進行分析。

目錄

  一、什麼是哈希表

  二、HashMap實現原理

  三、爲何HashMap的數組長度一定是2的次冪?

  四、重寫equals方法需同時重寫hashCode方法

  五、總結

一、什麼是哈希表

  在討論哈希表之前,我們先大概瞭解下其他數據結構在新增,查找等基礎操作執行性能

  數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

  線性鏈表:對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

  二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。

  哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1),接下來我們就來看看哈希表是如何實現達到驚豔的常數階O(1)的。

  我們知道,數據結構的物理存儲結構只有兩種:順序存儲結構鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面我們提到過,在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主幹就是數組

  比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字 通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作。

        存儲位置 = f(關鍵字)

  其中,這個函數f一般稱爲哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,比如我們要在哈希表中執行插入操作:

  

  查找操作同理,先通過哈希函數計算出實際存儲地址,然後從數組中對應地址取出即可。

  哈希衝突

  然而萬事無完美,如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。前面我們提到過,哈希函數的設計至關重要,好的哈希函數會儘可能地保證 計算簡單散列地址分佈均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap即是採用了鏈地址法,也就是數組+鏈表的方式,

二、HashMap實現原理

 HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。

//HashMap的主幹數組,可以看到就是一個Entry數組,初始值爲空數組{},主幹數組的長度一定是2的次冪,至於爲什麼這麼做,後面會有詳細分析。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

 Entry是HashMap中的一個靜態內部類。代碼如下

複製代碼

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
        int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

複製代碼

 所以,HashMap的整體結構如下

  

  簡單來說,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。

其他幾個重要字段

複製代碼

//實際存儲的key-value鍵值對的個數
transient int size;
//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold一般爲 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,後面會詳細談到
int threshold;
//負載因子,代表了table的填充度有多少,默認是0.75
final float loadFactor;
//用於快速失敗,由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;

複製代碼

HashMap有4個構造器,其他構造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值

initialCapacity默認爲16,loadFactory默認爲0.75

我們看下其中一個

複製代碼

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

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

複製代碼

  從上面這段代碼我們可以看出,在常規構造器中,沒有爲數組table分配內存空間(有一個入參爲指定Map的構造器例外),而是在執行put操作的時候才真正構建table數組

  OK,接下來我們來看看put操作的實現吧

複製代碼

    public V put(K key, V value) {
        //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
        int i = indexFor(hash, table.length);//獲取在table中的實際位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
        addEntry(hash, key, value, i);//新增一個entry
        return null;
    }    

複製代碼

 先來看看inflateTable這個方法

複製代碼

private void inflateTable(int toSize) {
        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

複製代碼

  inflateTable這個方法用於爲主幹數組table在內存中分配存儲空間,通過roundUpToPowerOf2(toSize)可以確保capacity爲大於或等於toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

複製代碼

 private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

複製代碼

roundUpToPowerOf2中的這段處理使得數組長度一定爲2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位爲0)所代表的數值.

hash函數

複製代碼

//這是一個神奇的函數,用了很多的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘量分佈均勻
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

複製代碼

以上hash函數計算出的值,通過indexFor進一步處理來獲取實際的存儲位置

複製代碼

  /**
     * 返回數組下標
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

複製代碼

h&(length-1)保證獲取的index一定在數組範圍內,舉個例子,默認容量16,length-1=15,h=18,轉換成二進制計算爲

        1  0  0  1  0
    &   0  1  1  1  1
    __________________
        0  0  0  1  0    = 2

  最終計算出的index=2。有些版本的對於此處的計算會使用 取模運算,也能保證index一定在數組範圍內,不過位運算對計算機來說,性能更高一些(HashMap中有大量位運算)

所以最終存儲位置的確定流程是這樣的:

再來看看addEntry的實現:

複製代碼

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

複製代碼

  通過以上代碼能夠得知,當發生哈希衝突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度爲之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度爲之前的2倍,所以擴容相對來說是個耗資源的操作。

三、爲何HashMap的數組長度一定是2的次冪?

我們來繼續看上面提到的resize方法

複製代碼

 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, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

複製代碼

如果數組進行擴容,數組長度發生變化,而存儲位置 index = h&(length-1),index也可能會發生變化,需要重新計算index,我們先來看看transfer這個方法

複製代碼

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循環中的代碼,逐個遍歷鏈表,重新計算索引位置,將老數組數據複製到新數組中去(數組不存儲實際數據,所以僅僅是拷貝引用而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
          //將當前entry的next鏈指向新的索引位置,newTable[i]有可能爲空,有可能也是個entry鏈,如果是entry鏈,直接在鏈表頭部插入。
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

複製代碼

  這個方法將老數組中的數據逐個鏈表地遍歷,扔到新的擴容後的數組中,我們的數組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算後,再通過和 length-1進行位運算得到最終數組索引位置。

  hashMap的數組長度一定保持2的次冪,比如16的二進制表示爲 10000,那麼length-1就是15,二進制爲01111,同理擴容後的數組長度爲32,二進制表示爲100000,length-1爲31,二進制表示爲011111。從下圖可以我們也能看到這樣會保證低位全爲1,而擴容後只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位爲0,就能保證得到的新的數組索引和老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換),個人理解。

  

 還有,數組長度保持2的次冪,length-1的低位都爲1,會使得獲得的數組索引index更加均勻,比如:

  我們看到,上面的&運算,高位是不會對結果產生影響的(hash函數採用各種位運算可能也是爲了使得低位更加散列),我們只關注低位bit,如果低位全部爲1,那麼對於h低位部分來說,任何一位的變化都會對結果產生影響,也就是說,要得到index=21這個存儲位置,h的低位只有這一種組合。這也是數組長度設計爲必須爲2的次冪的原因。

  如果不是2的次冪,也就是低位不是全爲1此時,要使得index=21,h的低位部分不再具有唯一性了,哈希衝突的機率會變的更大,同時,index對應的這個bit位無論如何不會等於1了,而對應的那些數組位置也就被白白浪費了。

get方法

複製代碼

 public V get(Object key) {
     //如果key爲null,則直接去table[0]處去檢索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

複製代碼

get方法通過key值返回對應value,如果key爲null,直接去table[0]處檢索。我們再看一下getEntry這個方法

複製代碼

final Entry<K,V> getEntry(Object key) {
            
        if (size == 0) {
            return null;
        }
        //通過key的hashcode值計算hash值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && 
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }    

複製代碼

  可以看出,get方法的實現相對簡單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置table[i],再查看是否有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄。要注意的是,有人覺得上面在定位到數組位置之後然後遍歷鏈表的時候,e.hash == hash這個判斷沒必要,僅通過equals判斷就可以。其實不然,試想一下,如果傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當前對象不一致,這種情況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null,後面的例子會做出進一步解釋。

四、重寫equals方法需同時重寫hashCode方法

  關於HashMap的源碼分析就介紹到這兒了,最後我們再聊聊老生常談的一個問題,各種資料上都會提到,“重寫equals時也要同時覆蓋hashcode”,我們舉個小例子來看看,如果重寫了equals而不重寫hashcode會發生什麼樣的問題

複製代碼

/**
 * Created by chengxiao on 2016/11/15.
 */
public class MyTest {
    private static class Person{
        int idCard;
        String name;

        public Person(int idCard, String name) {
            this.idCard = idCard;
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Person person = (Person) o;
            //兩個對象是否等值,通過idCard來確定
            return this.idCard == person.idCard;
        }

    }
    public static void main(String []args){
        HashMap<Person,String> map = new HashMap<Person, String>();
        Person person = new Person(1234,"喬峯");
        //put到hashmap中去
        map.put(person,"天龍八部");
        //get取出,從邏輯上講應該能輸出“天龍八部”
        System.out.println("結果:"+map.get(new Person(1234,"蕭峯")));
    }
}

複製代碼

實際輸出結果:

結果:null

  如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。儘管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,所以put操作時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過key取出value的時候 key(hashcode1)-->hash-->indexFor-->最終索引位置,由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)

  所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希衝突,應儘量避免)。

什麼時候使用Linkedhashmap、Concurrenthashmap、Weakhashmap

 

Java7 HashMap

HashMap 是最簡單的,一來我們非常熟悉,二來就是它不支持併發操作,所以源碼也非常簡單。

首先,我們用下面這張圖來介紹 HashMap 的結構。

1

這個僅僅是示意圖,因爲沒有考慮到數組要擴容的情況,具體的後面再說。

大方向上,HashMap 裏面是一個數組,然後數組中每個元素是一個單向鏈表。

上圖中,每個綠色的實體是嵌套類 Entry 的實例,Entry 包含四個屬性:key, value, hash 值和用於單向鏈表的 next。

capacity:當前數組容量,始終保持 2^n,可以擴容,擴容後數組大小爲當前的 2 倍。

loadFactor:負載因子,默認爲 0.75。

threshold:擴容的閾值,等於 capacity * loadFactor

put 過程分析

還是比較簡單的,跟着代碼走一遍吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

public V put(K key, V value) {

    // 當插入第一個元素的時候,需要先初始化數組大小

    if (table == EMPTY_TABLE) {

        inflateTable(threshold);

    }

    // 如果 key 爲 null,感興趣的可以往裏看,最終會將這個 entry 放到 table[0] 中

    if (key == null)

        return putForNullKey(value);

    // 1. 求 key 的 hash 值

    int hash = hash(key);

    // 2. 找到對應的數組下標

    int i = indexFor(hash, table.length);

    // 3. 遍歷一下對應下標處的鏈表,看是否有重複的 key 已經存在,

    //    如果有,直接覆蓋,put 方法返回舊值就結束了

    for (Entry<K,V> e = table[i]; e != null; e = e.next) {

        Object k;

        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

            V oldValue = e.value;

            e.value = value;

            e.recordAccess(this);

            return oldValue;

        }

    }

 

    modCount++;

    // 4. 不存在重複的 key,將此 entry 添加到鏈表中,細節後面說

    addEntry(hash, key, value, i);

    return null;

}

數組初始化

在第一個元素插入 HashMap 的時候做一次數組的初始化,就是先確定初始的數組大小,並計算數組擴容的閾值。

1

2

3

4

5

6

7

8

9

10

private void inflateTable(int toSize) {

    // 保證數組大小一定是 2 的 n 次方。

    // 比如這樣初始化:new HashMap(20),那麼處理成初始數組大小是 32

    int capacity = roundUpToPowerOf2(toSize);

    // 計算擴容閾值:capacity * loadFactor

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

    // 算是初始化數組吧

    table = new Entry[capacity];

    initHashSeedAsNeeded(capacity); //ignore

}

這裏有一個將數組大小保持爲 2 的 n 次方的做法,Java7 和 Java8 的 HashMap 和 ConcurrentHashMap 都有相應的要求,只不過實現的代碼稍微有些不同,後面再看到的時候就知道了。

計算具體數組位置

這個簡單,我們自己也能 YY 一個:使用 key 的 hash 值對數組長度進行取模就可以了。

1

2

3

4

static int indexFor(int hash, int length) {

    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

    return hash & (length-1);

}

這個方法很簡單,簡單說就是取 hash 值的低 n 位。如在數組長度爲 32 的時候,其實取的就是 key 的 hash 值的低 5 位,作爲它在數組中的下標位置。

添加節點到鏈表中

找到數組下標後,會先進行 key 判重,如果沒有重複,就準備將新值放入到鏈表的表頭。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

void addEntry(int hash, K key, V value, int bucketIndex) {

    // 如果當前 HashMap 大小已經達到了閾值,並且新值要插入的數組位置已經有元素了,那麼要擴容

    if ((size >= threshold) && (null != table[bucketIndex])) {

        // 擴容,後面會介紹一下

        resize(2 * table.length);

        // 擴容以後,重新計算 hash 值

        hash = (null != key) ? hash(key) : 0;

        // 重新計算擴容後的新的下標

        bucketIndex = indexFor(hash, table.length);

    }

    // 往下看

    createEntry(hash, key, value, bucketIndex);

}

// 這個很簡單,其實就是將新值放到鏈表的表頭,然後 size++

void createEntry(int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];

    table[bucketIndex] = new Entry<>(hash, key, value, e);

    size++;

}

這個方法的主要邏輯就是先判斷是否需要擴容,需要的話先擴容,然後再將這個新的數據插入到擴容後的數組的相應位置處的鏈表的表頭。

數組擴容

前面我們看到,在插入新值的時候,如果當前的 size 已經達到了閾值,並且要插入的數組位置上已經有元素,那麼就會觸發擴容,擴容後,數組大小爲原來的 2 倍。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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, initHashSeedAsNeeded(newCapacity));

    table = newTable;

    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

}

擴容就是用一個新的大數組替換原來的小數組,並將原來數組中的值遷移到新的數組中。

由於是雙倍擴容,遷移過程中,會將原來 table[i] 中的鏈表的所有節點,分拆到新的數組的 newTable[i] 和 newTable[i + oldLength] 位置上。如原來數組長度是 16,那麼擴容後,原來 table[0] 處的鏈表中的所有元素會被分配到新數組中 newTable[0] 和 newTable[16] 這兩個位置。代碼比較簡單,這裏就不展開了。

get 過程分析

相對於 put 過程,get 過程是非常簡單的。

  1. 根據 key 計算 hash 值。
  2. 找到相應的數組下標:hash & (length – 1)。
  3. 遍歷該數組位置處的鏈表,直到找到相等(==或equals)的 key。

1

2

3

4

5

6

7

8

9

public V get(Object key) {

    // 之前說過,key 爲 null 的話,會被放到 table[0],所以只要遍歷下 table[0] 處的鏈表就可以了

    if (key == null)

        return getForNullKey();

    //

    Entry<K,V> entry = getEntry(key);

 

    return null == entry ? null : entry.getValue();

}

getEntry(key):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

final Entry<K,V> getEntry(Object key) {

    if (size == 0) {

        return null;

    }

 

    int hash = (key == null) ? 0 : hash(key);

    // 確定數組下標,然後從頭開始遍歷鏈表,直到找到爲止

    for (Entry<K,V> e = table[indexFor(hash, table.length)];

         e != null;

         e = e.next) {

        Object k;

        if (e.hash == hash &&

            ((k = e.key) == key || (key != null && key.equals(k))))

            return e;

    }

    return null;

}

Java7 ConcurrentHashMap

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因爲它支持併發操作,所以要複雜一些。

整個 ConcurrentHashMap 由一個個 Segment 組成,Segment 代表”部分“或”一段“的意思,所以很多地方都會將其描述爲分段鎖。注意,行文中,我很多地方用了“槽”來代表一個 segment。

簡單理解就是,ConcurrentHashMap 是一個 Segment 數組,Segment 通過繼承 ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實現了全局的線程安全。

3

concurrencyLevel:並行級別、併發數、Segment 數,怎麼翻譯不重要,理解它。默認是 16,也就是說 ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,最多可以同時支持 16 個線程併發寫,只要它們的操作分別分佈在不同的 Segment 上。這個值可以在初始化的時候設置爲其他值,但是一旦初始化以後,它是不可以擴容的。

再具體到每個 Segment 內部,其實每個 Segment 很像之前介紹的 HashMap,不過它要保證線程安全,所以處理起來要麻煩些。

初始化

initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 的初始容量,實際操作的時候需要平均分給每個 Segment。

loadFactor:負載因子,之前我們說了,Segment 數組不可以擴容,所以這個負載因子是給每個 Segment 內部使用的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

public ConcurrentHashMap(int initialCapacity,

                         float loadFactor, int concurrencyLevel) {

    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

        throw new IllegalArgumentException();

    if (concurrencyLevel > MAX_SEGMENTS)

        concurrencyLevel = MAX_SEGMENTS;

    // Find power-of-two sizes best matching arguments

    int sshift = 0;

    int ssize = 1;

    // 計算並行級別 ssize,因爲要保持並行級別是 2 的 n 次方

    while (ssize < concurrencyLevel) {

        ++sshift;

        ssize <<= 1;

    }

    // 我們這裏先不要那麼燒腦,用默認值,concurrencyLevel 爲 16,sshift 爲 4

    // 那麼計算出 segmentShift 爲 28,segmentMask 爲 15,後面會用到這兩個值

    this.segmentShift = 32 - sshift;

    this.segmentMask = ssize - 1;

 

    if (initialCapacity > MAXIMUM_CAPACITY)

        initialCapacity = MAXIMUM_CAPACITY;

 

    // initialCapacity 是設置整個 map 初始的大小,

    // 這裏根據 initialCapacity 計算 Segment 數組中每個位置可以分到的大小

    // 如 initialCapacity 爲 64,那麼每個 Segment 或稱之爲"槽"可以分到 4 個

    int c = initialCapacity / ssize;

    if (c * ssize < initialCapacity)

        ++c;

    // 默認 MIN_SEGMENT_TABLE_CAPACITY 是 2,這個值也是有講究的,因爲這樣的話,對於具體的槽上,

    // 插入一個元素不至於擴容,插入第二個的時候纔會擴容

    int cap = MIN_SEGMENT_TABLE_CAPACITY;

    while (cap < c)

        cap <<= 1;

 

    // 創建 Segment 數組,

    // 並創建數組的第一個元素 segment[0]

    Segment<K,V> s0 =

        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

                         (HashEntry<K,V>[])new HashEntry[cap]);

    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

    // 往數組寫入 segment[0]

    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

    this.segments = ss;

}

初始化完成,我們得到了一個 Segment 數組。

我們就當是用 new ConcurrentHashMap() 無參構造函數進行初始化的,那麼初始化完成後:

  • Segment 數組長度爲 16,不可以擴容
  • Segment[i] 的默認大小爲 2,負載因子是 0.75,得出初始閾值爲 1.5,也就是以後插入第一個元素不會觸發擴容,插入第二個會進行第一次擴容
  • 這裏初始化了 segment[0],其他位置還是 null,至於爲什麼要初始化 segment[0],後面的代碼會介紹
  • 當前 segmentShift 的值爲 32 – 4 = 28,segmentMask 爲 16 – 1 = 15,姑且把它們簡單翻譯爲移位數和掩碼,這兩個值馬上就會用到

put 過程分析

我們先看 put 的主流程,對於其中的一些關鍵細節操作,後面會進行詳細介紹。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public V put(K key, V value) {

    Segment<K,V> s;

    if (value == null)

        throw new NullPointerException();

    // 1. 計算 key 的 hash 值

    int hash = hash(key);

    // 2. 根據 hash 值找到 Segment 數組中的位置 j

    //    hash 是 32 位,無符號右移 segmentShift(28) 位,剩下低 4 位,

    //    然後和 segmentMask(15) 做一次與操作,也就是說 j 是 hash 值的最後 4 位,也就是槽的數組下標

    int j = (hash >>> segmentShift) & segmentMask;

    // 剛剛說了,初始化的時候初始化了 segment[0],但是其他位置還是 null,

    // ensureSegment(j) 對 segment[j] 進行初始化

    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck

         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment

        s = ensureSegment(j);

    // 3. 插入新值到 槽 s 中

    return s.put(key, hash, value, false);

}

第一層皮很簡單,根據 hash 值很快就能找到相應的 Segment,之後就是 Segment 內部的 put 操作了。

Segment 內部是由 數組+鏈表 組成的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

    // 在往該 segment 寫入前,需要先獲取該 segment 的獨佔鎖

    //    先看主流程,後面還會具體介紹這部分內容

    HashEntry<K,V> node = tryLock() ? null :

        scanAndLockForPut(key, hash, value);

    V oldValue;

    try {

        // 這個是 segment 內部的數組

        HashEntry<K,V>[] tab = table;

        // 再利用 hash 值,求應該放置的數組下標

        int index = (tab.length - 1) & hash;

        // first 是數組該位置處的鏈表的表頭

        HashEntry<K,V> first = entryAt(tab, index);

 

        // 下面這串 for 循環雖然很長,不過也很好理解,想想該位置沒有任何元素和已經存在一個鏈表這兩種情況

        for (HashEntry<K,V> e = first;;) {

            if (e != null) {

                K k;

                if ((k = e.key) == key ||

                    (e.hash == hash && key.equals(k))) {

                    oldValue = e.value;

                    if (!onlyIfAbsent) {

                        // 覆蓋舊值

                        e.value = value;

                        ++modCount;

                    }

                    break;

                }

                // 繼續順着鏈表走

                e = e.next;

            }

            else {

                // node 到底是不是 null,這個要看獲取鎖的過程,不過和這裏都沒有關係。

                // 如果不爲 null,那就直接將它設置爲鏈表表頭;如果是null,初始化並設置爲鏈表表頭。

                if (node != null)

                    node.setNext(first);

                else

                    node = new HashEntry<K,V>(hash, key, value, first);

 

                int c = count + 1;

                // 如果超過了該 segment 的閾值,這個 segment 需要擴容

                if (c > threshold && tab.length < MAXIMUM_CAPACITY)

                    rehash(node); // 擴容後面也會具體分析

                else

                    // 沒有達到閾值,將 node 放到數組 tab 的 index 位置,

                    // 其實就是將新的節點設置成原鏈表的表頭

                    setEntryAt(tab, index, node);

                ++modCount;

                count = c;

                oldValue = null;

                break;

            }

        }

    } finally {

        // 解鎖

        unlock();

    }

    return oldValue;

}

整體流程還是比較簡單的,由於有獨佔鎖的保護,所以 segment 內部的操作並不複雜。至於這裏面的併發問題,我們稍後再進行介紹。

到這裏 put 操作就結束了,接下來,我們說一說其中幾步關鍵的操作。

初始化槽: ensureSegment

ConcurrentHashMap 初始化的時候會初始化第一個槽 segment[0],對於其他槽來說,在插入第一個值的時候進行初始化。

這裏需要考慮併發,因爲很可能會有多個線程同時進來初始化同一個槽 segment[k],不過只要有一個成功了就可以。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

private Segment<K,V> ensureSegment(int k) {

    final Segment<K,V>[] ss = this.segments;

    long u = (k << SSHIFT) + SBASE; // raw offset

    Segment<K,V> seg;

    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {

        // 這裏看到爲什麼之前要初始化 segment[0] 了,

        // 使用當前 segment[0] 處的數組長度和負載因子來初始化 segment[k]

        // 爲什麼要用“當前”,因爲 segment[0] 可能早就擴容過了

        Segment<K,V> proto = ss[0];

        int cap = proto.table.length;

        float lf = proto.loadFactor;

        int threshold = (int)(cap * lf);

 

        // 初始化 segment[k] 內部的數組

        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];

        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))

            == null) { // 再次檢查一遍該槽是否被其他線程初始化了。

 

            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);

            // 使用 while 循環,內部用 CAS,當前線程成功設值或其他線程成功設值後,退出

            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))

                   == null) {

                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))

                    break;

            }

        }

    }

    return seg;

}

總的來說,ensureSegment(int k) 比較簡單,對於併發操作使用 CAS 進行控制。

我沒搞懂這裏爲什麼要搞一個 while 循環,CAS 失敗不就代表有其他線程成功了嗎,爲什麼要再進行判斷?

獲取寫入鎖: scanAndLockForPut

前面我們看到,在往某個 segment 中 put 的時候,首先會調用 node = tryLock() ? null : scanAndLockForPut(key, hash, value),也就是說先進行一次 tryLock() 快速獲取該 segment 的獨佔鎖,如果失敗,那麼進入到 scanAndLockForPut 這個方法來獲取鎖。

下面我們來具體分析這個方法中是怎麼控制加鎖的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {

    HashEntry<K,V> first = entryForHash(this, hash);

    HashEntry<K,V> e = first;

    HashEntry<K,V> node = null;

    int retries = -1; // negative while locating node

 

    // 循環獲取鎖

    while (!tryLock()) {

        HashEntry<K,V> f; // to recheck first below

        if (retries < 0) {

            if (e == null) {

                if (node == null) // speculatively create node

                    // 進到這裏說明數組該位置的鏈表是空的,沒有任何元素

                    // 當然,進到這裏的另一個原因是 tryLock() 失敗,所以該槽存在併發,不一定是該位置

                    node = new HashEntry<K,V>(hash, key, value, null);

                retries = 0;

            }

            else if (key.equals(e.key))

                retries = 0;

            else

                // 順着鏈表往下走

                e = e.next;

        }

        // 重試次數如果超過 MAX_SCAN_RETRIES(單核1多核64),那麼不搶了,進入到阻塞隊列等待鎖

        //    lock() 是阻塞方法,直到獲取鎖後返回

        else if (++retries > MAX_SCAN_RETRIES) {

            lock();

            break;

        }

        else if ((retries & 1) == 0 &&

                 // 這個時候是有大問題了,那就是有新的元素進到了鏈表,成爲了新的表頭

                 //     所以這邊的策略是,相當於重新走一遍這個 scanAndLockForPut 方法

                 (f = entryForHash(this, hash)) != first) {

            e = first = f; // re-traverse if entry changed

            retries = -1;

        }

    }

    return node;

}

這個方法有兩個出口,一個是 tryLock() 成功了,循環終止,另一個就是重試次數超過了 MAX_SCAN_RETRIES,進到 lock() 方法,此方法會阻塞等待,直到成功拿到獨佔鎖。

這個方法就是看似複雜,但是其實就是做了一件事,那就是獲取該 segment 的獨佔鎖,如果需要的話順便實例化了一下 node。

擴容: rehash

重複一下,segment 數組不能擴容,擴容是 segment 數組某個位置內部的數組 HashEntry\[] 進行擴容,擴容後,容量爲原來的 2 倍。

首先,我們要回顧一下觸發擴容的地方,put 的時候,如果判斷該值的插入會導致該 segment 的元素個數超過閾值,那麼先進行擴容,再插值,讀者這個時候可以回去 put 方法看一眼。

該方法不需要考慮併發,因爲到這裏的時候,是持有該 segment 的獨佔鎖的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

// 方法參數上的 node 是這次擴容後,需要添加到新的數組中的數據。

private void rehash(HashEntry<K,V> node) {

    HashEntry<K,V>[] oldTable = table;

    int oldCapacity = oldTable.length;

    // 2 倍

    int newCapacity = oldCapacity << 1;

    threshold = (int)(newCapacity * loadFactor);

    // 創建新數組

    HashEntry<K,V>[] newTable =

        (HashEntry<K,V>[]) new HashEntry[newCapacity];

    // 新的掩碼,如從 16 擴容到 32,那麼 sizeMask 爲 31,對應二進制 ‘000...00011111’

    int sizeMask = newCapacity - 1;

 

    // 遍歷原數組,老套路,將原數組位置 i 處的鏈表拆分到 新數組位置 i 和 i+oldCap 兩個位置

    for (int i = 0; i < oldCapacity ; i++) {

        // e 是鏈表的第一個元素

        HashEntry<K,V> e = oldTable[i];

        if (e != null) {

            HashEntry<K,V> next = e.next;

            // 計算應該放置在新數組中的位置,

            // 假設原數組長度爲 16,e 在 oldTable[3] 處,那麼 idx 只可能是 3 或者是 3 + 16 = 19

            int idx = e.hash & sizeMask;

            if (next == null)   // 該位置處只有一個元素,那比較好辦

                newTable[idx] = e;

            else { // Reuse consecutive sequence at same slot

                // e 是鏈表表頭

                HashEntry<K,V> lastRun = e;

                // idx 是當前鏈表的頭結點 e 的新位置

                int lastIdx = idx;

 

                // 下面這個 for 循環會找到一個 lastRun 節點,這個節點之後的所有元素是將要放到一起的

                for (HashEntry<K,V> last = next;

                     last != null;

                     last = last.next) {

                    int k = last.hash & sizeMask;

                    if (k != lastIdx) {

                        lastIdx = k;

                        lastRun = last;

                    }

                }

                // 將 lastRun 及其之後的所有節點組成的這個鏈表放到 lastIdx 這個位置

                newTable[lastIdx] = lastRun;

                // 下面的操作是處理 lastRun 之前的節點,

                //    這些節點可能分配在另一個鏈表中,也可能分配到上面的那個鏈表中

                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {

                    V v = p.value;

                    int h = p.hash;

                    int k = h & sizeMask;

                    HashEntry<K,V> n = newTable[k];

                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);

                }

            }

        }

    }

    // 將新來的 node 放到新數組中剛剛的 兩個鏈表之一 的 頭部

    int nodeIndex = node.hash & sizeMask; // add the new node

    node.setNext(newTable[nodeIndex]);

    newTable[nodeIndex] = node;

    table = newTable;

}

這裏的擴容比之前的 HashMap 要複雜一些,代碼難懂一點。上面有兩個挨着的 for 循環,第一個 for 有什麼用呢?

仔細一看發現,如果沒有第一個 for 循環,也是可以工作的,但是,這個 for 循環下來,如果 lastRun 的後面還有比較多的節點,那麼這次就是值得的。因爲我們只需要克隆 lastRun 前面的節點,後面的一串節點跟着 lastRun 走就是了,不需要做任何操作。

我覺得 Doug Lea 的這個想法也是挺有意思的,不過比較壞的情況就是每次 lastRun 都是鏈表的最後一個元素或者很靠後的元素,那麼這次遍歷就有點浪費了。不過 Doug Lea 也說了,根據統計,如果使用默認的閾值,大約只有 1/6 的節點需要克隆。

get 過程分析

相對於 put 來說,get 真的不要太簡單。

  1. 計算 hash 值,找到 segment 數組中的具體位置,或我們前面用的“槽”
  2. 槽中也是一個數組,根據 hash 找到數組中具體的位置
  3. 到這裏是鏈表了,順着鏈表進行查找即可

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public V get(Object key) {

    Segment<K,V> s; // manually integrate access methods to reduce overhead

    HashEntry<K,V>[] tab;

    // 1. hash 值

    int h = hash(key);

    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;

    // 2. 根據 hash 找到對應的 segment

    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&

        (tab = s.table) != null) {

        // 3. 找到segment 內部數組相應位置的鏈表,遍歷

        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile

                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);

             e != null; e = e.next) {

            K k;

            if ((k = e.key) == key || (e.hash == h && key.equals(k)))

                return e.value;

        }

    }

    return null;

}

併發問題分析

現在我們已經說完了 put 過程和 get 過程,我們可以看到 get 過程中是沒有加鎖的,那自然我們就需要去考慮併發問題。

添加節點的操作 put 和刪除節點的操作 remove 都是要加 segment 上的獨佔鎖的,所以它們之間自然不會有問題,我們需要考慮的問題就是 get 的時候在同一個 segment 中發生了 put 或 remove 操作。

  1. put 操作的線程安全性。
    • 初始化槽,這個我們之前就說過了,使用了 CAS 來初始化 Segment 中的數組。
    • 添加節點到鏈表的操作是插入到表頭的,所以,如果這個時候 get 操作在鏈表遍歷的過程已經到了中間,是不會影響的。當然,另一個併發問題就是 get 操作在 put 之後,需要保證剛剛插入表頭的節點被讀取,這個依賴於 setEntryAt 方法中使用的 UNSAFE.putOrderedObject。
    • 擴容。擴容是新創建了數組,然後進行遷移數據,最後面將 newTable 設置給屬性 table。所以,如果 get 操作此時也在進行,那麼也沒關係,如果 get 先行,那麼就是在舊的 table 上做查詢操作;而 put 先行,那麼 put 操作的可見性保證就是 table 使用了 volatile 關鍵字。
  2. remove 操作的線程安全性。

    remove 操作我們沒有分析源碼,所以這裏說的讀者感興趣的話還是需要到源碼中去求實一下的。

    get 操作需要遍歷鏈表,但是 remove 操作會”破壞”鏈表。

    如果 remove 破壞的節點 get 操作已經過去了,那麼這裏不存在任何問題。

    如果 remove 先破壞了一個節點,分兩種情況考慮。 1、如果此節點是頭結點,那麼需要將頭結點的 next 設置爲數組該位置的元素,table 雖然使用了 volatile 修飾,但是 volatile 並不能提供數組內部操作的可見性保證,所以源碼中使用了 UNSAFE 來操作數組,請看方法 setEntryAt。2、如果要刪除的節點不是頭結點,它會將要刪除節點的後繼節點接到前驅節點中,這裏的併發保證就是 next 屬性是 volatile 的。

Java8 HashMap

Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 數組+鏈表+紅黑樹 組成。

根據 Java7 HashMap 的介紹,我們知道,查找的時候,根據 hash 值我們能夠快速定位到數組的具體下標,但是之後的話,需要順着鏈表一個個比較下去才能找到我們需要的,時間複雜度取決於鏈表的長度,爲 O(n)。

爲了降低這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個以後,會將鏈表轉換爲紅黑樹,在這些位置進行查找的時候可以降低時間複雜度爲 O(logN)。

來一張圖簡單示意一下吧:

2

注意,上圖是示意圖,主要是描述結構,不會達到這個狀態的,因爲這麼多數據的時候早就擴容了。

下面,我們還是用代碼來介紹吧,個人感覺,Java8 的源碼可讀性要差一些,不過精簡一些。

Java7 中使用 Entry 來代表每個 HashMap 中的數據節點,Java8 中使用 Node,基本沒有區別,都是 key,value,hash 和 next 這四個屬性,不過,Node 只能用於鏈表的情況,紅黑樹的情況需要使用 TreeNode。

我們根據數組元素中,第一個節點數據類型是 Node 還是 TreeNode 來判斷該位置下是鏈表還是紅黑樹的。

put 過程分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true);

}

 

// 第三個參數 onlyIfAbsent 如果是 true,那麼只有在不存在該 key 時纔會進行 put 操作

// 第四個參數 evict 我們這裏不關心

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

               boolean evict) {

    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 第一次 put 值的時候,會觸發下面的 resize(),類似 java7 的第一次 put 也要初始化數組長度

    // 第一次 resize 和後續的擴容有些不一樣,因爲這次是數組從 null 初始化到默認的 16 或自定義的初始容量

    if ((tab = table) == null || (n = tab.length) == 0)

        n = (tab = resize()).length;

    // 找到具體的數組下標,如果此位置沒有值,那麼直接初始化一下 Node 並放置在這個位置就可以了

    if ((p = tab[i = (n - 1) & hash]) == null)

        tab[i] = newNode(hash, key, value, null);

 

    else {// 數組該位置有數據

        Node<K,V> e; K k;

        // 首先,判斷該位置的第一個數據和我們要插入的數據,key 是不是"相等",如果是,取出這個節點

        if (p.hash == hash &&

            ((k = p.key) == key || (key != null && key.equals(k))))

            e = p;

        // 如果該節點是代表紅黑樹的節點,調用紅黑樹的插值方法,本文不展開說紅黑樹

        else if (p instanceof TreeNode)

            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

        else {

            // 到這裏,說明數組該位置上是一個鏈表

            for (int binCount = 0; ; ++binCount) {

                // 插入到鏈表的最後面(Java7 是插入到鏈表的最前面)

                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);

                    // TREEIFY_THRESHOLD 爲 8,所以,如果新插入的值是鏈表中的第 9 個

                    // 會觸發下面的 treeifyBin,也就是將鏈表轉換爲紅黑樹

                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                        treeifyBin(tab, hash);

                    break;

                }

                // 如果在該鏈表中找到了"相等"的 key(== 或 equals)

                if (e.hash == hash &&

                    ((k = e.key) == key || (key != null && key.equals(k))))

                    // 此時 break,那麼 e 爲鏈表中[與要插入的新值的 key "相等"]的 node

                    break;

                p = e;

            }

        }

        // e!=null 說明存在舊值的key與要插入的key"相等"

        // 對於我們分析的put操作,下面這個 if 其實就是進行 "值覆蓋",然後返回舊值

        if (e != null) {

            V oldValue = e.value;

            if (!onlyIfAbsent || oldValue == null)

                e.value = value;

            afterNodeAccess(e);

            return oldValue;

        }

    }

    ++modCount;

    // 如果 HashMap 由於新插入這個值導致 size 已經超過了閾值,需要進行擴容

    if (++size > threshold)

        resize();

    afterNodeInsertion(evict);

    return null;

}

和 Java7 稍微有點不一樣的地方就是,Java7 是先擴容後插入新值的,Java8 先插值再擴容,不過這個不重要。

數組擴容

resize() 方法用於初始化數組或數組擴容,每次擴容後,容量爲原來的 2 倍,並進行數據遷移。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

final Node<K,V>[] resize() {

    Node<K,V>[] oldTab = table;

    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    int oldThr = threshold;

    int newCap, newThr = 0;

    if (oldCap > 0) { // 對應數組擴容

        if (oldCap >= MAXIMUM_CAPACITY) {

            threshold = Integer.MAX_VALUE;

            return oldTab;

        }

        // 將數組大小擴大一倍

        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                 oldCap >= DEFAULT_INITIAL_CAPACITY)

            // 將閾值擴大一倍

            newThr = oldThr << 1; // double threshold

    }

    else if (oldThr > 0) // 對應使用 new HashMap(int initialCapacity) 初始化後,第一次 put 的時候

        newCap = oldThr;

    else {// 對應使用 new HashMap() 初始化後,第一次 put 的時候

        newCap = DEFAULT_INITIAL_CAPACITY;

        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

    }

 

    if (newThr == 0) {

        float ft = (float)newCap * loadFactor;

        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                  (int)ft : Integer.MAX_VALUE);

    }

    threshold = newThr;

 

    // 用新的數組大小初始化新的數組

    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

    table = newTab; // 如果是初始化數組,到這裏就結束了,返回 newTab 即可

 

    if (oldTab != null) {

        // 開始遍歷原數組,進行數據遷移。

        for (int j = 0; j < oldCap; ++j) {

            Node<K,V> e;

            if ((e = oldTab[j]) != null) {

                oldTab[j] = null;

                // 如果該數組位置上只有單個元素,那就簡單了,簡單遷移這個元素就可以了

                if (e.next == null)

                    newTab[e.hash & (newCap - 1)] = e;

                // 如果是紅黑樹,具體我們就不展開了

                else if (e instanceof TreeNode)

                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                else {

                    // 這塊是處理鏈表的情況,

                    // 需要將此鏈表拆成兩個鏈表,放到新的數組中,並且保留原來的先後順序

                    // loHead、loTail 對應一條鏈表,hiHead、hiTail 對應另一條鏈表,代碼還是比較簡單的

                    Node<K,V> loHead = null, loTail = null;

                    Node<K,V> hiHead = null, hiTail = null;

                    Node<K,V> next;

                    do {

                        next = e.next;

                        if ((e.hash & oldCap) == 0) {

                            if (loTail == null)

                                loHead = e;

                            else

                                loTail.next = e;

                            loTail = e;

                        }

                        else {

                            if (hiTail == null)

                                hiHead = e;

                            else

                                hiTail.next = e;

                            hiTail = e;

                        }

                    } while ((e = next) != null);

                    if (loTail != null) {

                        loTail.next = null;

                        // 第一條鏈表

                        newTab[j] = loHead;

                    }

                    if (hiTail != null) {

                        hiTail.next = null;

                        // 第二條鏈表的新的位置是 j + oldCap,這個很好理解

                        newTab[j + oldCap] = hiHead;

                    }

                }

            }

        }

    }

    return newTab;

}

get 過程分析

相對於 put 來說,get 真的太簡單了。

  1. 計算 key 的 hash 值,根據 hash 值找到對應數組下標: hash & (length-1)
  2. 判斷數組該位置處的元素是否剛好就是我們要找的,如果不是,走第三步
  3. 判斷該元素類型是否是 TreeNode,如果是,用紅黑樹的方法取數據,如果不是,走第四步
  4. 遍歷鏈表,直到找到相等(==或equals)的 key

1

2

3

4

public V get(Object key) {

    Node<K,V> e;

    return (e = getNode(hash(key), key)) == null ? null : e.value;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

final Node<K,V> getNode(int hash, Object key) {

    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (first = tab[(n - 1) & hash]) != null) {

        // 判斷第一個節點是不是就是需要的

        if (first.hash == hash && // always check first node

            ((k = first.key) == key || (key != null && key.equals(k))))

            return first;

        if ((e = first.next) != null) {

            // 判斷是否是紅黑樹

            if (first instanceof TreeNode)

                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

 

            // 鏈表遍歷

            do {

                if (e.hash == hash &&

                    ((k = e.key) == key || (key != null && key.equals(k))))

                    return e;

            } while ((e = e.next) != null);

        }

    }

    return null;

}

Java8 ConcurrentHashMap

Java7 中實現的 ConcurrentHashMap 說實話還是比較複雜的,Java8 對 ConcurrentHashMap 進行了比較大的改動。建議讀者可以參考 Java8 中 HashMap 相對於 Java7 HashMap 的改動,對於 ConcurrentHashMap,Java8 也引入了紅黑樹。

說實話,Java8 ConcurrentHashMap 源碼真心不簡單,最難的在於擴容,數據遷移操作不容易看懂。

我們先用一個示意圖來描述下其結構:

4

結構上和 Java8 的 HashMap 基本上一樣,不過它要保證線程安全性,所以在源碼上確實要複雜一些。

初始化

1

2

3

4

5

6

7

8

9

10

11

// 這構造函數裏,什麼都不幹

public ConcurrentHashMap() {

}

public ConcurrentHashMap(int initialCapacity) {

    if (initialCapacity < 0)

        throw new IllegalArgumentException();

    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?

               MAXIMUM_CAPACITY :

               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));

    this.sizeCtl = cap;

}

這個初始化方法有點意思,通過提供初始容量,計算了 sizeCtl,sizeCtl = 【 (1.5 * initialCapacity + 1),然後向上取最近的 2 的 n 次方】。如 initialCapacity 爲 10,那麼得到 sizeCtl 爲 16,如果 initialCapacity 爲 11,得到 sizeCtl 爲 32。

sizeCtl 這個屬性使用的場景很多,不過只要跟着文章的思路來,就不會被它搞暈了。

如果你愛折騰,也可以看下另一個有三個參數的構造方法,這裏我就不說了,大部分時候,我們會使用無參構造函數進行實例化,我們也按照這個思路來進行源碼分析吧。

put 過程分析

仔細地一行一行代碼看下去:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

public V put(K key, V value) {

    return putVal(key, value, false);

}

final V putVal(K key, V value, boolean onlyIfAbsent) {

    if (key == null || value == null) throw new NullPointerException();

    // 得到 hash 值

    int hash = spread(key.hashCode());

    // 用於記錄相應鏈表的長度

    int binCount = 0;

    for (Node<K,V>[] tab = table;;) {

        Node<K,V> f; int n, i, fh;

        // 如果數組"空",進行數組初始化

        if (tab == null || (n = tab.length) == 0)

            // 初始化數組,後面會詳細介紹

            tab = initTable();

 

        // 找該 hash 值對應的數組下標,得到第一個節點 f

        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

            // 如果數組該位置爲空,

            //    用一次 CAS 操作將這個新值放入其中即可,這個 put 操作差不多就結束了,可以拉到最後面了

            //          如果 CAS 失敗,那就是有併發操作,進到下一個循環就好了

            if (casTabAt(tab, i, null,

                         new Node<K,V>(hash, key, value, null)))

                break;                   // no lock when adding to empty bin

        }

        // hash 居然可以等於 MOVED,這個需要到後面才能看明白,不過從名字上也能猜到,肯定是因爲在擴容

        else if ((fh = f.hash) == MOVED)

            // 幫助數據遷移,這個等到看完數據遷移部分的介紹後,再理解這個就很簡單了

            tab = helpTransfer(tab, f);

 

        else { // 到這裏就是說,f 是該位置的頭結點,而且不爲空

 

            V oldVal = null;

            // 獲取數組該位置的頭結點的監視器鎖

            synchronized (f) {

                if (tabAt(tab, i) == f) {

                    if (fh >= 0) { // 頭結點的 hash 值大於 0,說明是鏈表

                        // 用於累加,記錄鏈表的長度

                        binCount = 1;

                        // 遍歷鏈表

                        for (Node<K,V> e = f;; ++binCount) {

                            K ek;

                            // 如果發現了"相等"的 key,判斷是否要進行值覆蓋,然後也就可以 break 了

                            if (e.hash == hash &&

                                ((ek = e.key) == key ||

                                 (ek != null && key.equals(ek)))) {

                                oldVal = e.val;

                                if (!onlyIfAbsent)

                                    e.val = value;

                                break;

                            }

                            // 到了鏈表的最末端,將這個新值放到鏈表的最後面

                            Node<K,V> pred = e;

                            if ((e = e.next) == null) {

                                pred.next = new Node<K,V>(hash, key,

                                                          value, null);

                                break;

                            }

                        }

                    }

                    else if (f instanceof TreeBin) { // 紅黑樹

                        Node<K,V> p;

                        binCount = 2;

                        // 調用紅黑樹的插值方法插入新節點

                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

                                                       value)) != null) {

                            oldVal = p.val;

                            if (!onlyIfAbsent)

                                p.val = value;

                        }

                    }

                }

            }

            // binCount != 0 說明上面在做鏈表操作

            if (binCount != 0) {

                // 判斷是否要將鏈表轉換爲紅黑樹,臨界值和 HashMap 一樣,也是 8

                if (binCount >= TREEIFY_THRESHOLD)

                    // 這個方法和 HashMap 中稍微有一點點不同,那就是它不是一定會進行紅黑樹轉換,

                    // 如果當前數組的長度小於 64,那麼會選擇進行數組擴容,而不是轉換爲紅黑樹

                    //    具體源碼我們就不看了,擴容部分後面說

                    treeifyBin(tab, i);

                if (oldVal != null)

                    return oldVal;

                break;

            }

        }

    }

    //

    addCount(1L, binCount);

    return null;

}

put 的主流程看完了,但是至少留下了幾個問題,第一個是初始化,第二個是擴容,第三個是幫助數據遷移,這些我們都會在後面進行一一介紹。

初始化數組:initTable

這個比較簡單,主要就是初始化一個合適大小的數組,然後會設置 sizeCtl。

初始化方法中的併發問題是通過對 sizeCtl 進行一個 CAS 操作來控制的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

private final Node<K,V>[] initTable() {

    Node<K,V>[] tab; int sc;

    while ((tab = table) == null || tab.length == 0) {

        // 初始化的"功勞"被其他線程"搶去"了

        if ((sc = sizeCtl) < 0)

            Thread.yield(); // lost initialization race; just spin

        // CAS 一下,將 sizeCtl 設置爲 -1,代表搶到了鎖

        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

            try {

                if ((tab = table) == null || tab.length == 0) {

                    // DEFAULT_CAPACITY 默認初始容量是 16

                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

                    // 初始化數組,長度爲 16 或初始化時提供的長度

                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

                    // 將這個數組賦值給 table,table 是 volatile 的

                    table = tab = nt;

                    // 如果 n 爲 16 的話,那麼這裏 sc = 12

                    // 其實就是 0.75 * n

                    sc = n - (n >>> 2);

                }

            } finally {

                // 設置 sizeCtl 爲 sc,我們就當是 12 吧

                sizeCtl = sc;

            }

            break;

        }

    }

    return tab;

}

鏈表轉紅黑樹: treeifyBin

前面我們在 put 源碼分析也說過,treeifyBin 不一定就會進行紅黑樹轉換,也可能是僅僅做數組擴容。我們還是進行源碼分析吧。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

private final void treeifyBin(Node<K,V>[] tab, int index) {

    Node<K,V> b; int n, sc;

    if (tab != null) {

        // MIN_TREEIFY_CAPACITY 爲 64

        // 所以,如果數組長度小於 64 的時候,其實也就是 32 或者 16 或者更小的時候,會進行數組擴容

        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)

            // 後面我們再詳細分析這個方法

            tryPresize(n << 1);

        // b 是頭結點

        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {

            // 加鎖

            synchronized (b) {

 

                if (tabAt(tab, index) == b) {

                    // 下面就是遍歷鏈表,建立一顆紅黑樹

                    TreeNode<K,V> hd = null, tl = null;

                    for (Node<K,V> e = b; e != null; e = e.next) {

                        TreeNode<K,V> p =

                            new TreeNode<K,V>(e.hash, e.key, e.val,

                                              null, null);

                        if ((p.prev = tl) == null)

                            hd = p;

                        else

                            tl.next = p;

                        tl = p;

                    }

                    // 將紅黑樹設置到數組相應位置中

                    setTabAt(tab, index, new TreeBin<K,V>(hd));

                }

            }

        }

    }

}

擴容:tryPresize

如果說 Java8 ConcurrentHashMap 的源碼不簡單,那麼說的就是擴容操作和遷移操作。

這個方法要完完全全看懂還需要看之後的 transfer 方法,讀者應該提前知道這點。

這裏的擴容也是做翻倍擴容的,擴容後數組容量爲原來的 2 倍。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

// 首先要說明的是,方法參數 size 傳進來的時候就已經翻了倍了

private final void tryPresize(int size) {

    // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。

    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :

        tableSizeFor(size + (size >>> 1) + 1);

    int sc;

    while ((sc = sizeCtl) >= 0) {

        Node<K,V>[] tab = table; int n;

 

        // 這個 if 分支和之前說的初始化數組的代碼基本上是一樣的,在這裏,我們可以不用管這塊代碼

        if (tab == null || (n = tab.length) == 0) {

            n = (sc > c) ? sc : c;

            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

                try {

                    if (table == tab) {

                        @SuppressWarnings("unchecked")

                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

                        table = nt;

                        sc = n - (n >>> 2); // 0.75 * n

                    }

                } finally {

                    sizeCtl = sc;

                }

            }

        }

        else if (c <= sc || n >= MAXIMUM_CAPACITY)

            break;

        else if (tab == table) {

            // 我沒看懂 rs 的真正含義是什麼,不過也關係不大

            int rs = resizeStamp(n);

 

            if (sc < 0) {

                Node<K,V>[] nt;

                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||

                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||

                    transferIndex <= 0)

                    break;

                // 2. 用 CAS 將 sizeCtl 加 1,然後執行 transfer 方法

                //    此時 nextTab 不爲 null

                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))

                    transfer(tab, nt);

            }

            // 1. 將 sizeCtl 設置爲 (rs << RESIZE_STAMP_SHIFT) + 2)

            //     我是沒看懂這個值真正的意義是什麼?不過可以計算出來的是,結果是一個比較大的負數

            //  調用 transfer 方法,此時 nextTab 參數爲 null

            else if (U.compareAndSwapInt(this, SIZECTL, sc,

                                         (rs << RESIZE_STAMP_SHIFT) + 2))

                transfer(tab, null);

        }

    }

}

這個方法的核心在於 sizeCtl 值的操作,首先將其設置爲一個負數,然後執行 transfer(tab, null),再下一個循環將 sizeCtl 加 1,並執行 transfer(tab, nt),之後可能是繼續 sizeCtl 加 1,並執行 transfer(tab, nt)。

所以,可能的操作就是執行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),這裏怎麼結束循環的需要看完 transfer 源碼才清楚。

數據遷移:transfer

下面這個方法很點長,將原來的 tab 數組的元素遷移到新的 nextTab 數組中。

雖然我們之前說的 tryPresize 方法中多次調用 transfer 不涉及多線程,但是這個 transfer 方法可以在其他地方被調用,典型地,我們之前在說 put 方法的時候就說過了,請往上看 put 方法,是不是有個地方調用了 helpTransfer 方法,helpTransfer 方法會調用 transfer 方法的。

此方法支持多線程執行,外圍調用此方法的時候,會保證第一個發起數據遷移的線程,nextTab 參數爲 null,之後再調用此方法的時候,nextTab 不會爲 null。

閱讀源碼之前,先要理解併發操作的機制。原數組長度爲 n,所以我們有 n 個遷移任務,讓每個線程每次負責一個小任務是最簡單的,每做完一個任務再檢測是否有其他沒做完的任務,幫助遷移就可以了,而 Doug Lea 使用了一個 stride,簡單理解就是步長,每個線程每次負責遷移其中的一部分,如每次遷移 16 個小任務。所以,我們就需要一個全局的調度者來安排哪個線程執行哪幾個任務,這個就是屬性 transferIndex 的作用。

第一個發起數據遷移的線程會將 transferIndex 指向原數組最後的位置,然後從後往前的 stride 個任務屬於第一個線程,然後將 transferIndex 指向新的位置,再往前的 stride 個任務屬於第二個線程,依此類推。當然,這裏說的第二個線程不是真的一定指代了第二個線程,也可以是同一個線程,這個讀者應該能理解吧。其實就是將一個大的遷移任務分爲了一個個任務包。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

    int n = tab.length, stride;

 

    // stride 在單核下直接等於 n,多核模式下爲 (n>>>3)/NCPU,最小值是 16

    // stride 可以理解爲”步長“,有 n 個位置是需要進行遷移的,

    //   將這 n 個任務分爲多個任務包,每個任務包有 stride 個任務

    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)

        stride = MIN_TRANSFER_STRIDE; // subdivide range

 

    // 如果 nextTab 爲 null,先進行一次初始化

    //    前面我們說了,外圍會保證第一個發起遷移的線程調用此方法時,參數 nextTab 爲 null

    //       之後參與遷移的線程調用此方法時,nextTab 不會爲 null

    if (nextTab == null) {

        try {

            // 容量翻倍

            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];

            nextTab = nt;

        } catch (Throwable ex) {      // try to cope with OOME

            sizeCtl = Integer.MAX_VALUE;

            return;

        }

        // nextTable 是 ConcurrentHashMap 中的屬性

        nextTable = nextTab;

        // transferIndex 也是 ConcurrentHashMap 的屬性,用於控制遷移的位置

        transferIndex = n;

    }

 

    int nextn = nextTab.length;

 

    // ForwardingNode 翻譯過來就是正在被遷移的 Node

    // 這個構造方法會生成一個Node,key、value 和 next 都爲 null,關鍵是 hash 爲 MOVED

    // 後面我們會看到,原數組中位置 i 處的節點完成遷移工作後,

    //    就會將位置 i 處設置爲這個 ForwardingNode,用來告訴其他線程該位置已經處理過了

    //    所以它其實相當於是一個標誌。

    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

 

 

    // advance 指的是做完了一個位置的遷移工作,可以準備做下一個位置的了

    boolean advance = true;

    boolean finishing = false; // to ensure sweep before committing nextTab

 

    /*

     * 下面這個 for 循環,最難理解的在前面,而要看懂它們,應該先看懂後面的,然後再倒回來看

     *

     */

 

    // i 是位置索引,bound 是邊界,注意是從後往前

    for (int i = 0, bound = 0;;) {

        Node<K,V> f; int fh;

 

        // 下面這個 while 真的是不好理解

        // advance 爲 true 表示可以進行下一個位置的遷移了

        //   簡單理解結局:i 指向了 transferIndex,bound 指向了 transferIndex-stride

        while (advance) {

            int nextIndex, nextBound;

            if (--i >= bound || finishing)

                advance = false;

 

            // 將 transferIndex 值賦給 nextIndex

            // 這裏 transferIndex 一旦小於等於 0,說明原數組的所有位置都有相應的線程去處理了

            else if ((nextIndex = transferIndex) <= 0) {

                i = -1;

                advance = false;

            }

            else if (U.compareAndSwapInt

                     (this, TRANSFERINDEX, nextIndex,

                      nextBound = (nextIndex > stride ?

                                   nextIndex - stride : 0))) {

                // 看括號中的代碼,nextBound 是這次遷移任務的邊界,注意,是從後往前

                bound = nextBound;

                i = nextIndex - 1;

                advance = false;

            }

        }

        if (i < 0 || i >= n || i + n >= nextn) {

            int sc;

            if (finishing) {

                // 所有的遷移操作已經完成

                nextTable = null;

                // 將新的 nextTab 賦值給 table 屬性,完成遷移

                table = nextTab;

                // 重新計算 sizeCtl:n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75 倍

                sizeCtl = (n << 1) - (n >>> 1);

                return;

            }

 

            // 之前我們說過,sizeCtl 在遷移前會設置爲 (rs << RESIZE_STAMP_SHIFT) + 2

            // 然後,每有一個線程參與遷移就會將 sizeCtl 加 1,

            // 這裏使用 CAS 操作對 sizeCtl 進行減 1,代表做完了屬於自己的任務

            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {

                // 任務結束,方法退出

                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

                    return;

 

                // 到這裏,說明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,

                // 也就是說,所有的遷移任務都做完了,也就會進入到上面的 if(finishing){} 分支了

                finishing = advance = true;

                i = n; // recheck before commit

            }

        }

        // 如果位置 i 處是空的,沒有任何節點,那麼放入剛剛初始化的 ForwardingNode ”空節點“

        else if ((f = tabAt(tab, i)) == null)

            advance = casTabAt(tab, i, null, fwd);

        // 該位置處是一個 ForwardingNode,代表該位置已經遷移過了

        else if ((fh = f.hash) == MOVED)

            advance = true; // already processed

        else {

            // 對數組該位置處的結點加鎖,開始處理數組該位置處的遷移工作

            synchronized (f) {

                if (tabAt(tab, i) == f) {

                    Node<K,V> ln, hn;

                    // 頭結點的 hash 大於 0,說明是鏈表的 Node 節點

                    if (fh >= 0) {

                        // 下面這一塊和 Java7 中的 ConcurrentHashMap 遷移是差不多的,

                        // 需要將鏈表一分爲二,

                        //   找到原鏈表中的 lastRun,然後 lastRun 及其之後的節點是一起進行遷移的

                        //   lastRun 之前的節點需要進行克隆,然後分到兩個鏈表中

                        int runBit = fh & n;

                        Node<K,V> lastRun = f;

                        for (Node<K,V> p = f.next; p != null; p = p.next) {

                            int b = p.hash & n;

                            if (b != runBit) {

                                runBit = b;

                                lastRun = p;

                            }

                        }

                        if (runBit == 0) {

                            ln = lastRun;

                            hn = null;

                        }

                        else {

                            hn = lastRun;

                            ln = null;

                        }

                        for (Node<K,V> p = f; p != lastRun; p = p.next) {

                            int ph = p.hash; K pk = p.key; V pv = p.val;

                            if ((ph & n) == 0)

                                ln = new Node<K,V>(ph, pk, pv, ln);

                            else

                                hn = new Node<K,V>(ph, pk, pv, hn);

                        }

                        // 其中的一個鏈表放在新數組的位置 i

                        setTabAt(nextTab, i, ln);

                        // 另一個鏈表放在新數組的位置 i+n

                        setTabAt(nextTab, i + n, hn);

                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,

                        //    其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了

                        setTabAt(tab, i, fwd);

                        // advance 設置爲 true,代表該位置已經遷移完畢

                        advance = true;

                    }

                    else if (f instanceof TreeBin) {

                        // 紅黑樹的遷移

                        TreeBin<K,V> t = (TreeBin<K,V>)f;

                        TreeNode<K,V> lo = null, loTail = null;

                        TreeNode<K,V> hi = null, hiTail = null;

                        int lc = 0, hc = 0;

                        for (Node<K,V> e = t.first; e != null; e = e.next) {

                            int h = e.hash;

                            TreeNode<K,V> p = new TreeNode<K,V>

                                (h, e.key, e.val, null, null);

                            if ((h & n) == 0) {

                                if ((p.prev = loTail) == null)

                                    lo = p;

                                else

                                    loTail.next = p;

                                loTail = p;

                                ++lc;

                            }

                            else {

                                if ((p.prev = hiTail) == null)

                                    hi = p;

                                else

                                    hiTail.next = p;

                                hiTail = p;

                                ++hc;

                            }

                        }

                        // 如果一分爲二後,節點數少於 8,那麼將紅黑樹轉換回鏈表

                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :

                            (hc != 0) ? new TreeBin<K,V>(lo) : t;

                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :

                            (lc != 0) ? new TreeBin<K,V>(hi) : t;

 

                        // 將 ln 放置在新數組的位置 i

                        setTabAt(nextTab, i, ln);

                        // 將 hn 放置在新數組的位置 i+n

                        setTabAt(nextTab, i + n, hn);

                        // 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,

                        //    其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了

                        setTabAt(tab, i, fwd);

                        // advance 設置爲 true,代表該位置已經遷移完畢

                        advance = true;

                    }

                }

            }

        }

    }

}

說到底,transfer 這個方法並沒有實現所有的遷移任務,每次調用這個方法只實現了 transferIndex 往前 stride 個位置的遷移工作,其他的需要由外圍來控制。

這個時候,再回去仔細看 tryPresize 方法可能就會更加清晰一些了。

get 過程分析

get 方法從來都是最簡單的,這裏也不例外:

  1. 計算 hash 值
  2. 根據 hash 值找到數組對應位置: (n – 1) & h
  3. 根據該位置處結點性質進行相應查找
    • 如果該位置爲 null,那麼直接返回 null 就可以了
    • 如果該位置處的節點剛好就是我們需要的,返回該節點的值即可
    • 如果該位置節點的 hash 值小於 0,說明正在擴容,或者是紅黑樹,後面我們再介紹 find 方法
    • 如果以上 3 條都不滿足,那就是鏈表,進行遍歷比對即可

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

public V get(Object key) {

    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

    int h = spread(key.hashCode());

    if ((tab = table) != null && (n = tab.length) > 0 &&

        (e = tabAt(tab, (n - 1) & h)) != null) {

        // 判斷頭結點是否就是我們需要的節點

        if ((eh = e.hash) == h) {

            if ((ek = e.key) == key || (ek != null && key.equals(ek)))

                return e.val;

        }

        // 如果頭結點的 hash 小於 0,說明 正在擴容,或者該位置是紅黑樹

        else if (eh < 0)

            // 參考 ForwardingNode.find(int h, Object k) 和 TreeBin.find(int h, Object k)

            return (p = e.find(h, key)) != null ? p.val : null;

 

        // 遍歷鏈表

        while ((e = e.next) != null) {

            if (e.hash == h &&

                ((ek = e.key) == key || (ek != null && key.equals(ek))))

                return e.val;

        }

    }

    return null;

}

簡單說一句,此方法的大部分內容都很簡單,只有正好碰到擴容的情況,ForwardingNode.find(int h, Object k) 稍微複雜一些,不過在瞭解了數據遷移的過程後,這個也就不難了,所以限於篇幅這裏也不展開說了。

哪些集合類是線程安全的

爲什麼Set、List、map不實現Cloneable和Serializable接口

Concurrenthashmap的實現,1.7和1.8的實現

Arrays.sort的實現

什麼時候使用CopyOnArrayList

 

1.   CopyOnWriteArrayList(寫數組的拷貝)是ArrayList的一個線程安全的變體,CopyOnWriteArrayList和CopyOnWriteSet都是線程安全的集合,其中所有可變操作(add、set等等)都是通過對底層數組進行一次新的複製來實現的。

2.    它絕對不會拋出ConcurrentModificationException的異常。因爲該列表(CopyOnWriteArrayList)在遍歷時將不會被做任何的修改。

3.    CopyOnWriteArrayList適合用在“讀多,寫少”的“併發”應用中,換句話說,它適合使用在讀操作遠遠大於寫操作的場景裏,比如緩存。它不存在“擴容”的概念,每次寫操作(add or remove)都要copy一個副本,在副本的基礎上修改後改變array引用,所以稱爲“CopyOnWrite”,因此在寫操作是加鎖,並且對整個list的copy操作時相當耗時的,過多的寫操作不推薦使用該存儲結構。

4.   CopyOnWriteArrayList的功能是是創建一個列表,有三種構造方法:

(1)CopyOnWriteArrayList ()創建一個空列表。

(2)CopyOnWriteArrayList (Collection<? extendsE> c)

創建一個按 collection的迭代器返回元素的順序包含指定 collection元素的列表。

(3)CopyOnWriteArrayList(E[] toCopyIn)

創建一個保存給定數組的副本的列表。


付:private static final變量詳解

1.      private是私有的,只在同一個類或內部類裏可以訪問。

2.      static是靜態的,靜態的變量是類的變量,不是對象的特性。

3.      final修飾的變量一次賦值將不能被改變。
--------------------- 
 

volatile的使用

synchronied的使用

reentrantlock的實現和Synchronied的區別

CAS的實現原理以及問題

AQS的實現原理

接口和抽象類的區別,什麼時候使用

類加載機制的步驟,每一步做了什麼,static和final修改的成員變量的加載時機

雙親委派模型

反射機制:反射動態擦除泛型、反射動態調用方法等

動態綁定:父類引用指向子類對象

JVM內存管理機制:有哪些區域,每個區域做了什麼

JVM垃圾回收機制:垃圾回收算法 垃圾回收器 垃圾回收策略

jvm參數的設置和jvm調優

什麼情況產生年輕代內存溢出、什麼情況產生年老代內存溢出

內部類:靜態內部類和匿名內部類的使用和區別

Redis和memcached:什麼時候選擇redis,什麼時候選擇memcached,內存模型和存儲策略是什麼樣的

MySQL的基本操作 主從數據庫一致性維護

mysql的優化策略有哪些

mysql索引的實現 B+樹的實現原理

什麼情況索引不會命中,會造成全表掃描

java中bio nio aio的區別和聯繫

爲什麼bio是阻塞的 nio是非阻塞的 nio是模型是什麼樣的

Java io的整體架構和使用的設計模式

Reactor模型和Proactor模型

http請求報文結構和內容

http三次握手和四次揮手

rpc相關:如何設計一個rpc框架,從io模型 傳輸協議 序列化方式綜合考慮

Linux命令 統計,排序,前幾問題等

StringBuff 和StringBuilder的實現,底層實現是通過byte數據,外加數組的拷貝來實現的

cas操作的使用

內存緩存和數據庫的一致性同步實現

微服務的優缺點

線程池的參數問題

ip問題 如何判斷ip是否在多個ip段中

判斷數組兩個中任意兩個數之和是否爲給定的值

樂觀鎖和悲觀鎖的實現

synchronized實現原理

你在項目中遇到的困難和怎麼解決的

你在項目中完成的比較出色的亮點

消息隊列廣播模式和發佈/訂閱模式的區別

生產者消費者代碼實現

死鎖代碼實現

線程池:參數,每個參數的作用,幾種不同線程池的比較,阻塞隊列的使用,拒絕策略

Future和ListenableFuture 異步回調相關

算法相關:判斷能否從數組中找出兩個數字和爲給定值,隨機生成1~10000不重複並放入數組,求數組的子數組的最大和,二分查找算法的實現及其時間複雜計算

3、其它

算法:常用排序算法,二分查找,鏈表相關,數組相關,字符串相關,樹相關等

常見序列化協議及其優缺點

memcached內存原理,爲什麼是基於塊的存儲

搭建一個rpc需要準備什麼

如果線上服務器頻繁地出現full gc ,如何去排查

如果某一時刻線上機器突然量變得很大,服務扛不住了,怎麼解決

LUR算法的實現

LinkedHashMap實現LRU

定義棧的數據結構,請在該類型中實現一個能夠找到棧最小元素的min函數

海量數據處理的解決思路

reactor模型的演變

阻塞、非阻塞、同步、異步區別

Collection的子接口

jvm調優相關

zookeeper相關,節點類型,如何實現服務發現和服務註冊

nginx負載均衡相關,讓你去實現負載均衡,該怎麼實現

linux命令,awk、cat、sort、cut、grep、uniq、wc、top等

壓力測試相關,怎麼分析,單接口壓測和多情況下的壓測

你覺得你的有點是什麼,你的缺點是什麼

spring mvc的實現原理

 

SpringMVC的工作原理圖:

SpringMVC流程

1、  用戶發送請求至前端控制器DispatcherServlet。

2、  DispatcherServlet收到請求調用HandlerMapping處理器映射器。

3、  處理器映射器找到具體的處理器(可以根據xml配置、註解進行查找),生成處理器對象及處理器攔截器(如果有則生成)一併返回給DispatcherServlet。

4、  DispatcherServlet調用HandlerAdapter處理器適配器。

5、  HandlerAdapter經過適配調用具體的處理器(Controller,也叫後端控制器)。

6、  Controller執行完成返回ModelAndView。

7、  HandlerAdapter將controller執行結果ModelAndView返回給DispatcherServlet。

8、  DispatcherServlet將ModelAndView傳給ViewReslover視圖解析器。

9、  ViewReslover解析後返回具體View。

10、DispatcherServlet根據View進行渲染視圖(即將模型數據填充至視圖中)。

11、 DispatcherServlet響應用戶。

組件說明:

以下組件通常使用框架提供實現:

DispatcherServlet:作爲前端控制器,整個流程控制的中心,控制其它組件執行,統一調度,降低組件之間的耦合性,提高每個組件的擴展性。

HandlerMapping:通過擴展處理器映射器實現不同的映射方式,例如:配置文件方式,實現接口方式,註解方式等。 

HandlAdapter:通過擴展處理器適配器,支持更多類型的處理器。

ViewResolver:通過擴展視圖解析器,支持更多類型的視圖解析,例如:jsp、freemarker、pdf、excel等。

組件:
1、前端控制器DispatcherServlet(不需要工程師開發),由框架提供
作用:接收請求,響應結果,相當於轉發器,中央處理器。有了dispatcherServlet減少了其它組件之間的耦合度。
用戶請求到達前端控制器,它就相當於mvc模式中的c,dispatcherServlet是整個流程控制的中心,由它調用其它組件處理用戶的請求,dispatcherServlet的存在降低了組件之間的耦合性。

2、處理器映射器HandlerMapping(不需要工程師開發),由框架提供
作用:根據請求的url查找Handler
HandlerMapping負責根據用戶請求找到Handler即處理器,springmvc提供了不同的映射器實現不同的映射方式,例如:配置文件方式,實現接口方式,註解方式等。

3、處理器適配器HandlerAdapter
作用:按照特定規則(HandlerAdapter要求的規則)去執行Handler
通過HandlerAdapter對處理器進行執行,這是適配器模式的應用,通過擴展適配器可以對更多類型的處理器進行執行。

4、處理器Handler(需要工程師開發)
注意:編寫Handler時按照HandlerAdapter的要求去做,這樣適配器纔可以去正確執行Handler
Handler 是繼DispatcherServlet前端控制器的後端控制器,在DispatcherServlet的控制下Handler對具體的用戶請求進行處理。
由於Handler涉及到具體的用戶業務請求,所以一般情況需要工程師根據業務需求開發Handler。

5、視圖解析器View resolver(不需要工程師開發),由框架提供
作用:進行視圖解析,根據邏輯視圖名解析成真正的視圖(view)
View Resolver負責將處理結果生成View視圖,View Resolver首先根據邏輯視圖名解析成物理視圖名即具體的頁面地址,再生成View視圖對象,最後對View進行渲染將處理結果通過頁面展示給用戶。 springmvc框架提供了很多的View視圖類型,包括:jstlView、freemarkerView、pdfView等。
一般情況下需要通過頁面標籤或頁面模版技術將模型數據通過頁面展示給用戶,需要由工程師根據業務需求開發具體的頁面。

6、視圖View(需要工程師開發jsp...)
View是一個接口,實現類支持不同的View類型(jsp、freemarker、pdf...)

核心架構的具體流程步驟如下:
1、首先用戶發送請求——>DispatcherServlet,前端控制器收到請求後自己不進行處理,而是委託給其他的解析器進行處理,作爲統一訪問點,進行全局的流程控制;
2、DispatcherServlet——>HandlerMapping, HandlerMapping 將會把請求映射爲HandlerExecutionChain 對象(包含一個Handler 處理器(頁面控制器)對象、多個HandlerInterceptor 攔截器)對象,通過這種策略模式,很容易添加新的映射策略;
3、DispatcherServlet——>HandlerAdapter,HandlerAdapter 將會把處理器包裝爲適配器,從而支持多種類型的處理器,即適配器設計模式的應用,從而很容易支持很多類型的處理器;
4、HandlerAdapter——>處理器功能處理方法的調用,HandlerAdapter 將會根據適配的結果調用真正的處理器的功能處理方法,完成功能處理;並返回一個ModelAndView 對象(包含模型數據、邏輯視圖名);
5、ModelAndView的邏輯視圖名——> ViewResolver, ViewResolver 將把邏輯視圖名解析爲具體的View,通過這種策略模式,很容易更換其他視圖技術;
6、View——>渲染,View會根據傳進來的Model模型數據進行渲染,此處的Model實際是一個Map數據結構,因此很容易支持其他視圖技術;
7、返回控制權給DispatcherServlet,由DispatcherServlet返回響應給用戶,到此一個流程結束。

下邊兩個組件通常情況下需要開發:

Handler:處理器,即後端控制器用controller表示。

View:視圖,即展示給用戶的界面,視圖中通常需要標籤語言展示模型數據。

 

在將SpringMVC之前我們先來看一下什麼是MVC模式

MVC:MVC是一種設計模式

MVC的原理圖:

分析:

M-Model 模型(完成業務邏輯:有javaBean構成,service+dao+entity)

V-View 視圖(做界面的展示  jsp,html……)

C-Controller 控制器(接收請求—>調用模型—>根據結果派發頁面)

 

springMVC是什麼: 

  springMVC是一個MVC的開源框架,springMVC=struts2+spring,springMVC就相當於是Struts2加上sring的整合,但是這裏有一個疑惑就是,springMVC和spring是什麼樣的關係呢?這個在百度百科上有一個很好的解釋:意思是說,springMVC是spring的一個後續產品,其實就是spring在原有基礎上,又提供了web應用的MVC模塊,可以簡單的把springMVC理解爲是spring的一個模塊(類似AOP,IOC這樣的模塊),網絡上經常會說springMVC和spring無縫集成,其實springMVC就是spring的一個子模塊,所以根本不需要同spring進行整合。

SpringMVC的原理圖:

看到這個圖大家可能會有很多的疑惑,現在我們來看一下這個圖的步驟:(可以對比MVC的原理圖進行理解)

第一步:用戶發起請求到前端控制器(DispatcherServlet)

第二步:前端控制器請求處理器映射器(HandlerMappering)去查找處理器(Handle):通過xml配置或者註解進行查找

第三步:找到以後處理器映射器(HandlerMappering)像前端控制器返回執行鏈(HandlerExecutionChain)

第四步:前端控制器(DispatcherServlet)調用處理器適配器(HandlerAdapter)去執行處理器(Handler)

第五步:處理器適配器去執行Handler

第六步:Handler執行完給處理器適配器返回ModelAndView

第七步:處理器適配器向前端控制器返回ModelAndView

第八步:前端控制器請求視圖解析器(ViewResolver)去進行視圖解析

第九步:視圖解析器像前端控制器返回View

第十步:前端控制器對視圖進行渲染

第十一步:前端控制器向用戶響應結果

看到這些步驟我相信大家很感覺非常的亂,這是正常的,但是這裏主要是要大家理解springMVC中的幾個組件:

前端控制器(DispatcherServlet):接收請求,響應結果,相當於電腦的CPU。

處理器映射器(HandlerMapping):根據URL去查找處理器

處理器(Handler):(需要程序員去寫代碼處理邏輯的)

處理器適配器(HandlerAdapter):會把處理器包裝成適配器,這樣就可以支持多種類型的處理器,類比筆記本的適配器(適配器模式的應用)

視圖解析器(ViewResovler):進行視圖解析,多返回的字符串,進行處理,可以解析成對應的頁面

 

談談對Spring IOC的理解

  學習過Spring框架的人一定都會聽過Spring的IoC(控制反轉) 、DI(依賴注入)這兩個概念,對於初學Spring的人來說,總覺得IoC 、DI這兩個概念是模糊不清的,是很難理解的,今天和大家分享網上的一些技術大牛們對Spring框架的IOC的理解以及談談我對Spring Ioc的理解。

1.1、IoC是什麼

  Ioc—Inversion of Control,即“控制反轉”,不是什麼技術,而是一種設計思想。在Java開發中,Ioc意味着將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制。如何理解好Ioc呢?理解好Ioc的關鍵是要明確“誰控制誰,控制什麼,爲何是反轉(有反轉就應該有正轉了),哪些方面反轉了”,那我們來深入分析一下:

  ●誰控制誰,控制什麼:傳統Java SE程序設計,我們直接在對象內部通過new進行創建對象,是程序主動去創建依賴對象;而IoC是有專門一個容器來創建這些對象,即由Ioc容器來控制對 象的創建;誰控制誰?當然是IoC 容器控制了對象;控制什麼?那就是主要控制了外部資源獲取(不只是對象包括比如文件等)。

  ●爲何是反轉,哪些方面反轉了:有反轉就有正轉,傳統應用程序是由我們自己在對象中主動控制去直接獲取依賴對象,也就是正轉;而反轉則是由容器來幫忙創建及注入依賴對象;爲何是反轉?因爲由容器幫我們查找及注入依賴對象,對象只是被動的接受依賴對象,所以是反轉;哪些方面反轉了?依賴對象的獲取被反轉了。

  用圖例說明一下,傳統程序設計如圖2-1,都是主動去創建相關對象然後再組合起來:

圖1-1 傳統應用程序示意圖

  當有了IoC/DI的容器後,在客戶端類中不再主動去創建這些對象了,如圖2-2所示:

圖1-2有IoC/DI容器後程序結構示意圖

1.2、IoC能做什麼

  IoC 不是一種技術,只是一種思想,一個重要的面向對象編程的法則,它能指導我們如何設計出鬆耦合、更優良的程序。傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難於測試;有了IoC容器後,把創建和查找依賴對象的控制權交給了容器,由容器進行注入組合對象,所以對象與對象之間是 鬆散耦合,這樣也方便測試,利於功能複用,更重要的是使得程序的整個體系結構變得非常靈活。

  其實IoC對編程帶來的最大改變不是從代碼上,而是從思想上,發生了“主從換位”的變化。應用程序原本是老大,要獲取什麼資源都是主動出擊,但是在IoC/DI思想中,應用程序就變成被動的了,被動的等待IoC容器來創建並注入它所需要的資源了。

  IoC很好的體現了面向對象設計法則之一—— 好萊塢法則:“別找我們,我們找你”;即由IoC容器幫對象找相應的依賴對象並注入,而不是由對象主動去找。

1.3、IoC和DI

  DI—Dependency Injection,即“依賴注入”組件之間依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中依賴注入的目的並非爲軟件系統帶來更多功能,而是爲了提升組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。通過依賴注入機制,我們只需要通過簡單的配置,而無需任何代碼就可指定目標需要的資源,完成自身的業務邏輯,而不需要關心具體的資源來自何處,由誰實現。

  理解DI的關鍵是:“誰依賴誰,爲什麼需要依賴,誰注入誰,注入了什麼”,那我們來深入分析一下:

  ●誰依賴於誰:當然是應用程序依賴於IoC容器

  ●爲什麼需要依賴:應用程序需要IoC容器來提供對象需要的外部資源

  ●誰注入誰:很明顯是IoC容器注入應用程序某個對象,應用程序依賴的對象

  ●注入了什麼:就是注入某個對象所需要的外部資源(包括對象、資源、常量數據)

  IoC和DI由什麼關係呢?其實它們是同一個概念的不同角度描述,由於控制反轉概念比較含糊(可能只是理解爲容器控制對象這一個層面,很難讓人想到誰來維護對象關係),所以2004年大師級人物Martin Fowler又給出了一個新的名字:“依賴注入”,相對IoC 而言,依賴注入”明確描述了“被注入對象依賴IoC容器配置依賴對象”。

  看過很多對Spring的Ioc理解的文章,好多人對Ioc和DI的解釋都晦澀難懂,反正就是一種說不清,道不明的感覺,讀完之後依然是一頭霧水,感覺就是開濤這位技術牛人寫得特別通俗易懂,他清楚地解釋了IoC(控制反轉) 和DI(依賴注入)中的每一個字,讀完之後給人一種豁然開朗的感覺。我相信對於初學Spring框架的人對Ioc的理解應該是有很大幫助的。

二、分享Bromon的blog上對IoC與DI淺顯易懂的講解

2.1、IoC(控制反轉)

  首先想說說IoC(Inversion of Control,控制反轉)。這是spring的核心,貫穿始終。所謂IoC,對於spring框架來說,就是由spring來負責控制對象的生命週期和對象間的關係。這是什麼意思呢,舉個簡單的例子,我們是如何找女朋友的?常見的情況是,我們到處去看哪裏有長得漂亮身材又好的mm,然後打聽她們的興趣愛好、qq號、電話號、ip號、iq號………,想辦法認識她們,投其所好送其所要,然後嘿嘿……這個過程是複雜深奧的,我們必須自己設計和麪對每個環節。傳統的程序開發也是如此,在一個對象中,如果要使用另外的對象,就必須得到它(自己new一個,或者從JNDI中查詢一個),使用完之後還要將對象銷燬(比如Connection等),對象始終會和其他的接口或類藕合起來。

  那麼IoC是如何做的呢?有點像通過婚介找女朋友,在我和女朋友之間引入了一個第三者:婚姻介紹所。婚介管理了很多男男女女的資料,我可以向婚介提出一個列表,告訴它我想找個什麼樣的女朋友,比如長得像李嘉欣,身材像林熙雷,唱歌像周杰倫,速度像卡洛斯,技術像齊達內之類的,然後婚介就會按照我們的要求,提供一個mm,我們只需要去和她談戀愛、結婚就行了。簡單明瞭,如果婚介給我們的人選不符合要求,我們就會拋出異常。整個過程不再由我自己控制,而是有婚介這樣一個類似容器的機構來控制。Spring所倡導的開發方式就是如此,所有的類都會在spring容器中登記,告訴spring你是個什麼東西,你需要什麼東西,然後spring會在系統運行到適當的時候,把你要的東西主動給你,同時也把你交給其他需要你的東西。所有的類的創建、銷燬都由 spring來控制,也就是說控制對象生存週期的不再是引用它的對象,而是spring。對於某個具體的對象而言,以前是它控制其他對象,現在是所有對象都被spring控制,所以這叫控制反轉。

2.2、DI(依賴注入)

  IoC的一個重點是在系統運行中,動態的向某個對象提供它所需要的其他對象。這一點是通過DI(Dependency Injection,依賴注入)來實現的。比如對象A需要操作數據庫,以前我們總是要在A中自己編寫代碼來獲得一個Connection對象,有了 spring我們就只需要告訴spring,A中需要一個Connection,至於這個Connection怎麼構造,何時構造,A不需要知道。在系統運行時,spring會在適當的時候製造一個Connection,然後像打針一樣,注射到A當中,這樣就完成了對各個對象之間關係的控制。A需要依賴 Connection才能正常運行,而這個Connection是由spring注入到A中的,依賴注入的名字就這麼來的。那麼DI是如何實現的呢? Java 1.3之後一個重要特徵是反射(reflection),它允許程序在運行的時候動態的生成對象、執行對象的方法、改變對象的屬性,spring就是通過反射來實現注入的。

  理解了IoC和DI的概念後,一切都將變得簡單明瞭,剩下的工作只是在spring的框架中堆積木而已。

三、我對IoC(控制反轉)和DI(依賴注入)的理解

  在平時的java應用開發中,我們要實現某一個功能或者說是完成某個業務邏輯時至少需要兩個或以上的對象來協作完成,在沒有使用Spring的時候,每個對象在需要使用他的合作對象時,自己均要使用像new object() 這樣的語法來將合作對象創建出來,這個合作對象是由自己主動創建出來的,創建合作對象的主動權在自己手上,自己需要哪個合作對象,就主動去創建,創建合作對象的主動權和創建時機是由自己把控的,而這樣就會使得對象間的耦合度高了,A對象需要使用合作對象B來共同完成一件事,A要使用B,那麼A就對B產生了依賴,也就是A和B之間存在一種耦合關係,並且是緊密耦合在一起,而使用了Spring之後就不一樣了,創建合作對象B的工作是由Spring來做的,Spring創建好B對象,然後存儲到一個容器裏面,當A對象需要使用B對象時,Spring就從存放對象的那個容器裏面取出A要使用的那個B對象,然後交給A對象使用,至於Spring是如何創建那個對象,以及什麼時候創建好對象的,A對象不需要關心這些細節問題(你是什麼時候生的,怎麼生出來的我可不關心,能幫我幹活就行),A得到Spring給我們的對象之後,兩個人一起協作完成要完成的工作即可。

  所以控制反轉IoC(Inversion of Control)是說創建對象的控制權進行轉移,以前創建對象的主動權和創建時機是由自己把控的,而現在這種權力轉移到第三方,比如轉移交給了IoC容器,它就是一個專門用來創建對象的工廠,你要什麼對象,它就給你什麼對象,有了 IoC容器,依賴關係就變了,原先的依賴關係就沒了,它們都依賴IoC容器了,通過IoC容器來建立它們之間的關係。

  這是我對Spring的IoC(控制反轉)的理解。DI(依賴注入)其實就是IOC的另外一種說法,DI是由Martin Fowler 在2004年初的一篇論文中首次提出的。他總結:控制的什麼被反轉了?就是:獲得依賴對象的方式反轉了。

AOP

AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行爲的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌代碼往往橫向地散佈在所有對象層次中,而與它對應的對象的核心功能毫無關係對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的代碼被稱爲橫切(cross cutting),在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

AOP技術恰恰相反,它利用一種稱爲"橫切"的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。

使用"橫切"技術,AOP把軟件系統分爲兩個部分:核心關注點橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在覈心關注點的多處,而各處基本相似,比如權限認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。

 

AOP核心概念

1、橫切關注點

對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之爲橫切關注點

2、切面(aspect)

類是對物體特徵的抽象,切面就是對橫切關注點的抽象

3、連接點(joinpoint)

被攔截到的點,因爲Spring只支持方法類型的連接點,所以在Spring中連接點指的就是被攔截到的方法,實際上連接點還可以是字段或者構造器

4、切入點(pointcut)

對連接點進行攔截的定義

5、通知(advice)

所謂通知指的就是指攔截到連接點之後要執行的代碼,通知分爲前置、後置、異常、最終、環繞通知五類

6、目標對象

代理的目標對象

7、織入(weave)

將切面應用到目標對象並導致代理對象創建的過程

8、引入(introduction)

在不修改代碼的前提下,引入可以在運行期爲類動態地添加一些方法或字段

 

Spring對AOP的支持

Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關係也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean實例作爲目標,這種關係可由IOC容器的依賴注入提供。Spring創建代理的規則爲:

1、默認使用Java動態代理來創建AOP代理,這樣就可以爲任何接口實例創建代理了

2、當需要代理的類不是代理接口的時候,Spring會切換爲使用CGLIB代理,也可強制使用CGLIB

AOP編程其實是很簡單的事情,縱觀AOP編程,程序員只需要參與三個部分:

1、定義普通業務組件

2、定義切入點,一個切入點可能橫切多個業務組件

3、定義增強處理,增強處理就是在AOP框架爲普通業務組件織入的處理動作

所以進行AOP編程的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理對象的方法=增強處理+被代理對象的方法。

下面給出一個Spring AOP的.xml文件模板,名字叫做aop.xml,之後的內容都在aop.xml上進行擴展:

複製代碼

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
            
</beans>

複製代碼

基於Spring的AOP簡單實現

注意一下,在講解之前,說明一點:使用Spring AOP,要成功運行起代碼,只用Spring提供給開發者的jar包是不夠的,請額外上網下載兩個jar包:

1、aopalliance.jar

2、aspectjweaver.jar

開始講解用Spring AOP的XML實現方式,先定義一個接口:

public interface HelloWorld
{
    void printHelloWorld();
    void doPrint();
}

定義兩個接口實現類:

public class HelloWorldImpl1 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl1.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl1.doPrint()");
        return ;
    }
}

 

public class HelloWorldImpl2 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl2.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl2.doPrint()");
        return ;
    }
}

 

橫切關注點,這裏是打印時間:

public class TimeHandler
{
    public void printTime()
    {
        System.out.println("CurrentTime = " + System.currentTimeMillis());
    }
}

複製代碼

有這三個類就可以實現一個簡單的Spring AOP了,看一下aop.xml的配置:

複製代碼

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" />
        <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" />
        <bean id="timeHandler" class="com.xrq.aop.TimeHandler" />
        
        <aop:config>
            <aop:aspect id="time" ref="timeHandler">
                <aop:pointcut id="addAllMethod" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addAllMethod" />
                <aop:after method="printTime" pointcut-ref="addAllMethod" />
            </aop:aspect>
        </aop:config>
</beans>

複製代碼

寫一個main函數調用一下:

複製代碼

public static void main(String[] args)
{
    ApplicationContext ctx = 
            new ClassPathXmlApplicationContext("aop.xml");
        
    HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1");
    HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2");
    hw1.printHelloWorld();
    System.out.println();
    hw1.doPrint();
    
    System.out.println();
    hw2.printHelloWorld();
    System.out.println();
    hw2.doPrint();
}

複製代碼

運行結果爲:

複製代碼

CurrentTime = 1446129611993
Enter HelloWorldImpl1.printHelloWorld()
CurrentTime = 1446129611993

CurrentTime = 1446129611994
Enter HelloWorldImpl1.doPrint()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.printHelloWorld()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.doPrint()
CurrentTime = 1446129611994

複製代碼

看到給HelloWorld接口的兩個實現類的所有方法都加上了代理,代理內容就是打印時間

基於Spring的AOP使用其他細節

1、增加一個橫切關注點,打印日誌,Java類爲:

複製代碼

public class LogHandler
{
    public void LogBefore()
    {
        System.out.println("Log before method");
    }
    
    public void LogAfter()
    {
        System.out.println("Log after method");
    }
}

複製代碼

複製代碼

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" />
        <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" />
        <bean id="timeHandler" class="com.xrq.aop.TimeHandler" />
        <bean id="logHandler" class="com.xrq.aop.LogHandler" />
        
        <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>
</beans>

複製代碼

測試類不變,打印結果爲:

複製代碼

CurrentTime = 1446130273734
Log before method
Enter HelloWorldImpl1.printHelloWorld()
Log after method
CurrentTime = 1446130273735

CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl1.doPrint()
Log after method
CurrentTime = 1446130273736

CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl2.printHelloWorld()
Log after method
CurrentTime = 1446130273736

CurrentTime = 1446130273737
Log before method
Enter HelloWorldImpl2.doPrint()
Log after method
CurrentTime = 1446130273737

複製代碼

要想讓logHandler在timeHandler前使用有兩個辦法:

(1)aspect裏面有一個order屬性,order屬性的數字就是橫切關注點的順序

(2)把logHandler定義在timeHandler前面,Spring默認以aspect的定義順序作爲織入順序

2、我只想織入接口中的某些方法

修改一下pointcut的expression就好了:

複製代碼

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" />
        <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" />
        <bean id="timeHandler" class="com.xrq.aop.TimeHandler" />
        <bean id="logHandler" class="com.xrq.aop.LogHandler" />
        
        <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.print*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.do*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>
</beans>

複製代碼

表示timeHandler只會織入HelloWorld接口print開頭的方法,logHandler只會織入HelloWorld接口do開頭的方法

3、強制使用CGLIB生成代理

前面說過Spring使用動態代理或是CGLIB生成代理是有規則的,高版本的Spring會自動選擇是使用動態代理還是CGLIB生成代理內容,當然我們也可以強制使用CGLIB生成代理,那就是<aop:config>裏面有一個"proxy-target-class"屬性,這個屬性值如果被設置爲true,那麼基於類的代理將起作用,如果proxy-target-class被設置爲false或者這個屬性被省略,那麼基於接口的代理將起作用

 

 

netty底層實現,IO模型,ChannelPipeline的實現和原理

緩存的設計和優化

緩存和數據庫一致性同步解決方案

你所在項目的系統架構,談談整體實現

消息隊列的使用場景

ActiveMQ、RabbitMQ、Kafka的區別

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章