java 面試題整理(不按期更新)

1、Java基礎html

一、Java面向對象的三個特徵與含義 前端

三大特徵是:封裝、繼承和多態java

    封裝是指將某事物的屬性和行爲包裝到對象中,這個對象只對外公佈須要公開的屬性和行爲,而這個公佈也是能夠有選擇性的公佈給其它對象。在Java中能使用private、protected、public三種修飾符或不用(即默認defalut)對外部對象訪問該對象的屬性和行爲進行限制。node

    繼承是子對象能夠繼承父對象的屬性和行爲,亦即父對象擁有的屬性和行爲,其子對象也就擁有了這些屬性和行爲。這很是相似大天然中的物種遺傳。mysql

    多態不是很好解釋:更傾向於使用java中的固定用法,即overriding(覆蓋)和overload(過載)。多態則是體如今overriding(覆蓋)上,而overload(過載)則不屬於面向對象中多態的範疇,由於overload(過載)概念在非面向對象中也存在。overriding(覆蓋)是面向對象中的多態,由於overriding(覆蓋)是與繼承緊密聯繫,是面向對象所特有的。多態是指父對象中的同一個行爲能在其多個子對象中有不一樣的表現。也就是說子對象能夠使用重寫父對象中的行爲,使其擁有不一樣於父對象和其它子對象的表現,這就是overriding(覆蓋)。c++

二、super 和 this 關鍵字程序員

在子類構造器中使用super()顯示調用父類的構造方法,super()必須寫在子類構造方法的第一行,不然編譯不經過; web

this面試

屬性:this屬性表示找到本類的屬性,若是本類沒有找到則繼續查找父類;算法

方法:this方法表示找到本類的方法,若是本類沒有找到則繼續查找父類;

構造:必須放在構造方法的首行,不能與super關鍵字同時出現;

特殊:表示當前對象;

super:

屬性:super屬性直接在子類之中查找父類中的指定屬性,再也不查找子類自己屬性;

方法:super方法直接在子類之中查找父類中的指定方法,再也不查找子類自己方法;

構造:必須放在構造方法首行,不能與this關鍵字同時出現。

super 和 this 關鍵字:

(1)調用super()必須寫在子類構造方法的第一行,不然編譯不經過。每一個子類構造方法的第一條語句,都是隱含地調用super(),若是父類沒有這種形式的構造函數,那麼在編譯的時候就會報錯。

(2)super從子類中調用父類的構造方法,this()在同一類內調用其它方法。

(3)super()和this()均需放在構造方法內第一行。

(4)儘管能夠用this調用一個構造器,但卻不能調用兩個。

(5)this和super不能同時出如今一個構造函數裏面,由於this必然會調用其它的構造函數,其它的構造函數必然也會有super語句的存在,因此在同一個構造函數裏面有相同的語句,就失去了語句的意義,編譯器也不會經過。

(6)this()和super()都指的是對象,因此,均不能夠在static環境中使用。包括:static變量,static方法,static語句塊。

(7)從本質上講,this是一個指向本對象的指針, 然而super是一個Java關鍵字。

三、訪問權限

(1)訪問權限修飾詞

1)public(公共的):代表該成員變量或方法對全部類或對象都是可見的,全部類或對象均可以直接訪問;

2)protected(受保護的):代表成員變量或方法對該類自己&與它在同一個包中的其它類&在其它包中的該類的子類均可見;

3)default(默認的,不加任何訪問修飾符):代表成員變量或方法只有本身&其位於同一個包內的類可見;

4)private(私有的):代表該成員變量或方法是私有的,只有當前類對其具備訪問權限。

由大到小:public(接口訪問權限)、protected(繼承訪問權限)、包訪問權限(沒有使用任何訪問權限修飾詞)、private(私有沒法訪問)。

protected表示就類用戶而言,這是private的,但對於任何繼承於此類的導出類或其餘任何位於同一個包內的類來講,倒是能夠訪問的(protected也提供了包內訪問權限)。

private和protected通常不用來修飾外部類,而public、abstract或final能夠用來修飾外部類(若是用private和protected修飾外部類,會使得該類變得訪問性受限)。

(2)訪問權限注意點:

1)類的訪問權限,只能是包訪問權限(默認無訪問修飾符便可)或者public。若把一個類中的構造器指定爲private,則不能訪問該類,若要建立該類的對象,則須要在該類的static成員內部建立,如單例模式。

2)若是沒能爲類訪問權限指定一個訪問修飾符,默認獲得包訪問權限,則該類的對象能夠由包內任何其餘類建立,可是包外不能夠。

3)訪問權限的控制,也稱爲具體實現的隱藏。制定規則(如使用訪問權限,設定成員所遵照的界限),是防止客戶端程序員對類爲所欲爲而爲。

(3)控制對成員的訪問權限的兩個緣由:

使用戶不要碰觸那些不應碰觸的部分,對類內部的操做是必要的,不屬於客戶端程序員所需接口的一部分;

讓類庫設計者能夠更改類的內部工做方式,而不會對客戶端程序員產生重大影響;訪問權限控制能夠確保不會有任何客戶端程序員依賴於類的底層實現的任何部分。

(4)對某成員的訪問權的惟一途徑:

1)該成員爲public;

2)經過不加訪問權限修飾詞並將其餘類放置在同一個包內的方式給成員賦予包訪問權;

3)繼承技術,訪問protected成員;

4)提供訪問器和變異器(get/set方法),以讀取和改變數值。

四、抽象類

(1)抽象類不能被實例化,實例化的工做應該交由它的子類來完成,它只須要有一個引用便可。

(2)抽象方法必須由子類來進行重寫。

(3)只要包含一個抽象方法的類,該類必需要定義成抽象類,無論是否還包含有其餘方法。

(4)抽象類中能夠包含具體的方法,固然也能夠不包含抽象方法。

(5)子類中的抽象方法不能與父類的抽象方法同名。

(6)abstract不能與final並列修飾同一個類。(abstract須要子類去實現,而final表示不能被繼承,矛盾。)

(7)abstract 不能與private、static、final或native並列修飾同一個方法。

注意:

A、final修飾的類爲終態類,不能被繼承,而抽象類是必須被繼承的纔有其意義的,所以,final是不能用來修飾抽象類的。

B、final修飾的方法爲終態方法,不能被重寫。而繼承抽象類,必須重寫其方法。

C、抽象方法是僅聲明,並不作實現的方法。

五、值傳遞與引用傳遞

值傳遞:Java中原始數據類型都是值傳遞,傳遞的是值的副本,形參的改變不會影響實際參數的值;

引用傳遞:傳遞的是引用類型數據,包括String,數組,列表,map,類對象等類型,形參與實參指向的是同一內存地址,所以形參改變會影響實參的值。

六、繼承

定義:按照現有類的類型來建立新類 ,無需改變現有類的形式,採用現有類的形式並在其增長新代碼,稱爲繼承。經過關鍵字extends實現。

特色:

(1)當建立一個類時,總在繼承。(除非明確指明繼承類,不然都是隱式第繼承根類Object);

(2)爲了繼承,通常將全部的數據成員都指定爲private,將全部的方法指定爲public;

(3)能夠將繼承視做是對類的複用;

(4)is-a關係用繼承;

(5)繼承容許對象視爲自身的類型或其基類型加以處理;

(6)若是向上轉型,不能調用那些新的方法(如Animal an = new Cat(),an是不能調用Cat中有的而Animal中沒有的方法,會返回一條編譯時出錯消息),因此向上轉型會丟失具體的類型信息。

注意:

(1)構造方法不能被繼承;方法和屬性能夠被繼承;

(2)子類的構造方法隱式地調用父類的不帶參數的構造方法;

(3)當父類沒有不帶參數的構造方法時,子類須要使用super來顯示調用父類的構造方法,super指的是對父類的引用;

(4)super關鍵字必須是構造方法中的第一行語句。特例以下:

      當兩個方法造成重寫關係時,可在子類方法中經過super.run() 形式調用父類的run()方法,其中super.run()沒必要放在第一行語句,所以此時父類對象已經構造完畢,先調用父類的run()方法仍是先調用子類的run()方法是根據程序的邏輯決定的。

七、是否能夠繼承String類?

答:String 類是final類,不能夠被繼承。
補充:繼承String自己就是一個錯誤的行爲,對String類型最好的重用方式是關聯關係(Has-A)和依賴關係(Use-A)而不是繼承關係(Is-A)。

八、final關鍵字

1)使用範圍:數據、方法和類。

2)final關鍵字:final能夠修飾屬性、方法、類。

3)final修飾類:當一個類被final所修飾時,表示該類是一個終態類,即不能被繼承

4)final修飾方法:當一個方法被final所修飾時,表示該方法是一個終態方法,即不能被重寫(Override)。

5)final修飾屬性:當一個屬性被final所修飾時,表示該屬性不能被改寫

(1)final數據:

1)編譯時常量:是使用static和 final修飾的常量,全用大寫字母命名,且字與字之間用下劃線隔開。(不能由於數據是final的就認爲在編譯時就知道值,在運行時也能夠用某數值來初始化某一常量)

2)final修飾基本數據類型和對象引用:對於基本類型,final修飾的數值是恆定不變;而final修飾對象引用,則引用恆定不變(一旦引用被初始化指向一個對象,就不能改成指向另外一個對象),可是對象自己的內容能夠修改。

3)空白final:空白final是指被聲明爲final但又未給定初值的域,不管什麼狀況,編譯器都保證空白final在使用被初始化。必須在域的定義處或每一個構造器中用表達式對final進行賦值。

4)final參數:final修飾參數後,在方法體中不容許對參數進行更改,只能夠讀final參數。主要用於向匿名類傳遞數據。

(2)final方法:

1)使用final修飾方法緣由:將方法鎖定以及效率問題。將方法鎖定:防止任何繼承類修改final方法的含義,確保該方法行爲保持不變,且不會被覆蓋;效率:早期Java實現中贊成編譯器將針對該方法的全部調用轉爲內嵌調用。

2)類中全部的private方法都隱式地指定爲final的。

(3)final類:

將某個類總體定義爲final時,則不能繼承該類,不能有子類。

九、static關鍵字是什麼意思?Java中是否能夠覆蓋(override)一個private或者是static的方法?

    static表示靜態的意思,可用於修飾成員變量和成員函數,被靜態修飾的成員函數只能訪問靜態成員,不能夠訪問非靜態成員。靜態是隨着類的加載而加載的,所以能夠直接用類進行訪問。 重寫是子類中的方法和子類繼承的父類中的方法同樣(函數名,參數,參數類型,返回值類型),可是子類中的訪問權限要不低於父類中的訪問權限。重寫的前提是必需要繼承,private修飾不支持繼承,所以被私有的方法不能夠被重寫。靜態方法形式上能夠被重寫,即子類中能夠重寫父類中靜態的方法。可是實際上從內存的角度上靜態方法不能夠被重寫。


①static能夠修飾內部類,可是不能修飾普通類。靜態內部類的話能夠直接調用靜態構造器(不用對象)。

②static修飾方法,static 方法就是沒有 this 的方法。在static方法內部不能調用非靜態方法,反過來是能夠的。並且能夠在沒有建立任何對象的前提下,僅僅經過類自己來調用 static 方法。這實際上正是 static 方法的主要用途,方便在沒有建立對象的狀況下來進行調用(方法/變量)。

最多見的static方法就是main,由於全部對象都是在該方法裏面實例化的,而main是程序入口,因此要經過類名來調用。還有就是main中須要常常訪問隨類加載的成員變量。

③static修飾變量,就變成了靜態變量,隨類加載一次,能夠被多個對象共享。

④static修飾代碼塊,造成靜態代碼塊,用來優化程序性能,將須要加載一次的代碼設置成隨類加載,靜態代碼塊能夠有多個。

Java中static方法不能被覆蓋,由於方法覆蓋是基於運行時動態綁定的,而static方法是編譯時靜態綁定的。還有私有的方法不能被繼承,子類就沒有訪問權限,確定也是不能別覆蓋的。

十、static方法可否被重寫

     在Java中,子類可繼承父類中的方法,而不須要從新編寫相同的方法。但有時子類並不想原封不動地繼承父類的方法,而是想做必定的修改,這就須要採用方法的重寫(Override)。方法重寫又稱方法覆蓋。 在《Java編程思想》中說起到:「覆蓋」只有在某方法是基類的接口的一部分時纔會出現。即,必須能將一個對象向上轉型爲它的基本類型並調用相同的方法。那麼,咱們即可以據此來對static方法可否被重寫的問題進行驗證。

例子:

class StaticSuper{
    public static String staticGet(){
        return "Base staticGet()";
    }
    public String dynamicGet(){
        return "Base dynamicGet()";
    }
}

class StaticSub extends StaticSuper{
    public static String staticGet(){
        return "Derived staticGet()";
    }
    public String dynamicGet(){
        return "Derived dynamicGet()";
    }
}

public class StaticPolyMorphism {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub();
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}

    在例子中,若是基類StaticSuper中的static方法staticGet()在子類StaticSub中被重寫了,那麼sup.staticGet()返回的結果應該是「Derived staticGet()」,實際上結果是如何呢?運行程序後,咱們看到輸出是: 

Base staticGet() 
Derived dynamicGet()  

這說明,非靜態方法dynamicGet()的確在子類中被重寫了,而靜態方法staticGet()卻沒有。對於這一點,咱們也能夠經過在子類方法上添加@Overide註解進行驗證: 

      如圖所示,在子類中的靜態方法staticGet()上添加@Override註解會致使編譯報錯:The method staticGet() of type StaticSub must override or implement a supertype method(StaticSub類的staticGet()方法必須覆蓋或者實現一個父型的方法),而非靜態方法dynamicGet()則無此報錯信息,這也就印證了咱們上面的推論。其實,在Java中,若是父類中含有一個靜態方法,且在子類中也含有一個返回類型、方法名、參數列表均與之相同的靜態方法,那麼該子類實際上只是將父類中的該同名方法進行了隱藏,而非重寫。換句話說,父類和子類中含有的實際上是兩個沒有關係的方法,它們的行爲也並不具備多態性。正如同《Java編程思想》中所說:「一旦你瞭解了多態機制,可能就會認爲全部事物均可以多態地發生。然而,只有普通方法的調用能夠是多態的。」這也很好地理解了,爲何在Java中,static方法和final方法(private方法屬於final方法)是前期綁定,而其餘全部的方法都是後期綁定了。

十一、靜態方法和實例方法的區別

(1)在外部調用靜態方法時,能夠使用"類名.方法名"的方式,也能夠使用"對象名.方法名"的方式。而實例方法只有後面這種方式。也就是說,調用靜態方法能夠無需建立對象。

(2)靜態方法在訪問本類的成員時,只容許訪問靜態成員(即靜態成員變量和靜態方法),而不容許訪問實例成員變量和實例方法;實例方法則無此限制。

例子1:調用靜態方法示例

package com.demo;

public class hasStaticMethod {

    //定義一個靜態方法
    public static void callMe(){
        System.out.println("This is a static method.");
    } 
}
package com.demo;

public class invokeStaticMethod {
    
    //下面這個程序使用兩種形式來調用靜態方法。
    public static void main(String args[]){
        hasStaticMethod.callMe();  //不建立對象,直接調用靜態方法      
        hasStaticMethod oa = new hasStaticMethod(); //建立一個對象  
        oa.callMe(); //利用對象來調用靜態方法}
    }
}

程序兩次調用靜態方法,都是容許的,程序的輸出以下:

This is a static method.
This is a static method.

例子2:靜態方法訪問成員變量示例

package com.demo;

public class accessMember {
    
    private static int sa; //定義一個靜態成員變量
    private int ia;  //定義一個實例成員變量
    
    //下面定義一個靜態方法
    static void statMethod(){
        int i = 0;    //正確,能夠有本身的局部變量  
        sa = 10;      //正確,靜態方法能夠使用靜態變量
        otherStat();  //正確,能夠調用靜態方法  
        ia = 20;      //錯誤,不能使用實例變量  
        insMethod();  //錯誤,不能調用實例方法
        
    }
    
    static void otherStat(){}
    
    //下面定義一個實例方法
    void  insMethod(){
        int i = 0;    //正確,能夠有本身的局部變量  
        sa = 15;      //正確,能夠使用靜態變量  
        ia = 30;      //正確,能夠使用實例變量  
        statMethod(); //正確,能夠調用靜態方法
    }
}

    本例其實能夠歸納成一句話:靜態方法只能訪問靜態成員,實例方法能夠訪問靜態和實例成員。之因此不容許靜態方法訪問實例成員變量,是由於實例成員變量是屬於某個對象的,而靜態方法在執行時,並不必定存在對象。一樣,由於實例方法能夠訪問實例成員變量,若是容許靜態方法調用實例方法,將間接地容許它使用實例成員變量,因此它也不能調用實例方法。基於一樣的道理,靜態方法中也不能使用關鍵字this。

    main()方法是一個典型的靜態方法,它一樣遵循通常靜態方法的規則,因此它能夠由系統在建立對象以前就調用。 

十二、闡述靜態變量和實例變量的區別

答:靜態變量是被static修飾符修飾的變量,也稱爲類變量,它屬於類,不屬於類的任何一個對象,一個類無論建立多少個對象,靜態變量在內存中有且僅有一個拷貝;實例變量必須依存於某一實例,須要先建立對象而後經過對象才能訪問到它。靜態變量能夠實現讓多個對象共享內存。

1三、是否能夠從一個靜態(static)方法內部發出對非靜態(non-static)方法的調用?

答:不能夠,靜態方法只能訪問靜態成員,由於非靜態方法的調用要先建立對象,在調用靜態方法時可能對象並無被初始化。

1四、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被synchronized修飾?

答:都不能。抽象方法須要子類重寫,而靜態的方法是沒法被重寫的,所以兩者是矛盾的。本地方法是由本地代碼(如C代碼)實現的方法,而抽象方法是沒有實現的,也是矛盾的。synchronized和方法的實現細節有關,抽象方法不涉及實現細節,所以也是相互矛盾的。

1五、闡述final、finally、finalize的區別

- final:修飾符(關鍵字)有三種用法:若是一個類被聲明爲final,意味着它不能再派生出新的子類,即不能被繼承,所以它和abstract是反義詞。將變量聲明爲final,能夠保證它們在使用中不被改變,被聲明爲final的變量必須在聲明時給定初值,而在之後的引用中只能讀取不可修改。被聲明爲final的方法也一樣只能使用,不能在子類中被重寫。

- finally:一般放在try…catch…的後面構造老是執行代碼塊,這就意味着程序不管正常執行仍是發生異常,這裏的代碼只要JVM不關閉都能執行,能夠將釋放外部資源的代碼寫在finally塊中。

- finalize:Object類中定義的方法,Java中容許使用finalize()方法在垃圾收集器將對象從內存中清除出去以前作必要的清理工做。這個方法是由垃圾收集器在銷燬對象時調用的,經過重寫finalize()方法能夠整理系統資源或者執行其餘清理工做。

1六、== 與 equals() 方法的區別

(1)基本數據類型與引用數據類型

1)基本數據類型的比較:只能用==;

2)引用數據類型的比較:==是比較棧內存中存放的對象在堆內存地址,equals是比較對象的內容是否相同。

(2)特殊:String做爲一個對象

例子一:經過構造函數建立對象時。對象不一樣,內容相同,"=="返回false,equals返回true。

String s1 = newString("java");
String s2 = new String("java");

System.out.println(s1==s2);            //false
System.out.println(s1.equals(s2));   //true

例子二:同一對象,"=="和equals結果相同

String s1 = new String("java");
String s2 = s1;  //兩個不一樣的引用變量指向同一個對象

System.out.println(s1==s2);            //true
System.out.println(s1.equals(s2));   //true
String s1 = "java";
String s2 = "java";  //此時String常量池中有java對象,直接返回引用給s2;

System.out.println(s1==s2);  //true

System.out.println(s1.equals(s2));   //true

字面量形式建立對象時:

若是String緩衝池內不存在與其指定值相同的String對象,那麼此時虛擬機將爲此建立新的String對象,並存放在String緩衝池內。

若是String緩衝池內存在與其指定值相同的String對象,那麼此時虛擬機將不爲此建立新的String對象,而直接返回已存在的String對象的引用。

(3)String的字面量形式和構造函數建立對象

String s = "aaa"; //採用字面值方式賦值

1)查找StringPool中是否存在「aaa」這個對象,若是不存在,則在String Pool中建立一個「aaa」對象,而後將String Pool中的這個「aaa」對象的地址返回來,賦給引用變量s,這樣s會指向String Pool中的這個「aaa」字符串對象;

2)若是存在,則不建立任何對象,直接將String Pool中的這個「aaa」對象地址返回來,賦給s引用。

String s = new String("aaa");

1)首先在String Pool中查找有沒有"aaa"這個字符串對象,若是有,則不在String Pool中再去建立"aaa"這個對象,直接在堆中建立一個"aaa"字符串對象,而後將堆中的這個"aaa"對象的地址返回來,賦給s引用,致使s指向了堆中建立的這個"aaa"字符串對象;

2)若是沒有,則首先在String Pool中建立一個"aaa"對象,而後再去堆中建立一個"aaa"對象,而後將堆中的這個"aaa"對象的地址返回來,賦給s引用,致使s指向了堆中所建立的這個"aaa"對象。

總結來講:

1)對於==,若是做用於基本數據類型的變量,則直接比較其存儲的 「值」是否相等;若是做用於引用類型的變量,則比較的是所指向的對象的地址。

2)對於equals方法,注意:equals方法不能做用於基本數據類型的變量。若是沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;諸如String、Date等類對equals方法進行了重寫的話,比較的是所指向的對象的內容。

1七、初始化及類的加載

(1)加載的含義:一般,加載發生在建立類的第一個對象時,但訪問static域或static方法時,也會發生加載。static的東西只會初始化一次。

(2)加載過程:加載一個類的時候,首先去加載父類的靜態域,而後再加載自身的靜態域,以後去初始化父類的成員變量,後加載父類的構造方法,最後初始化自身的成員變量,後加載自身的構造方法。(先初始化成員變量,後加載構造函數的緣由是,構造函數中可能要用到這些成員變量)

父類靜態塊——子類靜態塊——父類塊——父類構造器——子類塊——子類構造器

最終版本:父類靜態域——父類靜態塊——子類靜態域——子類靜態塊——父類成員變量及代碼塊——父類構造器——子類成員變量及代碼塊——子類構造器。

(3)加載次數:加載的動做只會加載一次,該類的靜態域或第一個實體的建立都會引發加載。

(4)變量的初始化:變量的初始化老是在當前類構造器主體執行以前進行的,且static的成員比普通的成員變量先初始化。

指出下面程序的運行結果

class A {
 
    static {
        System.out.print("1");
    }
 
    public A() {
        System.out.print("2");
    }
}
class B extends A{
 
    static {
        System.out.print("a");
    }
 
    public B() {
        System.out.print("b");
    }
}
public class Hello {
 
    public static void main(String[] args) {
        A ab = new B();
        ab = new B();
    }
 
}

答:執行結果:1a2b2b。建立對象時構造器的調用順序是:先初始化靜態成員,而後調用父類構造器,再初始化非靜態成員,最後調用自身構造器。

1八、多態

(1)多態只發生在普通方法中,對於域和static方法,不發生多態。子類對象轉化爲父類型引用時,對於任何域的訪問都是由編譯器解析。靜態方法是與類相關聯,而不與單個對象相關聯;

(2)在繼承時,若被覆寫的方法不是private,則父類調用方法時,會調用子類的方法,經常使用的多態性就是當父類引用指向子類對象時。

(3)多態就是指程序中定義的引用變量所指向的具體類型和經過該引用變量發出的方法調用在編程時並不肯定,而是在程序運行期間才肯定,即一個引用變量到底指向哪一個類的實例對象,該引用變量發出的方法調用究竟是哪一個類中實現的方法,必須在由程序運行期間才能決定。

(4)多態是同一個行爲具備多個不一樣表現形式或形態的能力。

(5)多態就是同一個接口,使用不一樣的實例而執行不一樣操做,多態性是對象多種表現形式的體現。

1九、基本數據類型與包裝類

全部的包裝類(8個)都位於java.lang包下,分別是Byte,Short,Integer,Long,Float,Double,Character,Boolean。

基本數據類型:byte:8位;short:16位;int:32位;long:64位;float:32位;double:64位;char:16位;boolean:8位。

20、Object類的公有方法

clone()(protected的)、toString()、equals(Object obj)、hashCode()、getClass()、finialize()(protected的)、notify()/notifyAll()、wait()/wait(long timeout)、wait(long timeout,intnaos)

(1)clone方法

保護方法,實現對象的淺複製,只有實現了Cloneable接口才能夠調用該方法,不然拋出CloneNotSupportedException異常。

主要是JAVA裏除了8種基本類型傳參數是值傳遞,其餘的類對象傳參數都是引用傳遞,咱們有時候不但願在方法裏講參數改變,這是就須要在類中複寫clone方法。

(2)getClass方法

final方法,得到運行時類型。

(3)toString方法

該方法用得比較多,通常子類都有覆蓋。

(4)finalize方法

該方法用於釋放資源。由於沒法肯定該方法何時被調用,不多使用。

(5)equals方法

該方法是很是重要的一個方法。通常equals和==是不同的,可是在Object中二者是同樣的。子類通常都要重寫這個方法。

(6)hashCode方法

該方法用於哈希查找,能夠減小在查找中使用equals的次數,重寫了equals方法通常都要重寫hashCode方法。這個方法在一些具備哈希功能的Collection中用到。

通常必須知足obj1.equals(obj2)==true,能夠推出obj1.hashCode()==obj2.hashCode(),可是hashCode相等不必定就知足equals。不過爲了提升效率,應該儘可能使上面兩個條件接近等價。

若是不重寫hashcode(),在HashSet中添加兩個equals的對象,會將兩個對象都加入進去。

(7)wait方法

wait方法就是使當前線程等待該對象的鎖,當前線程必須是該對象的擁有者,也就是具備該對象的鎖。wait()方法一直等待,直到得到鎖或者被中斷。wait(long timeout)設定一個超時間隔,若是在規定時間內沒有得到鎖就返回。

調用該方法後當前線程進入睡眠狀態,直到如下事件發生。

(7.1)其餘線程調用了該對象的notify方法。

(7.2)其餘線程調用了該對象的notifyAll方法。

(7.3)其餘線程調用了interrupt中斷該線程。

(7.4)時間間隔到了。

此時該線程就能夠被調度了,若是是被中斷的話就拋出一個InterruptedException異常。

(8)notify方法

該方法喚醒在該對象上等待的某個線程。

(9)notifyAll方法

該方法喚醒在該對象上等待的全部線程。

2一、Hashcode的做用

Hash是散列的意思,就是把任意長度的輸入,經過散列算法變換成固定長度的輸出,該輸出就是散列值。關於散列值,有如下幾個關鍵結論:

(1)、若是散列表中存在和散列原始輸入K相等的記錄,那麼K一定在f(K)的存儲位置上。

(2)、不一樣關鍵字通過散列算法變換後可能獲得同一個散列地址,這種現象稱爲碰撞。

(3)、若是兩個Hash值不一樣(前提是同一Hash算法),那麼這兩個Hash值對應的原始輸入一定不一樣。

HashCode

而後講下什麼是HashCode,總結幾個關鍵點:

(1)、HashCode的存在主要是爲了查找的快捷性,HashCode是用來在散列存儲結構中肯定對象的存儲地址的。

(2)、若是兩個對象equals相等,那麼這兩個對象的HashCode必定也相同。

(3)、若是對象的equals方法被重寫,那麼對象的HashCode方法也儘可能重寫。

(4)、若是兩個對象的HashCode相同,不表明兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置。

HashCode有什麼用

回到最關鍵的問題,HashCode有什麼用?不妨舉個例子:

(1)、假設內存中有0 1 2 3 4 5 6 7 8這8個位置,若是我有個字段叫作ID,那麼我要把這個字段存放在以上8個位置之一,若是不用HashCode而任意存放,那麼當查找時就須要到8個位置中去挨個查找。

(2)、使用HashCode則效率會快不少,把ID的HashCode%8,而後把ID存放在取得餘數的那個位置,而後每次查找該類的時候均可以經過ID的HashCode%8求餘數直接找到存放的位置了。

(3)、若是ID的 HashCode%8算出來的位置上自己已經有數據了怎麼辦?這就取決於算法的實現了,好比ThreadLocal中的作法就是從算出來的位置向後查找第一個爲空的位置,放置數據;HashMap的作法就是經過鏈式結構連起來。反正,只要保證放的時候和取的時候的算法一致就好了。

(4)、若是ID的 HashCode%8相等怎麼辦(這種對應的是第三點說的鏈式結構的場景)?這時候就須要定義equals了。先經過HashCode%8來判斷類在哪個位置,再經過equals來在這個位置上尋找須要的類。對比兩個類的時候也差很少,先經過HashCode比較,假如HashCode相等再判斷 equals。若是兩個類的HashCode都不相同,那麼這兩個類一定是不一樣的。

     舉個實際的例子Set。咱們知道Set裏面的元素是不能夠重複的,那麼如何作到?Set是根據equals()方法來判斷兩個元素是否相等的。比方說Set裏面已經有1000個元素了,那麼第1001個元素進來的時候,最多可能調用1000次equals方法,若是equals方法寫得複雜,對比的東西特別多,那麼效率會大大下降。使用HashCode就不同了,比方說HashSet,底層是基於HashMap實現的,先經過HashCode取一個模,這樣一會兒就固定到某個位置了,若是這個位置上沒有元素,那麼就能夠確定HashSet中一定沒有和新添加的元素equals的元素,就能夠直接存放了,都不須要比較;若是這個位置上有元素了,逐一比較,比較的時候先比較HashCode,HashCode都不一樣接下去都不用比了,確定不同,HashCode相等,再equals比較,沒有相同的元素就存,有相同的元素就不存。若是原來的Set裏面有相同的元素,只要HashCode的生 成方式定義得好(不重複),無論Set裏面原來有多少元素,只須要執行一次的equals就能夠了。這樣一來,實際調用equals方法的次數大大下降,提升了效率。

2二、兩個對象值相同(x.equals(y) == true),但卻可有不一樣的hashcode,這句話對不對?

答:不對,若是兩個對象x和y知足x.equals(y) == true,它們的哈希碼(hashcode)應當相同。Java對於eqauls方法和hashCode方法是這樣規定的:

(1) 若是兩個對象相同(equals方法返回true),那麼它們的hashCode值必定要相同;

(2) 若是兩個對象的hashCode相同,它們並不必定相同。

固然,你未必要按照要求去作,可是若是你違背了上述原則就會發如今使用容器時,相同的對象能夠出如今Set集合中,同時增長新元素的效率會大大降低(對於使用哈希存儲的系統,若是哈希碼頻繁的衝突將會形成存取性能急劇降低)。

2三、重寫equals()方法爲何要重寫hashcode()方法?

object對象中的 public boolean equals(Object obj),對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個對象時,此方法才返回 true;
注意:當此方法被重寫時,一般有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具備相等的哈希碼。以下:

(1)當obj1.equals(obj2)爲true時,obj1.hashCode() == obj2.hashCode()必須爲true
(2)當obj1.hashCode() == obj2.hashCode()爲false時,obj1.equals(obj2)必須爲false

     若是不重寫equals,那麼比較的將是對象的引用是否指向同一塊內存地址,重寫以後目的是爲了比較兩個對象的value值是否相等。特別指出利用equals比較八大包裝對象(如int,float等)和String類(由於該類已重寫了equals和hashcode方法)對象時,默認比較的是值,在比較其它自定義對象時都是比較的引用地址
hashcode是用於散列數據的快速存取,如利用HashSet/HashMap/Hashtable類來存儲數據時,都是根據存儲對象的hashcode值來進行判斷是否相同的。

     這樣若是咱們對一個對象重寫了equals,意思是隻要對象的成員變量值都相等那麼equals就等於true,但不重寫hashcode,那麼咱們再new一個新的對象,當原對象.equals(新對象)等於true時,二者的hashcode倒是不同的,由此將產生了理解的不一致,如在存儲散列集合時(如Set類),將會存儲了兩個值同樣的對象,致使混淆,所以,就也須要重寫hashcode()。

2四、Override 和 Overload的含義和區別

(1)、Override 特色  

1)、覆蓋的方法的標誌必需要和被覆蓋的方法的標誌徹底匹配,才能達到覆蓋的效果;  

2)、覆蓋的方法的返回值必須和被覆蓋的方法的返回一致;  

3)、覆蓋的方法所拋出的異常必須和被覆蓋方法的所拋出的異常一致,或者是其子類;

4)、方法被定義爲final不能被重寫。 

5)、對於繼承來講,若是某一方法在父類中是訪問權限是private,那麼就不能在子類對其進行重寫覆蓋,若是定義的話,也只是定義了一個新方法,而不會達到重寫覆蓋的效果。(一般存在於父類和子類之間。)

(2)、Overload 特色  

1)、在使用重載時只能經過不一樣的參數樣式。例如,不一樣的參數類型,不一樣的參數個數,不一樣的參數順序(固然,同一方法內的幾個參數類型必須不同,例如能夠是fun(int, float), 可是不能爲fun(int, int));  

2)、不能經過訪問權限、返回類型、拋出的異常進行重載;  

3)、方法的異常類型和數目不會對重載形成影響;  

4)、重載事件一般發生在同一個類中,不一樣方法之間的現象。

其具體實現機制:

overload是重載,重載是一種參數多態機制,即代碼經過參數的類型或個數不一樣而實現的多態機制。是一種靜態的綁定機制(在編譯時已經知道具體執行的是哪一個代碼段)。  

override是覆蓋。覆蓋是一種動態綁定的多態機制。即在父類和子類中同名元素(如成員函數)有不一樣的實現代碼。執行的是哪一個代碼是根據運行時實際狀況而定的。


     方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,然後者實現的是運行時的多態性。重載發生在一個類中,同名的方法若是有不一樣的參數列表(參數類型不一樣、參數個數不一樣或者兩者都不一樣)則視爲重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。重載對返回類型沒有特殊的要求。

2五、重載(Overload)與覆蓋(Override)

重載(overload):對於類的方法(包括從父類中繼承的方法),方法名相同參數列表不一樣的方法之間就構成了重載關係。這裏有兩個問題須要注意:

(1)什麼叫參數列表?參數列表又叫參數簽名,指三樣東西:參數的類型,參數的個數,參數的順序這三者只要有一個不一樣就叫作參數列表不一樣。

(2)重載關係只能發生在同一個類中嗎?非也。這時候你要深入理解繼承,要知道一個子類所擁有的成員除了本身顯式寫出來的之外,還有父類遺傳下來的。因此子類中的某個方法和父類中繼承下來的方法也能夠發生重載的關係。例如,父類中有一個方法是 func(){ ... },子類中有一個方法是 func(int i){ ... },就構成了方法的重載。

你們在使用的時候要緊扣定義,看方法之間是不是重載關係,不用管方法的修飾符和返回類型以及拋出的異常,只看方法名和參數列表。並且要記住,構造器也能夠重載。

返回值和異常以及訪問修飾符,不能做爲重載的條件(由於對於匿名調用,會出現歧義,eg:void a ()和int a() ,若是調用a(),出現歧義)


覆蓋(override):若是在子類中定義一個方法,其名稱、返回類型及參數簽名正好與父類中某個方法的名稱、返回類型及參數簽名相匹配,那麼能夠說,子類的方法覆蓋了父類的方法。

覆蓋 (override):也叫重寫,就是在當父類中的某些方法不能知足要求時,子類中改寫父類的方法。當父類中的方法被覆蓋了後,除非用super關鍵字,不然就沒法再調用父類中的方法了。

發生覆蓋的條件:

(1)、「三同一不低」 子類和父類的方法名稱參數列表返回類型必須徹底相同,並且子類方法的訪問修飾符的權限不能比父類

(2)、子類方法不能拋出比父類方法更多的異常。即子類方法所拋出的異常必須和父類方法所拋出的異常一致,或者是其子類,或者什麼也不拋出

(3)、被覆蓋的方法不能是final類型的。由於final修飾的方法是沒法覆蓋的。

(4)、被覆蓋的方法不能爲private。不然在其子類中只是新定義了一個方法,並無對其進行覆蓋。

(5)、被覆蓋的方法不能爲static。因此若是父類中的方法爲靜態的,而子類中的方法不是靜態的,可是兩個方法除了這一點外其餘都知足覆蓋條件,那麼會發生編譯錯誤。反之亦然。即子類實例方法不能覆蓋父類的靜態方法;子類的靜態方法也不能覆蓋父類的實例方法(編譯時報錯),總結爲方法不能交叉覆蓋。即便父類和子類中的方法都是靜態的,而且知足覆蓋條件,可是仍然不會發生覆蓋,由於靜態方法是在編譯的時候把靜態方法和類的引用類型進行匹配。

(6)、父類的抽象方法能夠被子類經過兩種途徑覆蓋(即實現和覆蓋)。

(7)、父類的非抽象方法能夠被覆蓋爲抽象方法。 

方法的覆蓋和重載具備如下相同點:

都要求方法同名

均可以用於抽象方法和非抽象方法之間

方法的覆蓋和重載具備如下不一樣點:

方法覆蓋要求參數列表(參數簽名)必須一致,而方法重載要求參數列表必須不一致。

方法覆蓋要求返回類型必須一致,方法重載對此沒有要求。

方法覆蓋只能用於子類覆蓋父類的方法,方法重載用於同一個類中的全部方法(包括從父類中繼承而來的方法)

方法覆蓋對方法的訪問權限和拋出的異常有特殊的要求,而方法重載在這方面沒有任何限制。

父類的一個方法只能被子類覆蓋一次,而一個方法能夠在全部的類中能夠被重載屢次。

另外,對於屬性(成員變量)而言,是不能重載的,只能覆蓋。

抽象類和普通類的區別

    包含抽象方法的類稱爲抽象類,但並不意味着抽象類中只能有抽象方法,它和普通類同樣,一樣能夠擁有成員變量和普通的成員方法。注意,抽象類和普通類的主要有三點區別:

1)抽象方法必須爲public或者protected(由於若是爲private,則不能被子類繼承,子類便沒法實現該方法),缺省狀況下默認爲public。

2)抽象類不能用來建立對象;

3)若是一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。若是子類沒有實現父類的抽象方法,則必須將子類也定義爲爲abstract類。

2六、Interface 與 abstract 類的區別

(1)、abstract class 在Java中表示的是一種繼承關係,一個類只能使用一次繼承關係。可是,一個類卻能夠實現多個interface。

(2)、在abstract class 中能夠有本身的數據成員,也能夠有非abstarct的方法,而在interface中,只可以有靜態的不能被修改的數據成員(也就是必須是static final的,不過在 interface中通常不定義數據成員),全部的方法都是public abstract的。

(3)、抽象類中的變量默認是 friendly 型,其值能夠在子類中從新定義,也能夠從新賦值。接口中定義的變量默認是public static final 型,且必須給其賦初值,因此實現類中不能從新定義,也不能改變其值。

(4)、abstract class和interface所反映出的設計理念不一樣。其實abstract class表示的是"is-a"關係,interface表示的是"like-a"關係。

(5)、實現抽象類和接口的類必須實現其中的全部方法。抽象類中能夠有非抽象方法。接口中則不能有實現方法。

       abstract class 和 interface 是 Java語言中的兩種定義抽象類的方式,它們之間有很大的類似性。可是對於它們的選擇卻又每每反映出對於問題領域中的概念本質的理解、對於設計意圖的反映是否正確、合理,由於它們表現了概念間的不一樣的關係。

一、語法層面上的區別

1)抽象類能夠提供成員方法的實現細節,而接口中只能存在public abstract 方法;

2)抽象類中的成員變量能夠是各類類型的,而接口中的成員變量只能是public static final類型的;

3)接口中不能含有靜態代碼塊以及靜態方法,而抽象類能夠有靜態代碼塊和靜態方法;

4)一個類只能繼承一個抽象類,而一個類卻能夠實現多個接口。

二、設計層面上的區別

    1)抽象類是對一種事物的抽象,即對類抽象,而接口是對行爲的抽象。抽象類是對整個類總體進行抽象,包括屬性、行爲,可是接口倒是對類局部(行爲)進行抽象。舉個簡單的例子,飛機和鳥是不一樣類的事物,可是它們都有一個共性,就是都會飛。那麼在設計的時候,能夠將飛機設計爲一個類Airplane,將鳥設計爲一個類Bird,可是不能將 飛行 這個特性也設計爲類,所以它只是一個行爲特性,並非對一類事物的抽象描述。此時能夠將 飛行 設計爲一個接口Fly,包含方法fly( ),而後Airplane和Bird分別根據本身的須要實現Fly這個接口。而後至於有不一樣種類的飛機,好比戰鬥機、民用飛機等直接繼承Airplane便可,對於鳥也是相似的,不一樣種類的鳥直接繼承Bird類便可。從這裏能夠看出,繼承是一個 "是否是"的關係,而 接口 實現則是 "有沒有"的關係。若是一個類繼承了某個抽象類,則子類一定是抽象類的種類,而接口實現則是有沒有、具有不具有的關係,好比鳥是否能飛(或者是否具有飛行這個特色),能飛行則能夠實現這個接口,不能飛行就不實現這個接口。

    2)設計層面不一樣,抽象類做爲不少子類的父類,它是一種模板式設計。而接口是一種行爲規範,它是一種輻射式設計。什麼是模板式設計?最簡單例子,你們都用過ppt裏面的模板,若是用模板A設計了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,若是它們的公共部分須要改動,則只須要改動模板A就能夠了,不須要從新對ppt B和ppt C進行改動。而輻射式設計,好比某個電梯都裝了某種報警器,一旦要更新報警器,就必須所有更新。也就是說對於抽象類,若是須要添加新的方法,能夠直接在抽象類中添加具體的實現,子類能夠不進行變動;而對於接口則不行,若是接口進行了變動,則全部實現這個接口的類都必須進行相應的改動

2七、try catch  finally

(1)finally裏面的代碼必定會執行的;

(2)當try和catch中有return時,先執行return中的運算結果可是先不返回,而後保存下來計算結果,接着執行finally,最後再返回return的值。

(3)finally中最好不要有return,不然,直接返回,而先前的return中計算後保存的值得不到返回。

2八、try 裏有return,finally還執行麼?

(1)、無論有木有出現異常,finally塊中代碼都會執行;

(2)、當try和catch中有return時,finally仍然會執行;

(3)、在try語句中,try要把返回的結果放置到不一樣的局部變量當中,執行finaly以後,從中取出返回結果,所以,即便finaly中對變量進行了改變,可是不會影響返回結果,由於使用棧保存返回值,即便在finaly當中進行數值操做,可是影響不到以前保存下來的具體的值,因此return影響不了基本類型的值,這裏使用的棧保存返回值。而若是修改list,map,自定義類等引用類型時,在進入了finaly以前保存了引用的地址, 因此在finaly中引用地址指向的內容改變了,影響了返回值。

總結:

    1.影響返回結果的前提是在非 finally 語句塊中有 return 且非基本類型

    2.不影響返回結果的前提是 非 finally 塊中有return 且爲基本類型

究其本質 基本類型在棧中存儲,返回的是真實的值,而引用類型返回的是其淺拷貝堆地址,因此纔會改變。

    return的如果對象,則先把對象的副本保存起來,也就是說保存的是指向對象的地址。若對原來的對象進行修改,對象的地址仍然不變,return的副本仍然是指向這個對象,因此finally中對對象的修改仍然有做用。而基本數據類型保存的是原本來本的數據,return保存副本後,在finally中修改都是修改原來的數據。副本中的數據仍是不變,因此finally中修改對return無影響。

(4)、finally中最好不要包含return,不然程序會提早退出,返回值不是try或catch中保存的返回值。

2九、String、StringBuffer 與 StringBuilder的區別

    String 類型和StringBuffer的主要性能區別:String是不可變的對象,所以在每次對String 類型進行改變的時候,都會生成一個新的String 對象,而後將指針指向新的String 對象,因此常常改變內容的字符串最好不要用 String ,由於每次生成對象都會對系統性能產生影響,特別當內存中無引用對象多了之後, JVM 的 GC 就會開始工做,性能就會下降。

    使用 StringBuffer 類時,每次都會對 StringBuffer 對象自己進行操做,而不是生成新的對象並改變對象引用。因此多數狀況下推薦使用 StringBuffer ,特別是字符串對象常常改變的狀況下。

    StringBuffer對方法加了同步鎖或者對調用的方法加了同步鎖,因此是線程安全的。StringBuilder並無對方法進行加同步鎖,因此是非線程安全的。

    StringBuilder與StringBuffer有公共父類AbstractStringBuilder(抽象類)。


     Java平臺提供了兩種類型的字符串:String和StringBuffer/StringBuilder,它們能夠儲存和操做字符串。其中String是隻讀字符串,也就意味着String引用的字符串內容是不能被改變的。而StringBuffer/StringBuilder類表示的字符串對象能夠直接進行修改。StringBuilder是Java 5中引入的,它和StringBuffer的方法徹底相同,區別在於它是在單線程環境下使用的,由於它的全部方面都沒有被synchronized修飾,所以它的效率也比StringBuffer要高。

30、不可變對象

     若是一個對象,在它建立完成以後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態的意思是,不能改變對象內的成員變量,包括基本數據類型的值不能改變,引用類型的變量不能指向其餘的對象,引用類型指向的對象的狀態也不能改變。 如何建立不可變類?

(1)將類聲明爲final,因此它不能被繼承。

(2)將全部的成員聲明爲私有的,這樣就不容許直接訪問這些成員。

(3)對變量不要提供setter方法。

(4)將全部可變的成員聲明爲final,這樣只能對它們賦值一次。

(5)經過構造器初始化全部成員,進行深拷貝(deep copy):若是某一個類成員不是原始變量(primitive)或者不可變類,必須經過在成員初始化(in)或者get方法(out)時經過深度clone方法,來確保類的不可變。

(6)在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝。

3一、爲何String 要設計成不可變的?

在Java中將String設計成不可變的是綜合考慮到各類因素的結果。如內存,同步,數據結構以及安全等方面的考慮。

(1)字符串常量池的須要。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字符串。但若是字符串是可變的,那麼String interning將不能實現(譯者注:String interning是指對不一樣的字符串僅僅只保存一個,即不會保存多個相同的字符串。),由於這樣的話,若是變量改變了它的值,那麼其它指向這個值的變量的值也會一塊兒改變。

    字符串常量池是方法區中一塊特殊的存儲區域,當建立一個字符串常量的時候,判斷該字符串在字符串常量池中是否已經存在。若是存在,返回已經存在的字符串的引用;若是不存在,則建立一個新的字符串常量,並返回其引用。

String string1 = "abcd";
String string2 = "abcd";

變量string1,string2指向常量池中的同一個字符串常量對象;若是String是可變的,給一個變量從新賦值一個引用,將會指向錯誤的值。

(2)線程安全考慮。 同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。

   由於不可變的對象不能被改變,他們能夠在多個線程中共享,就不須要使用線程的同步操做。

(3)類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。

(4)支持hash映射和緩存。 由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

    在Java中字符串的哈希值會常常被使用到。例如在HashMap中,String的不可變總能保證哈希值老是相等的,而且緩存起來,不用擔憂會改變,那意味着不須要每次都計算哈希值,這樣會提升效率。

總之,把String設計爲不可變,是爲了提升效率和安全性。在普遍的設計開發中,不可變類是首要選擇。

String類不可變性的好處

(1)只有當字符串是不可變的,字符串池纔有可能實現。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字 符串。但若是字符串是可變的,那麼String interning將不能實現(譯者注:String interning是指對不一樣的字符串僅僅只保存一個,即不會保存多個相同的字符串。),由於這樣的話,若是變量改變了它的值,那麼其它指向這個值的變量 的值也會一塊兒改變。

(2)若是字符串是可變的,那麼會引發很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的連 接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符 串指向的對象的值,形成安全漏洞。

(3)由於字符串是不可變的,因此是多線程安全的,同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。

(4)類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。

(5)由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

3二、內部類能夠引用它的包含類(外部類)的成員嗎?有沒有什麼限制?

答:一個內部類對象能夠訪問建立它的外部類對象的成員,包括私有成員。

3三、靜態嵌套類(Static Nested Class)和內部類(Inner Class)的不一樣?

Static Nested Class是被聲明爲靜態(static)的內部類,它能夠不依賴於外部類實例被實例化。而一般的內部類須要在外部類實例化後才能實例化。

看下面的代碼哪些地方會產生編譯錯誤?

package com.demo;

public class Outer {

    class Inner {}
     
    public static void foo() { new Inner(); }
 
    public void bar() { new Inner(); }
 
    public static void main(String[] args) {
        new Inner();
    }

}

注意:Java中非靜態內部類對象的建立要依賴其外部類對象,上面的面試題中foo和main方法都是靜態方法,靜態方法中沒有this,也就是說沒有所謂的外部類對象,所以沒法建立內部類對象,若是要在靜態方法中建立內部類對象,能夠這樣作:

package com.demo;

public class Outer {

    class Inner {}
     
    public static void foo() { new Outer().new Inner(); }
 
    public void bar() { new Inner(); }
 
    public static void main(String[] args) {
        new Outer().new Inner();
    }

}

3四、錯誤和異常的區別(Error vs Exception)

java.lang.Error:Throwable 的子類,用於標記嚴重錯誤,表示系統級的錯誤和程序沒必要處理的異常。合理的應用程序不該該去 try/catch 這種錯誤。是恢復不是不可能但很困難的狀況下的一種嚴重問題;好比內存溢出,不可能期望程序能處理這樣的狀況; java.lang.Exception:Throwable 的子類,表示須要捕捉或者須要程序進行處理的異常,是一種設計或實現問題;也就是說,它表示若是程序運行正常,從不會發生的狀況。而且鼓勵用戶程序去 catch 它。

3五、IO 和 NIO的主要區別

(1)面向流與面向緩衝。Java IO和NIO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。 Java NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。

(2)阻塞與非阻塞IO。Java IO的各類流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。

(3)選擇器(Selectors)。Java NIO的選擇器容許一個單獨的線程來監視多個輸入通道,你能夠註冊多個通道使用一個選擇器,而後使用一個單獨的線程來「選擇」通道:這些通道里已經有能夠處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。

Java內存模型

    Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。在此以前,主流程序語言(如C/C++等)直接使用物理硬件和操做系統的內存模型,所以,會因爲不一樣平臺上內存模型的差別,有可能致使程序在一套平臺上併發徹底正常,而在另一套平臺上併發訪問卻常常出錯,所以在某些場景下就不準針對不一樣的平臺來編寫程序。

    Java內存模型的主要目的是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。注意一下,此處的變量並不包括局部變量與方法參數,由於它們是線程私有的,不會被共享,天然也不會存在競爭,此處的變量應該是實例字段、靜態字段和構成數組對象的元素。

    Java內存模型中規定了全部的變量都存儲在主內存中(如虛擬機物理內存中的一部分),每條線程還有本身的工做內存(如CPU中的高速緩存),線程的工做內存中保存了該線程使用到的變量到主內存的副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成,線程、主內存和工做內存的交互關係以下圖所示:

2、Java集合

3六、ArrayList、LinkedList、Vector的區別

     ArrayList,Vector底層是由數組實現,LinkedList底層是由雙向鏈表實現,從底層的實現能夠得出它們的性能問題,ArrayList,Vector插入速度相對較慢,查詢速度相對較快,而LinkedList插入速度較快,而查詢速度較慢。再者因爲Vevtor使用了線程安全鎖,因此ArrayList的運行效率高於Vector。

3七、Map、Set、List、Queue、Stack的特色與用法

Collection            接口的接口   對象的集合

├ List                  子接口      按進入前後有序保存   可重複

│├ LinkedList      接口實現類   鏈表   插入刪除   沒有同步   線程不安全

│├ ArrayList        接口實現類   數組   隨機訪問   沒有同步   線程不安全

│└ Vector            接口實現類   數組     同步    線程安全

│   └ Stack

 

└ Set               子接口     僅接收一次,並作內部排序

  ├ HashSet

  │   └ LinkedHashSet

  └ TreeSet

    對於List ,關心的是順序,它保證維護元素特定的順序(容許有相同元素),使用此接口可以精確的控制每一個元素插入的位置。用戶可以使用索引(元素在 List 中的位置,相似於數組下標)來訪問 List 中的元素。

    對於 Set ,只關心某元素是否屬於 Set (不容許有相同元素 ),而不關心它的順序。

Map                    接口      鍵值對的集合

├ Hashtable       接口實現類       同步           線程安全

├ HashMap         接口實現類      沒有同步    線程不安全

│├ LinkedHashMap

│└ WeakHashMap

├ TreeMap

└ IdentifyHashMap

     對於 Map ,最大的特色是鍵值映射,且爲一一映射,鍵不能重複,值能夠,因此是用鍵來索引值。方法 put(Object key, Object value) 添加一個「值」 ( 想要得東西 ) 和與「值」相關聯的「鍵」 (key) ( 使用它來查找 ) 。方法 get(Object key) 返回與給定「鍵」相關聯的「值」。

    Map 一樣對每一個元素保存一份,但這是基於 " 鍵 " 的, Map 也有內置的排序,於是不關心元素添加的順序。若是添加元素的順序對你很重要,應該使用 LinkedHashSet 或者 LinkedHashMap.

    對於效率, Map 因爲採用了哈希散列,查找元素時明顯比 ArrayList 快。

更爲精煉的總結:

    Collection 是對象集合, Collection 有兩個子接口 List 和 Set。List 能夠經過下標 (1,2..) 來取得值,值能夠重複。而 Set 只能經過遊標來取值,而且值是不能重複的。ArrayList , Vector , LinkedList 是 List 的實現類。ArrayList 是線程不安全的, Vector 是線程安全的,這兩個類底層都是由數組實現的。LinkedList 是線程不安全的,底層是由鏈表實現的。  

    Map 是鍵值對集合。HashTable 和 HashMap 是 Map 的實現類。HashTable 是線程安全的,不能存儲 null 值。HashMap 不是線程安全的,能夠存儲 null 值。 

    Stack類:繼承自Vector,實現一個後進先出的棧。提供了幾個基本方法,push、pop、peak、empty、search等。

    Queue接口:提供了幾個基本方法,offer、poll、peek等。已知實現類有LinkedList、PriorityQueue等。

3八、HashMap 和 HashTable的區別

    HashMap和Hashtable都實現了Map接口,但決定用哪個以前先要弄清楚它們之間的分別。主要的區別有:線程安全性,同步(synchronization),以及速度。

    HashMap幾乎能夠等價於Hashtable,除了HashMap是非synchronized的,並能夠接受null(HashMap能夠接受爲null的鍵值(key)和值(value),而Hashtable則不行)。

    HashMap是非synchronized,而Hashtable是synchronized,這意味着Hashtable是線程安全的,多個線程能夠共享一個Hashtable;而若是沒有正確的同步的話,多個線程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。

    另外一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。因此當有其它線程改變了HashMap的結構(增長或者移除元素),將會拋出ConcurrentModificationException,但迭代器自己的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並非一個必定發生的行爲,要看JVM。這條一樣也是Enumeration和Iterator的區別。

    因爲Hashtable是線程安全的也是synchronized,因此在單線程環境下它比HashMap要慢。若是你不須要同步,只須要單一線程,那麼使用HashMap性能要好過Hashtable。

    HashMap不能保證隨着時間的推移Map中的元素次序是不變的。

3九、HashMap的工做原理

1)存儲:

當程序試圖將多個 key-value 放入 HashMap 中時,以以下代碼片斷爲例:

HashMap<String , Double> map = new HashMap<String , Double>();
map.put("語文" , 80.0);
map.put("數學" , 89.0);
map.put("英語" , 78.2); 

    HashMap 採用一種所謂的「Hash 算法」來決定每一個元素的存儲位置。

    當程序執行 map.put("語文" , 80.0); 時,系統將調用"語文"的 hashCode() 方法獲得其 hashCode 值——每一個 Java 對象都有 hashCode() 方法,均可經過該方法得到它的 hashCode 值。獲得這個對象的 hashCode 值以後,系統會根據該 hashCode 值來決定該元素的存儲位置。咱們能夠看 HashMap 類的 put(K key , V value) 方法的源代碼:

public V put(K key, V value) {
    // HashMap容許存放null鍵和null值。
    // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。
    if (key == null)
        return putForNullKey(value);
    // 根據key的keyCode從新計算hash值。
    int hash = hash(key.hashCode());
    // 搜索指定hash值在對應table中的索引。
    int i = indexFor(hash, table.length);
    // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素。
    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;
        }
    }
    // 若是i索引處的Entry爲null,代表此處尚未Entry。
    modCount++;
    // 將key、value添加到i索引處。
    addEntry(hash, key, value, i);
    return null;
}

    從上面的源代碼中能夠看出:當咱們往HashMap中put元素的時候,先根據key的hashCode從新計算hash值,根據hash值獲得這個元素在數組中的位置(即下標),若是數組該位置上已經存放有其餘元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最早加入的放在鏈尾。若是數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

   上面程序中用到了一個重要的內部接口:Map.Entry,每一個 Map.Entry 其實就是一個 key-value 對。從上面程序中能夠看出:當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。咱們徹底能夠把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置以後,value 隨之保存在那裏便可。

   上面方法提供了一個根據 hashCode() 返回值來計算 Hash 碼的方法:hash(),這個方法是一個純粹的數學計算,其方法以下:

static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

    對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 Hash 碼值老是相同的。接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:

static int indexFor(int h, int length) {
    return h & (length-1);
}

    這個方法很是巧妙,它老是經過 h &(table.length -1) 來獲得該對象的保存位置——而 HashMap 底層數組的長度老是 2 的 n 次方,這一點可參看後面關於 HashMap 構造器的介紹。

    當 length 老是 2 的倍數時,h & (length-1) 將是一個很是巧妙的設計:假設 h=5,length=16, 那麼 h & length - 1 將獲得 5;若是 h=6,length=16, 那麼 h & length - 1 將獲得 6 ……若是 h=15,length=16, 那麼 h & length - 1 將獲得 15;可是當 h=16 ,length=16 時,那麼 h & length - 1 將獲得 0 了;當 h=17 ,length=16 時,那麼 h & length - 1 將獲得 1 了……這樣保證計算獲得的索引值老是位於 table 數組的索引以內。

    根據上面 put 方法的源代碼能夠看出,當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

    當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 經過 eqauls() 比較值決定是採用覆蓋行爲(返回 true),仍是產生 Entry 鏈(返回 false)。

    上面程序中還調用了 addEntry(hash, key, value, i); 代碼,其中 addEntry 是 HashMap 提供的一個包訪問權限的方法,該方法僅用於添加一個 key-value 對。下面是該方法的代碼:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 獲取指定 bucketIndex 索引處的 Entry 
    Entry<K,V> e = table[bucketIndex];  //// 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    // 若是 Map 中的 key-value 對的數量超過了極限
    if (size++ >= threshold)
    // 把 table 對象的長度擴充到原來的2倍。
        resize(2 * table.length);   //
}

    上面方法的代碼很簡單,但其中包含了一個很是優雅的設計:系統老是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——若是 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),若是 bucketIndex 索引處沒有 Entry 對象,也就是上面程序①號代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。

2)讀取:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))
            return e.value;
    }
    return null;
}

    有了上面存儲時的hash算法做爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中能夠看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,而後經過key的equals方法在對應位置的鏈表中找到須要的元素。

3) 概括起來簡單地說,HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當須要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

40、List、Set、Map是否繼承自Collection接口?

答:List、Set 是,Map 不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不容許有重複元素(數學中的集合也是如此),List是線性結構的容器,適用於按數值索引訪問元素的情形。

4一、Collection和Collections的區別?

答:Collection是一個接口,它是Set、List等容器的父接口;Collections是一個工具類,提供了一系列的靜態方法來輔助容器操做,這些方法包括對容器的搜索、排序、線程安全化等等。

4二、List、Map、Set三個接口存取元素時,各有什麼特色?

答:List以特定索引來存取元素,能夠有重複元素。Set不能存放重複元素(用對象的equals()方法來區分元素是否重複)。Map保存鍵值對(key-value pair)映射,映射關係能夠是一對一或多對一。Set和Map容器都有基於哈希存儲和排序樹的兩種實現版本,基於哈希存儲的版本理論存取時間複雜度爲O(1),而基於排序樹版本的實如今插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

4三、HashMap、HashTable、LinkedHashMap、TreeMap的區別

   Map主要用於存儲鍵值對,根據鍵獲得值,所以不容許鍵重複(重複了覆蓋了),但容許值重複。

   Hashmap 是一個最經常使用的Map,它根據鍵的HashCode 值存儲數據,根據鍵能夠直接獲取它的值,具備很快的訪問速度,遍歷時,取得數據的順序是徹底隨機的。HashMap最多隻容許一條記錄的鍵爲Null,容許多條記錄的值爲 Null。HashMap不支持線程的同步,即任一時刻能夠有多個線程同時寫HashMap,可能會致使數據的不一致。若是須要同步,能夠用 Collections的synchronizedMap方法使HashMap具備同步的能力,或者使用ConcurrentHashMap。

   Hashtable與 HashMap相似,它繼承自Dictionary類,不一樣的是:它不容許記錄的鍵或者值爲空,它支持線程的同步,即任一時刻只有一個線程能寫Hashtable,所以也致使了 Hashtable在寫入時會比較慢。

   LinkedHashMap保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先獲得的記錄確定是先插入的。也能夠在構造時用帶參數,按照應用次數排序。在遍歷的時候會比HashMap慢,不過有種狀況例外,當HashMap容量很大,實際數據較少時,遍歷起來可能會比LinkedHashMap慢,由於LinkedHashMap的遍歷速度只和實際數據有關,和容量無關,而HashMap的遍歷速度和他的容量有關。

   TreeMap實現SortMap接口,可以把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也能夠指定排序的比較器,當用Iterator 遍歷TreeMap時,獲得的記錄是排過序的。

   通常狀況下,咱們用的最多的是HashMap,HashMap裏面存入的鍵值對在取出的時候是隨機的,它根據鍵的HashCode值存儲數據,根據鍵能夠直接獲取它的值,具備很快的訪問速度。在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。TreeMap取出來的是排序後的鍵值對。但若是您要按天然順序或自定義順序遍歷鍵,那麼TreeMap會更好。LinkedHashMap 是HashMap的一個子類,若是須要輸出的順序和輸入的相同,那麼用LinkedHashMap能夠實現,它還能夠按讀取順序來排列,像鏈接池中能夠應用。

3、Spring相關

4四、IOC(Inversion of Control)的理解

 (1)、IoC(Inversion of Control)是指容器控制程序對象之間的關係,而不是傳統實現中,由程序代碼直接操控。控制權由應用代碼中轉到了外部容器,控制權的轉移是所謂反轉。 對於Spring而言,就是由Spring來控制對象的生命週期和對象之間的關係;IoC還有另一個名字——「依賴注入(Dependency Injection)」。從名字上理解,所謂依賴注入,即組件之間的依賴關係由容器在運行期決定,即由容器動態地將某種依賴關係注入到組件之中。  

(2)、在Spring的工做方式中,全部的類都會在spring容器中登記,告訴spring這是個什麼東西,你須要什麼東西,而後spring會在系統運行到適當的時候,把你要的東西主動給你,同時也把你交給其餘須要你的東西。全部的類的建立、銷燬都由 spring來控制,也就是說控制對象生存週期的再也不是引用它的對象,而是spring。對於某個具體的對象而言,之前是它控制其餘對象,如今是全部對象都被spring控制,因此這叫控制反轉。

(3)、在系統運行中,動態的向某個對象提供它所須要的其餘對象。  

(4)、依賴注入的思想是經過反射機制實現的,在實例化一個類時,它經過反射調用類中set方法將事先保存在HashMap中的類屬性注入到類中。 總而言之,在傳統的對象建立方式中,一般由調用者來建立被調用者的實例,而在Spring中建立被調用者的工做由Spring來完成,而後注入調用者,即所謂的依賴注入or控制反轉。 注入方式有兩種:依賴注入和設置注入; IoC的優勢:下降了組件之間的耦合,下降了業務對象之間替換的複雜性,使之可以靈活的管理對象。

IOC的實現原理

Spring中的IOC的實現原理就是工廠模式加反射機制。 咱們首先看一下不用反射機制時的工廠模式:

interface fruit{  
    public abstract void eat();  
}   
class Apple implements fruit{  
     public void eat(){  
         System.out.println("Apple");  
     }  
}   
class Orange implements fruit{  
     public void eat(){  
         System.out.println("Orange");  
     }  
}  
//構造工廠類  
//也就是說之後若是咱們在添加其餘的實例的時候只須要修改工廠類就好了  
class Factory{  
     public static fruit getInstance(String fruitName){  
         fruit f=null;  
         if("Apple".equals(fruitName)){  
             f=new Apple();  
         }  
         if("Orange".equals(fruitName)){  
             f=new Orange();  
         }  
         return f;  
     }  
}  
class hello{  
     public static void main(String[] a){  
         fruit f=Factory.getInstance("Orange");  
         f.eat();  
     }  
}  

上面寫法的缺點是當咱們再添加一個子類的時候,就須要修改工廠類了。若是咱們添加太多的子類的時候,改動就會不少。下面用反射機制實現工廠模式:

interface fruit{  
     public abstract void eat();  
}  
class Apple implements fruit{  
public void eat(){  
         System.out.println("Apple");  
     }  
}  
class Orange implements fruit{  
public void eat(){  
        System.out.println("Orange");  
    }  
}  
class Factory{  
    public static fruit getInstance(String ClassName){  
        fruit f=null;  
        try{  
            f=(fruit)Class.forName(ClassName).newInstance();  
        }catch (Exception e) {  
            e.printStackTrace();  
        }  
        return f;  
    }  
}  
class hello{  
    public static void main(String[] a){  
        fruit f=Factory.getInstance("Reflect.Apple");  
        if(f!=null){  
            f.eat();  
        }  
    }  
}  

     如今就算咱們添加任意多個子類的時候,工廠類都不須要修改。使用反射機制實現的工廠模式能夠經過反射取得接口的實例,可是須要傳入完整的包和類名。並且用戶也沒法知道一個接口有多少個能夠使用的子類,因此咱們經過屬性文件的形式配置所須要的子類。

     下面編寫使用反射機制並結合屬性文件的工廠模式(即IoC)。首先建立一個fruit.properties的資源文件:

apple=Reflect.Apple  
orange=Reflect.Orange  

 而後編寫主類代碼:

interface fruit{  
    public abstract void eat();  
}  
class Apple implements fruit{  
    public void eat(){  
        System.out.println("Apple");  
    }  
}  
class Orange implements fruit{  
    public void eat(){  
        System.out.println("Orange");  
    }  
}  
//操做屬性文件類  
class init{  
    public static Properties getPro() throws FileNotFoundException, IOException{  
        Properties pro=new Properties();  
        File f=new File("fruit.properties");  
        if(f.exists()){  
            pro.load(new FileInputStream(f));  
        }else{  
            pro.setProperty("apple", "Reflect.Apple");  
            pro.setProperty("orange", "Reflect.Orange");  
            pro.store(new FileOutputStream(f), "FRUIT CLASS");  
        }  
        return pro;  
    }  
}  
class Factory{  
    public static fruit getInstance(String ClassName){  
        fruit f=null;  
        try{  
            f=(fruit)Class.forName(ClassName).newInstance();  
        }catch (Exception e) {  
            e.printStackTrace();  
        }  
        return f;  
    }  
}  
class hello{  
    public static void main(String[] a) throws FileNotFoundException, IOException{  
        Properties pro=init.getPro();  
        fruit f=Factory.getInstance(pro.getProperty("apple"));  
        if(f!=null){  
            f.eat();  
        }  
    }  
}  

運行結果:Apple

IOC容器的技術剖析

    IOC中最基本的技術就是「反射(Reflection)」編程,通俗來說就是根據給出的類名(字符串方式)來動態地生成對象,這種編程方式可讓對象在生成時才被決定究竟是哪種對象。只是在Spring中要生產的對象都在配置文件中給出定義,目的就是提升靈活性和可維護性。

    目前C#、Java和PHP5等語言均支持反射,其中PHP5的技術書籍中,有時候也被翻譯成「映射」。有關反射的概念和用法,你們應該都很清楚。反射的應用是很普遍的,不少的成熟的框架,好比像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把」反射「作爲最基本的技術手段。

    反射技術其實很早就出現了,但一直被忽略,沒有被進一步的利用。當時的反射編程方式相對於正常的對象生成方式要慢至少得10倍。如今的反射技術通過改良優化,已經很是成熟,反射方式生成對象和一般對象生成方式,速度已經相差不大了,大約爲1-2倍的差距。

    咱們能夠把IOC容器的工做模式看作是工廠模式的昇華,能夠把IOC容器看做是一個工廠,這個工廠裏要生產的對象都在配置文件中給出定義,而後利用編程語言提供的反射機制,根據配置文件中給出的類名生成相應的對象。從實現來看,IOC是把之前在工廠方法裏寫死的對象生成代碼,改變爲由配置文件來定義,也就是把工廠和對象生成這二者獨立分隔開來,目的就是提升靈活性和可維護性。

IOC底層實現原理

底層實現使用的技術:

(1)xml配置文件

(2)dom4j解析xml

(3)工廠模式

(4)反射

 

    首先,經過dom4j將咱們的配置文件讀取,這時咱們就能夠解析到全部相關的類的全路徑了。而後,它再利用反射機制經過以下代碼完成類的實例化:類1=Class.forName("類1的全路徑")。這時,咱們就獲得了類1。(這也是爲啥當咱們的類的全路徑寫錯了會致使出現classNotfind的錯誤。)

    當咱們獲得了類1之後,經過調用類1的set方法,將屬性給對象進行注入。並且,須要遵循首字母大寫的set規範。例如:咱們的類中有個字段的屬性爲name那麼set方法必須寫成setName(name 的首字母要大寫)不然會報一個屬性找不到的錯誤:

public void setName(String name){ 
     this.name= name; 
} 

    對象建立後,咱們將咱們的對象id和咱們的對象物理地址,一塊兒存入相似於HashMap的容器中,而後呢,咱們是如何得到咱們須要的對象,而後執行對象中的方法呢?咱們經過getBean的方法,經過對象Id得到對象的物理地址,獲得對象,而後調用對象的方法,完成對方法的調用。

4五、AOP(Aspect Oriented Programming)的理解

    AOP(Aspect-OrientedProgramming,面向方面編程),能夠說是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。提及AOP就不得不說下OOP了,OOP中引入封裝、繼承和多態性等概念來創建一種對象層次結構,用以模擬公共行爲的一個集合。可是,若是咱們須要爲部分對象引入公共部分的時候,OOP就會引入大量重複的代碼。例如:日誌功能。

  AOP技術利用一種稱爲「橫切」的技術,解剖封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,這樣就能減小系統的重複代碼,下降模塊間的耦合度,並有利於將來的可操做性和可維護性。AOP把軟件系統分爲兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特色是,他們常常發生在覈心關注點的多處,而各處都基本類似。好比權限認證、日誌、事務處理。Aop 的做用在於分離系統中的各類關注點,將核心關注點和橫切關注點分離開來。

    實現AOP的技術,主要分爲兩大類:一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;二是採用靜態織入的方式,引入特定的語法建立「方面」,從而使得編譯器能夠在編譯期間織入有關「方面」的代碼。

    Spring實現AOP:JDK動態代理和CGLIB代理。JDK動態代理:其代理對象必須是某個接口的實現,它是經過在運行期間建立一個接口的實現類來完成對目標對象的代理;其核心的兩個類是InvocationHandler和Proxy。 CGLIB代理:實現原理相似於JDK動態代理,只是它在運行期間生成的代理對象是針對目標類擴展的子類。CGLIB是高效的代碼生成包,底層是依靠ASM(開源的java字節碼編輯類庫)操做字節碼實現的,性能比JDK強;須要引入包asm.jar和cglib.jar。 使用AspectJ注入式切面和@AspectJ註解驅動的切面實際上底層也是經過動態代理實現的。

AOP使用場景:                     

Authentication 權限檢查        

Caching 緩存        

Context passing 內容傳遞        

Error handling 錯誤處理        

Lazy loading 延遲加載        

Debugging  調試      

logging, tracing, profiling and monitoring 日誌記錄,跟蹤,優化,校準        

Performance optimization 性能優化,效率檢查        

Persistence  持久化        

Resource pooling 資源池        

Synchronization 同步        

Transactions 事務管理    

另外Filter的實現和struts2的攔截器的實現都是AOP思想的體現。

AOP的實現原理

AOP的實現關鍵在於AOP框架自動建立的AOP代理。AOP代理主要分爲兩大類:

靜態代理:使用AOP框架提供的命令進行編譯,從而在編譯階段就能夠生成AOP代理類,所以也稱爲編譯時加強;靜態代理一Aspectj爲表明。

動態代理:在運行時藉助於JDK動態代理,CGLIB等在內存中臨時生成AOP動態代理類,所以也被稱爲運行時加強,Spring AOP用的就是動態代理。

    AOP分爲靜態AOP和動態AOP。靜態AOP是指AspectJ實現的AOP,他是將切面代碼直接編譯到Java類文件中。動態AOP是指將切面代碼進行動態織入實現的AOP。AspectJ 採用編譯時生成 AOP 代理類,所以具備更好的性能,但須要使用特定的編譯器進行處理;而 Spring AOP 則採用運行時生成 AOP 代理類,所以無需使用特定編譯器進行處理。因爲 Spring AOP 須要在每次運行時生成 AOP 代理,所以性能略差一些。Spring的AOP爲動態AOP,實現的技術爲:JDK提供的動態代理技術  CGLIB(動態字節碼加強技術)儘管實現技術不同,但都是基於代理模式,都是生成一個代理對象。

JDK動態代理

(1)JDK動態代理是面向接口的,必須提供一個委託類和代理類都要實現的接口,只有接口中的方法纔可以被代理。

(2)JDK動態代理的實現主要使用java.lang.reflect包裏的Proxy類和InvocationHandler接口。

InvocationHandler接口:

說明:每個動態代理類都必需要實現InvocationHandler這個接口,而且每一個代理類的實例都關聯到了一個handler,當咱們經過代理對象調用一個方法的時候,這個方法的調用就會被轉發爲由InvocationHandler這個接口的 invoke 方法來進行調用。同時在invoke的方法裏 咱們能夠對被代理對象的方法調用作加強處理(如添加事務、日誌、權限驗證等操做)。

Proxy類: 

Proxy類是專門完成代理的操做類,能夠經過此類爲一個或多個接口動態地生成實現類。

示例:

定義一個業務接口IUserService,以下:

package com.spring.aop;

public interface IUserService {
    //添加用戶
    public void addUser();
    //刪除用戶
    public void deleteUser();
}

一個簡單的實現類UserServiceImpl,以下:

package com.spring.aop;

public class UserServiceImpl implements IUserService{
    
    public void addUser(){
        System.out.println("新增了一個用戶!");
    }
    
    public void deleteUser(){
        System.out.println("刪除了一個用戶!");
    }
}

    如今咱們要實現的是,在addUser和deleteUser以前和以後分別動態植入處理。JDK動態代理主要用到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。InvocationHandler是一個接口,經過實現該接口定義橫切邏輯,並經過反射機制調用目標類的代碼,動態的將橫切邏輯和業務邏輯編織在一塊兒。Proxy利用InvocationHandler動態建立一個符合某一接口的實例,生成目標類的代理對象。以下,咱們建立一個InvocationHandler實例DynamicProxy:(當執行動態代理對象裏的目標方法時,實際上會替換成調用DynamicProxy的invoke方法)

package com.spring.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicProxy implements InvocationHandler{
    
    //被代理對象(就是要給這個目標類建立代理對象)
    private Object target;
    
    //傳遞代理目標的實例,由於代理處理器須要,也能夠用set等方法。
    public DynamicProxy(Object target){
        this.target=target;
    }
    
    /**
     * 覆蓋java.lang.reflect.InvocationHandler的方法invoke()進行織入(加強)的操做。
     * 這個方法是給代理對象調用的,留心的是內部的method調用的對象是目標對象,可別寫錯。
     * 參數說明:
     * proxy是生成的代理對象,method是代理的方法,args是方法接收的參數
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
        //目標方法以前執行
        System.out.println("do sth Before...");
        //經過反射機制來調用目標類方法
        Object result = method.invoke(target, args);
        //目標方法以後執行
        System.out.println("do sth After...\n");
        return result;
    }
}

下面是測試:

package com.spring.aop;

//用java.lang.reflect.Proxy.newProxyInstance()方法建立動態實例來調用代理實例的方法
import java.lang.reflect.Proxy;

public class DynamicTest {
    
    public static void main(String[] args){
        //但願被代理的目標業務類
        IUserService target = new UserServiceImpl();
        //將目標類和橫切類編織在一塊兒
        DynamicProxy handler= new DynamicProxy(target);
        //建立代理實例,它能夠看做是要代理的目標業務類的加多了橫切代碼(方法)的一個子類
        //建立代理實例(使用Proxy類和自定義的調用處理邏輯(handler)來生成一個代理對象)
        IUserService proxy = (IUserService)Proxy.newProxyInstance(
                target.getClass().getClassLoader(),//目標類的類加載器
                target.getClass().getInterfaces(), //目標類的接口
                handler); //橫切類
        proxy.addUser();
        proxy.deleteUser();
    }
}

   說明:上面的代碼完成業務類代碼和橫切代碼的編制工做,並生成了代理實例,newProxyInstance方法的第一個參數爲類加載器,第二個參數爲目標類所實現的一組接口,第三個參數是整合了業務邏輯和橫切邏輯的編織器對象。

   每個動態代理實例的調用都要經過InvocationHandler接口的handler(調用處理器)來調用,動態代理不作任何執行操做,只是在建立動態代理時,把要實現的接口和handler關聯,動態代理要幫助被代理執行的任務,要轉交給handler來執行。其實就是調用invoke方法。(能夠看到執行代理實例的addUser()和deleteUser()方法時執行的是DynamicProxy的invoke()方法。)

運行結果:

基本流程:用Proxy類建立目標類的動態代理,建立時須要指定一個本身實現InvocationHandler接口的回調類的對象,這個回調類中有一個invoke()用於攔截對目標類各個方法的調用。建立好代理後就能夠直接在代理上調用目標對象的各個方法。

實現動態代理步驟:
A.建立一個實現接口InvocationHandler的類,他必須實現invoke方法。
B.建立被代理的類以及接口。
C.經過Proxy的靜態方法newProxyInstance(ClassLoader loader, Class<?>[]interfaces, InvocationHandler handler)建立一個代理。
D.經過代理調用方法。

使用JDK動態代理有一個很大的限制,就是它要求目標類必須實現了對應方法的接口,它只能爲接口建立代理實例。咱們在上文測試類中的Proxy的newProxyInstance方法中能夠看到,該方法第二個參數即是目標類的接口。若是該類沒有實現接口,這就要靠cglib動態代理了。

CGLIB動態代理

CGLib採用很是底層的字節碼技術,能夠爲一個類建立一個子類,並在子類中採用方法攔截的技術攔截全部父類方法的調用,並順勢植入橫切邏輯。

字節碼生成技術實現AOP,其實就是繼承被代理對象,而後Override須要被代理的方法,在覆蓋該方法時,天然是能夠插入咱們本身的代碼的。由於須要Override被代理對象的方法,因此天然用CGLIB技術實現AOP時,就必需要求須要被代理的方法不能是final方法,由於final方法不能被子類覆蓋

a.使用CGLIB動態代理不要求必須有接口,生成的代理對象是目標對象的子類對象,因此須要代理的方法不能是private或者final或者static的。
b.使用CGLIB動態代理須要有對cglib的jar包依賴(導入asm.jar和cglib-nodep-2.1_3.jar)

CGLibProxy與JDKProxy的代理機制基本相似,只是其動態代理的代理對象並不是某個接口的實現,而是針對目標類擴展的子類。換句話說JDKProxy返回動態代理類,是目標類所實現接口的另外一個實現版本,它實現了對目標類的代理(如同UserDAOProxy與UserDAOImp的關係),而CGLibProxy返回的動態代理類,則是目標代理類的一個子類(代理類擴展了UserDaoImpl類)

cglib 代理特色:
CGLIB 是針對類來實現代理,它的原理是對指定的目標類生成一個子類,並覆蓋其中方法。由於採用的是繼承,因此不能對 finall 類進行繼承

咱們使用CGLIB實現上面的例子:

代理的最終操做類:

package com.spring.aop;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class CglibProxy implements MethodInterceptor{
    
    //加強器,動態代碼生成器
    Enhancer enhancer = new Enhancer();
    
    /**
     * 建立代理對象
     * @param clazz
     * @return 返回代理對象
     */
    public Object getProxy(Class clazz){
        //設置父類,也就是被代理的類(目標類)
        enhancer.setSuperclass(clazz);
        //設置回調(在調用父類方法時,回調this.intercept())
        enhancer.setCallback(this);
        //經過字節碼技術動態建立子類實例(動態擴展了UserServiceImpl類)
        return enhancer.create();
    }
    
    /**
     * 攔截方法:在代理實例上攔截並處理目標方法的調用,返回結果
     * obj:目標對象代理的實例;
     * method:目標對象調用父類方法的method實例;
     * args:調用父類方法傳遞參數;
     * proxy:代理的方法去調用目標方法
     */
    public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) 
        throws Throwable{
        
        System.out.println("--------測試intercept方法的四個參數的含義-----------");
        System.out.println("obj:"+obj.getClass());
        System.out.println("method:"+method.getName());
        System.out.println("proxy:"+proxy.getSuperName());
        if(args!=null&&args.length>0){
            for(Object value : args){
                System.out.println("args:"+value);
            }
        }

        //目標方法以前執行
        System.out.println("do sth Before...");
        //目標方法調用
        //經過代理類實例調用父類的方法,便是目標業務類方法的調用
        Object result = proxy.invokeSuper(obj, args);
        //目標方法以後執行
        System.out.println("do sth After...\n");
        return result;
    }
}

測試類:

package com.spring.aop;

public class CglibProxyTest {
    
    public static void main(String[] args){
        CglibProxy proxy=new CglibProxy();
        //經過java.lang.reflect.Proxy的getProxy()動態生成目標業務類的子類,便是代理類,再由此獲得代理實例
        //經過動態生成子類的方式建立代理類
        IUserService target=(IUserService)proxy.getProxy(UserServiceImpl.class);
        target.addUser();
        target.deleteUser();
    }
}

基本流程:須要本身寫代理類,它實現MethodInterceptor接口,有一個intercept()回調方法用於攔截對目標方法的調用,裏面使用methodProxy來調用目標方法。建立代理對象要用Enhance類,用它設置好代理的目標類、由intercept()回調的代理類實例、最後用create()建立並返回代理實例。

運行結果:

咱們看到達到了一樣的效果。它的原理是生成一個父類enhancer.setSuperclass(clazz)的子類enhancer.create(),而後對父類的方法進行攔截enhancer.setCallback(this). 對父類的方法進行覆蓋,因此父類方法不能是final的。

JDK動態代理和CGLIB字節碼生成的區別:

(1)JDK動態代理只能對實現了接口的類生成代理,而不能針對類。CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法。由於是繼承,因此該類或方法最好不要聲明成final。

(2)JDK代理是不須要依賴第三方的庫,只要JDK環境就能夠進行代理,它有幾個要求

* 實現InvocationHandler;

* 使用Proxy.newProxyInstance產生代理對象;

* 被代理的對象必需要實現接口。

CGLib 必須依賴於CGLib的類庫,可是它須要類來實現任何接口代理的是指定的類生成一個子類,覆蓋其中的方法,是一種繼承。

(3)jdk的核心是實現InvocationHandler接口,使用invoke()方法進行面向切面的處理,調用相應的通知。cglib的核心是實現MethodInterceptor接口,使用intercept()方法進行面向切面的處理,調用相應的通知。

(4)若是目標對象實現了接口,默認狀況下會採用JDK的動態代理實現AOP。 若是就是單純的用IOC生成一個對象,也沒有AOP的切入不會生成代理的,只會NEW一個實例,給Spring的Bean工廠。若是目標對象實現了接口,也能夠強制使用CGLIB實現AOP。若是目標對象沒有實現了接口,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換(沒有實現接口的就用CGLIB代理,使用了接口的類就用JDK動態代理

總結:

    Spring默認使用 JDK 動態代理做爲AOP的代理,缺陷是目標類的類必須實現接口,不然不能使用JDK動態代理。若是須要代理的是類而不是接口,那麼Spring會默認使用CGLIB代理,關於二者的區別:jdk動態代理是經過java的反射機制來實現的,目標類必需要實現接口,cglib是針對類來實現代理的,他的原理是動態的爲指定的目標類生成一個子類,並覆蓋其中方法實現加強,但由於採用的是繼承,因此不能對final修飾的類進行代理。

JDK動態代理:

    代理類與委託類實現同一接口,主要是經過代理類實現InvocationHandler並重寫invoke方法來進行動態代理的,在invoke方法中將對方法進行加強處理  優勢:不須要硬編碼接口,代碼複用率高,缺點:只可以代理實現了接口的委託類 

CGLIB動態代理:

    代理類將委託類做爲本身的父類併爲其中的非final委託方法建立兩個方法,一個是與委託方法簽名相同的方法,它在方法中會經過super調用委託方法;另外一個是代理類獨有的方法。在代理方法中,它會判斷是否存在實現了MethodInterceptor接口的對象,若存在則將調用intercept方法對委託方法進行代理  優勢:能夠在運行時對類或者是接口進行加強操做,且委託類無需實現接口,缺點:不能對final類以及final方法進行代理

AOP的實現方式有哪幾種?如何選擇?

JDK 動態代理實現和 cglib 實現。

選擇:

(1)若是目標對象實現了接口,默認狀況下會採用 JDK 的動態代理實現 AOP,也能夠強制使用 cglib 實現 AOP;

(2)若是目標對象沒有實現接口,必須採用 cglib 庫,Spring 會自動在 JDK 動態代理和 cglib 之間轉換。

SpringAOP 的具體加載步驟

(1)當 spring 容器啓動的時候,加載了 spring 的配置文件。

(2)爲配置文件中的全部 bean 建立對象。

(3)spring 容器會解析 aop:config 的配置

    解析切入點表達式,用切入點表達式和歸入 spring 容器中的 bean 作匹配。若是匹配成功,則會爲該 bean 建立代理對象,代理對象的方法=目標方法+通知,若是匹配不成功,不會建立代理對象。

(4)在客戶端利用 context.getBean() 獲取對象時,若是該對象有代理對象,則返回代理對象;若是沒有,則返回目標對象。

說明:若是目標類沒有實現接口,則 spring 容器會採用 cglib 的方式產生代理對象,若是實現了接口,則會採用 jdk 的方式

JDK動態代理如何實現?

    JDK 動態代理,只能對實現了接口的類生成代理,而不是針對類,該目標類型實現的接口都將被代理。原理是經過在運行期間建立一個接口的實現類來完成對目標對象的代理。

(1)定義一個實現接口 InvocationHandler 的類;

(2)經過構造函數,注入被代理類;

(3)實現 invoke( Object proxy, Method method, Object[] args)方法;

(4)在主函數中得到被代理類的類加載器;

(5)使用 Proxy.newProxyInstance( ) 產生一個代理對象;

(6)經過代理對象調用各類方法。

4六、BeanFactroy 與 ApplicationContext 的區別

    BeanFactory是Spring框架最核心的接口,它提供了高級IOC的配置機制。ApplicationContext創建在BeanFactory基礎之上,提供了更多面嚮應用的功能,它提供了國際化支持和框架事件體系,更易於建立實際應用。通常稱BeanFactory爲IOC容器,而稱ApplicationContext爲應用上下文。

    BeanFactory是一個類工廠,能夠建立並管理各類類的對象,Spring稱這些建立和管理的Java對象爲Bean。ApplicationContext由BeanFactory派生而來,提供了更多面向實際應用的功能。在BeanFactory中,不少功能須要以編程的方式方式實現,而在ApplicationContext中則能夠經過配置的方式實現。

    BeanFactory負責讀取bean配置文檔,管理bean的加載,實例化,維護bean之間的依賴關係,負責bean的聲明週期。ApplicationContext除了提供上述BeanFactory所能提供的功能以外,還提供了更完整的框架功能:

a. MessageSource, 提供國際化的消息訪問  
b. 資源訪問,如URL和文件  
c. 事件傳播特性,即支持aop特性
d. 載入多個(有繼承關係)上下文 ,使得每個上下文都專一於一個特定的層次,好比應用的web層 

(1)BeanFactroy採用的是延遲加載形式來注入Bean的,即只有在使用到某個Bean時(調用getBean()),纔對該Bean進行加載實例化,這樣,咱們就不能發現一些存在的Spring的配置問題。而ApplicationContext則相反,它是在容器啓動時,一次性建立了全部的Bean。這樣,在容器啓動時,咱們就能夠發現Spring中存在的配置錯誤。 相對於基本的BeanFactory,ApplicationContext 惟一的不足是佔用內存空間。當應用程序配置Bean較多時,程序啓動較慢。

BeanFacotry延遲加載,若是Bean的某一個屬性沒有注入,BeanFacotry加載後,直至第一次使用調用getBean方法纔會拋出異常;而ApplicationContext則在初始化自身是檢驗,這樣有利於檢查所依賴屬性是否注入;因此一般狀況下咱們選擇使用 ApplicationContext。
應用上下文則會在上下文啓動後預載入全部的單實例Bean。經過預載入單實例bean,確保當你須要的時候,你就不用等待,由於它們已經建立好了。

(2)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但二者之間的區別是:BeanFactory須要手動註冊,而ApplicationContext則是自動註冊。(Applicationcontext比 beanFactory 加入了一些更好使用的功能。並且 beanFactory 的許多功能須要經過編程實現而 Applicationcontext 能夠經過配置實現。好比後處理 bean , Applicationcontext 直接配置在配置文件便可而 beanFactory 這要在代碼中顯示的寫出來才能夠被容器識別。 )

(3)beanFactory主要是面對與 spring 框架的基礎設施,面對 spring 本身。而 Applicationcontex 主要面對與 spring 使用的開發者。基本都會使用 Applicationcontex 並不是 beanFactory 。

4七、Bean的做用域

做用域 描述
singleton

在每一個Spring IoC容器中一個bean定義對應一個對象實例。

(默認)在spring IOC容器中僅存在一個Bean實例,Bean以單實例的方式存在。

prototype

一個bean定義對應多個對象實例。

每次從容器中調用Bean時,都返回一個新的實例,即每次調用getBean()時,至關於執行new XxxBean()的操做。

request

在一次HTTP請求中,一個bean定義對應一個實例;即每次HTTP請求將會有各自的bean實例,它們依據某個bean定義建立而成。該做用域僅在基於web的Spring ApplicationContext情形下有效。

session

在一個HTTP Session中,一個bean定義對應一個實例。該做用域僅在基於web的Spring ApplicationContext情形下有效。

同一個HTTP session共享一個Bean,不一樣HTTP session使用不一樣的Bean,該做用域僅適用於webApplicationContext環境。

globalSession

在一個全局的HTTP Session中,一個bean定義對應一個實例。典型狀況下,僅在使用portlet context的時候有效。該做用域僅在基於web的Spring ApplicationContext情形下有效。

 

 

 

 

 

 

 

 

 

 

 

 

依賴注入方式

    對於spring配置一個bean時,若是須要給該bean提供一些初始化參數,則須要經過依賴注入方式,所謂的依賴注入就是經過spring將bean所須要的一些參數傳遞到bean實例對象的過程,spring的依賴注入有3種方式:

·使用屬性的setter方法注入 ,這是最經常使用的方式。屬性注入即經過setXxx()方法注入Bean的屬性值或依賴對象,因爲屬性注入方式具備可選擇性和靈活性高的優勢,所以屬性注入是實際應用中最常採用的注入方式。

·使用構造器注入。在類中,不用爲屬性設置setter方法,可是須要生成該類帶參的構造方法。同時,在配置文件中配置該類的bean,並配置構造器,在配置構造器中用到了<constructor-arg>節點,能夠指定按類型匹配入參仍是按索引匹配入參。

·使用Filed注入(用於註解方式)。使用註解注入依賴對象不用再在代碼中寫依賴對象的setter方法或者該類的構造方法,而且不用再配置文件中配置大量的依賴對象,使代碼更加簡潔,清晰,易於維護。在Spring IOC編程的實際開發中推薦使用註解的方式進行依賴注入。

自動裝配

    在應用中,咱們經常使用<ref>標籤爲JavaBean注入它依賴的對象,同時也Spring爲咱們提供了一個自動裝配的機制,在定義Bean時,<bean>標籤有一個autowire屬性,咱們能夠經過指定它來讓容器爲受管JavaBean自動注入依賴對象。

自動裝配是在配置文件中實現的,以下:<bean id="***" class="***" autowire="byType">

只須要配置一個autowire屬性便可完成自動裝配,不用再配置文件中寫<property>,可是在類中仍是要生成依賴對象的setter方法。

<bean>的autowire屬性有以下六個取值,他們的說明以下:

(1)No:即不啓用自動裝配。Autowire默認的值。默認狀況下,須要經過"ref"來裝配bean。

(2)byName:按名稱裝配。能夠根據屬性的名稱在容器中查詢與該屬性名稱相同的bean,若是沒有找到,則屬性值爲null。假設Boss類中有一個名爲car的屬性,若是容器中恰好有一個名爲car的Bean,Spring就會自動將其裝配給Boss的car屬性。

(3)byType:按類型裝配。能夠根據屬性類型,在容器中尋找該類型匹配的bean,若有多個,則會拋出異常,若是沒有找到,則屬性值爲null。假設Boss類中有一個Car類型的屬性,若是容器中恰好有一個Car類型的Bean,Spring就會自動將其裝配給Boss的這個屬性。

(4)constructor:與byType方式類似,不一樣之處在與它應用於構造器參數,若是在容器中沒有找到與構造器參數類型一致的bean,那麼將拋出異常。(根據構造函數參數的數據類型,進行byType模式的自動裝配。)

(5)autodetect:經過bean類的自省機制(introspection)來決定是使用constructor仍是byType的方式進行自動裝配。若是Bean有空構造器那麼將採用「byType」自動裝配方式,不然使用「constructor」自動裝配方式。

(6)default:由上級標籤<beans>的default-autowire屬性肯定。

注:不是全部類型都能自動裝配,不能自動裝配的數據類型:Object、基本數據類型(Date、CharSequence、Number、URI、URL、Class、int)等。

Spring經常使用註解

傳統的Spring作法是使用.xml文件來對bean進行注入或者是配置aop、事物,這麼作有兩個缺點:

(1)若是全部的內容都配置在.xml文件中,那麼.xml文件將會十分龐大;若是按需求分開.xml文件,那麼.xml文件又會很是多。總之這將致使配置文件的可讀性與可維護性變得很低。

(2)在開發中在.java文件和.xml文件之間不斷切換,是一件麻煩的事,同時這種思惟上的不連貫也會下降開發的效率。

爲了解決這兩個問題,Spring引入了註解,經過"@XXX"的方式,讓註解與Java Bean緊密結合,既大大減小了配置文件的體積,又增長了Java Bean的可讀性與內聚性

Spring經常使用註解總結:

@Configuration把一個類做爲一個IoC容器,它的某個方法頭上若是註冊了@Bean,就會做爲這個Spring容器中的Bean。

     @Configuration標註在類上,至關於把該類做爲spring的xml配置文件中的<beans>,做用爲:配置spring容器(應用上下文)。@Bean標註在方法上(返回某個實例的方法),等價於spring的xml配置文件中的<bean>,做用爲:註冊bean對象。用@Configuration註解類,等價於XML中配置beans;用@Bean標註方法等價於XML中配置bean。

@Component泛指組件,當組件很差歸類的時候,咱們能夠使用這個註解進行標註。

@Controller用於標註控制層組件(如struts中的action)。

@Service用於標註業務層組件。

@Repository用於標註數據訪問組件,即DAO組件。

@Autowired:顧名思義,就是自動裝配,其做用是爲了消除代碼Java代碼裏面的getter/setter與bean屬性中的property。固然,getter看我的需求,若是私有屬性須要對外提供的話,應當予以保留。@Autowired默認按類型匹配的方式,在容器查找匹配的Bean,當有且僅有一個匹配的Bean時,Spring將其注入@Autowired標註的變量中。

@Qualifier:用來指定注入Bean的名稱。若是容器中有一個以上匹配的Bean,則能夠經過@Qualifier註解限定Bean的名稱。一般@Qualifier能夠結合@Autowired註解一塊兒使用。以下:@Autowired @Qualifier("personDaoBean") 存在多個實例配合使用。

@Resource默認按名稱裝配,當找不到與名稱匹配的bean纔會按類型裝配。

說一下@Resource的裝配順序:

(1)@Resource後面沒有任何內容,默認經過name屬性去匹配bean,找不到再按type去匹配。

(2)指定了name或者type則根據指定的類型去匹配bean。

(3)指定了name和type則根據指定的name和type去匹配bean,任何一個不匹配都將報錯。

而後,區分一下@Autowired和@Resource兩個註解的區別:

(1)@Autowired默認按照byType方式進行bean匹配,@Resource默認按照byName方式進行bean匹配。

(2)@Autowired是Spring的註解,@Resource是J2EE的註解,這個看一下導入註解的時候這兩個註解的包名就一清二楚了。

Spring屬於第三方的,J2EE是Java本身的東西,所以,建議使用@Resource註解,以減小代碼和Spring之間的耦合。

Spring事務管理

spring支持編程式事務管理和聲明式事務管理兩種方式。

    編程式事務管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務管理,spring推薦使用TransactionTemplate。

    聲明式事務管理創建在AOP之上的。其本質是對方法先後進行攔截,而後在目標方法開始以前建立或者加入一個事務,在執行完目標方法以後根據執行狀況提交或者回滾事務。聲明式事務最大的優勢就是不須要經過編程的方式管理事務,這樣就不須要在業務邏輯代碼中摻瑣事務管理的代碼,只需在配置文件中作相關的事務規則聲明(或經過基於@Transactional註解的方式),即可以將事務規則應用到業務邏輯中。

    顯然聲明式事務管理要優於編程式事務管理,這正是spring倡導的非侵入式的開發方式。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上註解就能夠得到徹底的事務支持。和編程式事務相比,聲明式事務惟一不足地方是,後者的最細粒度只能做用到方法級別,沒法作到像編程式事務那樣能夠做用到代碼塊級別。可是即使有這樣的需求,也存在不少變通的方法,好比,能夠將須要進行事務管理的代碼塊獨立爲方法等等。

    聲明式事務管理也有兩種經常使用的方式,一種是基於tx和aop名字空間的xml配置文件,另外一種就是基於@Transactional註解。顯然基於註解的方式更簡單易用,更清爽。

spring事務回滾規則

     指示spring事務管理器回滾一個事務的推薦方法是在當前事務的上下文內拋出異常。spring事務管理器會捕捉任何未處理的異常,而後依據規則決定是否回滾拋出異常的事務。

     默認配置下,spring只有在拋出的異常爲運行時unchecked異常時纔回滾該事務,也就是拋出的異常爲RuntimeException的子類(Errors也會致使事務回滾),而拋出checked異常則不會致使事務回滾。能夠明確的配置在拋出哪些異常時回滾事務,包括checked異常也能夠明肯定義哪些異常拋出時不回滾事務。還能夠編程性的經過setRollbackOnly()方法來指示一個事務必須回滾,在調用完setRollbackOnly()後你所能執行的惟一操做就是回滾。

checked異常: 表示無效,不是程序中能夠預測的。好比無效的用戶輸入,文件不存在,網絡或者數據庫連接錯誤。這些都是外在的緣由,都不是程序內部能夠控制的。 必須在代碼中顯式地處理。好比try-catch塊處理,或者給所在的方法加上throws說明,將異常拋到調用棧的上一層。 繼承自java.lang.Exception(java.lang.RuntimeException除外)。

unchecked異常: 表示錯誤,程序的邏輯錯誤。是RuntimeException的子類,好比IllegalArgumentException, NullPointerException和IllegalStateException。 不須要在代碼中顯式地捕獲unchecked異常作處理。 繼承自java.lang.RuntimeException(而java.lang.RuntimeException繼承自java.lang.Exception)。

    java裏面將派生於Error或者RuntimeException(好比空指針,1/0)的異常稱爲unchecked異常,其餘繼承自java.lang.Exception的異常統稱爲Checked Exception,如IOException、TimeoutException等。那麼再通俗一點:你寫代碼出現的空指針等異常,會被回滾,文件讀寫,網絡出問題,spring就無法回滾了。

@Transactional註解

@Transactional屬性 

屬性 類型 描述
value String 可選的限定描述符,指定使用的事務管理器
propagation enum: Propagation 可選的事務傳播行爲設置
isolation enum: Isolation 可選的事務隔離級別設置
readOnly boolean 讀寫或只讀事務,默認讀寫
timeout int (in seconds granularity) 事務超時時間設置
rollbackFor Class對象數組,必須繼承自Throwable 致使事務回滾的異常類數組
rollbackForClassName 類名數組,必須繼承自Throwable 致使事務回滾的異常類名字數組
noRollbackFor Class對象數組,必須繼承自Throwable 不會致使事務回滾的異常類數組
noRollbackForClassName 類名數組,必須繼承自Throwable 不會致使事務回滾的異常類名字數組

 

 

 

 

 

 

 

 

用法:

    @Transactional 能夠做用於接口、接口方法、類以及類方法上。看成用於類上時,該類的全部 public 方法將都具備該類型的事務屬性,同時,咱們也能夠在方法級別使用該標註來覆蓋類級別的定義。

    雖然 @Transactional 註解能夠做用於接口、接口方法、類以及類方法上,可是 Spring 建議不要在接口或者接口方法上使用該註解,由於這隻有在使用基於接口的代理時它纔會生效。另外, @Transactional 註解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。若是你在 protected、private 或者默承認見性的方法上使用 @Transactional 註解,這將被忽略,也不會拋出任何異常。

    默認狀況下,只有來自外部的方法調用纔會被AOP代理捕獲,也就是,類內部方法調用本類內部的其餘方法並不會引發事務行爲,即便被調用方法使用@Transactional註解進行修飾。

   Spring使用聲明式事務處理,默認狀況下,若是被註解的數據庫操做方法中發生了unchecked異常,全部的數據庫操做將rollback;若是發生的異常是checked異常,默認狀況下數據庫操做仍是會提交的。

如何改變默認規則: 

(1)讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class) 

(2)讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class) 

(3)不須要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED) 

在整個方法運行前就不會開啓事務 ,還能夠加上:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),這樣就作成一個只讀事務,能夠提升效率。 

@Autowired  
private MyBatisDao dao;  
  
@Transactional  
@Override  
public void insert(Test test) {  
    dao.insert(test);  
    throw new RuntimeException("test");//拋出unchecked異常,觸發事物,回滾  
}  

noRollbackFor

@Transactional(noRollbackFor=RuntimeException.class)  
@Override  
public void insert(Test test) {  
    dao.insert(test);  
    //拋出unchecked異常,觸發事物,noRollbackFor=RuntimeException.class,不回滾  
    throw new RuntimeException("test");  
}

類,看成用於類上時,該類的全部 public 方法將都具備該類型的事務屬性

@Transactional  
public class MyBatisServiceImpl implements MyBatisService {  
  
    @Autowired  
    private MyBatisDao dao;  
      
      
    @Override  
    public void insert(Test test) {  
        dao.insert(test);  
        //拋出unchecked異常,觸發事物,回滾  
        throw new RuntimeException("test");  
    }
}

propagation=Propagation.NOT_SUPPORTED

@Transactional(propagation=Propagation.NOT_SUPPORTED)  
@Override  
public void insert(Test test) {  
    //事物傳播行爲是PROPAGATION_NOT_SUPPORTED,以非事務方式運行,不會存入數據庫  
    dao.insert(test);  
}  

例子:

@Service
public class SysConfigService {

    @Autowired
    private SysConfigRepository sysConfigRepository;
    
    public SysConfigEntity getSysConfig(String keyName) {
        SysConfigEntity entity = sysConfigRepository.findOne(keyName);
        return entity;
    }
    
    public SysConfigEntity saveSysConfig(SysConfigEntity entity) {
        
        if(entity.getCreateTime()==null){
            entity.setCreateTime(new Date());
        }
        
        return sysConfigRepository.save(entity);
                
    }
    
    @Transactional
    public void testSysConfig(SysConfigEntity entity) throws Exception {
        //不會回滾
        this.saveSysConfig(entity);
        throw new Exception("sysconfig error");
        
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void testSysConfig1(SysConfigEntity entity) throws Exception {
        //會回滾
        this.saveSysConfig(entity);
        throw new Exception("sysconfig error");
        
    }
    
    @Transactional
    public void testSysConfig2(SysConfigEntity entity) throws Exception {
        //會回滾
        this.saveSysConfig(entity);
        throw new RuntimeException("sysconfig error");
        
    }
    
    @Transactional
    public void testSysConfig3(SysConfigEntity entity) throws Exception {
        //事務仍然會被提交
        this.testSysConfig4(entity);
        throw new Exception("sysconfig error");
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void testSysConfig4(SysConfigEntity entity) throws Exception {
        
        this.saveSysConfig(entity);
    }
}

@Transactional事務使用總結:

(1)異常在A方法內拋出,則A方法就得加註解

(2)多個方法嵌套調用,若是都有 @Transactional 註解,則產生事務傳遞,默認 Propagation.REQUIRED

(3)若是註解上只寫 @Transactional 默認只對 RuntimeException 回滾,而非 Exception 進行回滾。若是要對 checked Exceptions 進行回滾,則須要 @Transactional(rollbackFor = Exception.class),rollbackFor這屬性指定了,即便你出現了checked這種例外,那麼它也會對事務進行回滾。

解決@Transactional註解不回滾

(1)檢查你方法是否是public的。

(2)你的異常類型是否是unchecked異常。若是我想check異常也想回滾怎麼辦,註解上面寫明異常類型便可。

@Transactional(rollbackFor=Exception.class) 

相似的還有norollbackFor,自定義不回滾的異常。

(3)數據庫引擎要支持事務,若是是MySQL,注意表要使用支持事務的引擎,好比innodb,若是是myisam,事務是不起做用的。

(4)是否開啓了對註解的解析。

 <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

(5)spring是否掃描到你這個包,以下是掃描到org.test下面的包。

<context:component-scan base-package="org.test" ></context:component-scan>

(6)檢查是否是同一個類中的方法調用(如a方法調用同一個類中的b方法)。 

(7)異常是否是被你catch住了。

 

SpringMVC工做流程

(1)用戶發起請求到前端控制器(Controller)

(2)前端控制器沒有處理業務邏輯的能力,須要找到具體的模型對象處理(Handler),處處理器映射器(HandlerMapping)中查找Handler對象(Model)。

(3)HandlerMapping返回執行鏈,包含了2部份內容: ① Handler對象、② 攔截器數組

(4)前端處理器經過處理器適配器包裝後執行Handler對象。

(5)處理業務邏輯。

(6)Handler處理完業務邏輯,返回ModelAndView對象,其中view是視圖名稱,不是真正的視圖對象。

(7)將ModelAndView返回給前端控制器。

(8)視圖解析器(ViewResolver)返回真正的視圖對象(View)。

(9)(此時前端控制器中既有視圖又有Model對象數據)前端控制器根據模型數據和視圖對象,進行視圖渲染。

(10)返回渲染後的視圖(html/json/xml)返回。

(11)給用戶產生響應。


(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響應用戶。

(1)用戶向服務器發送HTTP請求,請求被前端控制器 DispatcherServlet 捕獲。

(2)DispatcherServlet 根據 <servlet-name>-servlet.xml 中的配置對請求的URL進行解析,獲得請求資源標識符(URI)。 而後根據該URI,調用 HandlerMapping 得到該Handler配置的全部相關的對象(包括Handler對象以及Handler對象對應的攔截器),最後以 HandlerExecutionChain 對象的形式返回。

(3)DispatcherServlet 根據得到的Handler,選擇一個合適的 HandlerAdapter。(附註:若是成功得到HandlerAdapter後,此時將開始執行攔截器的preHandler(…​)方法)。

(4)提取Request中的模型數據,填充Handler入參,開始執行Handler(Controller)。 在填充Handler的入參過程當中,根據你的配置,Spring將幫你作一些額外的工做:

HttpMessageConveter: 將請求消息(如Json、xml等數據)轉換成一個對象,將對象轉換爲指定的響應信息。

數據轉換:對請求消息進行數據轉換。如String轉換成Integer、Double等。

數據根式化:對請求消息進行數據格式化。 如將字符串轉換成格式化數字或格式化日期等。

數據驗證: 驗證數據的有效性(長度、格式等),驗證結果存儲到BindingResult或Error中。

(5)Handler(Controller)執行完成後,向 DispatcherServlet 返回一個 ModelAndView 對象;

(6)根據返回的ModelAndView,選擇一個適合的 ViewResolver(必須是已經註冊到Spring容器中的ViewResolver)返回給DispatcherServlet。

(7)ViewResolver 結合Model和View,來渲染視圖。

(8)視圖負責將渲染結果返回給客戶端。

組件:

(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...)。

SpringMVC註解

@RequestMapping:RequestMapping是一個用來處理請求地址映射的註解(將請求映射到對應的控制器方法中),可用於類或方法上。用於類上,表示類中的全部響應請求的方法都是以該地址做爲父路徑。RequestMapping請求路徑映射,若是標註在某個controller的類級別上,則代表訪問此類路徑下的方法都要加上其配置的路徑;最經常使用是標註在方法上,代表哪一個具體的方法來接受處理某次請求。

RequestMapping的屬性

value:指定請求的實際url

method:指定請求的method類型, GET、POST、PUT、DELETE等;
@RequestMapping(value="/get/{bookid}",method={RequestMethod.GET,RequestMethod.POST})

params:指定request中必須包含某些參數值是,才讓該方法處理。
@RequestMapping(params="action=del"),請求參數包含「action=del」,如:http://localhost:8080/book?action=del

headers:指定request中必須包含某些指定的header值,才能讓該方法處理請求。

consumes:指定處理請求的提交內容類型(Content-Type),例如application/json, text/html。

produces: 指定返回的內容類型,僅當request請求頭中的(Accept)類型中包含該指定類型才返回。 

@RequestParam:綁定單個請求參數值

@RequestParam有如下三個參數:

value:參數名字,即入參的請求參數名字,如username表示請求的參數區中的名字爲username的參數的值將傳入;

required:是否必須,默認是true,表示請求中必定要有相應的參數,不然將拋出異常;

defaultValue:默認值,表示若是請求中沒有同名參數時的默認值,設置該參數時,自動將required設爲false。

@PathVariable:綁定URI模板變量值

@PathVariable用於將請求URL中的模板變量映射到功能處理方法的參數上。

@ModelAttribute:ModelAttribute能夠應用在方法參數上或方法上,他的做用主要是當註解在方法參數上時會將註解的參數對象添加到Model中;當註解在請求處理方法Action上時會將該方法變成一個非請求處理的方法,但其它Action被調用時會首先調用該方法。

@Responsebody:@Responsebody表示該方法的返回結果直接寫入HTTP response body中。通常在異步獲取數據時使用,在使用@RequestMapping後,返回值一般解析爲跳轉路徑,加上@Responsebody後返回結果不會被解析爲跳轉路徑,而是直接寫入HTTP response body中。好比異步獲取json數據,加上@Responsebody後,會直接返回json數據。

@RequestBody:將HTTP請求正文插入方法中,使用適合的HttpMessageConverter將請求體寫入某個對象。

3、JVM

4四、GC是什麼?爲何要有GC?

答:GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會致使程序或系統的不穩定甚至崩潰,Java提供的GC功能能夠自動監測對象是否超過做用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操做方法。Java程序員不用擔憂內存管理,由於垃圾收集器會自動進行管理。要請求垃圾收集,能夠調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM能夠屏蔽掉顯示的垃圾回收調用。

垃圾回收能夠有效的防止內存泄露,有效的使用能夠使用的內存。垃圾回收器一般是做爲一個單獨的低優先級的線程運行,不可預知的狀況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或全部對象進行垃圾回收。在Java誕生初期,垃圾回收是Java最大的亮點之一,由於服務器端的編程須要有效的防止內存泄露問題,然而時過境遷,現在Java的垃圾回收機制已經成爲被詬病的東西。移動智能終端用戶一般以爲iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的緣由就在於Android系統中垃圾回收的不可預知性。

4五、介紹JVM中7個區域,並把每一個區域中可能形成的內存溢出狀況進行說明

程序計數器:看作當前線程所執行的字節碼行號指示器。是線程私有的內存,且惟一一塊不報OutOfMemoryError異常。

Java虛擬機棧:用於描述java方法的內存模型:每一個方法被執行時都會同時建立一個棧幀用於存儲局部變量表,操做數棧,動態連接,方法出口等信息。每個方法被調用直至執行完成的過程就對應着一個棧幀在虛擬機中從入棧到出棧的過程。若是線程請求的棧深度大於虛擬機所容許的深度就報StackOverflowError,,若是虛擬機棧能夠動態擴展,當擴展時沒法申請到足夠的內存會拋出OutOfMemoryError。Java虛擬機棧是線程私有的。

本地方法棧:與虛擬機棧類似,不一樣的在於它是爲虛擬機使用到的Native方法服務的。會拋出StackOverflowError和OutOfMemoryError。是線程私有的。 

Java堆:是全部線程共享的一塊內存,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。若是堆上沒有內存完成實例的分配就會報OutOfMemoryError。

方法區(永久代):用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。當方法區沒法知足內存分配需求時,會拋出OutOfMemoryError。是共享內存。

運行時常量池:用於存放編譯器生成的各類字面量和符號引用,是方法區的一部分。沒法申請內存時拋出OutOfMemoryError。

直接內存:不是虛擬機運行時數據的一部分,也不是java虛擬機規範中定義的區域,是計算機直接的內存空間。這部分也被頻繁使用,如JAVA NIO的引入基於通道和緩存區的I/O使用native函數直接分配堆外內存。若是內存不足會報OutOfMemoryError。

4六、怎樣判斷一個對象是否須要收集?

GC的兩種斷定方法:引用計數與可達性分析法。

引用計數:給對象添加一個引用計數器,每當有一個地方引用該對象時,計數器值加1,當引用失效時,計數器值減1。任什麼時候候計數器都爲0的對象就是不可能再被使用的。引用計數缺陷:引用計數沒法解決循環引用問題:假設對象A,B都已經被實例化,讓A=B,B=A,除此以外這兩個對象再無任何引用,此時計數器的值就永遠不可能爲0,可是引用計數器沒法通知gc回收他們。

可達性分析法(GC Roots Traceing): 經過一系列名爲「GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索走過的路徑成爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象不可用。 GC Roots對象通常是:虛擬機棧中的引用對象,方法區中類靜態屬性引用的對象,方法區常量引用的對象等。

對於可達性分析算法而言,未到達的對象並不是是「非死不可」的,若要宣判一個對象死亡,至少須要經歷兩次標記階段。

1. 若是對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件爲是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法已經被虛擬機執行過了,則均視做沒必要要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法而且該finalize方法並無被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,以後會由虛擬機自動創建的、優先級低的Finalizer線程去執行,而虛擬機沒必要要等待該線程執行結束,即虛擬機只負責創建線程,其餘的事情交給此線程去處理。

2.對F-Queue中對象進行第二次標記,若是對象在finalize方法中拯救了本身,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其餘變量,那麼在第二次標記的時候該對象將從「即將回收」的集合中移除,若是對象仍是沒有拯救本身,那就會被回收。注意,它只能拯救本身一次,第二次就被回收了。

4七、Java中的四種引用

強引用:程序代碼中的普通引用。如Object obj = new Object(),只要強引用存在,垃圾回收器就不會回收。

軟引用:描述一些有用但並不是必須的對象。對於軟引用關聯的對象在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。

弱引用(SoftRefence:描述非必須對象,比軟引用弱一些。被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

虛引用(WeakRefence:最弱的引用,無論是否有虛引用存在,徹底不會對對象生存時間構成影響,也沒法經過虛引用來取得一個對象實例。惟一目的是但願可以在這個對象被垃圾收集器回收以前收到系統通知。

4八、對象建立方法,對象的內存分配,對象的訪問定位

Object obj = new Object(); obj 保存在java棧中的局部變量表裏,做爲一個引用數據出現。New Object()會在java堆上分配一塊存儲Object類型實例的全部數值的結構化內存,根據類型以及虛擬機實現的對象內存佈局不一樣。這塊內存是不固定的。 對象訪問方式有兩種:句柄和直接指針。

句柄:在java堆中會劃分出一塊內存做爲句柄池,reference中存儲的對象是句柄地址。而句柄中包含對象實例數據和類型數據各自的具體地址信息。最大的好處是若是對象地址發生變化不須要改變reference的值,只須要改變句柄中實例數據指針。

直接指針訪問:reference直接存儲對象的地址,最大的好處是速度更快

4九、Java中堆和棧的區別

Java把內存劃分紅兩種:一種是棧內存,一種是堆內存。

      在函數中定義的一些基本類型的變量和對象的引用變量都在函數的棧內存中分配。當在一段代碼塊定義一個變量時,Java就在棧中爲這個變量分配內存空間,當超過變量的做用域後,Java會自動釋放掉爲該變量所分配的內存空間,該內存空間能夠當即被另做它用。

      堆內存用來存放由 new 建立的對象和數組,在堆中分配的內存,由 Java 虛擬機的自動垃圾回收器來管理。在堆中產生了一個數組或者對象以後,還能夠在棧中定義一個特殊的變量,讓棧中的這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量,之後就能夠在程序中使用棧中的引用變量來訪問堆中的數組或者對象,引用變量就至關因而爲數組或者對象起的一個名稱。引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其做用域以外後被釋放。而數組和對象自己在堆中分配,即便程序運行到使用 new 產生數組或者對象的語句所在的代碼塊以外,數組和對象自己佔據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變爲垃圾,不能在被使用,但仍然佔據內存空間不放,在隨後的一個不肯定的時間被垃圾回收器收走(釋放掉)。 

具體的說:

      棧與堆都是Java用來在Ram中存放數據的地方。與C++不一樣,Java自動管理棧和堆,程序員不能直接地設置棧或堆。

      Java堆是一個運行時數據區,類的對象從中分配空間。這些對象經過 new 創建,它們不須要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優點是能夠動態地分配內存大小,生存期也沒必要事先告訴編譯器,由於它是在運行時動態分配內存的,Java 的垃圾收集器會自動收走這些再也不使用的數據。但缺點是,因爲要在運行時動態分配內存,存取速度較慢。 java 中的對象和數組都存放在堆中。

      棧的優點是,存取速度比堆要快,僅次於寄存器,棧數據能夠共享。但缺點是,存在棧中的數據大小與生存期必須是肯定的,缺少靈活性。棧中主要存放一些基本類型的變量(int, short, long, byte, float, double, boolean, char)和對象句柄。

棧有一個很重要的特殊性,就是存在棧中的數據能夠共享。 假設咱們同時定義:

int a = 3; 
int b = 3;

     編譯器先處理 int a = 3 ;首先它會在棧中建立一個變量爲 a 的引用,而後查找棧中是否有 3 這個值,若是沒找到,就將 3 存放進來,而後將 a 指向 3 。接着處理 int b = 3 ;在建立完 b 的引用變量後,由於在棧中已經有 3 這個值,便將 b 直接指向 3 。這樣,就出現了 a 與 b 同時均指向 3 的狀況。這時,若是再令 a=4 ;那麼編譯器會從新搜索棧中是否有 4 值,若是沒有,則將 4 存放進來,並令 a 指向 4 ;若是已經有了,則直接將 a 指向這個地址。所以 a 值的改變不會影響到 b 的值。要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不一樣的,由於這種狀況 a 的修改並不會影響到 b,它是由編譯器完成的,它有利於節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另外一個對象引用變量。

JVM分代垃圾回收策略

爲何要分代

    分代的垃圾回收策略,是基於這樣一個事實:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的收集方式,以便提升回收效率。

    在Java程序運行的過程當中,會產生大量的對象,其中有些對象是與業務信息相關,好比Http請求中的Session對象、線程、Socket鏈接,這類對象跟業務直接掛鉤,所以生命週期比較長。可是還有一些對象,主要是程序運行過程當中生成的臨時變量,這些對象生命週期會比較短,好比:String對象,因爲其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次便可回收。

    試想,在不進行對象存活時間區分的狀況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,由於每次回收都須要遍歷全部存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,由於可能進行了不少次遍歷,可是他們依舊存在。所以,分代垃圾回收採用分治的思想,進行代的劃分,把不一樣生命週期的對象放在不一樣代上,不一樣代上採用最適合它的垃圾回收方式進行回收。

如何分代

如圖所示:

    虛擬機中的共劃分爲三個代:年輕代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

年輕代:

    全部新生成的對象首先都是放在年輕代的。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。年輕代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的而且此時還存活的對象,將被複制「年老區(Tenured)」。須要注意,Survivor的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從Eden複製過來的對象,和從前一個Survivor複製過來的對象,而複製到年老區的只有從第一個Survivor區過來的對象。並且,Survivor區總有一個是空的。同時,根據程序須要,Survivor區是能夠配置爲多個的(多於兩個),這樣能夠增長對象在年輕代中的存在時間,減小被放到年老代的可能。

    新生代有劃分爲Eden、From Survivor和To Survivor三個部分,他們對應的內存空間的大小比例爲8:1:1,也就是,爲對象分配內存的時候,首先使用Eden空間,通過GC後,沒有被回收的會首先進入From Survivor區域,任什麼時候候,都會保持一個Survivor區域(From Survivor或To Survivor)徹底空閒,也就是說新生代的內存利用率最大爲90%。From Survivor和To Survivor兩個區域會根據GC的實際狀況,進行互換,將From Survivor區域中的對象所有複製到To Survivor區域中,或者反過來,將To Survivor區域中的對象所有複製到From Survivor區域中。

年老代:

    在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

    GC過程當中,當某些對象通過屢次GC都沒有被回收,可能會進入到年老代。或者,當新生代沒有足夠的空間來爲對象分配內存時,可能會直接在年老代進行分配。

持久代:

    用於存放靜態文件,現在Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。持久代大小經過-XX:MaxPermSize=<N>進行設置。

   永久代實際上對應着虛擬機運行時數據區的「方法區」,這裏主要存放類信息、靜態變量、常量等數據。通常狀況下,永久代中對應的對象的GC效率很是低,由於這裏的的大部分對象在運行都不要進行GC,它們會一直被利用,直到JVM退出。

JVM內存分配與回收策略

1.對象優先在Eden區分配

對象一般在新生代的Eden區進行分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。

Minor GC:指發生在新生代的垃圾收集動做,很是頻繁,速度較快。

Major GC:指發生在老年代的GC,出現Major GC,常常會伴隨一次Minor GC,同時Minor GC也會引發Major GC,通常在GC日誌中統稱爲GC,不頻繁。

Full GC:指發生在老年代和新生代的GC,速度很慢,須要Stop The World。

2.大對象直接進入老年代

   須要大量連續內存空間的Java對象稱爲大對象,大對象的出現會致使提早觸發垃圾收集以獲取更大的連續的空間來進行大對象的分配。虛擬機提供了-XX:PretenureSizeThreadshold參數來設置大對象的閾值,超過閾值的對象直接分配到老年代。

3.長期存活的對象進入老年代

    每一個對象有一個對象年齡計數器,與前面的對象的存儲佈局中的GC分代年齡對應。對象出生在Eden區、通過一次Minor GC後仍然存活,並可以被Survivor容納,設置年齡爲1,對象在Survivor區每次通過一次Minor GC,年齡就加1,當年齡達到必定程度(默認15),就晉升到老年代,虛擬機提供了-XX:MaxTenuringThreshold來進行設置。

4.動態對象年齡判斷

    對象的年齡到達了MaxTenuringThreshold能夠進入老年代,同時,若是在survivor區中相同年齡全部對象大小的總和大於survivor區的一半,年齡大於等於該年齡的對象就能夠直接進入老年代。無需等到MaxTenuringThreshold中要求的年齡。

5.空間分配擔保

   在發生Minor GC時,虛擬機會檢查老年代連續的空閒區域是否大於新生代全部對象的總和,若成立,則說明Minor GC是安全的,不然,虛擬機須要查看HandlePromotionFailure的值,看是否運行擔保失敗,若容許,則虛擬機繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若大於,將嘗試進行一次Minor GC;若小於或者HandlePromotionFailure設置不運行冒險,那麼此時將改爲一次Full GC,以上是JDK Update 24以前的策略,以後的策略改變了,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

    冒險是指通過一次Minor GC後有大量對象存活,而新生代的survivor區很小,放不下這些大量存活的對象,因此須要老年代進行分配擔保,把survivor區沒法容納的對象直接進入老年代。

50、內存溢出和內存泄漏

內存溢出:通俗理解就是內存不夠,程序所須要的內存遠遠超出了你虛擬機分配的內存大小,就叫內存溢出。

內存泄漏:內存泄漏也稱做「存儲滲漏」,用動態存儲分配函數動態開闢的空間,在使用完畢後未釋放,結果致使一直佔據該內存單元。直到程序結束。(其實說白了就是該內存空間使用完畢以後未回收)即所謂內存泄漏。

5一、內存溢出了怎麼辦?

     經過內存映像工具如jhat,jconsole等對dump出來的堆轉存儲快照進行分析,重點是確認內存是出現內存泄露仍是內存溢出。 若是是內存泄露進一步使用工具查看泄露的對象到GC Roots的引用鏈。因而就能找到泄露對象是經過怎樣的路徑與GC Roots相關聯並致使垃圾收集器沒法自動回收它們。掌握泄露對象的信息,以及GC Roots引用鏈的信息,就能夠比較準肯定位泄露代碼的位置。 若是不存在**內存泄露,那就須要經過jinfo,Jconsole等工具分析java堆參數與機器物理內存對比是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長,持有狀態過長的狀況,嘗試減小程序的運行消耗。

5二、Java中有內存泄露嗎?

     有,Java中,形成內存泄露的緣由有不少種。典型的例子是長生命週期的對象持有短生命週期對象的引用就極可能發生內存泄露,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收,這就是java中內存泄露的發生場景,通俗地說,就是程序員可能建立了一個對象,之後一直再也不使用這個對象,這個對象卻一直被引用,即這個對象無用可是卻沒法被垃圾回收器回收的,這就是java中可能出現內存泄露的狀況,例如,緩存系統,咱們加載了一個對象放在緩存中(例如放在一個全局map對象中),而後一直再也不使用它,這個對象一直被緩存引用,但卻再也不被使用。檢查java中的內存泄露,必定要讓程序將各類分支狀況都完整執行到程序結束,而後看某個對象是否被使用過,若是沒有,則才能斷定這個對象屬於內存泄露。(採用什麼工具?) 若是一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即便那個外部類實例對象再也不被使用,但因爲內部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會形成內存泄露。

5三、何時會發生jvm堆內存溢出?

    簡單的來講 java的堆內存分爲兩塊:permantspace(持久代) 和 heap space。 持久帶中主要用於存放靜態類型數據,如 Java Class,,Method 等, 與垃圾收集器要收集的Java對象關係不大。 而heap space分爲年輕代和年老代 年輕代的垃圾回收叫 Young GC, 年老代的垃圾回收叫 Full GC。 在年輕代中經歷了N次(可配置)垃圾回收後仍然存活的對象,就會被複制到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象 年老代溢出緣由有 循環上萬次的字符串處理、建立上千萬個對象、在一段代碼內申請上百M甚至上G的內存 持久代溢出緣由動態加載了大量Java類而致使溢出,以及生產大量的常量。 永久代內存泄露:以一個部署到應用程序服務器的Java web程序來講,當該應用程序被卸載的時候,你的EAR/WAR包中的全部類都將變得無用。只要應用程序服務器還活着,JVM將繼續運行,可是一大堆的類定義將再也不使用,理應將它們從永久代(PermGen)中移除。若是不移除的話,咱們在永久代(PermGen)區域就會有內存泄漏。

5四、OOM你遇到過哪些狀況?

java.lang.OutOfMemoryError:Java heap space ------>java堆內存溢出,此種狀況最多見,通常因爲內存泄露或者堆的大小設置不當引發。

java.lang.OutOfMemoryError:PermGen space ------>java永久代溢出,即方法區溢出了,通常出現於大量Class或者jsp頁面,或者採用cglib等反射機制的狀況,由於上述狀況會產生大量的Class信息存儲於方法區。

java.lang.StackOverflowError ------> 不會拋OOM error,但也是比較常見的Java內存溢出。JAVA虛擬機棧溢出,通常是因爲程序中存在死循環或者深度遞歸調用形成的,棧大小設置過小也會出現此種溢出。能夠經過虛擬機參數-Xss來設置棧的大小。

5五、GC的三種收集方法:標記清除、標記整理、複製算法的原理與特色,分別用在什麼地方?

標記清除:首先標記全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象,它的標記的對象。缺點是效率低,且存在內存碎片。主要用於老生代垃圾回收。 

複製算法:將內存按容量劃分爲大小相等的一塊,每次只用其中一塊。當內存用完了,將還存活的對象複製到另外一塊內存,而後把已使用過的內存空間一次清理掉。實現簡單,高效。通常用於新生代。通常是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間。HotSpot虛擬機默認比例是8:1,。每次使用Eden和一塊Survivor,當回收時將這兩塊內存中還存活的對象複製到Survivor而後清理掉剛纔Eden和Survivor的空間。若是複製過程內存不夠使用則向老年代分配擔保。

標記整理:首先標記全部須要回收的對象,在標記完成後讓全部存活的對象都向一端移動,而後直接清理掉端邊界意外的內存。用於老年代。

分代收集算法:根據對象的生存週期將內存劃分爲新生代和老年代,根據年代的特色採用最適當的收集算法。

5六、Minor GC 與 Full GC 分別在何時發生?

FullGC 通常是發生在老年代的GC,出現一個FullGC常常會伴隨至少一次的Minor GC。速度比MinorGC慢10倍以上。FULL GC發生的狀況:

1) 老年代空間不足。老年代空間只有在新生代對象轉入及建立爲大對象、大數組時纔會出現不足的現象,當執行Full GC後空間仍然不足,則拋出以下錯誤:java.lang.OutOfMemoryError: Java heap space 。措施:爲避免以上兩種情況引發的FullGC,調優時應儘可能作到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。

2) Permanet Generation(方法區或永久代)空間滿。PermanetGeneration中存放的爲一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的狀況下會執行Full GC。若是通過Full GC仍然回收不了,那麼JVM會拋出以下錯誤信息: java.lang.OutOfMemoryError: PermGen space。措施:爲避免Perm Gen佔滿形成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。

3) CMS GC時出現promotion failed和concurrent mode failure 對於採用CMS進行老年代GC的程序而言,尤爲要注意GC日誌中是否有promotion failed和concurrent mode failure兩種情況,當這兩種情況出現時可能會觸發Full GC。 promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入老年代,而此時老年代也放不下形成的; concurrent mode failure: CMS在執行垃圾回收時須要一部分的內存空間而且此刻用戶程序也在運行須要預留一部份內存給用戶程序,若是預留的內存沒法知足程序需求就出現一次"Concurrent mod failure",並觸發一次Full GC。 應對措施爲:增大survivor space、老年代空間或調低觸發併發GC的比率。

4) 統計獲得的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間。 Hotspot爲了不因爲新生代對象晉升到舊生代致使舊生代空間不足的現象,在進行Minor GC時,作了一個判斷,若是以前統計所獲得的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。若是小於而且不容許擔保失敗也會發生一次Full GC。

MinorGC 指發生在新生代的垃圾收集動做,很是頻繁,回收速度也快。通常發生在新生代空間不足時。另一個FullGC常常會伴隨至少一次的Minor GC.。當虛擬機檢測晉升到老年代的平均大小是否小於老年代剩餘空間大小,若是小於而且容許擔保失敗,則執行Minor GC。

5七、類加載的五個過程:加載、驗證、準備、解析、初始化。

    類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。

加載: 根據全限定名來獲取定義類的二進制字節流,而後將該字節流所表明的靜態結構轉化爲方法區的運行時數據結構,最後在java堆上生成一個表明該類的Class對象,做爲方法區這些數據的訪問入口。

驗證:主要是爲了確保class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。包含四個階段的驗證過程:

1)文件格式驗證:保證輸入的字節流可以正確地解析並存儲在方法區以內,格式上符合描述一個java類型信息的要求。

2)元數據驗證:字節碼語義信息的驗證,以保證描述的信息符合java語言規範。驗證點有:這個類是否有父類等。

3)字節碼驗證:主要是進行數據流和控制流分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲.。

4)符號引用驗證:對符號引用轉化爲直接引用過程的驗證。

準備:爲類變量分配內存並設置變量的初始值,這些內存在方法區進行分配。

這個階段正式爲類變量(被static修飾的變量)分配內存並設置類變量初始值,這個內存分配是發生在方法區中。

(1)注意這裏並無對實例變量進行內存分配,實例變量將會在對象實例化時隨着對象一塊兒分配在JAVA堆中。

(2)這裏設置的初始值,一般是指數據類型的零值

private static int a = 3;

這個類變量a在準備階段後的值是0,將3賦值給變量a是發生在初始化階段。

解析:將虛擬機常量池中的符號引用轉化爲直接引用的過程。解析主要是針對類或接口,字段,類方法。

初始化:執行靜態變量的賦值操做以及靜態代碼塊,完成初識化。初始化過程保證了父類中定義的初始化優先於子類的初始化,但接口不須要執行父類的初始化。

    初始化是類加載機制的最後一步,這個時候才正真開始執行類中定義的JAVA程序代碼。在前面準備階段,類變量已經賦過一次系統要求的初始值,在初始化階段最重要的事情就是對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:

  ①聲明類變量時指定初始值

  ②使用靜態代碼塊爲類變量指定初始值

JVM初始化步驟

① 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類。

② 假如該類的直接父類尚未被初始化,則先初始化其直接父類。

③ 假如類中有初始化語句,則系統依次執行這些初始化語句。

類初始化時機:只有當對類主動使用的時候纔會致使類的初始化,類的主動使用包括如下四種:

– 使用new關鍵字實例化對象、讀取或者設置一個類的靜態字段(被final修飾的靜態字段除外)、調用一個類的靜態方法的時候。

– 使用java.lang.reflect包中的方法對類進行反射調用的時候。

– 初始化一個類,發現其父類尚未初始化過的時候。

– 虛擬機啓動的時候,虛擬機會先初始化用戶指定的包含main()方法的那個類。

以上四種狀況稱爲主動使用,其餘的狀況均稱爲被動使用,被動使用不會致使初始化。

- 初始化示例說明

① 子類引用父類靜態字段(非final),不會致使子類初始化。

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

運行結果:

super
123

說明:並無初始化子類,雖然使用SubClass.value,但實際使用的是子類繼承父類的靜態字段,不會初始化SubClass。即只有直接定義了這個字段的類纔會被初始化。

② 對於類或接口而言,使用其常量字段(final、static)不會致使其初始化。

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SuperClass.value);
    }
}

運行結果:

123

說明:類或接口的常量並不會致使類或接口的初始化。由於常量在編譯時進行優化,直接嵌入在TestInit.class文件的字節碼中。用final修飾某個類變量時,它的值在編譯時就已經肯定好放入常量池了,因此在訪問該類變量時,等於直接從常量池中獲取,並無初始化該類。

③ 對於類而言,初始化子類會致使父類(不包括接口)的初始化。

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

class SubClass extends SuperClass {
    public static int i = 3;
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.i);
    }
}

運行結果:

super
sub
3

說明:初始化子類會致使父類的初始化,而且父類的初始化在子類初始化的前面。

 ④ 經過數組定義引用類,不會觸發此類的初始化。

package com.demo;
 
class SuperClass1{
    
    public static int value = 123;
    
    static
    {
        System.out.println("SuperClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        SuperClass1[] scs = new SuperClass1[10];
    }
}

運行結果:什麼也不輸出

⑤ 對於接口而言,初始化子接口不會致使父接口的初始化,只有在真正使用到父接口的時候(如使用父接口中定義的常量),纔會初始化。

Class.forName()和ClassLoader.loadClass()區別

Class.forName():將類的.class文件加載到jvm中以外,還會對類進行解釋,執行類中的static塊;

ClassLoader.loadClass():只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。

:Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。而且只有調用了newInstance()方法採用調用構造函數,建立類的對象 。

5八、雙親委派模型

除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。順序依次是:

Bootstrap ClassLoader::啓動類加載器,加載java_home/lib中的類。

Extension ClassLoader:擴展類加載器,加載java_home/lib/ext目錄下的類庫。

Application ClassLoader::應用程序類加載器,加載用戶類路徑上指定類庫。

      雙親委派模型的工做原理是:若是一個類加載器收到了類加載請求,它首先不會本身去嘗試加載這個類,而把這個請求委派給父類加載器去完成,每一層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成加載請求時,加載器才嘗試本身加載。這種方式保證了Oject類在各個加載器加載環境中都是同一個類。

雙親委派模型的好處

     它的好處能夠用一句話總結,即防止內存中出現多份一樣的字節碼。從反向思考這個問題,若是沒有雙親委派模型而是由各個類加載器自行加載的話,若是用戶編寫了一個java.lang.Object的同名類並放在ClassPath中,多個類加載器都去加載這個類到內存中,系統中將會出現多個不一樣的Object類,那麼類之間的比較結果及類的惟一性將沒法保證,並且若是不使用這種雙親委派模型將會給虛擬機的安全帶來隱患。因此,要讓類對象進行比較有意義,前提是他們要被同一個類加載器加載。

     系統安全性:Java類隨着加載它的類加載器一塊兒具有了一種帶有優先級的層次關係。好比,Java中的Object類,它存放在rt.jar之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,所以Object在各類類加載環境中都是同一個類。若是不採用雙親委派模型,那麼由各個類加載器本身取加載的話,那麼系統中會存在多種不一樣的Object類。
(1)啓動類加載器:<JAVA_HOME>\lib
(2)擴展類加載器:<JAVA_HOME>\lib\ext
(3)應用程序類加載器:加載用戶類路徑上所指定的類庫

 

4、Java多線程

5九、進程和線程之間有什麼不一樣?

一個進程是一個獨立(self contained)的運行環境,它能夠被看做一個程序或者一個應用。而線程是在進程中執行的一個任務。Java運行環境是一個包含了不一樣的類和程序的單一進程。線程能夠被稱爲輕量級進程。線程須要較少的資源來建立和駐留在進程中,而且能夠共享進程中的資源。

60、多線程編程的好處是什麼?

在多線程程序中,多個線程被併發的執行以提升程序的效率,CPU不會由於某個線程須要等待資源而進入空閒狀態。多個線程共享堆內存(heap memory),所以建立多個線程去執行一些任務會比建立多個進程更好。舉個例子,Servlets比CGI更好,是由於Servlets支持多線程而CGI不支持。

6一、多線程有什麼用?

     一個可能在不少人看來很扯淡的一個問題:我會用多線程就行了,還管它有什麼用?在我看來,這個回答更扯淡。所謂」知其然知其因此然」,」會用」只是」知其然」,」爲何用」纔是」知其因此然」,只有達到」知其然知其因此然」的程度才能夠說是把一個知識點運用自如。OK,下面說說我對這個問題的見解:

(1)發揮多核CPU的優點

    隨着工業的進步,如今的筆記本、臺式機乃至商用的應用服務器至少也都是雙核的,4核、8核甚至16核的也都很多見,若是是單線程的程序,那麼在雙核CPU上就浪費了50%,在4核CPU上就浪費了75%。單核CPU上所謂的」多線程」那是假的多線程,同一時間處理器只會處理一段邏輯,只不過線程之間切換得比較快,看着像多個線程」同時」運行罷了。多核CPU上的多線程纔是真正的多線程,它能讓你的多段邏輯同時工做,多線程,能夠真正發揮出多核CPU的優點來,達到充分利用CPU的目的。

(2)防止阻塞

     從程序運行效率的角度來看,單核CPU不但不會發揮出多線程的優點,反而會由於在單核CPU上運行多線程致使線程上下文的切換,而下降程序總體的效率。可是單核CPU咱們仍是要應用多線程,就是爲了防止阻塞。試想,若是單核CPU使用單線程,那麼只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那麼你的整個程序在數據返回回來以前就中止運行了。多線程能夠防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行。

(3)便於建模

     這是另一個沒有這麼明顯的優勢了。假設有一個大的任務A,單線程編程,那麼就要考慮不少,創建整個程序模型比較麻煩。可是若是把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別創建程序模型,並經過多線程分別運行這幾個任務,那就簡單不少了。

6二、建立多線程的方式

比較常見的一個問題了,通常就是兩種:

(1)繼承Thread類

(2)實現Runnable接口

至於哪一個好,不用說確定是後者好,由於實現接口的方式比繼承類的方式更靈活,也能減小程序之間的耦合度,面向接口編程也是設計模式6大原則的核心

6三、start()方法和run()方法的區別

只有調用了start()方法,纔會表現出多線程的特性,不一樣線程的run()方法裏面的代碼交替執行。若是隻是調用run()方法,那麼代碼仍是同步執行的,必須等待一個線程的run()方法裏面的代碼所有執行完畢以後,另一個線程才能夠執行其run()方法裏面的代碼。

start()方法來啓動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,能夠直接繼續執行下面的代碼;經過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並無運行。 而後經過此Thread類調用方法run()來完成其運行操做的, 這裏方法run()稱爲線程體,它包含了要執行的這個線程的內容,run方法運行結束, 此線程終止。而後CPU再調度其它線程。

run()方法看成普通方法的方式調用。程序仍是要順序執行,要等待run方法體執行完畢後,纔可繼續執行下面的代碼; 程序中只有主線程這一個線程, 其程序執行路徑仍是隻有一條, 這樣就沒有達到寫線程的目的。

記住:多線程就是分時利用CPU,宏觀上讓全部線程一塊兒執行 ,也叫併發。

6四、有哪些不一樣的線程生命週期?

 關於Java中線程的生命週期,首先看一下下面這張較爲經典的圖:

Java線程具備五種基本狀態

新建狀態(New):當線程對象對建立後,即進入了新建狀態,如:Thread t = new MyThread()。

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經作好了準備,隨時等待CPU調度執行,並非說執行了t.start()此線程當即就會執行。

運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就緒狀態是進入到運行狀態的惟一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中。

阻塞狀態(Blocked):處於運行狀態中的線程因爲某種緣由,暫時放棄對CPU的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,纔有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的緣由不一樣,阻塞狀態又能夠分爲三種:

1.等待阻塞 -- 運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(由於鎖被其它線程所佔用),它會進入同步阻塞狀態;

3.其餘阻塞 -- 經過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新轉入就緒狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

6五、sleep方法和wait方法有什麼區別?

(1)sleep()方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。

(2)最主要是sleep方法沒有釋放鎖,而wait方法釋放了鎖。

(3)wait,notify和notifyAll只能在同步控制方法或者同步控制塊裏面使用,而sleep能夠在任何地方使用。

(4)sleep必須捕獲異常,而wait,notify和notifyAll不須要捕獲異常。

詳解:

     sleep方法屬於Thread類中方法,表示讓一個線程進入睡眠狀態,等待必定的時間以後,自動醒來進入到可運行狀態,但它不會立刻進入運行狀態,由於其它線程可能正在運行並且沒有被調度爲放棄執行,除非(a) 「醒來」的線程具備更高的優先級;(b)正在運行的線程由於其它緣由而阻塞。 一個線程對象調用了sleep方法以後,並不會釋放他所持有的全部對象鎖,因此也就不會影響其餘進程對象的運行。但在 sleep的過程當中過程當中有可能被其餘對象調用它的interrupt(),產生InterruptedException異常,若是你的程序不捕獲這個異常,線程就會異常終止,進入TERMINATED狀態,若是你的程序捕獲了這個異常,那麼程序就會繼續執行catch語句塊(可能還有finally語句塊)以及之後的代碼。注意sleep()方法是一個靜態方法,也就是說他只對當前對象有效,不能經過t.sleep()讓t對象進入sleep。

    wait屬於Object的成員方法,一旦一個對象調用了wait方法,必需要採用notify()和notifyAll()方法喚醒該進程。若是線程擁有某個或某些對象的同步鎖,那麼在調用了wait()後,這個線程就會釋放它持有的全部同步資源,而不限於這個被調用了wait()方法的對象。從而使線程所在對象中的其它synchronized數據可被別的線程使用。 wait()方法也一樣會在wait的過程當中有可能被其餘對象調用interrupt()方法而產生InterruptedException,效果以及處理方式同sleep()方法。

    sleep指線程被調用時,佔着CPU不工做,形象地說明爲「佔着CPU睡覺」,此時,系統的CPU部分資源被佔用,其餘線程沒法進入,會增長時間限制。wait指線程處於進入等待狀態,形象地說明爲「等待使用CPU」,此時線程不佔用任何資源,不增長時間限制。因此sleep(100L)意思爲:佔用CPU,線程休眠100毫秒;wait(100L)意思爲:不佔用CPU,線程等待100毫秒。

例子:

package com.demo.test;

/**
 * Thread sleep和wait區別
 * @author lixiaoxi
 * 2017-12-22
 */
public class SleepWaitTest implements Runnable{
    
    int number = 10;

    public void firstMethod() throws Exception {
        synchronized (this) {
            number += 100;
            System.out.println(number);
        }
    }

    public void secondMethod() throws Exception {
        synchronized (this) {
            /**
             * (休息2S,阻塞線程)
             * 以驗證當前線程對象的機鎖被佔用時,
             * 是否能夠訪問其餘同步代碼塊
             */
            Thread.sleep(2000);
            //this.wait(2000);
            number *= 200;
        }
    }

    @Override
    public void run() {
        try {
            firstMethod();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        SleepWaitTest threadTest = new SleepWaitTest();
        Thread thread = new Thread(threadTest);
        thread.start();
        threadTest.secondMethod();
        
        System.out.println("number="+threadTest.number);
    }

}

使用sleep() 方法輸出結果:

2100
number=2100

使用wait() 方法輸出結果:

110
number=22000

    咱們來大體分析一下此段代碼,main()方法中實例化ThreadTest並啓動該線程,而後調用該線程的一個方法(secondMethod()),由於在主線程中調用方法,因此調用的普通方法secondMethod())會先被執行(但並非普通方法執行完畢,該對象的線程方法才執行,普通方法執行過程當中,該線程的方法也會被執行,他們是交替執行的,只是在主線程的普通方法會先被執行而已),因此程序運行時會先執行secondMethod(),而secondMethod()方法代碼片斷中有synchronized block,所以secondMethod方法被執行後,該方法會佔有該對象機鎖致使該對象的線程方法一直處於阻塞狀態,不能執行,直到secondeMethod釋放鎖。

    使用Thread.sleep(2000)方法時,由於sleep在阻塞線程的同時,並持有該對象鎖,因此該對象的其餘同步方法(firstMethod())沒法執行,直到synchronized block執行完畢(sleep休眠完畢),firstMethod()方法才能夠執行,所以輸出結果爲number*200+100。最後在main方法中輸出的number值跟在firstMethod() 方法中輸出的結果同樣,都是2100。

    使用this.wait(2000)方法時,secondMethod()方法被執行後也鎖定了該對象的機鎖,執行到this.wait(2000)時,該方法會休眠2s並釋放當前持有的鎖,此時該線程的同步方法會被執行(由於secondMethod持有的鎖,已經被wait()所釋放),所以輸出的結果爲:number+100。而2s 事後,secondMethod方法被喚醒,繼續執行wait後面的語句,即number *=200,所以在main方法中輸出的結果爲number=22000。

6六、爲何線程通訊的方法wait(), notify()和notifyAll()被定義在Object類裏?

(1)這些方法存在於同步中;

(2)使用這些方法必須標識同步所屬的鎖;

(3)鎖能夠是任意對象,因此任意對象調用方法必定定義在Object類中。

簡單說:由於synchronized中的這把鎖能夠是任意對象,因此任意對象均可以調用wait()和notify();因此wait和notify屬於Object。

專業說:由於這些方法在操做同步線程時,都必需要標識它們操做線程的鎖,只有同一個鎖上的被等待線程,能夠被同一個鎖上的notify喚醒,不能夠對不一樣鎖中的線程進行喚醒。也就是說,等待和喚醒必須是同一個鎖。而鎖能夠是任意對象,因此能夠被任意對象調用的方法是定義在object類中。

6七、爲何wait(), notify()和notifyAll()必須在同步方法或者同步塊中被調用?

     當一個線程須要調用對象的wait()方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其餘線程調用這個對象上的notify()方法。一樣的,當一個線程須要調用對象的notify()方法時,它會釋放這個對象的鎖,以便其餘在等待的線程就能夠獲得這個對象鎖。因爲全部的這些方法都須要線程持有對象的鎖,這樣就只能經過同步來實現,因此他們只能在同步方法或者同步塊中被調用。

注意:wait(),notify(),notifyAll()都必須使用在同步中,由於要對持有監視器(鎖)的線程操做。因此要使用在同步中,由於只有同步 才具備鎖。

6八、線程的sleep()方法和yield()方法有什麼區別?

① sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;

② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;

③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;

④ sleep()方法比yield()方法(跟操做系統CPU調度相關)具備更好的可移植性。

    sleep 方法使當前運行中的線程睡眠一段時間,進入不能夠運行狀態,這段時間的長短是由程序設定的,yield方法使當前線程讓出CPU佔有權,但讓出的時間是不可設定的。yield()也不會釋放鎖標誌。

    實際上,yield()方法對應了以下操做:先檢測當前是否有相同優先級的線程處於同可運行狀態,若有,則把CPU的佔有權交給此線程,不然繼續運行原來的線程,因此yield()方法稱爲「退讓」,它把運行機會讓給了同等級的其餘線程。

    sleep 方法容許較低優先級的線程得到運行機會,但yield() 方法執行時,當前線程仍處在可運行狀態,因此不可能讓出較低優先級的線程此時獲取CPU佔有權。在一個運行系統中,若是較高優先級的線程沒有調用sleep方法,也沒有受到I/O阻塞,那麼較低優先級線程只能等待全部較高優先級的線程運行結束,方可有機會運行。

   yield()只是使當前線程從新回到可執行狀態,全部執行yield()的線程有可能在進入到可執行狀態後立刻又被執行,因此yield()方法只能使同優先級的線程有執行的機會。

6九、wait() 與 yield() 的比較

咱們知道,wait()的做用是讓當前線程由「運行狀態」進入「等待(阻塞)狀態」的同時,也會釋放同步鎖。而yield()的做用是讓步,它也會讓當前線程離開「運行狀態」。它們的區別是:

(1) wait()是讓線程由「運行狀態」進入到「等待(阻塞)狀態」,而yield()是讓線程由「運行狀態」進入到「就緒狀態」。

(2) wait()是會讓線程釋放它所持有對象的同步鎖,而yield()方法不會釋放鎖。

70、volatile關鍵字的做用

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:

1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。

2)禁止進行指令重排序。

volatile關鍵字禁止指令重排序有兩層意思:

1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

可能上面說的比較繞,舉個簡單的例子:

//x、y爲非volatile變量
//flag爲volatile變量
 
x = 2;        //語句1
y = 0;        //語句2
volatile flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

    因爲flag變量爲volatile變量,那麼在進行指令重排序的過程的時候,不會將語句3放到語句一、語句2前面,也不會將語句3放到語句四、語句5後面。可是要注意語句1和語句2的順序、語句4和語句5的順序是不做任何保證的。而且volatile關鍵字能保證,執行到語句3時,語句1和語句2一定是執行完畢了的,且語句1和語句2的執行結果對語句三、語句四、語句5是可見的。

volatile與加鎖機制的區別:

加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性。注意:volatile沒辦法保證對變量的操做的原子性。

當且僅當知足如下全部條件時,才應該使用volatile變量:

1)對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。

2)該變量不會與其餘狀態變量一塊兒歸入不變性條件中。

3)在訪問變量時不須要

volatile的原理和實現機制

    下面這段話摘自《深刻理解Java虛擬機》:

  「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

 1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

 2)它會強制將對緩存的修改操做當即寫入主存;

 3)若是是寫操做,它會致使其餘CPU中對應的緩存行無效。

使用volatile關鍵字的場景

  synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。一般來講,使用volatile必須具有如下2個條件:

    1)對變量的寫操做不依賴於當前值

    2)該變量沒有包含在具備其餘變量的不變式中

  實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。事實上,個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。


把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)和 可見性(visibility)。

  • 原子性意味着某個時刻,只有一個線程可以執行一段代碼,這段代碼經過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。
  • 可見性則更爲微妙,它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的。 —— 若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。

volatile的使用條件

volatile 變量具備 synchronized 的可見性特性,可是不具有原子性。這就是說線程可以自動發現 volatile 變量的最新值。

volatile 變量可用於提供線程安全,可是隻能應用於很是有限的一組用例:多個變量之間或者某個變量的當前值與修改後值之間沒有約束。所以,單獨使用 volatile 還不足以實現計數器、互斥鎖或任何具備與多個變量相關的不變式(Invariants)的類(例如 「start <=end」)。

使用條件

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

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

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由(讀取-修改-寫入)操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。實現正確的操做須要使x 的值在操做期間保持不變,而 volatile 變量沒法實現這點。(然而,若是隻從單個線程寫入,那麼能夠忽略第一個條件。)

volatile適用場景

場景1:volatile最適用一個線程寫,多個線程讀的場合。

若是有多個線程併發寫操做,仍然須要使用鎖或者線程安全的容器或者原子變量來代替。(摘自Netty權威指南)

疑問:若是隻是賦值的原子操做,是否能夠多個線程寫?(答案:能夠,可是通常沒有這樣的必要,即沒有這樣的應用場景)

最經典的使用案例:狀態標誌

也許實現 volatile 變量的規範使用僅僅是使用一個布爾狀態標誌,用於指示發生了一個重要的一次性事件,例如完成初始化或請求停機。

volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

線程1執行doWork()的過程當中,可能有另外的線程2調用了shutdown,因此boolean變量必須是volatile。

而若是使用 synchronized 塊編寫循環要比使用 volatile 狀態標誌編寫麻煩不少。因爲 volatile 簡化了編碼,而且狀態標誌並不依賴於程序內任何其餘狀態,所以此處很是適合使用 volatile。

這種類型的狀態標記的一個公共特性是:一般只有一種狀態轉換;shutdownRequested 標誌從false 轉換爲true,而後程序中止。這種模式能夠擴展到來回轉換的狀態標誌,可是隻有在轉換週期不被察覺的狀況下才能擴展(從false 到true,再轉換到false)。此外,還須要某些原子狀態轉換機制,例如原子變量。

    將 volatile 變量做爲狀態標誌使用線程以volatile變量做爲循環控制變量(例如控制線程是否繼續執行,控制線程的生命週期),由另一個線程控制該變量的值(true or false)。這種狀況須要變量具備可見性,volatile變量適合。然而,使用 synchronized 塊編寫循環要比使用volatile 狀態標誌編寫麻煩不少。因爲 volatile 簡化了編碼,而且狀態標誌並不依賴於程序內任何其餘狀態,所以此處很是適合使用 volatile。

場景2:結合使用 volatile 和 synchronized 實現 「開銷較低的讀-寫鎖」

若是讀操做遠遠超過寫操做,您能夠結合使用內部鎖和 volatile 變量來減小公共代碼路徑的開銷。

以下顯示的線程安全的計數器,使用 synchronized 確保增量操做是原子的,並使用 volatile 保證當前結果的可見性。若是更新不頻繁的話,該方法可實現更好的性能,由於讀路徑的開銷僅僅涉及 volatile 讀操做,這一般要優於一個無競爭的鎖獲取的開銷。

public class CheesyCounter {  
    // Employs the cheap read-write lock trick  
    // All mutative operations MUST be done with the 'this' lock held  
    @GuardedBy("this") private volatile int value;  
  
    //讀操做,沒有synchronized,提升性能  
    public int getValue() {   
        return value;   
    }   
  
    //寫操做,必須synchronized。由於x++不是原子操做  
    public synchronized int increment() {  
        return value++;  
    }  
}

使用鎖進行全部變化的操做,使用 volatile 進行只讀操做。其中,鎖一次只容許一個線程訪問值,volatile 容許多個線程執行讀操做。

正確使用volatile關鍵字

    synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。

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

    出於簡易性或可伸縮性的考慮,您可能傾向於使用 volatile 變量而不是鎖。當使用 volatile 變量而非鎖時,某些習慣用法(idiom)更加易於編碼和閱讀。此外,volatile 變量不會像鎖那樣形成線程阻塞,所以也不多形成可伸縮性問題。在某些狀況下,若是讀操做遠遠大於寫操做,volatile 變量還能夠提供優於鎖的性能優點。

使用條件

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

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

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

    第一個條件就是不能是自增自減等操做。第一個條件的限制使 volatile 變量不能用做線程安全計數器。雖然增量操做(x++)看上去相似一個單獨操做,實際上它是一個由(讀取-修改-寫入)操做序列組成的組合操做,必須以原子方式執行,而 volatile 不能提供必須的原子特性。

    第二個條件咱們來舉個例子它包含了一個不變式 :下界老是小於或等於上界。

public class NumberRange {  
    private int lower, upper;  
  
    public int getLower() { return lower; }  
    public int getUpper() { return upper; }  
  
    public void setLower(int value) {   
        if (value > upper)   
            throw new IllegalArgumentException(...);  
        lower = value;  
    }  
  
    public void setUpper(int value) {   
        if (value < lower)   
            throw new IllegalArgumentException(...);  
        upper = value;  
    }  
}

    這種方式限制了範圍的狀態變量。所以將 lower 和 upper 字段定義爲 volatile 類型不可以充分實現類的線程安全;而仍然須要使用同步——使 setLower() 和 setUpper() 操做原子化。

    不然,若是湊巧兩個線程在同一時間使用不一致的值執行 setLower 和 setUpper 的話,則會使範圍處於不一致的狀態。例如,若是初始狀態是(0, 5),同一時間內,線程 A 調用setLower(4) 而且線程 B 調用setUpper(3),顯然這兩個操做交叉存入的值是不符合條件的,那麼兩個線程都會經過用於保護不變式的檢查,使得最後的範圍值是(4, 3) —— 一個無效值,這顯然是不對的。

volatile 與 synchronized 的比較

(1)volatile本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住;

(2)volatile僅能使用在變量級別;synchronized則能夠使用在變量、方法、和類級別的;

(3)volatile僅能實現變量的修改可見性,不能保證原子性而synchronized則能夠保證變量的修改可見性和原子性;

(4)volatile不會形成線程的阻塞,即volatile不能用來同步,由於多個線程併發訪問volatile修飾的變量不會阻塞;synchronized可能會形成線程的阻塞;

(5)當一個域的值依賴於它以前的值時,volatile就沒法工做了,如n=n+1,n++等。若是某個域的值受到其餘域的值的限制,那麼volatile也沒法工做,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。

(6)volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。

總結:

    與鎖相比,volatile 變量是一種很是簡單但同時又很是脆弱的同步機制,它在某些狀況下將提供優於鎖的性能和伸縮性。若是嚴格遵循 volatile 的使用條件即變量真正獨立於其餘變量和本身之前的值 ,在某些狀況下能夠使用 volatile 代替 synchronized 來簡化代碼。然而,使用 volatile 的代碼每每比使用鎖的代碼更加容易出錯。

線程同步小結

(1)線程同步的目的是爲了保護多個線程訪問一個資源時對資源的破壞。

(2)線程同步方法是經過鎖來實現,每一個對象都有且僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘同步方法。

(3)對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。

(4)對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。

(5)編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。

(6)當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。

7一、synchronized關鍵字

    當synchronized關鍵字修飾一個方法的時候,該方法叫作同步方法。若是一個對象有多個synchronized方法,某一時刻某個線程已經進入到了某個synchronized方法,那麼在該方法沒有執行完畢前,其餘線程是沒法訪問該對象的任何synchronized方法的。

 Java中的每一個對象都有一個鎖(lock),或者叫作監視器(monitor),當一個線程訪問某個對象的synchronized方法時,將該對象上鎖其餘任何線程都沒法再去訪問該對象的synchronized方法了(這裏是指全部的同步方法,而不只僅是同一個方法),直到以前的那個線程執行方法完畢後(或者是拋出了異常),纔將該對象的鎖釋放掉,其餘線程纔有可能再去訪問該對象的synchronized方法。注意這時候是給對象上鎖,若是是不一樣的對象,則各個對象之間沒有限制關係。

    當一個synchronized關鍵字修飾的方法同時又被static修飾,以前說過,非靜態的同步方法會將對象上鎖,可是靜態方法不屬於對象,而是屬於類,它會將這個方法所在的類的Class對象上鎖一個類無論生成多少個對象,它們所對應的是同一個Class對象。

(1)當一個線程訪問「某對象」的「synchronized方法」或者「synchronized代碼塊」時,其餘線程對「該對象」的該「synchronized方法」或者「synchronized代碼塊」的訪問將被阻塞。

(2)當一個線程訪問「某對象」的「synchronized方法」或者「synchronized代碼塊」時,其餘線程仍然能夠訪問「該對象」的非同步代碼塊。

(3)當一個線程訪問「某對象」的「synchronized方法」或者「synchronized代碼塊」時,其餘線程對「該對象」的其餘的「synchronized方法」或者「synchronized代碼塊」的訪問將被阻塞。

(4)若是某個synchronized方法是static的,那麼它鎖的並非synchronized方法所在的對象,而是synchronized方法所在對象所對應的Class對象,由於Java中無論一個類有多少對象,這些對象會對應惟一一個Class對象。所以當線程分別訪問同一個類的兩個對象的兩個static synchronized方法時,是順序執行的,亦即一個線程先執行,完畢以後,另外一個纔開始執行。

(5)synchronized 方法是一種粗粒度的併發控制,某一時刻,只能有一個線程執行synchronized方法;synchronized塊則是一種細粒度的併發控制,只會將塊中代碼同步,位於方法內,synchronized塊以外的代碼是能夠被多個線程同時訪問的。

(6)瞭解了鎖的概念和synchronized修飾方法的用法以後咱們能夠總結出,兩個方法是否是互斥的關鍵是看兩個方法取得的鎖是否是互斥的,若是鎖是互斥的,那麼方法也是互斥訪問的,若是鎖不是互斥的,那麼不一樣的鎖之間是不會有什麼影響的,因此這時方法是能夠同時訪問的。

(7)synchronized進過編譯,會在同步塊的先後分別形成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器爲0時,鎖就被釋放了。若是獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另外一個線程釋放爲止。

public class SynDemo{  
  
    public static void main(String[] arg){  
        Runnable t1=new MyThread();  
        new Thread(t1,"t1").start();  
        new Thread(t1,"t2").start();  
    }  
  
}  
class MyThread implements Runnable {  
  
    @Override  
    public void run() {  
        synchronized (this) {  
            for(int i=0;i<10;i++)  
                System.out.println(Thread.currentThread().getName()+":"+i);  
        }  
          
    }  
  
}  

查看字節碼指令:

synchronized原理分析

  synchronized 使用的通常場景,在對象方法和類方法上使用,以及自定義同步代碼塊。可是在方法上使用 synchronized 關鍵字和使用同步代碼塊是不同的,方法上採用同步是採用的字節碼中的標誌位 ACC_SYNCHRONIZED 來進行同步的。而同步代碼塊則是採用了對象頭中的鎖指針指向一個監視器(鎖),來完成同步。

 當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取 monitor ,獲取成功以後才能執行方法體,方法執行完後再釋放 monitor 。在方法執行期間,其餘任何線程都沒法再得到同一個 monitor 對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。

(1)synchronized代碼塊原理

反編譯下面的代碼獲得的字節碼以下:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("hello");
        }
    }

    public synchronized void test(){

    }
}

    當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器爲 0,那線程能夠成功取得 monitor,並將計數器值設置爲 1,取鎖成功。若是當前線程已經擁有 objectref 的 monitor 的持有權,那它能夠重入這個 monitor ,重入時計數器的值也會加 1。假若其餘線程已經擁有 objectref 的 monitor 的全部權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值爲0 ,其餘線程將有機會持有 monitor 。值得注意的是編譯器將會確保不管方法經過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而不管這個方法是正常結束仍是異常結束。爲了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然能夠正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理全部的異常,它的目的就是用來執行 monitorexit 指令。因此看到上面有兩條 monitorexit !

    每一個對象都有一個與其關聯的監視器。當監視器被佔有的時候,監視器就處於鎖定狀態。線程執行moniterenter嘗試得到監視器的全部權。若是監視器的進入次數爲0,線程進入監視器,並將監視器的進入次數置爲1次。接下來此線程就是這個監視器的全部者。若是線程已經擁有了監視器的全部權,當線程重現進入監視器,監視器的進入次數加1。若是其餘線程已經擁有了監視器的全部權,那麼線程將會阻塞,直到監視器的進入次數減到0時,線程纔會再次嘗試獲取監視器的全部權。

(2)synchronized方法原理

    先看一個反編譯的實例方法的結果,確實比普通的方法多了一個標誌字段。方法級的同步是隱式,即無需經過字節碼指令來控制的,它實如今方法調用和返回操做之中。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有 monitor , 而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其餘任何線程都沒法再得到同一個monitor。若是一個同步方法執行期間拋 出了異常,而且在方法內部沒法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法以外時自動釋放。

    對同步方法,JVM採用ACC_SYNCHRONIZED標記符來實現同步。 對於同步代碼塊。JVM採用monitorentermonitorexit兩個指令來實現同步。同步方法經過ACC_SYNCHRONIZED關鍵字隱式的對方法進行加鎖。當線程要執行的方法被標註上ACC_SYNCHRONIZED時,須要先得到鎖才能執行該方法。同步代碼塊經過monitorentermonitorexit執行來進行加鎖,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。當線程執行到monitorenter的時候要先得到所鎖,才能執行後面的方法。當線程執行到monitorexit的時候則要釋放鎖。

    方法的同步並無經過指令monitorenter和monitorexit來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。

    每一個對象自身維護這一個被加鎖次數的計數器,當計數器數字爲0時表示能夠被任意線程得到鎖。當計數器不爲0時,只有得到鎖的線程才能再次得到鎖,便可重入鎖。換句話說,一個線程獲取到鎖以後能夠無限次地進入該臨界區。

synchronized關鍵字鎖住的是什麼東西?

    不管你將synchronized加在方法【非static,static的後面還會說】前仍是加在一個變量【非static,static的後面還會說】前,其鎖定的都是一個對象。 每個對象都只有一個鎖與之相關聯。

上面兩種寫法是同樣的,都是鎖定實例對象

下面的寫法都是鎖定類對象。在下面的例子中是鎖定的Demo3這個類。【當鎖定static變量的時候,因爲static變量只有一份拷貝,因此此時鎖定的也是類對象】

 在這種狀況下,若是有一個線程thread 訪問了這4個方法中的任何一個, 在同一時間內其它的線程都不能訪問這4個方法。


     在Java中,synchronized關鍵字是用來控制線程同步的,就是在多線程的環境下,控制synchronized代碼段不被多個線程同時執行。synchronized能夠保證在同一時刻,只有一個線程能夠執行某一個方法或者代碼塊,同時它還能夠保證共享變量的內存可見性。

    synchronized是Java中解決併發問題的一種最經常使用的方法,也是最簡單的一種方法。synchronized的做用主要有三個:

(1)確保線程互斥的訪問同步代碼

(2)保證共享變量的修改可以及時可見

(3)有效解決重排序問題。從語法上講,它總共有三種使用場合:

(1)修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖

(2)修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖

(3)修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

 

  1. 普通同步方法,鎖是當前實例對象
  2. 靜態同步方法,鎖是當前類的class對象
  3. 同步方法塊,鎖是括號裏面的對象

 

synchronized既能夠加在一段代碼上,也能夠加在方法上。關鍵是,不要認爲給方法或者代碼段加上synchronized就萬事大吉,看下面一段代碼:

class Sync {  
  
    public synchronized void test() {  
        System.out.println("test開始..");  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("test結束..");  
    }  
}  
  
class MyThread extends Thread {  
  
    public void run() {  
        Sync sync = new Sync();  
        sync.test();  
    }  
}  
  
public class Main {  
  
    public static void main(String[] args) {  
        for (int i = 0; i < 3; i++) {  
            Thread thread = new MyThread();  
            thread.start();  
        }  
    }  
}  

運行結果: test開始.. test開始.. test開始.. test結束.. test結束.. test結束..

能夠看出來,上面的程序起了三個線程,同時運行Sync類中的test()方法,雖然test()方法加上了synchronized,可是仍是同時運行起來,貌似synchronized沒起做用。 將test()方法上的synchronized去掉,在方法內部加上synchronized(this):

public void test() {  
    synchronized(this){  
        System.out.println("test開始..");  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("test結束..");  
    }  
}  

運行結果: test開始.. test開始.. test開始.. test結束.. test結束.. test結束..

一切仍是這麼平靜,沒有看到synchronized起到做用。 實際上,synchronized(this)以及非static的synchronized方法(至於static synchronized方法請往下看),只能防止多個線程同時執行同一個對象的同步代碼段。

回到本文的題目上:synchronized鎖住的是代碼仍是對象。答案是:synchronized鎖住的是括號裏的對象,而不是代碼。對於非static的synchronized方法,鎖的就是對象自己也就是this。

當synchronized鎖住一個對象後,別的線程若是也想拿到這個對象的鎖,就必須等待這個線程執行完成釋放鎖,才能再次給對象加鎖,這樣才達到線程同步的目的。即便兩個不一樣的代碼段,都要鎖同一個對象,那麼這兩個代碼段也不能在多線程環境下同時運行。

因此咱們在用synchronized關鍵字的時候,能縮小代碼段的範圍就儘可能縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減少鎖的粒度,使代碼更大程度的併發。緣由是基於以上的思想,鎖的代碼段太長了,別的線程是否是要等好久。固然這段是題外話,與本文核心思想並沒有太大關聯。

再看上面的代碼,每一個線程中都new了一個Sync類的對象,也就是產生了三個Sync對象,因爲不是同一個對象,因此能夠多線程同時運行synchronized方法或代碼段。爲了驗證上述的觀點,修改一下代碼,讓三個線程使用同一個Sync的對象。

class MyThread extends Thread {  
  
    private Sync sync;  
  
    public MyThread(Sync sync) {  
        this.sync = sync;  
    }  
  
    public void run() {  
        sync.test();  
    }  
}  
  
public class Main {  
  
    public static void main(String[] args) {  
        Sync sync = new Sync();  
        for (int i = 0; i < 3; i++) {  
            Thread thread = new MyThread(sync);  
            thread.start();  
        }  
    }  
}  

運行結果: test開始.. test結束.. test開始.. test結束.. test開始.. test結束..

能夠看到,此時的synchronized就起了做用。 那麼,若是真的想鎖住這段代碼,要怎麼作?也就是,若是仍是最開始的那段代碼,每一個線程new一個Sync對象,怎麼才能讓test方法不會被多線程執行。 

解決也很簡單,只要鎖住同一個對象不就好了。例如,synchronized後的括號中鎖同一個固定對象,這樣就好了。這樣是沒問題,可是,比較多的作法是讓synchronized鎖這個類對應的Class對象。

class Sync {  
  
    public void test() {  
        synchronized (Sync.class) {  
            System.out.println("test開始..");  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            System.out.println("test結束..");  
        }  
    }  
}  
  
class MyThread extends Thread {  
  
    public void run() {  
        Sync sync = new Sync();  
        sync.test();  
    }  
}  
  
public class Main {  
  
    public static void main(String[] args) {  
        for (int i = 0; i < 3; i++) {  
            Thread thread = new MyThread();  
            thread.start();  
        }  
    }  
}  

運行結果: test開始.. test結束.. test開始.. test結束.. test開始.. test結束..

上面代碼用synchronized(Sync.class)實現了全局鎖的效果。

static synchronized方法,static方法能夠直接用類名調用,方法中沒法使用this,因此它鎖的不是this,而是類的Class對象,因此,static synchronized方法也至關於全局鎖。

synchronized若是做用在一段代碼上,那麼是鎖什麼?

synchronized關鍵字。除了修飾方法以外,還能夠修飾代碼塊,一共有如下5種用法。

(1)this

synchronizedthis){
    //互斥代碼
}

 這裏的this指的是執行這段代碼的對象,synchronized獲得的鎖就是this這個對象的鎖

(2)A.class

synchronized(A.class){
    //互斥代碼
}

這裏A.class獲得的是A這類,因此synchronized關鍵字獲得的鎖是類的鎖,這種方法同下面的方法功能是相同的:

public static synchronized void fun(){
    //互斥代碼
}

全部須要類的鎖的方法等不能同時執行,可是它和須要某個對象的鎖的方法或者是不須要任何鎖的方法能夠同時執行。

(3)object.getClass()

synchronized(object.getClass){
    //互斥代碼
}

這種方法通常狀況下同第二種是相同,可是出現繼承和多態時,獲得的結果倒是不相同的。因此通常狀況下推薦使用A.class的方式。

(4)object

synchronized(object){
    //互斥代碼
}

這裏synchronized關鍵字拿到的鎖是對象object的鎖,全部須要這個對象的鎖的方法都不能同時執行。

public class Trans {
    private Object lock = new Object();

    public void printNum(int num){
        synchronized (lock) {
            System.out.print(Thread.currentThread());  
            for(int i=0;i<25;i++){  
                System.out.print(i+" ");  
            }  
            System.out.println();
        }          
    }
}
class MyThread implements Runnable {  
    private Trans trans;  
    private int num;  

    public MyThread(Trans trans, int num) {  
        this.trans = trans;  
        this.num = num;  
    }  

    public void run() {  
        while (true)  
        {  
            trans.printNum(num);  
            try {  
                Thread.sleep(500);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  

    }  
}

public class Test {  

    public static void main(String[] args) {  

        Trans t = new Trans();  
        Trans t1 = new Trans();  
        Thread a = new Thread(new MyThread(t, 1));  
        Thread b = new Thread(new MyThread(t1, 2));  

        a.start();  
        b.start();  

    }  
}

在上邊的例子中試圖使用這種方法達到互斥方法打印方法,可是事實是這樣作是沒有效果的,由於每一個Trans對象都有本身的Object對象,這兩個對象都有本身的鎖,因此兩個線程須要的是不一樣鎖,兩個鎖之間沒有任何相互做用,因此不會起到互斥做用。

(5)static object

上邊的代碼稍做修改就能夠起到互斥做用,將Trans類中Object對象的聲明改成下面這樣:

private static Object lock = new Object();

這樣不一樣的類使用的就是同一個object對象,須要的鎖也是同一個鎖,就能夠達到互斥的效果了。

總結:

synchronized關鍵字的用法,看似很是複雜,其實抓住要點以後仍是很好區分的,只要看synchronized得到的是哪一個對象或者類的鎖就行啦,其餘須要這個鎖的方法都不能同時執行,不須要這個鎖的方法都能同時執行。最後還要告別一個誤區,相信你們都不會再犯這種錯誤了,synchronized鎖住的是一個對象或者類(其實也是對象),而不是方法或者代碼段。

volatile和synchronized講一下?

    synchronized保證了當有多個線程同時操做共享數據時,任什麼時候刻只有一個線程能進入臨界區操做共享數據,其餘線程必須等待。所以它能夠保證操做的原子性。synchronized經過同步鎖保證線程安全,進入臨界區前必須得到對象的鎖,其餘沒有得到鎖的線程不可進入。當臨界區中的線程操做完畢後,它會釋放鎖,此時其餘線程能夠競爭鎖,獲得鎖的那個線程即可以進入臨界區。

    synchronized還能夠保證可見性。由於對一個變量的unlock操做以前,必須先把此變量同步回主內存中。它還能夠保證有序性,由於一個變量在任什麼時候刻只能有一個線程對其進行lock操做(也就是任什麼時候刻只有一個線程能夠得到該鎖對象),這決定了持有同一把鎖的兩個同步塊只能串行進入。

    volatile是一個關鍵字,用於修飾變量。被其修飾的變量具備可見性和有序性。

    可見性,當一條線程修改了這個變量的值,新值能被其餘線程馬上觀察到。具體來講,volatile的做用是:在本CPU對變量的修改直接寫入主內存中,同時這個寫操做使得其餘CPU中對應變量的緩存行無效,這樣其餘線程在讀取這個變量時候必須從主內存中讀取,因此讀取到的是最新的,這就是上面說得能被當即「看到」。

    有序性,volatile能夠禁止指令重排。volatile在其彙編代碼中有一個lock操做,這個操做至關於一個內存屏障,指令重排不能越過內存屏障。具體來講在執行到volatile變量時,內存屏障以前的語句必定被執行過了且結果對後面是已知的,而內存屏障後面的語句必定還沒執行到;在volatile變量以前的語句不能被重排後其以後,相反其後的語句也不能被重排到以前。

 

7二、ReentrantLock

    ReentrantLock 是一個可重入的互斥(/獨佔)鎖,又稱爲「獨佔鎖」。ReentrantLock經過自定義隊列同步器(AQS-AbstractQueuedSychronized,是實現鎖的關鍵)來實現鎖的獲取與釋放。其能夠徹底替代 synchronized 關鍵字。JDK 5.0 早期版本,其性能遠好於 synchronized,但 JDK 6.0 開始,JDK 對 synchronized 作了大量的優化,使得二者差距並不大。

「獨佔」,就是在同一時刻只能有一個線程獲取到鎖,而其它獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程纔可以獲取鎖。

「可重入」,就是支持重進入的鎖,它表示該鎖可以支持一個線程對資源的重複加鎖。

該鎖還支持獲取鎖時的公平和非公平性選擇。「公平」是指「不一樣的線程獲取鎖的機制是公平的」,而「不公平」是指「不一樣的線程獲取鎖的機制是非公平的」。

  ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和內存語義,可是添加了相似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用狀況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 能夠花更少的時候來調度線程,把更多時間用在執行線程上。)

    reentrant 鎖意味着什麼呢?簡單來講,它有一個與鎖相關的獲取計數器,若是擁有鎖的某個線程再次獲得鎖,那麼獲取計數器就加1,而後鎖須要被釋放兩次才能得到真正釋放。這模仿了 synchronized 的語義,若是線程進入由線程已經擁有的監控器保護的 synchronized 塊,就容許線程繼續進行,當線程退出第二個(或者後續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。

    Lock 和 synchronized 有一點明顯的區別 —— lock 必須在 finally 塊中釋放。不然,若是受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什麼,可是實際上,它極爲重要。忘記在 finally 塊中釋放鎖,可能會在程序中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣纔有找到源頭在哪。而使用同步,JVM 將確保鎖會得到自動釋放。

    因爲ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有如下3項:

  (1)等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程能夠選擇放棄等待,這至關synchronized來講能夠避免出現死鎖的狀況。

  (2)公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序得到鎖,synchronized鎖非公平鎖,ReentrantLock默認的構造函數是建立的非公平鎖,能夠經過參數true設爲公平鎖,但公平鎖表現的性能不是很好。

  (3)鎖綁定多個條件,一個ReentrantLock對象能夠同時綁定對個對象。

ReenTrantLock實現的原理:

    簡單來講,ReenTrantLock的實現是一種自旋鎖,經過循環調用CAS操做來實現加鎖。它的性能比較好也是由於避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是咱們去分析和理解鎖設計的關鍵鑰匙。

何時選擇用 ReentrantLock 代替 synchronized

    既然如此,咱們何時才應該使用 ReentrantLock 呢?答案很是簡單 —— 在確實須要一些 synchronized 所沒有的特性的時候,好比時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者鎖投票。 ReentrantLock 還具備可伸縮性的好處,應當在高度爭用的狀況下使用它,可是請記住,大多數 synchronized 塊幾乎歷來沒有出現過爭用,因此能夠把高度爭用放在一邊。我建議用 synchronized 開發,直到確實證實 synchronized 不合適,而不要僅僅是假設若是使用 ReentrantLock 「性能會更好」。請記住,這些是供高級用戶使用的高級工具。(並且,真正的高級用戶喜歡選擇可以找到的最簡單工具,直到他們認爲簡單的工具不適用爲止。)。一如既往,首先要把事情作好,而後再考慮是否是有必要作得更快。

7三、synchronized 與 ReentrantLock

類似點:

(1)synchronized 與 ReentrantLock關鍵字同樣,屬於互斥鎖。所謂互斥鎖, 指的是一次最多隻能有一個線程持有的鎖。這兩種同步方式有不少類似之處,它們都是加鎖方式同步,並且都是阻塞式的同步,也就是說當若是一個線程得到了對象鎖,進入了同步塊,其餘訪問該同步塊的線程都必須阻塞在同步塊外面等待。

(2)從名字上理解,ReenTrantLock的字面意思就是再進入的鎖,其實synchronized關鍵字所使用的鎖也是可重入的,二者關於這個的區別不大。二者都是同一個線程每進入一次,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。

區別:

(1)鎖的實現:

    synchronized是依賴於JVM實現的,而ReentrantLock是JDK實現的,有什麼區別,說白了就相似於操做系統來控制實現和用戶本身敲代碼實現的區別。前者的實現是比較難見到的,後者有直接的源碼可供閱讀。

    synchronized是在JVM層面上實現的,不但能夠經過一些監控工具監控synchronized的鎖定,並且在代碼執行時出現異常,JVM會自動釋放鎖定,可是使用Lock則不行,lock是經過代碼實現的,要保證鎖定必定會被釋放,就必須將unLock()放到finally{}中。

(2)等待可中斷:

    ReenTrantLock提供了一種可以中斷等待鎖的線程的機制,經過lock.lockInterruptibly()來實現這個機制。等待可中斷,即持有鎖的線程長期不釋放的時候,正在等待的線程能夠選擇放棄等待,這至關於synchronized來講能夠避免出現死鎖的狀況。

   具體來講,假如業務代碼中有兩個線程,Thread1 Thread2。假設 Thread1 獲取了對象object的鎖,Thread2將等待Thread1釋放object的鎖。

   使用synchronized。若是Thread1不釋放,Thread2將一直等待,不能被中斷。synchronized也能夠說是Java提供的原子性內置鎖機制。內部鎖扮演了互斥鎖(mutual exclusion lock ,mutex)的角色,一個線程引用鎖的時候,別的線程阻塞等待。

   使用ReentrantLock。若是Thread1不釋放,Thread2等待了很長時間之後,能夠中斷等待,轉而去作別的事情。

(3)公平鎖:

     ReenTrantLock能夠指定是公平鎖仍是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先得到鎖。

    公平鎖是指多個線程在等待同一個鎖時,必須按照申請的時間順序來依次得到鎖;而非公平鎖則不能保證這一點。非公平鎖在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。 synchronized的鎖是非公平鎖,ReentrantLock默認狀況下也是非公平鎖,但能夠經過帶布爾值的構造函數要求使用公平鎖。

(4)綁定多個條件

    ReentrantLock能夠同時綁定多個Condition對象,只需屢次調用newCondition方法便可。 synchronized中,鎖對象的wait()和notify()或notifyAll()方法能夠實現一個隱含的條件。但若是要和多於一個的條件關聯的時候,就不得不額外添加一個鎖。

(5)喚醒指定線程

ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒須要喚醒的線程們,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒所有線程。

(6)性能的區別:

    在synchronized優化之前,synchronized的性能是比ReenTrantLock差不少的,可是自從synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,二者的性能就差很少了,在兩種方法均可用的狀況下,官方甚至建議使用synchronized,其實synchronized的優化我感受就借鑑了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。

synchronized和重入鎖的區別?

synchronized是JVM的內置鎖,而重入鎖是Java代碼實現的。重入鎖是synchronized的擴展,能夠徹底代替後者。重入鎖能夠重入,容許同一個線程連續屢次得到同一把鎖。其次,重入鎖獨有的功能有:

  • 能夠響應中斷,synchronized要麼得到鎖執行,要麼保持等待。而重入鎖能夠響應中斷,使得線程在遲遲得不到鎖的狀況下,能夠再也不等待。主要由lockInterruptibly()實現,這是一個能夠對中斷進行響應的鎖申請動做,鎖中斷能夠避免死鎖。
  • 鎖的申請能夠有等待時限,用tryLock()能夠實現限時等待,若是超時還未得到鎖會返回false,也防止了線程遲遲得不到鎖時一直等待,可避免死鎖。
  • 公平鎖,即鎖的得到按照線程先來後到的順序依次得到,不會產生飢餓現象。synchronized的鎖默認是不公平的,重入鎖可經過傳入構造方法的參數實現公平鎖。
  • 重入鎖能夠綁定多個Condition條件,這些condition經過調用await/singal實現線程間通訊。

7四、爲何要使用線程池?

    假設一個服務器完成一項任務所需時間爲:t1 建立線程時間,t2 在線程中執行任務的時間,t3 銷燬線程時間,那麼線程運行的總時間 T= t1+t2+t3。假如,有些業務邏輯須要頻繁的使用線程執行某些簡單的任務,那麼不少時間都會浪費t1和t3上。爲了不這種問題,JAVA提供了線程池。在線程池中的線程能夠複用,當線程運行完任務以後,不被銷燬,而是能夠繼續執行其餘的任務。

    相對來講,使用線程池,會預建立一些線程,它們不斷的從工做隊列中取出任務,而後執行該任務。當工做線程執行完一個任務後,就會繼續執行工做隊列中的另外一個任務。優勢以下:

  • 減小了建立和銷燬的次數,每一個工做線程均可以一直被重用,能執行多個任務。
  • 能夠根據系統的承載能力,方便的調整線程池中線程的數目,防止由於消耗過量的系統資源而致使系統崩潰。

     在Java中,若是每當一個請求到達就建立一個新線程,開銷是至關大的。在實際使用中,每一個請求建立新線程的服務器在建立和銷燬線程上花費的時間和消耗的系統資源,甚至可能要比花在處理實際的用戶請求的時間和資源要多得多。除了建立和銷燬線程的開銷以外,活動的線程也須要消耗系統資源。若是在一個JVM裏建立太多的線程,可能會致使系統因爲過分消耗內存或「切換過分」而致使系統資源不足。爲了防止資源不足,服務器應用程序須要一些辦法來限制任何給定時刻處理的請求數目,儘量減小建立和銷燬線程的次數,特別是一些資源耗費比較大的線程的建立和銷燬,儘可能利用已有對象來進行服務,這就是「池化資源」技術產生的緣由。 

     線程池主要用來解決線程生命週期開銷問題和資源不足問題。經過對多個任務重用線程,線程建立的開銷就被分攤到了多個任務上了,並且因爲在請求到達時線程已經存在,因此消除了線程建立所帶來的延遲。這樣,就能夠當即爲請求服務,使應用程序響應更快。另外,經過適當地調整線程池中的線程數目能夠防止出現資源不足的狀況。 

線程池適合應用的場合

    當一個Web服務器接受到大量短小線程的請求時,使用線程池技術是很是合適的,它能夠大大減小線程的建立和銷燬次數,提升服務器的工做效率。但若是線程要求的運行時間比較長,此時線程的運行時間比建立時間要長得多,單靠減小建立時間對系統效率的提升不明顯,此時就不適合應用線程池技術,須要藉助其它的技術來提升服務器的服務效率。

7五、使用線程池的好處

(1)、下降資源消耗

能夠重複利用已建立的線程下降線程建立和銷燬形成的消耗。

(2)、提升響應速度

當任務到達時,任務能夠不須要等到線程建立就能當即執行。

(3)、提升線程的可管理性

線程是稀缺資源,若是無限制地建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一分配、調優和監控。

線程池的核心參數

    線程池的核心實現即 ThreadPoolExecutor 類。該類包含了幾個核心屬性,這些屬性在可在構造方法進行初始化。在介紹核心屬性前,咱們先來看看 ThreadPoolExecutor 的構造方法,以下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

如上所示,構造方法的參數即核心參數,這裏我用一個表格來簡要說明一下各個參數的意義。以下:

參數 說明
corePoolSize 核心線程數。當線程數小於該值時,線程池會優先建立新線程來執行新任務
maximumPoolSize 線程池所能維護的最大線程數
keepAliveTime 空閒線程的存活時間
workQueue 任務隊列,用於緩存未執行的任務
threadFactory 線程工廠。可經過工廠爲新建的線程設置更有意義的名字
handler 拒絕策略。當線程池和任務隊列均處於飽和狀態時,使用拒絕策略處理新任務。默認是 AbortPolicy,即直接拋出異常

 

 

 

 

 

 

 

 

 

7六、線程池的任務處理策略

  • 若是當前線程池中的線程數目小於corePoolSize(核心池大小),則每來一個任務,就會建立一個線程去執行這個任務;
  • 若是當前線程池中的線程數目>=corePoolSize,則每來一個任務,會嘗試將其添加到任務緩存隊列當中,若添加成功,則該任務會等待空閒線程將其取出去執行;若添加失敗(通常來講是任務緩存隊列已滿),則會嘗試建立新的線程去執行這個任務;
  • 若是當前線程池中的線程數目達到maximumPoolSize(線程池最大線程數),則會採起任務拒絕策略進行處理;
  • 若是線程池中的線程數量大於 corePoolSize時,若是某線程空閒時間超過keepAliveTime(表示線程沒有任務執行時最多保持多久時間會終止),線程將被終止,直至線程池中的線程數目不大於corePoolSize;若是容許爲核心池中的線程設置存活時間,那麼核心池中的線程空閒時間超過keepAliveTime,線程也會被終止。

簡化一下上面的規則:

序號 條件 動做
1 線程數 < corePoolSize 建立新線程
2 線程數 ≥ corePoolSize,且 workQueue 未滿 緩存新任務
3 corePoolSize ≤ 線程數 < maximumPoolSize,且 workQueue 已滿 建立新線程
4 線程數 ≥ maximumPoolSize,且 workQueue 已滿 使用拒絕策略處理

 

 

 

 

 

 

 

流程圖:

排隊策略

    如線程建立規則中所說,當線程數量大於等於 corePoolSize,workQueue 未滿時,則緩存新任務。這裏要考慮使用什麼類型的容器緩存新任務,經過 JDK 文檔介紹,咱們可知道有3中類型的容器可供使用,分別是同步隊列有界隊列無界隊列。對於有優先級的任務,這裏還能夠增長優先級隊列。以上所介紹的4中類型的隊列,對應的實現類以下:

實現類 類型 說明
SynchronousQueue 同步隊列 該隊列不存儲元素,每一個插入操做必須等待另外一個線程調用移除操做,不然插入操做會一直阻塞
ArrayBlockingQueue 有界隊列 基於數組的阻塞隊列,按照 FIFO 原則對元素進行排序
LinkedBlockingQueue 無界隊列 基於鏈表的阻塞隊列,按照 FIFO 原則對元素進行排序
PriorityBlockingQueue 優先級隊列 具備優先級的阻塞隊列

 

 

 

 

 

 

 

 

拒絕策略

    如線程建立規則中所說,線程數量大於等於 maximumPoolSize,且 workQueue 已滿,則使用拒絕策略處理新任務。Java 線程池提供了4中拒絕策略實現類,以下:

實現類 說明
AbortPolicy 丟棄新任務,並拋出 RejectedExecutionException
DiscardPolicy 不作任何操做,直接丟棄新任務
DiscardOldestPolicy 丟棄隊列隊首的元素,並執行新任務
CallerRunsPolicy 由調用線程執行新任務

 

 

 

 

 

 

 

以上4個拒絕策略中,AbortPolicy 是線程池實現類所使用的策略。咱們也能夠經過方法

public void setRejectedExecutionHandler(RejectedExecutionHandler)

修改線程池決絕策略。

幾種經常使用的線程池

(1)、FixedThreadPool - 線程池大小固定,任務隊列無界

下面是 Executors 類 newFixedThreadPool 方法的源碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

能夠看到 corePoolSize 和 maximumPoolSize 設置成了相同的值,此時不存在線程數量大於核心線程數量的狀況,因此KeepAlive時間設置不會生效。任務隊列使用的是不限制大小的 LinkedBlockingQueue ,因爲是無界隊列因此容納的任務數量沒有上限,所以,FixedThreadPool的行爲以下:

1)從線程池中獲取可用線程執行任務,若是沒有可用線程則使用ThreadFactory建立新的線程,直到線程數達到nThreads。

2)線程池線程數達到nThreads之後,新的任務將被放入隊列。

 FixedThreadPool的優勢是可以保證全部的任務都被執行,永遠不會拒絕新的任務;同時缺點是隊列數量沒有限制,在任務執行時間無限延長的這種極端狀況下會形成內存問題。

(2)、SingleThreadExecutor - 線程池大小固定爲1,任務隊列無界

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

這個工廠方法中使用無界LinkedBlockingQueue,而且將線程數設置成1,除此之外還使用FinalizableDelegatedExecutorService類進行了包裝。這個包裝類的主要目的是爲了屏蔽ThreadPoolExecutor中動態修改線程數量的功能,僅保留ExecutorService中提供的方法。雖然是單線程處理,一旦線程由於處理異常等緣由終止的時候,ThreadPoolExecutor會自動建立一個新的線程繼續進行工做。

 SingleThreadExecutor 適用於在邏輯上須要單線程處理任務的場景,同時無界的LinkedBlockingQueue保證新任務都可以放入隊列,不會被拒絕;缺點和FixedThreadPool相同,當處理任務無限等待的時候會形成內存問題。

(3)、CachedThreadPool - 線程池無限大(MAX INT),等待隊列長度爲1

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

SynchronousQueue是一個只有1個元素的隊列,入隊的任務須要一直等待直到隊列中的元素被移出。核心線程數是0,意味着全部任務會先入隊列;最大線程數是Integer.MAX_VALUE,能夠認爲線程數量是沒有限制的。KeepAlive時間被設置成60秒,意味着在沒有任務的時候線程等待60秒之後退出。CachedThreadPool對任務的處理策略是提交的任務會當即分配一個線程進行執行,線程池中線程數量會隨着任務數的變化自動擴張和縮減,在任務執行時間無限延長的極端狀況下會建立過多的線程。

三種ExecutorService特性總結

類型

核心線程數

最大線程數

Keep Alive 時間

任務隊列

任務處理策略

FixedThreadPool 固定大小 固定大小(與核心線程數相同) 0 LinkedBlockingQueue 線程池大小固定,沒有可用線程的時候任務會放入隊列等待,隊列長度無限制
SingleThreadExecutor 1 1 0 LinkedBlockingQueue 與 FixedThreadPool 相同,區別在於線程池的大小爲1,適用於業務邏輯上只容許1個線程進行處理的場景
CachedThreadPool 0 Integer.MAX_VALUE 1分鐘 SynchronousQueue 線程池的數量無限大,新任務會直接分配或者建立一個線程進行執行

 

 

 

 

 

 

 

 

 

 

 

總結:

1. newSingleThreadExecutor

建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。

2.newFixedThreadPool

建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。

3. newCachedThreadPool

建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,

那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。

4.newScheduledThreadPool

建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

線程池的種類,區別和使用場景

newCachedThreadPool:

  • 底層:返回ThreadPoolExecutor實例,corePoolSize爲0;maximumPoolSize爲Integer.MAX_VALUE;keepAliveTime爲60L;unit爲TimeUnit.SECONDS;workQueue爲SynchronousQueue(同步隊列)
  • 通俗:當有新任務到來,則插入到SynchronousQueue中,因爲SynchronousQueue是同步隊列,所以會在池中尋找可用線程來執行,如有可用線程則執行,若沒有可用線程則建立一個線程來執行該任務;若池中線程空閒時間超過指定大小,則該線程會被銷燬。
  • 適用:執行不少短時間異步的小程序或者負載較輕的服務器

newFixedThreadPool:

  • 底層:返回ThreadPoolExecutor實例,接收參數爲所設定線程數量nThread,corePoolSize爲nThread,maximumPoolSize爲nThread;keepAliveTime爲0L(不限時);unit爲:TimeUnit.MILLISECONDS;WorkQueue爲:new LinkedBlockingQueue<Runnable>() 無解阻塞隊列
  • 通俗:建立可容納固定數量線程的池子,每一個線程的存活時間是無限的,當池子滿了就不在添加線程了;若是池中的全部線程均在繁忙狀態,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
  • 適用:執行長期的任務,性能好不少

newSingleThreadExecutor:

  • 底層:FinalizableDelegatedExecutorService包裝的ThreadPoolExecutor實例,corePoolSize爲1;maximumPoolSize爲1;keepAliveTime爲0L;unit爲:TimeUnit.MILLISECONDS;workQueue爲:new LinkedBlockingQueue<Runnable>() 無解阻塞隊列
  • 通俗:建立只有一個線程的線程池,且線程的存活時間是無限的;當該線程正繁忙時,對於新任務會進入阻塞隊列中(無界的阻塞隊列)
  • 適用:一個任務一個任務執行的場景

NewScheduledThreadPool:

  • 底層:建立ScheduledThreadPoolExecutor實例,corePoolSize爲傳遞來的參數,maximumPoolSize爲Integer.MAX_VALUE;keepAliveTime爲0;unit爲:TimeUnit.NANOSECONDS;workQueue爲:new DelayedWorkQueue() 一個按超時時間升序排序的隊列
  • 通俗:建立一個固定大小的線程池,線程池內線程存活時間無限制,線程池能夠支持定時及週期性任務執行,若是全部線程均處於繁忙狀態,對於新任務會進入DelayedWorkQueue隊列中,這是一種按照超時時間排序的隊列結構
  • 適用:週期性執行任務的場景

備註:

     通常若是線程池任務隊列採用LinkedBlockingQueue隊列的話,那麼不會拒絕任何任務(由於隊列大小沒有限制),這種狀況下,ThreadPoolExecutor最多僅會按照最小線程數來建立線程,也就是說線程池大小被忽略了。

     若是線程池任務隊列採用ArrayBlockingQueue隊列的話,那麼ThreadPoolExecutor將會採起一個很是負責的算法,好比假定線程池的最小線程數爲4,最大爲8所用的ArrayBlockingQueue最大爲10。隨着任務到達並被放到隊列中,線程池中最多運行4個線程(即最小線程數)。即便隊列徹底填滿,也就是說有10個處於等待狀態的任務,ThreadPoolExecutor也只會利用4個線程。若是隊列已滿,而又有新任務進來,此時纔會啓動一個新線程,這裏不會由於隊列已滿而拒接該任務,相反會啓動一個新線程。新線程會運行隊列中的第一個任務,爲新來的任務騰出空間。

     這個算法背後的理念是:該池大部分時間僅使用核心線程(4個),即便有適量的任務在隊列中等待運行。這時線程池就能夠用做節流閥。若是擠壓的請求變得很是多,這時該池就會嘗試運行更多的線程來清理;這時第二個節流閥—最大線程數就起做用了。

Java阻塞隊列

    隊列以一種先進先出的方式管理數據,阻塞隊列(BlockingQueue)是一個支持兩個附加操做的隊列,這兩個附加的操做是:當從隊列中獲取或者移除元素時,若是隊列爲空,須要等待,直到隊列不爲空;同時若是向隊列中添加元素時,此時若是隊列無可用空間,也須要等待。在多線程進行合做時,阻塞隊列是頗有用的工具。

    生產者-消費者模式:阻塞隊列經常使用於生產者和消費者的場景,生產者線程能夠按期的把中間結果存到阻塞隊列中,而消費者線程把中間結果取出並在未來修改它們。隊列會自動平衡負載,若是生產者線程集運行的比消費者線程集慢,則消費者線程集在等待結果時就會阻塞;若是生產者線程集運行的快,那麼它將等待消費者線程集遇上來。

Java中的阻塞隊列

(1)ArrayBlockingQueue:基於數組的FIFO隊列,是有界的,建立時必須指定大小

(2)LinkedBlockingQueue: 基於鏈表的FIFO隊列,是無界的,默認大小是 Integer.MAX_VALUE

(3)synchronousQueue:一個比較特殊的隊列,雖然它是無界的,但它不會保存任務,每個新增任務的線程必須等待另外一個線程取出任務(即在某次添加任務後必須等待其餘線程取走後才能繼續添加),也能夠把它當作容量爲0的隊列。

總結:

ArrayBlockingQueue: 基於數組的阻塞隊列,在內部維護了一個定長數組,以便緩存隊列中的數據對象。並無實現讀寫分離,也就意味着生產和消費不能徹底並行。是一個有界隊列。

LinkedBlockingQueue:基於列表的阻塞隊列,在內部維護了一個數據緩衝隊列(由一個鏈表構成),實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操做的徹底並行運行。是一個無界隊列。 LinkedBlockingQueue之因此可以高效的處理併發數據,是由於其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的狀況下生產者和消費者能夠並行地操做隊列中的數據,以此來提升整個隊列的併發性能。

SynchronousQueue: 沒有緩存的隊列,生存者生產的數據直接會被消費者獲取並消費。若沒有數據就直接調用出棧方法則會報錯。

三種隊列使用場景

newFixedThreadPool 線程池採用的隊列是LinkedBlockingQueue。其優勢是無界可緩存,內部實現讀寫分離,併發的處理能力高於ArrayBlockingQueue。

newCachedThreadPool 線程池採用的隊列是SynchronousQueue。其優勢就是無緩存,接收到的任務都可直接處理,再次強調,慎用!

併發量不大,服務器性能較好,能夠考慮使用SynchronousQueue。併發量較大,服務器性能較好,能夠考慮使用LinkedBlockingQueue。併發量很大,服務器性能沒法知足,能夠考慮使用ArrayBlockingQueue。系統的穩定最重要。

 

任務隊列主要有ArrayBlockingQueue有界隊列、LinkedBlockingQueue無界隊列、SynchronousQueue直接提交隊列。

使用ArrayBlockingQueue,當線程池中實際線程數小於核心線程數時,直接建立線程執行任務;當大於核心線程數而小於最大線程數時,提交到任務隊列中;由於這個隊列是有界的,當隊列滿時,在不大於最大線程的前提下,建立線程執行任務;若大於最大線程數,執行拒絕策略。

使用LinkedBlockingQueue時,當線程池中實際線程數小於核心線程數時,直接建立線程執行任務;當大於核心線程數而小於最大線程數時,提交到任務隊列中;由於這個隊列是有無界的,因此以後提交的任務都會進入任務隊列中。newFixedThreadPool就採用了無界隊列,同時指定核心線程和最大線程數同樣。

使用SynchronousQueue時,該隊列沒有容量,對提交任務的不作保存,直接增長新線程來執行任務。newCachedThreadPool使用的是直接提交隊列,核心線程數是0,最大線程數是整型的最大值,keepAliveTime是60s,所以當新任務提交時,若沒有空閒線程都是新增線程來執行任務,不過因爲核心線程數是0,當60s就會回收空閒線程。

當線程池中的線程達到最大線程數時,就要開始執行拒絕策略了。有以下幾種

  • 直接拋出異常
  • 在調用者的線程中,運行當前任務
  • 丟棄最老的一個請求,也就是將隊列頭的任務poll出去
  • 默默丟棄沒法處理的任務,不作任何處理

 7七、ThreadLocal總結

(1)ThreadLocal大概的意思有兩點:

 1)、ThreadLocal提供了一種訪問某個變量的特殊方式:訪問到的變量屬於當前線程,即保證每一個線程的變量不同,而同一個線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。

2)、若是要使用ThreadLocal,一般定義爲private static類型,在我看來最好是定義爲private static final類型。

(2)ThreadLocal能夠總結爲一句話:ThreadLocal的做用是提供線程內的局部變量,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。

(3)ThreadLocal 的實現思想,即每一個線程維護一個 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 實例自己,value 是要存儲的副本變量。ThreadLocal 實例自己並不存儲值,它只是提供一個在當前線程中找到副本值的 key。

(4)線程隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(對比Map對象來理解),每一個線程中都有一個獨立的ThreadLocalMap副本,它所存儲的值,只能被當前線程讀取和修改。ThreadLocal類經過操做每個線程特有的ThreadLocalMap副本,從而實現了變量訪問在不一樣線程中的隔離。由於每一個線程的變量都是本身特有的,徹底不會有併發錯誤。還有一點就是,ThreadLocalMap存儲的鍵值對中的鍵是this對象指向的ThreadLocal對象,而值就是你所設置的對象了。

(5)ThreadLocalMap並非爲了解決線程安全問題,而是提供了一種將實例綁定到當前線程的機制,相似於隔離的效果,實際上本身在方法中new出來變量也能達到相似的效果。ThreadLocalMap跟線程安全基本不搭邊,綁定上去的實例也不是多線程公用的,而是每一個線程new一份,這個實例確定不是共用的,若是共用了,那就會引起線程安全問題。ThreadLocalMap最大的用處就是用來把實例變量共享成全局變量,在程序的任何方法中均可以訪問到該實例變量而已。網上不少人說ThreadLocalMap是解決了線程安全問題,實際上是望文生義,二者不是同類問題。

(6)ThreadLocal對象一般用於防止對可變的單實例變量或全局變量進行共享。當你想在多個方法中使用某個變量,這個變量是當前線程的狀態,其它線程不依賴這個變量,你第一時間想到的就是把變量定義在方法內部,而後再方法之間傳遞參數來使用,這個方法能解決問題,可是有個煩人的地方就是,每一個方法都須要聲明形參,多處聲明,多處調用。影響代碼的美觀和維護。有沒有一種方法能將變量像private static形式來訪問呢?這樣在類的任何一處地方就都能使用。這個時候ThreadLocal大顯身手了。

    ThreadLocal的主要應用場景爲多線程多實例(每一個線程對應一個實例)的對象的訪問,而且這個對象不少地方都要用到。例如:同一個網站登陸用戶,每一個用戶服務器會爲其開一個線程,每一個線程中建立一個ThreadLocal,裏面存用戶基本信息等,在不少頁面跳轉時,會顯示用戶信息或者獲得用戶的一些信息等頻繁操做,這樣多線程之間並無聯繫並且當前線程也能夠及時獲取想要的數據。

(7)ThreadLocal 與 synchronized 的對比

1)ThreadLocal和synchonized都用於解決多線程併發訪問。可是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal爲每個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。而synchronized卻正好相反,它用於在多個線程間通訊時可以得到數據共享。

    synchronized採起的是「以時間換空間」的策略,本質上是對關鍵資源上鎖,讓你們排隊操做。而ThreadLocal採起的是「以空間換時間」的思路,爲每一個使用該變量的線程提供獨立的變量副本,在本線程內部,它至關於一個「全局變量」,能夠保證本線程任什麼時候間操縱的都是同一個對象。

2)synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。


    每一個Thread線程內部都有一個Map,Tread類的ThreadLocal.ThreadLocalMap屬性。Map裏面存儲線程本地對象(key也就是當前的ThreadLoacal對象)和線程的變量副本(value)。Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設置線程的變量值。

數據結構:

 ThreadLocal核心方法:

  • get():返回此線程局部變量的當前線程副本中的值。
  • initialValue():返回此線程局部變量的當前線程的「初始值」。
  • remove():移除此線程局部變量當前線程的值。
  • set(T value):將此線程局部變量的當前線程副本中的值設置爲指定值。

內部類 ThreadLocalMap:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry繼承自WeakReference(弱引用,生命週期只能存活到下次GC前),但只有Key是弱引用類型的,Value並不是弱引用。

set和get的實現:

// set方法
public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}

// 上面的getMap方法
ThreadLocalMap getMap(Thread t) {
   return t.threadLocals;
}

// get方法
public T get() {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null) {
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   return setInitialValue();
}

從源碼中能夠看出:每個線程擁有一個ThreadLocalMap,這個map存儲了該線程擁有的全部局部變量。

set時先經過Thread.currentThread()獲取當前線程,進而獲取到當前線程的ThreadLocalMap,而後以ThreadLocal本身爲key,要存儲的對象爲值,存到當前線程的ThreadLocalMap中。

get時也是先得到當前線程的ThreadLocalMap,以ThreadLocal本身爲key,取出和該線程的局部變量。

內存泄漏問題:

ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value,若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠沒法回收,形成內存泄漏。

其實,ThreadLocalMap的設計中已經考慮到這種狀況,也加上了一些防禦措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏全部key爲null的value。

可是這些被動的預防措施並不能保證不會內存泄漏:

  使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能致使的內存泄漏。
  分配使用了ThreadLocal又再也不調用get(),set(),remove()方法,那麼就會致使內存泄漏。

解決:

每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

在使用線程池的狀況下,沒有及時清理ThreadLocal,不只是內存泄漏的問題,更嚴重的是可能致使業務邏輯出現問題。因此,使用ThreadLocal就跟加鎖完要解鎖同樣,用完就清理。


 

    在上面提到過,每一個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key爲一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每一個key都弱引用指向threadlocal. 當把threadlocal實例置爲null之後,沒有任何強引用指向threadlocal實例,因此threadlocal將會被gc回收. 可是,咱們的value卻不能回收,由於存在一條從current thread鏈接過來的強引用. 只有當前thread結束之後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將所有被GC回收. 
  因此得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間不會被回收的,就發生了咱們認爲的內存泄露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是線程對象不被回收的狀況,這就發生了真正意義上的內存泄露。好比使用線程池的時候,線程結束是不會銷燬的,會再次使用的。就可能出現內存泄露。

 

Runnable 與 Callable的區別

(1)Runnable是自從java1.1就有了,而Callable是1.5以後才加上去的。

(2)Callable規定的方法是call(),Runnable規定的方法是run()。

(3)Callable的任務執行後可返回值,而Runnable的任務是不能返回值(是void),這是核心區別。

(4)call方法能夠拋出異常,run方法不能夠。

(5)運行Callable任務能夠拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。經過Future對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。

(6)加入線程池運行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。

注意點:

Callable接口支持返回執行結果,此時須要調用FutureTask.get()方法實現,此方法會阻塞主線程直到獲取到結果;當不調用此方法時,主線程不會阻塞!

死鎖

所謂死鎖是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些進程都將沒法向前推動。

所謂死鎖是指兩個或兩個以上的線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。

下面咱們經過一些實例來講明死鎖現象。

    先看生活中的一個實例,兩我的面對面過獨木橋,甲和乙都已經在橋上走了一段距離,即佔用了橋的資源,甲若是想經過獨木橋的話,乙必須退出橋面讓出橋的資源,讓甲經過,可是乙不服,爲何讓我先退出去,我還想先過去呢,因而就僵持不下,致使誰也過不了橋,這就是死鎖。

死鎖產生的緣由

一、系統資源的競爭

    一般系統中擁有的不可剝奪資源,其數量不足以知足多個進程運行的須要,使得進程在運行過程當中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可剝奪資源的競爭纔可能產生死鎖,對可剝奪資源的競爭是不會引發死鎖的。

二、進程推動順序非法

    進程在運行過程當中,請求和釋放資源的順序不當,也一樣會致使死鎖。例如,併發進程 P一、P2分別保持了資源R一、R2,而進程P1申請資源R2,進程P2申請資源R1時,二者都會由於所需資源被佔用而阻塞。

    Java中死鎖最簡單的狀況是,一個線程T1持有鎖L1而且申請得到鎖L2,而另外一個線程T2持有鎖L2而且申請得到鎖L1,由於默認的鎖申請操做都是阻塞的,因此線程T1和T2永遠被阻塞了。致使了死鎖。這是最容易理解也是最簡單的死鎖的形式。可是實際環境中的死鎖每每比這個複雜的多。可能會有多個線程造成了一個死鎖的環路,好比:線程T1持有鎖L1而且申請得到鎖L2,而線程T2持有鎖L2而且申請得到鎖L3,而線程T3持有鎖L3而且申請得到鎖L1,這樣致使了一個鎖依賴的環路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而致使了死鎖。

    從上面兩個例子中,咱們能夠得出結論,產生死鎖可能性的最根本緣由是:線程在得到一個鎖L1的狀況下再去申請另一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在得到了鎖L1,而且沒有釋放鎖L1的狀況下,又去申請得到鎖L2,這個是產生死鎖的最根本緣由。另外一個緣由是默認的鎖申請操做是阻塞的

死鎖產生的必要條件

產生死鎖必須同時知足如下四個條件,只要其中任一條件不成立,死鎖就不會發生。

(1)互斥條件:一個資源每次只能被一個進程使用。獨木橋每次只能經過一我的。

(2)請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。乙不退出橋面,甲也不退出橋面。

(3)不剝奪條件: 進程已得到的資源,在未使用完以前,不能強行剝奪。甲不能強制乙退出橋面,乙也不能強制甲退出橋面。

(4)循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。若是乙不退出橋面,甲不能經過,甲不退出橋面,乙不能經過。

死鎖例子

package com.demo.test;

/**
 * 一個簡單的死鎖類
 * t1先運行,這個時候flag==true,先鎖定obj1,而後睡眠1秒鐘
 * 而t1在睡眠的時候,另外一個線程t2啓動,flag==false,先鎖定obj2,而後也睡眠1秒鐘
 * t1睡眠結束後須要鎖定obj2才能繼續執行,而此時obj2已被t2鎖定
 * t2睡眠結束後須要鎖定obj1才能繼續執行,而此時obj1已被t1鎖定
 * t一、t2相互等待,都須要獲得對方鎖定的資源才能繼續執行,從而死鎖。 
 */
public class DeadLock implements Runnable{
    
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private boolean flag;
    
    public DeadLock(boolean flag){
        this.flag = flag;
    }
    
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + "運行");
        
        if(flag){
            synchronized(obj1){
                System.out.println(Thread.currentThread().getName() + "已經鎖住obj1");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj2){
                    // 執行不到這裏
                    System.out.println("1秒鐘後,"+Thread.currentThread().getName()
                                + "鎖住obj2");
                }
            }
        }else{
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName() + "已經鎖住obj2");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj1){
                    // 執行不到這裏
                    System.out.println("1秒鐘後,"+Thread.currentThread().getName()
                                + "鎖住obj1");
                }
            }
        }
    }

}
package com.demo.test;

public class DeadLockTest {

     public static void main(String[] args) {
         
         Thread t1 = new Thread(new DeadLock(true), "線程1");
         Thread t2 = new Thread(new DeadLock(false), "線程2");

         t1.start();
         t2.start();
    }
}

運行結果:

線程1運行
線程1已經鎖住obj1
線程2運行
線程2已經鎖住obj2

    線程1鎖住了obj1(甲佔有橋的一部分資源),線程2鎖住了obj2(乙佔有橋的一部分資源),線程1企圖鎖住obj2(甲讓乙退出橋面,乙不從),進入阻塞,線程2企圖鎖住obj1(乙讓甲退出橋面,甲不從),進入阻塞,死鎖了。

    從這個例子也能夠反映出,死鎖是由於多線程訪問共享資源,因爲訪問的順序不當所形成的,一般是一個線程鎖定了一個資源A,而又想去鎖定資源B;在另外一個線程中,鎖定了資源B,而又想去鎖定資源A以完成自身的操做,兩個線程都想獲得對方的資源,而不肯釋放本身的資源,形成兩個線程都在等待,而沒法執行的狀況。

避免死鎖的方式

一、讓程序每次至多隻能得到一個鎖。固然,在多線程環境下,這種狀況一般並不現實。

二、設計時考慮清楚鎖的順序,儘可能避免嵌套封鎖。避免嵌套封鎖:這是死鎖最主要的緣由的,若是你已經有一個資源了就要避免封鎖另外一個資源。若是你運行時只有一個對象封鎖,那是幾乎不可能出現一個死鎖局面的。

三、加鎖時限。既然死鎖的產生是兩個線程無限等待對方持有的鎖,那麼只要等待時間有個上限不就行了。固然synchronized不具有這個功能,可是咱們能夠使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法能夠指定一個超時時限,在等待超過該時限以後便會返回一個失敗信息。

     咱們能夠使用ReentrantLock.tryLock()方法,在一個循環中,若是tryLock()返回失敗,那麼就釋放以及得到的鎖,並睡眠一小段時間。這樣就打破了死鎖的閉環。好比:線程T1持有鎖L1而且申請得到鎖L2,而線程T2持有鎖L2而且申請得到鎖L3,而線程T3持有鎖L3而且申請得到鎖L1。此時若是T3申請鎖L1失敗,那麼T3釋放鎖L3,並進行睡眠,那麼T2就能夠得到L3了,而後T2執行完以後釋放L2, L3,因此T1也能夠得到L2了執行完而後釋放鎖L1, L2,而後T3睡眠醒來,也能夠得到L1, L3了。打破了死鎖的閉環。

Java中的Atomic類

    Java1.5的Atomic包名爲java.util.concurrent.atomic。這個包提供了一系列原子類。這些類能夠保證多線程環境下,當某個線程在執行atomic的方法時,不會被其餘線程打斷,而別的線程就像自旋鎖同樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個線程執行。Atomic類在軟件層面上是非阻塞的,它的原子性實際上是在硬件層面上藉助相關的指令來保證的。

    Atomic包是java.util.concurrent下的另外一個專門爲線程安全設計的Java包,包含多個原子操做類。這個包裏面提供了一組原子變量類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具備排他性,即當某個線程進入方法,執行其中的指令時,不會被其餘線程打斷,而別的線程就像自旋鎖同樣,一直等到該方法執行完成,才由JVM從等待隊列中選擇一個另外一個線程進入,這只是一種邏輯上的理解。其實是藉助硬件的相關指令來實現的,不會阻塞線程(或者說只是在硬件級別上阻塞了)。能夠對基本數據、數組中的基本數據、對類中的基本數據進行操做。原子變量類至關於一種泛化的volatile變量,可以支持原子的和有條件的讀-改-寫操做。

Atomic包中的類能夠分紅4組:

(1)原子方式更新基本類型:AtomicBoolean(原子更新布爾類型)、AtomicInteger(原子更新整型)、AtomicLong(原子更新長整型)。

(2)原子方式更新數組:AtomicIntegerArray(原子更新整型數組裏的元素)、AtomicLongArray(原子更新長整型數組裏的元素)、AtomicReferenceArray(原子更新引用類型數組裏的元素)。注意這裏操做的不是整個數組,而是數組中的單個元素。

(3)原子方式更新引用:AtomicReference(原子更新引用類型)、AtomicReferenceFieldUpdater(原子更新引用類型裏的字段)、AtomicMarkableReference:(原子更新帶有標記位的引用類型)。以上三個類是以原子方式更新引用,與其它不一樣的是,更新引用能夠更新多個變量,而不是一個變量。

(4)原子方式更新字段:AtomicIntegerFieldUpdater(原子更新整型字段的更新器)、AtomicLongFieldUpdater(原子更新長整型字段的更新器)、AtomicStampedReference(原子更新帶有版本號的引用類型,用於解決使用CAS進行原子更新時,可能出現的ABA問題)。

AtomicInteger

在Java語言中,i++這類的操做不是原子操做,並不是是線程安全的。i++能夠分紅三個操做:

(1)獲取變量當前值

(2)給獲取的當前變量值+1

(3)寫回新的值到變量

    假設i的初始值爲10,當進行併發操做的時候,可能出現線程A和線程B都進行到了1操做,以後又同時進行2操做。A先進行到3操做+1,如今值爲11;注意剛纔AB獲取到的當前值都是10,因此B執行3操做後,i的值依然是11。這個結果顯然不符合咱們的要求。這時候能夠使用synchronized關鍵字進行同步。而AtomicInteger則經過一種線程安全的操做接口自動實現同步,不須要再人爲的增長同步控制。

先來看一下最簡單的AtomicInteger有哪些常見的方法以及這些方法的做用。

 1 // 取得當前值
 2 public final int get() 
 3 // 設置當前值
 4 public final void set(int newValue)
 5 // 設置新值,並返回舊值
 6 public final int getAndSet(int newValue)
 7 // 若是當前值爲expect,則設置爲u
 8 public final boolean compareAndSet(int expect, int u)
 9 // 當前值加1,返回舊值
10 public final int getAndIncrement()
11 // 當前值減1,返回舊值
12 public final int getAndDecrement() 
13 // 當前值增長delta,返回舊值
14 public final int getAndAdd(int delta)
15 // 當前值加1,返回新值
16 public final int incrementAndGet() 
17 // 當前值減1,返回新值
18 public final int decrementAndGet() 
19 // 當前值增長delta,返回新值
20 public final int addAndGet(int delta)

AtomicInteger源碼分析:

    下面經過AtomicInteger的源碼來看一下是怎麼在沒有鎖的狀況下保證數據正確性。首先看一下AtomicInteger類變量的定義:

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2 private static final long valueOffset;
 3 
 4 static {
 5  try {
 6     valueOffset = unsafe.objectFieldOffset
 7         (AtomicInteger.class.getDeclaredField("value"));
 8   } catch (Exception ex) { throw new Error(ex); }
 9 }
10 
11 private volatile int value;

關於這段代碼中出現的幾個成員屬性:

一、Unsafe是CAS的核心類。

Unsafe簡介

sun.misc.Unsafe類型從名字看,這個類應該是封裝了一些不安全的操做。

(1)能夠用來在任意內存地址位置處讀寫數據,可見,對於普通用戶來講,使用起來仍是比較危險的;

(2)還支持一些CAS原子操做。

    簡單講一下這個類。Java沒法直接訪問底層操做系統,而是經過本地(native)方法來訪問不過儘管如此,JVM仍是開了一個後門,JDK中有一個類Unsafe,它提供了硬件級別的原子操做

    這個類儘管裏面的方法都是public的,可是並無辦法使用它們,JDK API文檔也沒有提供任何關於這個類的方法的解釋。總而言之,對於Unsafe類的使用都是受限制的,只有授信的代碼才能得到該類的實例,固然JDK庫裏面的類是能夠隨意使用的。

    從第一行的描述能夠了解到Unsafe提供了硬件級別的操做,好比說獲取某個屬性在內存中的位置,好比說修改對象的字段值,即便它是私有的。不過Java自己就是爲了屏蔽底層的差別,對於通常的開發而言也不多會有這樣的需求。

舉個例子,比方說:

public native long staticFieldOffset(Field paramField);

這個方法能夠用來獲取給定的paramField的內存地址偏移量,這個值對於給定的field是惟一的且是固定不變的。

二、valueOffset表示的是變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的原值的。

三、value是用volatile修飾的,這是很是關鍵的。

volatile包含如下語義:

  1. Java 存儲模型不會對valatile指令的操做進行重排序:這個保證對volatile變量的操做時按照指令的出現順序執行的。
  2. volatile變量不會被緩存在寄存器中(只有擁有線程可見)或者其餘對CPU不可見的地方,每次老是從主存中讀取volatile變量的結果。也就是說對於volatile變量的修改,其它線程老是可見的,而且不是使用本身線程棧內部的變量。也就是在happens-before法則中,對一個valatile變量的寫操做後,其後的任何讀操做理解可見此寫操做的結果。

簡而言之volatile 的做用是當一個線程修改了共享變量時,另外一個線程能夠讀取到這個修改後的值。

    AtomicInteger中有不少方法,例如incrementAndGet() 至關於i++ 和getAndAdd() 至關於i+=n 。從源碼中咱們能夠看出這幾種方法的實現很類似,因此咱們主要分析incrementAndGet() 方法的源碼。

源碼以下:

 1 public final int incrementAndGet() {
 2     for (;;) {
 3         // 獲取當前值value
 4         int current = get();
 5         // 當前值+1
 6         int next = current + 1;
 7         //循環執行到遞增成功  
 8         if (compareAndSet(current, next))
 9             // 若是成功, 則返回新值
10             return next;
11             
12             // 若是失敗了, 說明其餘線程已經修改了數據, 與指望不相符,
13             // 則繼續無限循環, 直到成功. 這種樂觀鎖, 理論上只要等兩三個時鐘週期就能夠設值成功
14             // 相比於直接經過synchronized獨佔鎖的方式操做int, 要大大節約等待時間.
15     }
16 }

 在這裏採用了CAS操做,每次從內存中讀取數據而後將此數據和+1後的結果進行CAS操做,若是成功就返回結果,不然重試直到成功爲止。

而compareAndSet利用JNI來完成CPU指令的操做:

public final boolean compareAndSet(int expect, int update) {
    // 經過unsafe 基於CPU的CAS指令來實現, 能夠認爲無阻塞.
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

總體的過程就是這樣子的,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操做都是利用相似的特性完成的。

其中unsafe.compareAndSwapInt(this, valueOffset, expect, update) 相似:

if (this == expect) {
  this = update
  return true;
} else {
  return false;
}

    compareAndSet()方法調用的compareAndSwapInt()方法是一個native方法。其做用是每次從內存中根據內存偏移量(valueOffset)取出數據,將取出的值跟expect 比較,若是數據一致就把內存中的值改成update。這樣使用CAS就保證了原子操做。其他幾個方法的原理跟這個相同。

   compareAndSet所作的爲調用 Sun 的 UnSafe 的 compareAndSwapInt 方法來完成,此方法爲 native 方法,compareAndSwapInt 基於的是CPU 的 CAS指令來實現的。因此基於 CAS 的操做可認爲是無阻塞的,一個線程的失敗或掛起不會引發其它線程也失敗或掛起。而且因爲 CAS 操做是 CPU 原語,因此性能比較好。

下面結合實例來分析一下incrementAndGet()方法如何在不加鎖的狀況下經過CAS實現線程安全,咱們不妨考慮一下方法的執行:

(1)假設AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的value爲3,根據Java內存模型,線程1和線程2各自持有一份value的副本,值爲3。

(2)線程1運行到第四行獲取到當前的value爲3,線程切換。

(3)線程2開始運行,獲取到value爲3,利用CAS對比內存中的值也爲3,比較成功,修改內存,此時內存中的value改變,加1後值爲4,線程切換。

(4)線程1恢復運行,利用CAS比較發現本身的value爲3,內存中的value爲4,獲得一個重要的結論-->此時value正在被另一個線程修改,因此我不能去修改它。

(5)線程1的compareAndSet失敗,循環判斷,由於value是volatile修飾的,因此它具有可見性的特性,線程2對於value的改變能被線程1看到,只要線程1發現當前獲取的value是4,內存中的value也是4,說明線程2對於value的修改已經完畢而且線程1能夠嘗試去修改它。

(6)最後說一點,好比說此時線程3也準備修改value了,不要緊,由於比較-交換是一個原子操做不可被打斷,線程3修改了value,線程1進行compareAndSet的時候必然返回的false,這樣線程1會繼續循環去獲取最新的value並進行compareAndSet,直至獲取的value和內存中的value一致爲止。

整個過程當中,利用CAS機制保證了對於value的修改的線程安全性。


 

具體看下實現的源碼:

(1)遞增的方法:incrementAndGet()

incrementAndGet方法是在一個死循環裏面調用compareAndSet方法,若是compareAndSet返回失敗,就會一直從頭開始循環,不會退出incrementAndGet方法,直到compareAndSet返回true。

(2)compareAndSet方法:

AtomicInteger中Unsafe實例調用compareAndSwapInt方法。

(3)compareAndSwapInt源碼:

    看到這裏知道是一個本地方法的調用,比較並置換,這裏利用Unsafe類的JNI方法實現,使用CAS指令,能夠保證讀-改-寫是一個原子操做。compareAndSwapInt有4個參數,this - 當前AtomicInteger對象,valueOffset- value屬性在內存中的位置(須要強調的不是value值在內存中的位置),expect - 預期值,update - 新值,根據上面的CAS操做過程,當內存中的value值等於expect值時,則將內存中的value值更新爲update值,並返回true,不然返回false。在這裏咱們有必要對Unsafe有一個簡單點的認識,從名字上來看,不安全,確實,這個類是用於執行低級別的、不安全操做的方法集合,這個類中的方法大部分是對內存的直接操做,因此不安全,但當咱們使用反射、併發包時,都間接的用到了Unsafe。

併發狀況處理流程

(1)首先valueOffset獲取value的偏移量,假設value=0,valueOffset=0(valueOffset實際上是內存地址,便於表達-後面用valueOffset=n表示對應值的地址)。

(2)線程A調用getAndIncrement方法,執行到161行,獲取current=0,next=1,準備執行compareAndSet方法。

(3)線程B幾乎與線程A同時調用getAndIncrement方法,執行完161行後,獲取current=0,next=1,而且先於線程A執行compareAndSet方法,此時value=1,valueOffset=1。

(4)線程A調用compareAndSet發現預期值(current=0)與內存中對應的值(valueOffset=1,被線程B修改)不相等,即在本線程執行期間有被修改過,則放棄這次修改,返回false。

(5)線程B接着循環,經過get()獲取的值是最新的(volatile修飾的value的值會強迫線程從主內存獲取),current=1,next=2,而後發現valueOffset=current=1,修改valueOffset=2。

AtomicInteger的使用:

package com.demo.Atomic;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo extends Thread{
    
    private static AtomicInteger i = new AtomicInteger();

    @Override
    public void run() {
      for (int k = 0; k < 100; k++) {
          i.incrementAndGet();
          //System.out.println(Thread.currentThread().getName()+"-------------"+i.get());
      }
    }
        
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo[] ts = new AtomicIntegerDemo[10];
        
        for (int k = 0; k < 10; k++) {
            ts[k] = new AtomicIntegerDemo();
        }
        for (int k = 0; k < 10; k++) {
            ts[k].start();
        }
        for (int k = 0; k < 10; k++) {
            ts[k].join();
        }
      
        System.out.println("最終結果:"+ i);
    }

}

運行結果:

最終結果:1000

多個線程對AtomicInteger類型的變量進行自增操做,運算結果無誤,也就是說AtomicInteger能夠實現原子操做,即在多線程環境中,執行的操做不會被其餘線程打斷。若用普通的int變量,i++多線程操做可能致使結果有誤。

如今再來思考這個問題:AtomicInteger是如何實現線程安全呢?請你們本身先考慮一下這個問題,其實咱們在語言層面是沒有作任何同步的操做的,你們也能夠看到源碼沒有任何鎖加在上面,可它爲何是線程安全的呢?這就是Atomic包下這些類的奧祕:語言層面不作處理,咱們將其交給硬件—CPU和內存,利用CPU的多處理能力,實現硬件層面的阻塞,再加上volatile變量的特性便可實現基於原子操做的線程安全。因此說,CAS並非無阻塞,只是阻塞並不是在語言、線程方面,而是在硬件層面,因此無疑這樣的操做會更快更高效!

總結一下,AtomicInteger 中主要實現了整型的原子操做,防止併發狀況下出現異常結果,其內部主要依靠JDK 中的unsafe 類操做內存中的數據來實現的。volatile 修飾符保證了value在內存中其餘線程能夠看到其值得改變。CAS操做保證了AtomicInteger 能夠安全的修改value 的值。

CAS

    CAS 指的是現代 CPU 普遍支持的一種對內存中的共享數據進行操做的一種特殊指令。這個指令會對內存中的共享數據作原子的讀寫操做。簡單介紹一下這個指令的操做過程:首先,CPU 會將內存中將要被更改的數據與指望的值作比較。而後,當這兩個值相等時,CPU 纔會將內存中的數值替換爲新的值。不然便不作操做。最後,CPU 會將舊的數值返回。這一系列的操做是原子的。它們雖然看似複雜,但倒是 Java 5 併發機制優於原有鎖機制的根本。簡單來講,CAS 的含義是「我認爲原有的值應該是什麼,若是是,則將原有的值更新爲新值,不然不作修改,並告訴我原來的值是多少」。(這段描述引自《Java併發編程實踐》)
    簡單的來講,CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然返回V。這是一種樂觀鎖的思路,它相信在它修改以前,沒有其它線程去修改它;而synchronized是一種悲觀鎖,它認爲在它修改以前,必定會有其它線程去修改它,悲觀鎖效率很低

CAS應用

CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

樂觀鎖:其實現機制是基於CAS的,每次不加鎖,假設沒有衝突完成操做,若是有衝突,重試直到成功爲止。

(1)非阻塞算法 (nonblocking algorithms)

即一個線程的失敗或者掛起不該該影響其餘線程的失敗或掛起的算法。

現代的CPU提供了特殊的指令,能夠自動更新共享數據,並且可以檢測到其餘線程的干擾,而 compareAndSet() 就用這些代替了鎖定。

(2)Java原子類:AtomicInteger等

CAS原理

CAS經過調用JNI的代碼實現的。JNI:Java Native Interface爲JAVA本地調用,容許java調用其餘語言。

compareAndSwapInt就是藉助C來調用CPU底層指令實現的

下面從分析比較經常使用的CPU(intel x86)來解釋CAS的實現原理。下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

能夠看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實如今openjdk的以下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應於windows操做系統,X86處理器)。下面是對應於intel x86處理器的源代碼的片斷:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

如上面源代碼所示,能夠看出最後調用的是Atomic:comxchg這個方法,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。若是程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,若是程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不須要lock前綴提供的內存屏障效果)。

intel的手冊對lock前綴的說明以下:

(1)確保對內存的讀-改-寫操做原子執行。在Pentium及Pentium以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其餘 處理器暫時沒法經過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上作了一個頗有意義的優化:若是要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),而且該內存區域被徹底包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。因爲在指令執行期間該緩存行會一直被鎖定,其它處理器沒法讀/寫該指令要訪問的內存區域,所以能保證指令執行的原子性。這個操做過程叫作緩存鎖定(cache locking),緩存鎖定將大大下降lock前綴指令的執行開銷,可是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。

(2)禁止該指令與以前和以後的讀和寫指令重排序。

(3)把寫緩衝區中的全部數據刷新到內存中。

關於處理器如何實現原子操做有如下三種:

(1)處理器自動保證基本內存操做的原子性

    首先處理器會自動保證基本的內存操做的原子性處理器保證從系統內存當中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其餘處理器不能訪問這個字節的內存地址。奔騰6和最新的處理器能自動保證單處理器對同一個緩存行裏進行16/32/64位的操做是原子的,可是複雜的內存操做處理器不能自動保證其原子性,好比跨總線寬度,跨多個緩存行,跨頁表的訪問。可是處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。

(2)使用總線鎖保證原子性

    第一個機制是經過總線鎖保證原子性。若是多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操做)操做,那麼共享變量就會被多個處理器同時進行操做,這樣讀改寫操做就不是原子的,操做完以後共享變量的值會和指望的不一致,舉個例子:若是i=1,咱們進行兩次i++操做,咱們指望的結果是3,可是有可能結果是2。以下圖:

    緣由是有可能多個處理器同時從各自的緩存中讀取變量i,分別進行加一操做,而後分別寫入系統內存當中。那麼想要保證讀改寫共享變量的操做是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操做緩存了該共享變量內存地址的緩存。

    處理器使用總線鎖就是來解決這個問題的所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔使用共享內存。

(3)使用緩存鎖保證原子性

    第二個機制是經過緩存鎖定保證原子性。在同一時刻咱們只需保證對某個內存地址的操做是原子性便可,但總線鎖會把CPU和內存之間通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

    頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操做就能夠直接在處理器內部緩存中進行,並不須要聲明總線鎖,在奔騰6和最近的處理器中能夠使用「緩存鎖定」的方式來實現複雜的原子性。所謂「緩存鎖定」就是若是緩存在處理器緩存行中內存區域在LOCK操做期間被鎖定,當它執行鎖操做回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並容許它的緩存一致性機制來保證操做的原子性,由於緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其餘處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。

    可是有兩種狀況下處理器不會使用緩存鎖定。第一種狀況是:當操做的數據不能被緩存在處理器內部,或操做的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種狀況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

   以上兩個機制咱們能夠經過Inter處理器提供了不少LOCK前綴的指令來實現。好比位測試和修改指令BTS,BTR,BTC,交換指令XADD,CMPXCHG和其餘一些操做數和邏輯指令,好比ADD(加),OR(或)等,被這些指令操做的內存區域就會加鎖,致使其餘處理器不能同時訪問它。

CAS存在的問題

CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做

(1)ABA問題

    由於CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。


 所謂ABA問題基本是這個樣子:

(1)進程P1在共享變量中讀到值爲A

(2)P1被搶佔了,進程P2執行

(3)P2把共享變量裏的值從A改爲了B,再改回到A,此時被P1搶佔。

(4)P1回來看到共享變量裏的值沒有被改變,因而繼續執行。

    雖然P1覺得變量值沒有改變,繼續執行了,可是這個會引起一些潛在的問題。ABA問題最容易發生在lock free 的算法中的,CAS首當其衝,由於CAS判斷的是指針的地址。若是這個地址被重用了呢,問題就很大了。(地址被重用是很常常發生的,一個內存分配後釋放了,再分配,頗有可能仍是原來的地址)

這個例子你可能沒有看懂,維基百科上給了一個活生生的例子——

你拿着一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女,而後她很暖昧地挑逗着你,並趁你不注意的時候,把用一個如出一轍的手提箱和你那裝滿錢的箱子
調了個包,而後就離開了,你看到你的手提箱還在那,因而就提着手提箱去趕飛機去了。

這就是ABA問題。

(2)循環時間長開銷大

    自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷。若是JVM能支持處理器提供的pause指令那麼效率會有必定的提高,pause指令有兩個做用,第一它能夠延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發CPU流水線被清空(CPU pipeline flush),從而提升CPU的執行效率。

(3)只能保證一個共享變量的原子操做

    當對一個共享變量執行操做時,咱們能夠使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你能夠把多個變量放在一個對象裏來進行CAS操做。

總結

CAS利用CPU調用底層指令實現,即CAS操做正是利用了處理器提供的CMPXCHG指令實現的。

單一處理器,進行簡單的讀寫操做時,能保證自身讀取的原子性,多處理器或複雜的內存操做時,CAS採用總線加鎖或緩存加鎖方式保證原子性。

(1)總線加鎖

如i=0初始化,多處理器多線程環境下進行i++操做下,處理器A和B同時讀取i值到各自緩存,分別進行遞增,回寫值i=1相同。處理器提供LOCK#信號,進行總線加鎖後,處理器A讀取i值並遞增,處理器B被阻塞不能讀取i值。

(2)緩存加鎖

總線加鎖,在LOCK#信號下,其餘線程沒法操做內存,性能較差,緩存加鎖能較好處理該問題。

緩存加鎖,處理器A和B同時讀取i值到緩存,處理器A提早完成遞增,數據當即回寫到主內存,並讓處理器B緩存該數據失效,處理器B需從新讀取i值。

    雖然基於CAS的線程安全機制很好很高效,但要說的是,並不是全部線程安全均可以用這樣的方法來實現,這隻適合一些粒度比較小,型如計數器這樣的需求用起來纔有效,不然也不會有鎖的存在了。

AQS

    AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如經常使用的ReentrantLock/Semaphore/CountDownLatch...。

    AQS,它維護了一個volatile int state(表明共享資源)狀態變量和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。

    AQS是JUC中不少同步組件的構建基礎,簡單來說,它內部實現主要是狀態變量state和一個FIFO隊列來完成,同步隊列的頭結點是當前獲取到同步狀態的結點,獲取同步狀態state失敗的線程,會被構形成一個結點(或共享式或獨佔式)加入到同步隊列尾部(採用自旋CAS來保證此操做的線程安全),隨後線程會阻塞;釋放時喚醒頭結點的後繼結點,使其加入對同步狀態的爭奪中。

    AQS的主要使用方式是繼承,子類經過繼承同步器並實現它的抽象方法來管理同步狀態。

    AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操做,固然AQS能夠確保對state的操做是安全的。

    AQS經過內置的FIFO同步隊列來完成資源獲取線程的排隊工做,若是當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構形成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

CountDownLatch

    CountDownLatch類位於java.util.concurrent包下,利用它能夠實現相似計數器的功能。好比有一個任務A,它要等待其餘4個任務執行完畢以後才能執行,此時就能夠利用CountDownLatch來實現這種功能了。

   CountDownLatch類只提供了一個構造器:

public CountDownLatch(int count) {  };  //參數count爲計數值

而後下面這3個方法是CountDownLatch類中最重要的方法:

//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException { }; 
//和await()相似,只不過等待必定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//將count值減1 
public void countDown() { };  

    CountDownLatch類是一個同步計數器,構造時傳入int參數,該參數就是計數器的初始值,每調用一次countDown()方法,計數器減1,計數器大於0 時,await()方法會阻塞程序繼續執行。CountDownLatch能夠看做是一個倒計數的鎖存器,當計數減至0時觸發特定的事件。利用這種特性,可讓主線程等待子線程的結束。

    java.util.concurrent.CountDownLatch它是一個同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待。

   用給定的計數初始化 CountDownLatch。在調用countDown() 方法,使當前計數減一,且當前計數到達零以前,await 方法會一直受阻塞。當前計數到達零以後,會釋放全部等待的線程,await 的全部後續調用都將當即返回。這種現象只出現一次——計數沒法被重置。若是須要重置計數,請考慮使用 CyclicBarrier。

   CountDownLatch的一個很是典型的應用場景是:有一個任務想要往下執行,但必需要等到其餘的任務執行完畢後才能夠繼續往下執行。假如咱們這個想要繼續往下執行的任務調用一個CountDownLatch對象的await()方法,其餘的任務執行完本身的任務後調用同一個CountDownLatch對象上的countDown()方法,這個調用await()方法的任務將一直阻塞等待,直到這個CountDownLatch對象的計數值減到0爲止。

CountDownLatch 是一個通用同步工具,主要有如下三種用法:

(1)將計數1初始化的 CountDownLatch 用做一個簡單的開/關鎖存器,或入口。在經過調用 countDown() 的線程打開入口前,全部調用 await 的線程都一直在入口處等待。

(2)用N初始化的 CountDownLatch 能夠使一個線程在 N 個線程完成某項操做以前一直等待,或者使其在某項操做完成 N 次以前一直等待。

(3)它不要求調用 countDown 方法的線程等到計數到達零時才繼續,而在全部線程都能經過以前,它只是阻止任何線程繼續經過一個await。即調用countDown 方法的線程並不會阻塞。CountDownLatch調用await方法將阻塞當前線程,直到其餘線程調用countDown 方法,使其計數到達零時才繼續。 

實現原理

    CountDownLatch是經過「共享鎖」實現的。在建立CountDownLatch中時,會傳遞一個int類型參數count,該參數是「鎖計數器」的初始狀態,表示該「共享鎖」最多能被count個線程同時獲取。當某線程調用該CountDownLatch對象的await()方法時,該線程會等待「共享鎖」可用時,才能獲取「共享鎖」進而繼續運行。而「共享鎖」可用的條件,就是「鎖計數器」的值爲0!而「鎖計數器」的初始值爲count,每當一個線程調用該CountDownLatch對象的countDown()方法時,纔將「鎖計數器」-1;經過這種方式,必須有count個線程調用countDown()以後,「鎖計數器」才爲0,而前面提到的等待線程才能繼續運行!

示例:

下面經過CountDownLatch實現:"主線程"等待"5個子線程"所有都完成"指定的工做(休眠1000ms)"以後,再繼續運行。

package com.demo.aqs;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

    private static int LATCH_SIZE = 5;
    private static CountDownLatch doneSignal;
    
    public static void main(String[] args) {

        try {
            doneSignal = new CountDownLatch(LATCH_SIZE);

            // 新建5個任務
            for(int i=0; i<LATCH_SIZE; i++)
                new InnerThread().start();

            System.out.println("main await begin.");
            // "主線程"等待5個任務的完成
            doneSignal.await();

            System.out.println("main await finished.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class InnerThread extends Thread{
        public void run() {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + " sleep 1000ms.");
                // 將CountDownLatch的數值減1
                doneSignal.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:

main await begin.
Thread-2 sleep 1000ms.
Thread-0 sleep 1000ms.
Thread-1 sleep 1000ms.
Thread-3 sleep 1000ms.
Thread-4 sleep 1000ms.
main await finished.

結果說明:主線程經過doneSignal.await()等待其它線程將doneSignal遞減至0。其它的5個InnerThread線程,每個都經過doneSignal.countDown()將doneSignal的值減1;當doneSignal爲0時,main被喚醒後繼續執行。

示例2:

package com.demo.aqs;

import java.util.concurrent.CountDownLatch;

public class Test {

    public static void main(String[] args) {   
        final CountDownLatch latch = new CountDownLatch(2);
         
        new Thread(){
            public void run() {
                try {
                    System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");
                   Thread.sleep(3000);
                   System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");
                   latch.countDown();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
            };
        }.start();
         
        new Thread(){
            public void run() {
                try {
                    System.out.println("子線程"+Thread.currentThread().getName()+"正在執行");
                    Thread.sleep(3000);
                    System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢");
                    latch.countDown();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
            };
        }.start();
         
        try {
            System.out.println("等待2個子線程執行完畢...");
           latch.await();
           System.out.println("2個子線程已經執行完畢");
           System.out.println("繼續執行主線程");
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
    }
}

運行結果:

子線程Thread-0正在執行
等待2個子線程執行完畢...
子線程Thread-1正在執行
子線程Thread-1執行完畢
子線程Thread-0執行完畢
2個子線程已經執行完畢
繼續執行主線程

CyclicBarrier

    字面意思迴環柵欄,經過它能夠實現讓一組線程等待至某個狀態以後再所有同時執行。叫作迴環是由於當全部等待線程都被釋放之後,CyclicBarrier能夠被重用。咱們暫且把這個狀態就叫作barrier,當調用await()方法以後,線程就處於barrier了。

    CyclicBarrier是一個同步輔助類,容許一組線程互相等待,直到到達某個公共屏障點 (common barrier point)。由於該 barrier 在釋放等待線程後能夠重用,因此稱它爲循環 的 barrier。CyclicBarrier是經過ReentrantLock(獨佔鎖)和Condition來實現的。

CyclicBarrier類位於java.util.concurrent包下,CyclicBarrier提供2個構造器:

public CyclicBarrier(int parties, Runnable barrierAction) {
}
 
public CyclicBarrier(int parties) {
}

參數parties指讓多少個線程或者任務等待至barrier狀態;參數barrierAction爲當這些線程都達到barrier狀態時會執行的內容。而後CyclicBarrier中最重要的方法就是await方法,它有2個重載版本:

public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };

第一個版本比較經常使用,用來掛起當前線程,直至全部線程都到達barrier狀態再同時執行後續任務;第二個版本是讓這些線程等待至必定的時間,若是還有線程沒有到達barrier狀態就直接讓到達barrier的線程執行後續任務。

下面舉幾個例子就明白了:

倘若有若干個線程都要進行寫數據操做,而且只有全部線程都完成寫數據操做以後,這些線程才能繼續作後面的事情,此時就能夠利用CyclicBarrier了:

package com.demo.aqs;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class TestCyclicBarrier {

    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(5000);//以睡眠來模擬寫入數據操做
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其餘線程寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("全部線程寫入完畢,繼續處理其餘任務...");
        }
    }
}

運行結果:

線程Thread-0正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-3寫入數據完畢,等待其餘線程寫入完畢
線程Thread-2寫入數據完畢,等待其餘線程寫入完畢
線程Thread-0寫入數據完畢,等待其餘線程寫入完畢
線程Thread-1寫入數據完畢,等待其餘線程寫入完畢
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...

從上面輸出結果能夠看出,每一個寫入線程執行完寫數據操做以後,就在等待其餘線程寫入操做完畢。當全部線程線程寫入操做完畢以後,全部線程就繼續進行後續的操做了。

若是說想在全部線程寫入操做完以後,進行額外的其餘操做能夠爲CyclicBarrier提供Runnable參數:

package com.demo.aqs;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class TestCyclicBarrier1 {

    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
            @Override
            public void run() {
                System.out.println("當前線程"+Thread.currentThread().getName());   
            }
        });
         
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬寫入數據操做
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其餘線程寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("全部線程寫入完畢,繼續處理其餘任務...");
        }
    }
}

運行結果:

線程Thread-0正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-0寫入數據完畢,等待其餘線程寫入完畢
線程Thread-1寫入數據完畢,等待其餘線程寫入完畢
線程Thread-2寫入數據完畢,等待其餘線程寫入完畢
線程Thread-3寫入數據完畢,等待其餘線程寫入完畢
當前線程Thread-3
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...
全部線程寫入完畢,繼續處理其餘任務...

從結果能夠看出,當四個線程都到達barrier狀態後,會從四個線程中選擇一個線程去執行Runnable。

另外CyclicBarrier是能夠重用的,看下面這個例子:

package com.demo.aqs;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class TestCyclicBarrier2 {

    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
         
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         
        System.out.println("CyclicBarrier重用");
         
        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
    }
    
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(1000);      //以睡眠來模擬寫入數據操做
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其餘線程寫入完畢");
             
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"全部線程寫入完畢,繼續處理其餘任務...");
        }
    }
}

運行結果: 

線程Thread-0正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-2寫入數據完畢,等待其餘線程寫入完畢
線程Thread-1寫入數據完畢,等待其餘線程寫入完畢
線程Thread-0寫入數據完畢,等待其餘線程寫入完畢
線程Thread-3寫入數據完畢,等待其餘線程寫入完畢
Thread-3全部線程寫入完畢,繼續處理其餘任務...
Thread-1全部線程寫入完畢,繼續處理其餘任務...
Thread-0全部線程寫入完畢,繼續處理其餘任務...
Thread-2全部線程寫入完畢,繼續處理其餘任務...
CyclicBarrier重用
線程Thread-4正在寫入數據...
線程Thread-5正在寫入數據...
線程Thread-6正在寫入數據...
線程Thread-7正在寫入數據...
線程Thread-7寫入數據完畢,等待其餘線程寫入完畢
線程Thread-6寫入數據完畢,等待其餘線程寫入完畢
線程Thread-5寫入數據完畢,等待其餘線程寫入完畢
線程Thread-4寫入數據完畢,等待其餘線程寫入完畢
Thread-4全部線程寫入完畢,繼續處理其餘任務...
Thread-7全部線程寫入完畢,繼續處理其餘任務...
Thread-6全部線程寫入完畢,繼續處理其餘任務...
Thread-5全部線程寫入完畢,繼續處理其餘任務...

從執行結果能夠看出,在初次的4個線程越過barrier狀態後,又能夠用來進行新一輪的使用。而CountDownLatch沒法進行重複使用。

比較CountDownLatch和CyclicBarrier:

(1) CountDownLatch的做用是容許1或N個線程等待其餘線程完成執行;而CyclicBarrier則是容許N個線程相互等待。

(2) CountDownLatch的計數器沒法被重置;CyclicBarrier的計數器能夠被重置後使用,所以它被稱爲是循環的barrier。

Semaphore

Semaphore翻譯成字面意思爲信號量,Semaphore能夠控同時訪問的線程個數,經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可。

Semaphore也是一個線程同步的輔助類,能夠維護當前訪問自身的線程個數,並提供了同步機制。使用Semaphore能夠控制同時訪問資源的線程個數,例如,實現一個文件容許的併發訪問數。

Semaphore類位於java.util.concurrent包下,它提供了2個構造器:

public Semaphore(int permits) {//參數permits表示許可數目,即同時能夠容許多少線程進行訪問
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {//這個多了一個參數fair表示是不是公平的,即等待時間越久的越先獲取許可
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

下面說一下Semaphore類中比較重要的幾個方法,首先是acquire()、release()方法:

public void acquire() throws InterruptedException {  } //獲取一個許可
public void acquire(int permits) throws InterruptedException { } //獲取permits個許可
public void release() { } //釋放一個許可
public void release(int permits) { } //釋放permits個許可

acquire()用來獲取一個許可,若無許可可以得到,則會一直等待,直到得到許可。release()用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。這4個方法都會被阻塞,若是想當即獲得執行結果,能夠使用下面幾個方法:

//嘗試獲取一個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false
public boolean tryAcquire() { }; 
//嘗試獲取一個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; 
//嘗試獲取permits個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false
public boolean tryAcquire(int permits) { }; 
//嘗試獲取permits個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; 

另外還能夠經過availablePermits()方法獲得可用的許可數目。

下面經過一個例子來看一下Semaphore的具體使用。倘若一個工廠有5臺機器,可是有8個工人,一臺機器同時只能被一個工人使用,只有使用完了,其餘工人才能繼續使用。那麼咱們就能夠經過Semaphore來實現:

package com.demo.aqs;

import java.util.concurrent.Semaphore;

public class TestSemaphore {

    public static void main(String[] args) {
        int N = 8; //工人數
        Semaphore semaphore = new Semaphore(5); //機器數目
        for(int i=0;i<N;i++)
            new Worker(i,semaphore).start();
    }
     
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
         
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println("工人"+this.num+"佔用一個機器在生產...");
                Thread.sleep(2000);
                System.out.println("工人"+this.num+"釋放出機器");
                semaphore.release();           
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:

工人1佔用一個機器在生產...
工人0佔用一個機器在生產...
工人4佔用一個機器在生產...
工人3佔用一個機器在生產...
工人2佔用一個機器在生產...
工人4釋放出機器
工人0釋放出機器
工人5佔用一個機器在生產...
工人3釋放出機器
工人1釋放出機器
工人7佔用一個機器在生產...
工人2釋放出機器
工人6佔用一個機器在生產...
工人5釋放出機器
工人7釋放出機器
工人6釋放出機器

總結

(1)CountDownLatch和CyclicBarrier都可以實現線程之間的等待,只不過它們側重點不一樣:CountDownLatch通常用於某個線程A等待若干個其餘線程執行完任務以後,它才執行;而CyclicBarrier通常用於一組線程互相等待至某個狀態,而後這一組線程再同時執行;另外,CountDownLatch是不可以重用的,而CyclicBarrier是能夠重用的。

(2)Semaphore其實和鎖有點相似,它通常用於控制對某組資源的訪問權限。

兩個線程交替打印奇偶數

方法一:使用同步方法實現

package com.demo.printOddEven;

public class Num {
    
    private int count = 1;
    
    /**
     * 打印奇數
     */
    public synchronized void printOdd() {
        try {
            if (count % 2 != 1) {
                this.wait();
            }
            System.out.println(Thread.currentThread().getName() + "----------" + count);
            count++;
            this.notify();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
        
    
    /**
     * 打印偶數
     */
    public synchronized void printEven() {
        try {
            if (count % 2 != 0) {
                this.wait();
            }
            System.out.println(Thread.currentThread().getName() + "----------" + count);
            count++;
            this.notify();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

 打印奇數的線程:

package com.demo.printOddEven;

/**
 * 打印奇數的線程
 * @author lixiaoxi
 *
 */
public class Odd implements Runnable{
    
    private Num num;
    
    public Odd(Num num) {
        this.num = num;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            num.printOdd();
        }
    }
}

打印偶數的線程:

package com.demo.printOddEven;

/**
 * 打印偶數的線程
 * @author lixiaoxi
 *
 */
public class Even implements Runnable {

    private Num num;
    
    public Even(Num num) {
        this.num = num;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            num.printEven();
        }
    }
    
}

測試:

package com.demo.printOddEven;

public class TestPrint {
    
    public static void main(String[] args) {
        Num num = new Num();
        Odd odd = new Odd(num);
        Even even = new Even(num);
        
        Thread t1 = new Thread(odd, "threadOdd");
        Thread t2 = new Thread(even, "threadEven");
        t1.start();
        t2.start();
    }

}

運行結果:

threadOdd----------1
threadEven----------2
threadOdd----------3
threadEven----------4
threadOdd----------5
threadEven----------6
threadOdd----------7
threadEven----------8
threadOdd----------9
threadEven----------10

方法二:使用同步塊實現

package com.demo.printOddEven;

public class Number {

    private int count = 1;
    
    private Object lock = new Object();
    
    /**
     * 打印奇數
     */
    public void printOdd() {
        synchronized(lock) {
            try {
                if(count % 2 != 1) {
                    lock.wait();
                }
                System.out.println(Thread.currentThread().getName() + "=======" + count);
                count++;
                lock.notify();
            } catch(InterruptedException e) {
                
            }
        }    
    }
    
    /**
     * 打印偶數
     */
    public void printEven() {
        synchronized(lock) {
            try {
                if(count % 2 != 0) {
                    lock.wait();
                }
                System.out.println(Thread.currentThread().getName() + "=======" + count);
                count++;
                lock.notify();
            } catch(InterruptedException e) {
                
            }
        }    
    }
    
    
}

打印奇數的線程:

package com.demo.printOddEven;

/**
 * 打印奇數的線程
 * @author lixiaoxi
 *
 */
public class OddPrinter implements Runnable {
    
    private Number num;
    
    public OddPrinter(Number num) {
        this.num = num;
    }
    
    public void run() {
        for (int i = 0; i < 5; i++) {
            num.printOdd();
        }
    }
}

打印偶數的線程:

package com.demo.printOddEven;

/**
 * 打印偶數的線程
 * @author lixiaoxi
 *
 */
public class EvenPrinter implements Runnable{
    
    private Number num;
    
    public EvenPrinter(Number num) {
        this.num = num;
    }
    
    public void run() {
        for (int i = 0; i < 5; i++) {
            num.printEven();
        }
    }

}

測試:

package com.demo.printOddEven;

public class PrintOddEven {

    public static void main(String[] args) {
        
        Number num = new Number();
        OddPrinter oddPrinter = new OddPrinter(num);
        EvenPrinter evenPrinter = new EvenPrinter(num);
        
        Thread oddThread = new Thread(oddPrinter, "oddThread");
        Thread evenThread = new Thread(evenPrinter, "evenThread");
        oddThread.start();
        evenThread.start();
    }
}

運行結果:

oddThread=======1
evenThread=======2
oddThread=======3
evenThread=======4
oddThread=======5
evenThread=======6
oddThread=======7
evenThread=======8
oddThread=======9
evenThread=======10

方法三:使用ReentrantLock實現

package com.demo.printOddEven;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Number1 {
    
    private int count = 1;
    
    private ReentrantLock lock = new ReentrantLock();
    // 爲打印奇數的線程註冊一個Condition
    public Condition conditionOdd = lock.newCondition();
    // 爲打印偶數的線程註冊一個Condition
    public Condition conditionEven = lock.newCondition();
    
    /**
     * 打印奇數
     */
    public void printOdd() {
        try {
            lock.lock();
            if(count % 2 != 1) {
                conditionOdd.await();
            }
            System.out.println(Thread.currentThread().getName() + "=======" + count);
            count++;
            conditionEven.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 打印偶數
     */
    public void printEven() {
        try {
            lock.lock();
            if(count % 2 != 0) {
                conditionEven.await();
            }
            System.out.println(Thread.currentThread().getName() + "=======" + count);
            count++;
            conditionOdd.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

測試:

package com.demo.printOddEven;

public class PrintOddEven1 {

    public static void main(String[] args) {
        
        final Number1 num = new Number1();
        
        /**
         * 打印奇數的線程
         */
        Thread oddThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    num.printOdd();
                }
            }
        }, "oddThread");
        
        /**
         * 打印偶數的線程
         */
        Thread evenThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    num.printEven();
                }
            }
        }, "evenThread");
        oddThread.start();
        evenThread.start();
    }
}

運行結果:

oddThread=======1
evenThread=======2
oddThread=======3
evenThread=======4
oddThread=======5
evenThread=======6
oddThread=======7
evenThread=======8
oddThread=======9
evenThread=======10

三個線程交替打印ABC

若是要實現3個線程交替打印ABC呢?此次打算使用重入鎖,和上面沒差多少,可是因爲如今有三個線程了,在打印完後須要喚醒其餘線程,注意不可以使用sigal(),由於喚醒的線程是隨機的,不能保證打印順序不說,還會形成死循環。必定要使用sigalAll()喚醒全部線程。

package com.demo.printABC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeThreadPrintABC {

     private static ReentrantLock lock = new ReentrantLock();
     private static Condition wait = lock.newCondition();
     // 用來控制該打印的線程
     private static int count = 0;
     
     static class PrintA implements Runnable {
         @Override
         public void run() {
             for (int i = 0; i < 10; i++) {
                 lock.lock();
                 try {
                     while ((count % 3) != 0) {
                         wait.await();
                     }
                     System.out.println(Thread.currentThread().getName() + " A");
                     count++;
                     wait.signalAll();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } finally {
                     lock.unlock();
                 }
             }
         }
     }

     static class PrintB implements Runnable {
         @Override
         public void run() {
             for (int i = 0; i < 10; i++) {
                 lock.lock();
                 try {
                     while ((count % 3) != 1) {
                         wait.await();
                     }
                     System.out.println(Thread.currentThread().getName() + " B");
                     count++;
                     wait.signalAll();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } finally {
                     lock.unlock();
                 }
             }
         }
     }

     static class PrintC implements Runnable {
         @Override
         public void run() {
             for (int i = 0; i < 10; i++) {
                 lock.lock();
                 try {
                     while ((count % 3) != 2) {
                         wait.await();
                     }
                     System.out.println(Thread.currentThread().getName() + " C");
                     count++;
                     wait.signalAll();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } finally {
                     lock.unlock();
                 }
             }
         }
     }
     
     public static void main(String[] args) {
         Thread printA = new Thread(new PrintA());
         Thread printB = new Thread(new PrintB());
         Thread printC = new Thread(new PrintC());
         printA.start();
         printB.start();
         printC.start();

     }
}

運行結果:

Thread-0 A
Thread-1 B
Thread-2 C
Thread-0 A
Thread-1 B
Thread-2 C
Thread-0 A
Thread-1 B
Thread-2 C
Thread-0 A
Thread-1 B
Thread-2 C

若是以爲很差理解,重入鎖是能夠綁定多個條件的。建立3個Condition分別讓三個打印線程在上面等待。A打印完了,喚醒等待在conditionB對象上的PrintB;B打印完了喚醒在conditionC對象上的PrintC;C打印完了,喚醒在conditionA對象上等待的PrintA,如此循環地喚醒對方便可。

package com.demo.printABC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ThreeThreadPrintABC1 {
    
    private static ReentrantLock lock = new ReentrantLock();
    
    private static Condition conditionA = lock.newCondition();
    private static Condition conditionB = lock.newCondition();
    private static Condition conditionC = lock.newCondition();
    // 用來控制該打印的線程
    private static int count = 0;
    
    static class PrintA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    if (count % 3 != 0) {
                        conditionA.await();
                    }
                    System.out.println(Thread.currentThread().getName() + "------A");
                    count++;
                    conditionB.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    
    static class PrintB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    if (count % 3 != 1) {
                        conditionB.await();
                    }
                    System.out.println(Thread.currentThread().getName() + "------B");
                    count++;
                    conditionC.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    
    static class PrintC implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                lock.lock();
                try {
                    if (count % 3 != 2) {
                        conditionC.await();
                    }
                    System.out.println(Thread.currentThread().getName() + "------C");
                    count++;
                    conditionA.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Thread printA = new Thread(new PrintA());
        Thread printB = new Thread(new PrintB());
        Thread printC = new Thread(new PrintC());
        printA.start();
        printB.start();
        printC.start();
    }

}

運行結果:

Thread-0------A
Thread-1------B
Thread-2------C
Thread-0------A
Thread-1------B
Thread-2------C
Thread-0------A
Thread-1------B
Thread-2------C
Thread-0------A
Thread-1------B
Thread-2------C

另外一種實現:

package com.demo.printABC;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 基於一個ReentrantLock和三個conditon實現連續打印abcabc...
 * @author lixiaoxi
 *
 */
public class PrintABCTest implements Runnable {
    
    // 打印次數
    private static final int PRINT_COUNT = 10;
    // 打印鎖
    private final ReentrantLock lock;
    // 本線程打印所需的condition
    private final Condition thisCondition;
    // 下一個線程打印所需的condition
    private final Condition nextCondition;
    // 打印字符
    private final char printChar;
    
    public PrintABCTest(ReentrantLock lock, Condition thisCondition, Condition nextCondition, char printChar) {
        this.lock = lock;
        this.thisCondition = thisCondition;
        this.nextCondition = nextCondition;
        this.printChar = printChar;
    }
    
    @Override
    public void run() {
        // 獲取打印鎖 進入臨界區
        lock.lock();
        try {
            // 連續打印PRINT_COUNT次
            for (int i = 0; i < PRINT_COUNT; i++) {
                //打印字符
                System.out.println(printChar);
                // 使用nextCondition喚醒下一個線程
                // 由於只有一個線程在等待,因此signal或者signalAll均可以
                nextCondition.signal();
                // 不是最後一次則經過thisCondtion等待被喚醒
                // 必需要加判斷,否則雖然可以打印10次,但10次後就會直接死鎖
                if (i < PRINT_COUNT -1) {
                    try {
                        // 本線程讓出鎖並等待喚醒
                        thisCondition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            // 釋放打印鎖
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        // 寫鎖
        ReentrantLock lock = new ReentrantLock();
        // 打印a線程的condition
        Condition conditionA = lock.newCondition();
        // 打印b線程的condition
        Condition conditionB = lock.newCondition();
        // 打印c線程的condition
        Condition conditionC = lock.newCondition();
        // 實例化A線程
        Thread printerA = new Thread(new PrintABCTest(lock, conditionA, conditionB, 'A'));
        // 實例化B線程
        Thread printerB = new Thread(new PrintABCTest(lock, conditionB, conditionC, 'B'));
        // 實例化C線程
        Thread printerC = new Thread(new PrintABCTest(lock, conditionC, conditionA, 'C'));
        // 依次開始A B C線程
        printerA.start();
        Thread.sleep(100);
        printerB.start();
        Thread.sleep(100);
        printerC.start();
    }

}

運行結果:

A
B
C
A
B
C
A
B
C
A
B
C

 

5、Mybatis

 一、Mybatis的實現原理

    mybatis底層仍是採用原生jdbc來對數據庫進行操做的,只是經過 SqlSessionFactory,SqlSession,Executor,StatementHandler,ParameterHandler,ResultHandler和TypeHandler等幾個處理器封裝了這些過程。其中StatementHandler用經過ParameterHandler與ResultHandler分別進行參數預編譯與結果處理。而ParameterHandler與ResultHandler都使用TypeHandler進行映射。以下圖: 

二、Mybatis的工做過程

Mybatis的運行分爲兩部分,第一部分是讀取配置文件緩存到Coufiguration對象,用以建立SqlSessionFactory,第二部分是SqlSession的執行過程。

初始化過程:

MyBatis的初始化的過程其實就是解析配置文件和初始化Configuration的過程,MyBatis的初始化過程可用如下幾行代碼來表述:

String resource = "mybatis.xml";

// 加載mybatis的配置文件(它也加載關聯的映射文件)
InputStream inputStream = null;
try {
    inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
    e.printStackTrace();
}

// 構建sqlSession的工廠
sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

MyBatis初始化基本過程總結以下:SqlSessionFactoryBuilder根據傳入的數據流生成Configuration對象,而後根據Configuration對象建立默認的SqlSessionFactory實例。其中序列圖以下:

上圖的初始化過程通過如下的幾步:

  • 1. 調用SqlSessionFactoryBuilder對象的build(inputStream)方法;
  • 2. SqlSessionFactoryBuilder會根據輸入流inputStream等信息建立XMLConfigBuilder對象;
  • 3. SqlSessionFactoryBuilder調用XMLConfigBuilder對象的parse()方法;
  • 4. XMLConfigBuilder對象返回Configuration對象;
  • 5. SqlSessionFactoryBuilder根據Configuration對象建立一個DefaultSessionFactory對象;
  • 6. SqlSessionFactoryBuilder返回 DefaultSessionFactory對象給Client,供Client使用。

總結:MyBatis的初始化的過程其實就是解析配置文件和初始化Configuration的過程。首先把核心配置文件也就是mybatis.xml文件加載進來,而後調用SqlSessionFactoryBuilder對象的build(inputStream)方法,根據輸入流inputStream等信息建立XMLConfigBuilder對象。XMLConfigBuilder對象再解析配置的XML文件,讀取配置參數,並將讀取的數據存入到Configuration類中。其次使用Configuration對象去建立SqlSessionFactory。

SqlSession的工做過程

1.開啓一個數據庫訪問會話---建立SqlSession對象:MyBatis使用SQLSession對象來封裝一次數據庫的會話訪問。經過該對象實現對事務的控制和數據查詢。

SqlSession sqlSession = factory.openSession();

MyBatis封裝了對數據庫的訪問,把對數據庫的會話和事務控制放到了SqlSession對象中。

2.爲SqlSession傳遞一個配置的Sql語句的StatementId和參數params,而後返回結果:

List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);  

上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params 是傳遞的查詢參數。

MyBatis在初始化的時候,會將MyBatis的配置信息所有加載到內存中,使用org.apache.ibatis.session.Configuration實例來維護。使用者能夠使用sqlSession.getConfiguration()方法來獲取。MyBatis的配置文件中配置信息的組織格式和內存中對象的組織格式幾乎徹底對應的。例如:

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" >  
  select   
    EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY  
    from LOUIS.EMPLOYEES  
    <if test="min_salary != null">  
        where SALARY < #{min_salary,jdbcType=DECIMAL}  
    </if>  
</select>  

加載到內存中會生成一個對應的MappedStatement對象,而後會以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value爲MappedStatement對象的形式維護到Configuration的一個Map中。當之後須要使用的時候,只須要經過Id值來獲取就能夠了。

從上述的代碼中咱們能夠看到SqlSession的職能是:SqlSession根據Statement Id,在mybatis配置對象Configuration中獲取到對應的MappedStatement對象,而後調用mybatis執行器來執行具體的操做。

3.MyBatis執行器Executor根據SqlSession傳遞的參數執行query()方法。Executor.query()方法會建立一個StatementHandler對象,而後將必要的參數傳遞給StatementHandler,使用StatementHandler來完成對數據庫的查詢,最終返回List結果集。

Executor的功能和做用是:

(1)、根據傳遞的參數,完成SQL語句的動態解析,生成BoundSql對象,供StatementHandler使用;

(2)、爲查詢建立緩存,以提升性能;

(3)、建立JDBC的Statement鏈接對象,傳遞給StatementHandler對象,返回List查詢結果。

4.StatementHandler對象負責設置Statement對象中的查詢參數、處理JDBC返回的resultSet,將resultSet加工爲List 集合返回。

StatementHandler對象主要完成兩個工做:

(1)、對於JDBC的PreparedStatement類型的對象,建立的過程當中,SQL語句字符串會包含若干個'?'佔位符,以後再對佔位符進行設值。StatementHandler經過parameterize(statement)方法對Statement進行設值;

(2)、StatementHandler經過List<E> query(Statement statement, ResultHandler resultHandler)方法來完成執行Statement,和將Statement對象返回的resultSet封裝成List。

5.StatementHandler 的parameterize(statement) 方法調用了 ParameterHandler的setParameters(statement)方法。

6.ParameterHandler的setParameters(Statement)方法負責 根據咱們輸入的參數,對statement對象的'?'佔位符處進行賦值。StatementHandler 的List<E> query(Statement statement, ResultHandler resultHandler)方法調用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執行後生成的resultSet 結果集轉換成List<E> 結果集。

主要過程:

1.DefaultSqlSession根據id在configuration中找到MappedStatement對象(要執行的語句)

2.Executor調用MappedStatement對象的getBoundSql獲得可執行的sql和參數列表

3.StatementHandler根據Sql生成一個Statement

4.ParameterHandler爲Statement設置相應的參數

5.Executor中執行sql語句

6.若是是更新(update/insert/delete)語句,sql的執行工做得此結束

7.若是是查詢語句,ResultSetHandler再根據執行結果生成ResultMap相應的對象返回。

三、Mybatis的主要構件及其相互關係

 從MyBatis代碼實現的角度來看,MyBatis的主要的核心部件有如下幾個:

  • Configuration         MyBatis全部的配置信息都保存在Configuration對象之中,配置文件中的大部分配置都會存儲到該類中
  • SqlSession            做爲MyBatis工做的主要頂層API,表示和數據庫交互時的會話,完成必要數據庫增刪改查功能
  • Executor                MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護
  • StatementHandler  封裝了JDBC Statement操做,負責對JDBC statement 的操做,如設置參數等
  • ParameterHandler  負責對用戶傳遞的參數轉換成JDBC Statement 所對應的數據類型
  • ResultSetHandler   負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合
  • TypeHandler          負責java數據類型和jdbc數據類型(也能夠說是數據表列類型)之間的映射和轉換
  • MappedStatement  MappedStatement維護一條<select|update|delete|insert>節點的封裝。Mapped Statement也是mybatis一個底層封裝對象,它包裝了                                   mybatis配置信息及sql映射信息等。mapper.xml文件中一個sql對應一個Mapped Statement對象,sql的id便是Mapped statement的id。
  • SqlSource              負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,並返回
  • BoundSql              表示動態生成的SQL語句以及相應的參數信息

它們的關係以下圖所示:

四、Mybatis和數據庫的交互方式

(1)使用傳統的MyBatis提供的API

    這是傳統的傳遞Statement Id 和查詢參數給 SqlSession 對象,使用 SqlSession對象完成和數據庫的交互;MyBatis 提供了很是方便和簡單的API,供用戶實現對數據庫的增刪改查數據操做,以及對數據庫鏈接信息和MyBatis 自身配置信息的維護操做。

    上述使用MyBatis 的方法,是建立一個和數據庫打交道的SqlSession對象,而後根據Statement Id 和參數來操做數據庫,這種方式當然很簡單和實用,可是它不符合面嚮對象語言的概念和麪向接口編程的編程習慣。因爲面向接口的編程是面向對象的大趨勢,MyBatis 爲了適應這一趨勢,增長了第二種使用MyBatis支持接口(Interface)調用方式。

(2)使用Mapper接口

    MyBatis 將配置文件中的每個<mapper> 節點抽象爲一個 Mapper 接口,而這個接口中聲明的方法和跟<mapper> 節點中的<select|update|delete|insert> 節點相對應,即<select|update|delete|insert> 節點的id值爲Mapper 接口中的方法名稱,parameterType 值表示Mapper 對應方法的入參類型,而resultMap 值則對應了Mapper 接口表示的返回值類型或者返回結果集的元素類型

     根據MyBatis 的配置規範配置好後,經過SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 會根據相應的接口聲明的方法信息,經過動態代理機制生成一個Mapper 實例,咱們使用Mapper 接口的某一個方法時,MyBatis 會根據這個方法的方法名和參數類型,肯定Statement Id,底層仍是經過SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等來實現對數據庫的操做。

    MyBatis 引用Mapper 接口這種調用方式,純粹是爲了知足面向接口編程的須要。(其實還有一個緣由是在於,面向接口的編程,使得用戶在接口上能夠使用註解來配置SQL語句,這樣就能夠脫離XML配置文件,實現「0配置」)。

五、#{} 和 ${}的區別是什麼?

#{}是sql的參數佔位符,Mybatis會將sql中的#{}替換爲?號,在sql執行前會使用PreparedStatement的參數設置方法,按序給sql的?號佔位符設置參數值,好比

PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1,id);

這樣作的好處是:更安全,更迅速,一般也是首選作法。#{item.name}的取值方式爲使用反射從參數對象中獲取item對象的name屬性值,至關於param.getItem().getName()。

${}是Properties文件中的變量佔位符,它能夠用於標籤屬性值和sql內部,屬於靜態文本替換,好比${driver}會被靜態替換爲com.mysql.jdbc.Driver。


(1)#至關於對數據加上雙引號,$至關於直接顯示數據。

(2) #將傳入的數據都當成一個字符串,會對自動傳入的數據加一個雙引號。如:order by #user_id#,若是傳入的值是111,那麼解析成sql時的值爲order by "111",若是傳入的值是id,則解析成的sql爲order by "id"。

(3)$將傳入的數據直接顯示生成在sql中。如:order by $user_id$,若是傳入的值是111,那麼解析成sql時的值爲order by 111,若是傳入的值是id,則解析成的sql爲order by id。

(4)#方式可以很大程度防止sql注入,$方式沒法防止Sql注入

(5)$方式通常用於傳入數據庫對象,例如傳入表名。

(6)通常能用#的就別用$。

(7)MyBatis排序時使用order by 動態參數時須要注意,用$而不是#。

默認狀況下,使用#{}格式的語法會致使MyBatis建立預處理語句屬性並以它爲背景設置安全的值(好比?)。這樣作很安全,很迅速也是首選作法,有時你只是想直接在SQL語句中插入一個不改變的字符串。好比,像ORDER BY,你能夠這樣來使用:ORDER BY ${columnName} 這裏MyBatis不會修改或轉義字符串。


(1)先上結論

#{}:佔位符號,好處防止sql注入

${}:sql拼接符號

(2)具體分析

動態 SQL 是 mybatis 的強大特性之一,也是它優於其餘 ORM 框架的一個重要緣由。mybatis 在對 sql 語句進行預編譯以前,會對 sql 進行動態解析,解析爲一個 BoundSql 對象,也是在此處對動態 SQL 進行處理的。在動態 SQL 解析階段, #{ } 和 ${ } 會有不一樣的表現。

#{ }:解析爲一個 JDBC 預編譯語句(prepared statement)的參數標記符。例如,Mapper.xml中以下的 sql 語句:

select * from user where name = #{name}; 

動態解析爲:

select * from user where name = ?; 

一個 #{ } 被解析爲一個參數佔位符 ? ,而${ } 僅僅爲一個純碎的 string 替換,在動態 SQL 解析階段將會進行變量替換

例如,Mapper.xml中以下的 sql:

select * from user where name = ${name}; 

當咱們傳遞的參數爲 "Jack" 時,上述 sql 的解析爲:

select * from user where name = "Jack"; 

預編譯以前的 SQL 語句已經不包含變量了,徹底已是常量數據了。 綜上所得, ${ } 變量的替換階段是在動態 SQL 解析階段,而 #{ }變量的替換是在 DBMS 中

(3)用法

一、能使用 #{ } 的地方就用 #{ }

首先這是爲了性能考慮的,相同的預編譯 sql 能夠重複利用。其次,${ } 在預編譯以前已經被變量替換了,這會存在 sql 注入問題。例如,以下的 sql:

select * from ${tableName} where name = #{name} 

假如,咱們的參數 tableName 爲 user; delete user; --,那麼 SQL 動態解析階段以後,預編譯以前的 sql 將變爲:

select * from user; delete user; -- where name = ?; 

-- 以後的語句將做爲註釋,不起做用,所以原本的一條查詢語句偷偷的包含了一個刪除表數據的 SQL。

二、表名做爲變量時,必須使用 ${ }

這是由於,表名是字符串,使用 sql 佔位符替換字符串時會帶上單引號 '',這會致使 sql 語法錯誤,例如:

select * from #{tableName} where name = #{name};

預編譯以後的sql 變爲:

select * from ? where name = ?; 

假設咱們傳入的參數爲 tableName = "user" , name = "Jack",那麼在佔位符進行變量替換後,sql 語句變爲:

select * from 'user' where name='Jack'; 

上述 sql 語句是存在語法錯誤的,表名不能加單引號 ''(注意,反引號 ``是能夠的)。

(4)sql預編譯

 一、定義:

sql 預編譯指的是數據庫驅動在發送 sql 語句和參數給 DBMS 以前對 sql 語句進行編譯,這樣 DBMS 執行 sql 時,就不須要從新編譯。

 二、爲何須要預編譯

JDBC 中使用對象 PreparedStatement 來抽象預編譯語句,使用預編譯。預編譯階段能夠優化 sql 的執行。預編譯以後的 sql 多數狀況下能夠直接執行,DBMS 不須要再次編譯,越複雜的sql,編譯的複雜度將越大,預編譯階段能夠合併屢次操做爲一個操做。預編譯語句對象能夠重複利用。把一個 sql 預編譯後產生的 PreparedStatement 對象緩存下來,下次對於同一個sql,能夠直接使用這個緩存的 PreparedState 對象。mybatis 默認狀況下,將對全部的 sql 進行預編譯。

六、最佳實踐中,一般一個Xml映射文件,都會寫一個Dao接口與之對應,請問,這個Dao接口的工做原理是什麼?Dao接口裏的方法,參數不一樣時,方法能重載嗎?

    Dao接口,就是人們常說的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法內的參數,就是傳遞給sql的參數。Mapper接口是沒有實現類的,當調用接口方法時,接口全限名+方法名拼接字符串做爲key值,可惟必定位一個MappedStatement,舉例:com.mybatis3.mappers.StudentDao.findStudentById,能夠惟一找到namespace爲com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每個<select>、<insert>、<update>、<delete>標籤,都會被解析爲一個MappedStatement對象。

   Dao接口裏的方法,是不能重載的,由於是全限名+方法名的保存和尋找策略。

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

七、使用Mybatis的mapper接口調用時有哪些要求?

①  Mapper接口方法名和mapper.xml中定義的每一個sql的id相同 
②  Mapper接口方法的輸入參數類型和mapper.xml中定義的每一個sql 的parameterType的類型相同 
③  Mapper接口方法的輸出參數類型和mapper.xml中定義的每一個sql的resultType的類型相同 
④  Mapper.xml文件中的namespace便是mapper接口的類路徑。

八、Mybatis是如何進行分頁的?分頁插件的原理是什麼?

Mybatis使用RowBounds對象進行分頁,它是針對ResultSet結果集執行的內存分頁,而非物理分頁,能夠在sql內直接書寫帶有物理分頁的參數來完成物理分頁功能,也能夠使用分頁插件來完成物理分頁。

分頁插件的基本原理是使用Mybatis提供的插件接口,實現自定義插件,在插件的攔截方法內攔截待執行的sql,而後重寫sql,根據dialect方言,添加對應的物理分頁語句和物理分頁參數。

舉例:select * from student,攔截sql後重寫爲:select t.* from (select * from student)t limit 0,10

九、簡述Mybatis的插件運行原理,以及如何編寫一個插件。

Mybatis僅能夠編寫針對ParameterHandler、ResultSetHandler、StatementHandler、Executor這4種接口的插件,Mybatis使用JDK的動態代理,爲須要攔截的接口生成代理對象以實現接口方法攔截功能,每當執行這4種接口對象的方法時,就會進入攔截方法,具體就是InvocationHandler的invoke()方法,固然,只會攔截那些你指定須要攔截的方法。

實現Mybatis的Interceptor接口並複寫intercept()方法,而後在給插件編寫註解,指定要攔截哪個接口的哪些方法便可,記住,別忘了在配置文件中配置你編寫的插件。

十、Mybatis動態sql是作什麼的?都有哪些動態sql?能簡述一下動態sql的執行原理不?

Mybatis動態sql可讓咱們在Xml映射文件內,以標籤的形式編寫動態sql,完成邏輯判斷和動態拼接sql的功能。整體說來mybatis 動態SQL 語句主要有如下幾類:

1. if 語句 (簡單的條件判斷)

2. choose (when,otherwize) ,至關於java 語言中的 switch ,與 jstl 中的choose 很相似.

3. trim (對包含的內容加上 prefix,或者 suffix 等,前綴,後綴)

4. where (主要是用來簡化sql語句中where條件判斷的,能智能的處理 and or ,沒必要擔憂多餘致使語法錯誤)

5. set (主要用於更新時)

6. foreach (在實現 mybatis in 語句查詢時特別有用)

下面分別介紹這幾種處理方式

一、mybatis if語句處理

<select id="dynamicIfTest" parameterType="Blog" resultType="Blog">
    select * from t_blog where 1 = 1
    <if test="title != null">
        and title = #{title}
    </if>
    <if test="content != null">
        and content = #{content}
    </if>
    <if test="owner != null">
        and owner = #{owner}
    </if>
</select>

解析

    若是你提供了title參數,那麼就要知足title=#{title},一樣若是你提供了Content和Owner的時候,它們也須要知足相應的條件,以後就是返回知足這些條件的全部Blog,這是很是有用的一個功能。

    以往咱們使用其餘類型框架或者直接使用JDBC的時候, 若是咱們要達到一樣的選擇效果的時候,咱們就須要拼SQL語句,這是極其麻煩的,比起來,上述的動態SQL就要簡單多了。

二、choose (when,otherwize) ,至關於java 語言中的 switch ,與 jstl 中的choose 很相似

<select id="dynamicChooseTest" parameterType="Blog" resultType="Blog">
    select * from t_blog where 1 = 1 
    <choose>
        <when test="title != null">
            and title = #{title}
        </when>
        <when test="content != null">
            and content = #{content}
        </when>
        <otherwise>
            and owner = "owner1"
        </otherwise>
    </choose>
</select>

    when元素表示當when中的條件知足的時候就輸出其中的內容,跟JAVA中的switch效果差很少的是按照條件的順序,當when中有條件知足的時候,就會跳出choose,即全部的when和otherwise條件中,只有一個會輸出,當全部的我很條件都不知足的時候就輸出otherwise中的內容。因此上述語句的意思很是簡單,當title!=null的時候就輸出and titlte = #{title},再也不往下判斷條件,當title爲空且content!=null的時候就輸出and content = #{content},當全部條件都不知足的時候就輸出otherwise中的內容。

三、trim (對包含的內容加上 prefix,或者 suffix 等,前綴,後綴)

<select id="dynamicTrimTest" parameterType="Blog" resultType="Blog">
    select * from t_blog 
    <trim prefix="where" prefixOverrides="and |or">
        <if test="title != null">
            title = #{title}
        </if>
        <if test="content != null">
            and content = #{content}
        </if>
        <if test="owner != null">
            or owner = #{owner}
        </if>
    </trim>
</select>

    trim元素的主要功能是能夠在本身包含的內容前加上某些前綴也能夠在其後加上某些後綴,與之對應的屬性是prefix和suffix;能夠把包含內容的首部某些內容覆蓋,即忽略,也能夠把尾部的某些內容覆蓋,對應的屬性是prefixOverrides和suffixOverrides;正由於trim有這樣的功能,因此咱們也能夠很是簡單的利用trim來代替where元素的功能。

trim標記是一個格式化的標記,能夠完成set或者是where標記的功能,以下代碼:

select * from user 
<trim prefix="WHERE" prefixoverride="AND |OR">
    <if test="name != null and name.length()>0"> 
        AND name=#{name}
    </if>
    <if test="gender != null and gender.length()>0"> 
        AND gender=#{gender}
    </if>
</trim>

假如說name和gender的值都不爲null的話打印的SQL爲:select * from user where    name = 'xx' and gender = 'xx'

在紅色標記的地方是不存在第一個and的,上面兩個屬性的意思以下:

prefix:前綴      

prefixoverride:去掉第一個and或者是or

update user
<trim prefix="set" suffixoverride="," suffix=" where id = #{id} ">
    <if test="name != null and name.length()>0">
        name=#{name} ,
    </if>
    <if test="gender != null and gender.length()>0">
        gender=#{gender} ,  
    </if>
</trim>

假如說name和gender的值都不爲null的話打印的SQL爲:update user set name='xx' , gender='xx'     where id='x'

在紅色標記的地方不存在逗號,並且自動加了一個set前綴和where後綴,上面三個屬性的意義以下,其中prefix意義如上:

suffixoverride:去掉最後一個逗號(也能夠是其餘的標記,就像是上面前綴中的and同樣)

suffix:後綴

四、where (主要是用來簡化sql語句中where條件判斷的,能智能的處理 and or 條件)

<select id="dynamicWhereTest" parameterType="Blog" resultType="Blog">
    select * from t_blog 
    <where>
        <if test="title != null">
            title = #{title}
        </if>
        <if test="content != null">
            and content = #{content}
        </if>
        <if test="owner != null">
            and owner = #{owner}
        </if>
    </where>
</select>

    where元素的做用是會在寫入where元素的地方輸出一個where,另一個好處是你不須要考慮where元素裏面的條件輸出是什麼樣子的,MyBatis會智能的幫你處理,若是全部的條件都不知足那麼MyBatis就會查出全部的記錄,若是輸出後是and 開頭的,MyBatis會把第一個and忽略,固然若是是or開頭的,MyBatis也會把它忽略;此外,在where元素中你不須要考慮空格的問題,MyBatis會智能的幫你加上。像上述例子中,若是title=null, 而content != null,那麼輸出的整個語句會是select * from t_blog where content = #{content},而不是select * from t_blog where and content = #{content},由於MyBatis會智能的把首個and 或 or 給忽略。

五、set (主要用於更新時) 

<update id="dynamicSetTest" parameterType="Blog">
    update t_blog
    <set>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="content != null">
            content = #{content},
        </if>
        <if test="owner != null">
            owner = #{owner}
        </if>
    </set>
    where id = #{id}
</update>

    set元素主要是用在更新操做的時候,它的主要功能和where元素實際上是差很少的,主要是在包含的語句前輸出一個set,而後若是包含的語句是以逗號結束的話將會把該逗號忽略,若是set包含的內容爲空的話則會出錯。有了set元素咱們就能夠動態的更新那些修改了的字段。

六、foreach (在實現 mybatis in 語句查詢時特別有用)

foreach的主要用在構建in條件中,它能夠在SQL語句中進行迭代一個集合。foreach元素的屬性主要有item,index,collection,open,separator,close。

(1)item表示集合中每個元素進行迭代時的別名。

(2)index指定一個名字,用於表示在迭代過程當中,每次迭代到位置。

(3)open表示該語句以什麼開始。

(4)separator表示在每次進行迭代之間以什麼符號做爲分隔符。

(5)close表示以什麼結束。

在使用foreach的時候最關鍵的也是最容易出錯的就是collection屬性,該屬性是必須指定的,可是在不一樣狀況下,該屬性的值是不同的,主要有一下3種狀況:

(1)若是傳入的是單參數參數類型是一個List的時候,collection屬性值爲list

(2)若是傳入的是單參數且參數類型是一個array數組的時候,collection的屬性值爲array

(3)若是傳入的參數是多個的時候,咱們就須要把它們封裝成一個Map了,固然單參數也能夠封裝成map,實際上若是你在傳入參數的時候,在MyBatis裏面也是會把它封裝成一個Map的,map的key就是參數名,因此這個時候collection屬性值就是傳入的List或array對象在本身封裝的map裏面的key。

一、單參數List的類型

<select id="dynamicForeachTest" resultType="com.mybatis.entity.User">
    select * from t_user where id in
    <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

上述collection的值爲list,對應的Mapper是這樣的:

/**mybatis Foreach測試 */
public List<User> dynamicForeachTest(List<Integer> ids); 

測試:

@Test
public void dynamicForeachTest() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<Integer> ids = new ArrayList<Integer>();
    ids.add(1);
    ids.add(2);
    ids.add(6);
    List<User> userList = mapper.dynamicForeachTest(ids);
    for (User user : userList){
        System.out.println(user);
    }
    sqlSession.close();
}

二、數組類型的參數

<select id="dynamicForeach2Test" resultType="com.mybatis.entity.User">
    select * from t_user where id in
    <foreach collection="array" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

對應mapper:

public List<User> dynamicForeach2Test(int[] ids);  

測試:

@Test
public void dynamicForeach2Test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    int[] ids = {1,2,6};
    List<User> userList = mapper.dynamicForeach2Test(ids);
    for (User user : userList){
        System.out.println(user);
    }
    sqlSession.close();
}

三、Map類型的參數

<select id="dynamicForeach3Test" resultType="com.mybatis.entity.User">
    select * from t_user where username like '%${username}%' and id in
    <foreach collection="ids" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

mapper 應該是這樣的接口:

/**mybatis Foreach測試 */
public List<User> dynamicForeach3Test(Map<String, Object> params); 

測試:

@Test
public void dynamicForeach3Test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<Integer> ids = new ArrayList<Integer>();
    ids.add(1);
    ids.add(2);
    ids.add(6);
    Map map =new HashMap();
    map.put("username", "小");
    map.put("ids", ids);
    List<User> userList = mapper.dynamicForeach3Test(map);
    System.out.println("------------------------");
    for (User user : userList){
        System.out.println(user);
    }
    sqlSession.close();
}

mybatis的動態sql的執行原理爲,使用OGNL從sql參數對象中計算表達式的值,根據表達式的值動態拼接sql,以此來完成動態sql的功能。

 十一、mybatis中resultType和resultMap使用時的區別

   MyBatis中關於resultType和resultMap的具體區別以下:

   MyBatis中在查詢進行select映射的時候,返回類型能夠用resultType,也能夠用resultMap,resultType是直接表示返回類型的(對應着咱們的model對象中的實體),而resultMap則是對外部ResultMap的引用(提早定義了db和model之間的隱射key-->value關係),可是resultType跟resultMap不能同時存在

   在MyBatis進行查詢映射時,其實查詢出來的每個屬性都是放在一個對應的Map裏面的,其中鍵是列名,值則是其對應的值。

   1.當提供的返回類型屬性是resultType時,MyBatis會將Map裏面的鍵值對取出賦給resultType所指定的對象對應的屬性。因此其實MyBatis的每個查詢映射的返回類型都是ResultMap,只是當提供的返回類型屬性是resultType的時候,MyBatis會自動把對應的值賦給resultType所指定對象的屬性。

   2.當提供的返回類型是resultMap時,由於Map不能很好表示領域模型,就須要本身再進一步的把它轉化爲對應的對象,這經常在複雜查詢中頗有做用。


(1)resultType能夠映射結果集爲基本類型的,而resultMap不能映射結果集爲基本類型。

(2)使用resultType進行輸出映射,只有查詢出來的列名和pojo中的屬性名一致,該列才能夠映射成功。若是查詢出來的列名和pojo中的屬性名所有不一致,沒有建立pojo對象。只要查詢出來的列名和pojo中的屬性有一個一致,就會建立pojo對象。對於列名和屬性名不一致的狀況,就須要經過resultMap來解決。即用resultType進行輸出映射,只有查詢出來的列名和pojo中的屬性名一致,該列才能夠映射成功。若是查詢出來的列名和pojo的屬性名不一致,經過定義一個resultMap對列名和pojo屬性名之間做一個映射關係。

(3)resultType 一般用於接收基本類型,包裝類型的結果集映射(包裝類型的時候就有要求了,必須包裝類型中的屬性值跟查詢結果的字段對應的上,不然的話對應不上的屬性是接收不到查詢結果的)。而resultMap用於解決複雜查詢時的映射問題。好比:列名和對象屬性名不一致時能夠使用resultMap來配置;還有查詢的對象中包含其餘的對象等。

(4)resultMap能夠實現延遲加載,resultType沒法實現延遲加載。

十二、Mybatis中的一對1、一對多查詢

一對一查詢:

a.resultType:使用resultType實現較爲簡單,若是pojo中沒有包括查詢出來的列名,須要增長列名對應的屬性,便可完成映射。

b.若是沒有查詢結果的特殊要求建議使用resultType。

c.resultMap:須要單獨定義resultMap,實現有點麻煩,若是對查詢結果有特殊的要求,使用resultMap能夠完成將關聯查詢映射pojo的屬性中。

d.resultMap能夠實現延遲加載,resultType沒法實現延遲加載。

在一對一結果映射時,使用resultType更加簡單方便,若是有特殊要求(對象嵌套對象)時,須要使用resultMap進行映射,好比:查詢訂單列表,而後在點擊列表中的查看訂單明細按鈕,這個時候就須要使用resultMap進行結果映射。而resultType更適用於查詢明細信息,好比,查詢訂單明細列表。

一對多查詢(例如查詢訂單及訂單明細):

mybatis使用resultMap的collection對關聯查詢的多條記錄映射到一個list集合屬性中。

而使用resultType實現:將訂單明細映射到orders中的orderdetails中,須要本身處理,使用雙重循環遍歷,去掉重複記錄,將訂單明細放在orderdetails中。

總結:

resultType:

做用:將查詢結果按照sql列名pojo屬性名一致性映射到pojo中。

場合:常見一些明細記錄的展現,好比用戶購買商品明細,將關聯查詢信息所有展現在頁面時,此時可直接使用resultType將每一條記錄映射到pojo中,在前端頁面遍歷list(list中是pojo)便可。

resultMap

使用association和collection完成一對一和一對多高級映射(對結果有特殊的映射要求)。

association

做用:將關聯查詢信息映射到一個pojo對象中。

場合:爲了方便查詢關聯信息能夠使用association將關聯訂單信息映射爲用戶對象的pojo屬性中,好比:查詢訂單及關聯用戶信息。使用resultType沒法將查詢結果映射到pojo對象的pojo屬性中,根據對結果集查詢遍歷的須要選擇使用resultType仍是resultMap。

collection:

做用:將關聯查詢信息映射到一個list集合中。

場合:爲了方便查詢遍歷關聯信息能夠使用collection將關聯信息映射到list集合中,好比:查詢用戶權限範圍模塊及模塊下的菜單,可以使用collection將模塊映射到模塊list中,將菜單列表映射到模塊對象的菜單list屬性中,這樣的做的目的也是方便對查詢結果集進行遍歷查詢。若是使用resultType沒法將查詢結果映射到list集合中。

1三、Mybatis緩存

MyBatis 提供了查詢緩存來緩存數據,以提升查詢的性能。MyBatis 的緩存分爲一級緩存二級緩存

一、一級緩存是sqlSession級別的緩存。在操做數據庫時須要構造sqlSession對象,在對象中有一個數據結構(HashMap),用於存儲緩存數據。不一樣的sqlSession之間的緩存區域(HashMap)是互不影響的。

二、二級緩存是mapper級別的緩存,多個sqlSession去操做同一個Mapper的sql語句,多個SqlSession能夠公用二級緩存,二級緩存是跨sqlSession的。

一級緩存

一級緩存是 SqlSession 級別的緩存,是基於 HashMap 的本地緩存。不一樣的 SqlSession 之間的緩存數據區域互不影響。

一級緩存的做用域是 SqlSession 範圍,當同一個 SqlSession 執行兩次相同的 sql 語句時,第一次執行完後會將數據庫中查詢的數據寫到緩存,第二次查詢時直接從緩存獲取不用去數據庫查詢。當 SqlSession 執行 insert、update、delete 操作並提交到數據庫時,會清空緩存,保證緩存中的信息是最新的。

MyBatis默認開啓一級緩存。

注意事項:

1.若是SqlSession執行了DML操做(insert、update、delete),並commit了,那麼mybatis就會清空當前SqlSession緩存中的全部緩存數據,這樣能夠保證緩存中的存的數據永遠和數據庫中一致,避免出現髒讀。

2.當一個SqlSession結束後那麼他裏面的一級緩存也就不存在了,mybatis默認是開啓一級緩存,不須要配置。

3.mybatis的緩存是基於[namespace:sql語句:參數]來進行緩存的,意思就是,SqlSession的HashMap存儲緩存數據時,是使用[namespace:sql:參數]做爲key,查詢返回的語句做爲value保存的。

二級緩存

二級緩存是 mapper 級別的緩存,一樣是基於 HashMap 進行存儲,多個 SqlSession 能夠共用二級緩存,其做用域是 mapper 的同一個 namespace。不一樣的 SqlSession 兩次執行相同的 namespace 下的 sql 語句,會執行相同的 sql,第二次查詢只會查詢第一次查詢時讀取數據庫後寫到緩存的數據,不會再去數據庫查詢。

MyBatis 默認沒有開啓二級緩存,開啓只需在配置文件中寫入以下代碼:

<settings>  
      <setting name="cacheEnabled" value="true"/>  
</settings>

注意事項:

1.若是SqlSession執行了DML操做(insert、update、delete),並commit了,那麼mybatis就會清空當前mapper緩存中的全部緩存數據,這樣能夠保證緩存中的存的數據永遠和數據庫中一致,避免出現髒讀

2.mybatis的緩存是基於[namespace:sql語句:參數]來進行緩存的,意思就是,SqlSession的HashMap存儲緩存數據時,是使用[namespace:sql:參數]做爲key,查詢返回的語句做爲value保存的。

總結:

一、一級緩存: 基於PerpetualCache 的 HashMap本地緩存,其存儲做用域爲 Session,當 Session flush  close 以後,該Session中的全部 Cache 就將清空

二、二級緩存與一級緩存其機制相同,默認也是採用 PerpetualCache,HashMap存儲,不一樣在於其存儲做用域爲 Mapper(Namespace),而且可自定義存儲源,如 Ehcache。

三、對於緩存數據更新機制,當某一個做用域(一級緩存Session/二級緩存Namespaces)的進行了 C/U/D 操做後,默認該做用域下全部 select 中的緩存將被clear。

1四、Mybatis是否支持延遲加載?若是支持,它的實現原理是什麼?

Mybatis僅支持association關聯對象和collection關聯集合對象的延遲加載,association指的就是一對一,collection指的就是一對多查詢。在Mybatis配置文件中,能夠配置是否啓用延遲加載lazyLoadingEnabled=true|false。

它的原理是,使用CGLIB建立目標對象的代理對象,當調用目標方法時,進入攔截器方法,好比調用a.getB().getName(),攔截器invoke()方法發現a.getB()是null值,那麼就會單獨發送事先保存好的查詢關聯B對象的sql,把B查詢上來,而後調用a.setB(b),因而a的對象b屬性就有值了,接着完成a.getB().getName()方法的調用。這就是延遲加載的基本原理。

1五、爲何說Mybatis是半自動ORM映射工具?它與全自動的區別在哪裏?

Hibernate屬於全自動ORM映射工具,使用Hibernate查詢關聯對象或者關聯集合對象時,能夠根據對象關係模型直接獲取,因此它是全自動的。而Mybatis在查詢關聯對象或關聯集合對象時,須要手動編寫sql來完成,因此,稱之爲半自動ORM映射工具。

 

 

6、MySQL

B+Tree的定義

B+Tree是B樹的變種,有着比B樹更高的查詢性能,來看下m階B+Tree特徵:

(1)有m個子樹的節點包含有m個元素(B-Tree中是m-1)。

(2)根節點和分支節點中不保存數據,只用於索引,全部數據都保存在葉子節點中。

(3)全部分支節點和根節點都同時存在於子節點中,在子節點元素中是最大或者最小的元素。

(4)葉子節點會包含全部的關鍵字,以及指向數據記錄的指針,而且葉子節點自己是根據關鍵字的大小從小到大順序連接。

B+Tree相對於B-Tree有幾點不一樣:

(1)非葉子節點只存儲鍵值信息。

(2)全部葉子節點之間都有一個鏈指針。

(3)數據記錄都存放在葉子節點中。

做爲B樹的增強版,B+樹與B樹的差別在於

(1)有n棵子樹的節點含有n個關鍵字(也有認爲是n-1個關鍵字)

(2)全部的葉子節點包含了所有的關鍵字,及指向含這些關鍵字記錄的指針,且葉子節點自己根據關鍵字自小而大順序鏈接

(3)非葉子節點能夠當作索引部分,節點中僅含有其子樹(根節點)中的最大(或最小)關鍵字

數據庫爲何要用B+樹結構?

爲何使用B+樹?言簡意賅,就是由於:

(1)索引文件很大,不可能所有存儲在內存中,故要存儲到磁盤上

(2)索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數(爲何使用B-/+Tree,還跟磁盤存取原理有關。)

(3)局部性原理與磁盤預讀,預讀的長度通常爲頁(page)的整倍數,(在許多操做系統中,頁得大小一般爲4k)。

(4)數據庫系統巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次I/O就能夠徹底載入,(因爲節點中有兩個數組,因此地址連續)。而紅黑樹這種結構,h明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性。

    通常來講,索引自己也很大,不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級,因此評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤I/O操做次數的漸進複雜度。換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數。

    對於B-Tree而言,可知檢索一次最多須要訪問h個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次I/O就能夠徹底載入。B樹的每一個節點能夠存儲多個關鍵字,它將節點大小設置爲磁盤頁的大小,充分利用了磁盤預讀的功能。每次讀取磁盤頁時就會讀取一整個節點。也正因每一個節點存儲着很是多個關鍵字,樹的深度就會很是的小。進而要執行的磁盤讀取操做次數就會很是少,更多的是在內存中對讀取進來的數據進行查找。

爲何紅黑樹不適合作索引?

    紅黑樹這種結構,h明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性,因此紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差不少。也就是說,使用紅黑樹(平衡二叉樹)結構的話,每次磁盤預讀中的不少數據是用不上的數據。所以,它沒能利用好磁盤預讀的提供的數據。而後又因爲深度大(較B樹而言),因此進行的磁盤IO操做更多。

    B樹的查詢,主要發生在內存中,而平衡二叉樹的查詢,則是發生在磁盤讀取中。所以,雖然B樹查詢查詢的次數不比平衡二叉樹的次數少,可是相比起磁盤IO速度,內存中比較的耗時就能夠忽略不計了。所以,B樹更適合做爲索引。

比B樹更適合做爲索引的結構——B+樹

比B樹更適合做爲索引的結構是B+樹。MySQL中也是使用B+樹做爲索引。它是B樹的變種,所以是基於B樹來改進的。爲何B+樹會比B樹更加優秀呢?

B樹:有序數組+平衡多叉樹; 
B+樹:有序數組鏈表+平衡多叉樹;

    從B-Tree結構圖中能夠看到每一個節點中不只包含數據的key值,還有data值。而每個頁的存儲空間是有限的,若是data數據較大時將會致使每一個節點(即一個頁)能存儲的key的數量很小,當存儲的數據量很大時一樣會致使B-Tree的深度較大,增大查詢時的磁盤I/O次數,進而影響查詢效率。在B+Tree中,全部數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣能夠大大加大每一個節點存儲的key值數量,下降B+Tree的高度。

    B+樹的關鍵字所有存放在葉子節點中,非葉子節點用來作索引,而葉子節點中有一個指針指向一下個葉子節點。作這個優化的目的是爲了提升區間訪問的性能。而正是這個特性決定了B+樹更適合用來存儲外部數據。

    數據庫索引採用B+樹的主要緣由是B樹在提升了磁盤IO性能的同時並無解決元素遍歷的效率低下的問題。正是爲了解決這個問題,B+樹應運而生。B+樹只要遍歷葉子節點就能夠實現整棵樹的遍歷。並且在數據庫中基於範圍的查詢是很是頻繁的,而B樹不支持這樣的操做(或者說效率過低)。

B+樹的優點

(1)單節點能夠存儲更多的元素,使得查詢磁盤IO次數更少。首先B+樹的中間節點不存儲數據,因此一樣大小的磁盤頁能夠容納更多的節點元素,如此一來,相同數量的數據下,B+樹就相對來講要更加矮胖些,磁盤IO的次數更少。

(2)全部查詢都要查找到葉子節點,查詢性能穩定。因爲只有葉子節點才保存數據,B+樹每次查詢都要到葉子節點;而B樹每次查詢則不同,最好的狀況是根節點,最壞的狀況是葉子節點,沒有B+樹穩定。

(3)全部葉子節點造成有序鏈表,便於範圍查詢。

servlet的生命週期

Servlet運行在Servlet容器中,其生命週期由容器來管理。Servlet的生命週期經過javax.servlet.Servlet接口中的init()、service()和destroy()方法來表示。

Servlet的生命週期包含了下面4個階段:

(1)加載和實例化

     Servlet容器負責加載和實例化Servlet。當Servlet容器啓動時,或者在容器檢測到須要這個Servlet來響應第一個請求時,建立Servlet實例。當Servlet容器啓動後,它必需要知道所需的Servlet類在什麼位置,Servlet容器能夠從本地文件系統、遠程文件系統或者其餘的網絡服務中經過類加載器加載Servlet類,成功加載後,容器建立Servlet的實例。由於容器是經過Java的反射API來建立Servlet實例,調用的是Servlet的默認構造方法(即不帶參數的構造方法),因此咱們在編寫Servlet類的時候,不該該提供帶參數的構造方法。

(2)初始化

      在Servlet實例化以後,容器將調用Servlet的init()方法初始化這個對象。初始化的目的是爲了讓Servlet對象在處理客戶端請求前完成一些初始化的工做,如創建數據庫的鏈接,獲取配置信息等。對於每個Servlet實例,init()方法只被調用一次。在初始化期間,Servlet實例能夠使用容器爲它準備的ServletConfig對象從Web應用程序的配置信息(在web.xml中配置)中獲取初始化的參數信息。在初始化期間,若是發生錯誤,Servlet實例能夠拋出ServletException異常或者UnavailableException異常來通知容器。ServletException異經常使用於指明通常的初始化失敗,例如沒有找到初始化參數;而UnavailableException異經常使用於通知容器該Servlet實例不可用。例如,數據庫服務器沒有啓動,數據庫鏈接沒法創建,Servlet就能夠拋出UnavailableException異常向容器指出它暫時或永久不可用。

(3)請求處理

      Servlet容器調用Servlet的service()方法對請求進行處理。service()方法爲Servlet的核心方法,客戶端的業務邏輯應該在該方法內執行,典型的服務方法的開發流程爲:解析客戶端請求-〉執行業務邏輯-〉輸出響應頁面到客戶端。要注意的是,在service()方法調用以前,init()方法必須成功執行。在service()方法中,Servlet實例經過ServletRequest對象獲得客戶端的相關信息和請求信息,在對請求進行處理後,調用ServletResponse對象的方法設置響應信息。在service()方法執行期間,若是發生錯誤,Servlet實例能夠拋出ServletException異常或者UnavailableException異常。若是UnavailableException異常指示了該實例永久不可用,Servlet容器將調用實例的destroy()方法,釋放該實例。此後對該實例的任何請求,都將收到容器發送的HTTP 404(請求的資源不可用)響應。若是UnavailableException異常指示了該實例暫時不可用,那麼在暫時不可用的時間段內,對該實例的任何請求,都將收到容器發送的HTTP 503(服務器暫時忙,不能處理請求)響應。

(4)服務終止

     當容器檢測到一個Servlet實例應該從服務中被移除的時候,容器就會調用實例的destroy()方法,以便讓該實例能夠釋放它所使用的資源,保存數據到持久存儲設備中。當須要釋放內存或者容器關閉時,容器就會調用Servlet實例的destroy()方法。在destroy()方法調用以後,容器會釋放這個Servlet實例,該實例隨後會被Java的垃圾收集器所回收。若是再次須要這個Servlet處理請求,Servlet容器會建立一個新的Servlet實例。

      在整個Servlet的生命週期過程當中,建立Servlet實例、調用實例的init()和destroy()方法都只進行一次,當初始化完成後,Servlet容器會將該實例保存在內存中,經過調用它的service()方法,爲接收到的請求服務。

      總結:web容器加載servlet,生命週期開始。經過調用servlet的init() 方法進行servlet的初始化。經過調用service() 方法實現,根據請求的不一樣調用不一樣的do***() 方法。結束服務,web容器調用servlet 的 destory() 方法。

 

設計模式

一、單例模式

單例模式,它的定義就是確保某一個類只有一個實例,而且提供一個全局訪問點。

單例模式具有典型的3個特色:一、只有一個實例。 二、自我實例化。 三、提供全局訪問點。

所以當系統中只須要一個實例對象或者系統中只容許一個公共訪問點,除了這個公共訪問點外,不能經過其餘訪問點訪問該實例時,能夠使用單例模式。

單例模式的主要優勢就是節約系統資源、提升了系統效率,同時也可以嚴格控制客戶對它的訪問。也許就是由於系統中只有一個實例,這樣就致使了單例類的職責太重,違背了「單一職責原則」,同時也沒有抽象類,因此擴展起來有必定的困難。

實現方式

 

 

 

JSP和Servlet有哪些相同點和不一樣點,他們之間的聯繫是什麼?

      JSP是Servlet技術的擴展,本質上是Servlet的簡易方式,更強調應用的外表表達。JSP 編譯後是"類servlet"。Servlet和JSP最主要的不一樣點在於,Servlet的應用邏輯是在Java文件中,而且徹底從表示層中的HTML 裏分離開來。而JSP的狀況是Java和HTML能夠組合成一個擴展名爲.jsp的文件。JSP側重於視圖,Servlet主要用於控制邏輯。

 

三、Hashcode的做用,與 equals 有什麼區別

      一樣用於鑑定2個對象是否相等的,java集合中有 list 和 set 兩類,其中 set不容許元素重複出現,那麼這個不容許重複出現的方法,若是用 equals 去比較的話,若是存在1000個元素,你 new 一個新的元素出來,須要去調用1000次 equals 去逐個和他們比較是不是同一個對象,這樣會大大下降效率。hashcode其實是返回對象的存儲地址,若是這個位置上沒有元素,就把元素直接存儲在上面,若是這個位置上已經存在元素,這個時候纔去調用equals方法與新元素進行比較,相同的話就不存了,散列到其餘地址上。

四、String、StringBuffer與StringBuilder的區別

String 類型和 StringBuffer 類型的主要性能區別其實在於 String 是不可變的對象。

StringBuffer和StringBuilder底層是 char[]數組實現的。

StringBuffer是線程安全的,而StringBuilder是線程不安全的。

相關文章
相關標籤/搜索