面試筆試 - Java基礎(chǔ)

面向?qū)ο笕筇卣骱推叽笤瓌t

面向?qū)ο笕筇卣骱推叽笤瓌t

三大特征

封裝

封裝 就是對屬性和方法的載體類,只能通過其提供的接口(方法)來訪問,而把實(shí)現(xiàn)細(xì)節(jié)隱藏起來.具體實(shí)現(xiàn)對程序員來說是透明的,封裝的好處在于對類內(nèi)部的改變,不會影響到其他代碼

  • 封裝的做法: 私有屬性(private修飾符修飾屬性)、提供public的讀(getXX)寫(setXX)方法、在構(gòu)造中調(diào)用方法.所有的非常量屬性基本都需要封裝.
  • 封裝的好處:隱藏類的實(shí)現(xiàn)細(xì)節(jié)、對所有用戶提供統(tǒng)一的接口、增強(qiáng)執(zhí)行效果、易于維護(hù)和擴(kuò)展

繼承

繼承 是一種關(guān)系,邏輯上滿足子類is a 父類的關(guān)系才使用繼承. 子類繼承父類的屬性和非私有方法.不能繼承父類的構(gòu)造,繼承使用關(guān)鍵字extends,類單繼承,接口多繼承.

  • 在構(gòu)造子類對象時(shí),依次調(diào)用父類的構(gòu)造(子類默認(rèn)調(diào)用父類的無參構(gòu)造.可以使用super(參數(shù)列表)來調(diào)用指定的父類的含參構(gòu)造)到Object為止.再調(diào)用子類自身的; 子類調(diào)用父類的構(gòu)造時(shí),父類的構(gòu)造只能調(diào)用一個(gè)且必須寫在子類構(gòu)造的第一句.

多態(tài)

多態(tài) 是指允許不同類的對象對同一消息作出響應(yīng)。多態(tài)性包括參數(shù)化多態(tài)性和包含多態(tài)性。多態(tài)性語言具有靈活、抽象、行為共享、代碼共享的優(yōu)勢,很好的解決了應(yīng)用程序函數(shù)同名問題; 多態(tài)的類型有下面4種:

基本類型的多態(tài): 拆箱、裝箱.

本質(zhì)上是基本類型之間的自動類型轉(zhuǎn)換,Java語言中將8種數(shù)據(jù)類型都分別封裝了一個(gè)類,這些類中封裝了各基本數(shù)據(jù)類型的屬性和基本運(yùn)算.

image

基本類型自動轉(zhuǎn)換為對應(yīng)的封裝類的操作叫做自動裝箱操作,反之叫自動拆箱操作,自動裝箱操作有一個(gè)自動裝箱池(范圍為-128~127).只要自動裝箱的數(shù)在自動裝箱池范圍內(nèi),則直接去池中找數(shù)據(jù).

方法的多態(tài): 重載、重寫.
  • 重寫(overriding): 父類繼承過來的方法對子類不合適時(shí)子類可以改變該方法的實(shí)現(xiàn),這種操作叫做方法的重寫/覆蓋(繼承是重寫的前提條件); 重寫要求:

    1. 返回值、方法名和參數(shù)相同((5.0以后允許返回子類類型));
    2. 子類異常不能超出父類異常;
    3. 子類訪問級別不能低于父類訪問級別.
  • 重載(overloading): 重載是在同一個(gè)類中存在兩個(gè)或兩個(gè)以上的同名方法,但是參數(shù)不同(參數(shù)個(gè)數(shù)不同、類型不同、順序不同<(int,String)和(String,int)是不一樣的>),方法體也不相同. 返回值類型可以相同可以不相同.最常用的重載例子便是構(gòu)造函數(shù)。

類或者接口的多態(tài): 父類的引用指向子類的對象

父類的引用指向子類的對象(Person p = new Student())就發(fā)生了多態(tài), 該場景下:

  • 只能使用父類中方定義的屬性和方法
  • 子類中定義的不能直接使用
  • 子類復(fù)寫了父類的方法,此時(shí)調(diào)用情況根據(jù)方法是否static而不同 [static(調(diào)用父類),非static(調(diào)用子類)].
  • 如果想使用子類中定義的方法,可以強(qiáng)制類型轉(zhuǎn)換(判斷是否可以轉(zhuǎn)換,用instance of運(yùn)算符來判斷對象的類型)

程序示例

class A {
    int a = 1;
    static int b = 20;
    int c = 3;
    double d = 2.0;
    
    void show() {
        System.out.println("Class A: a=" + a + "\td=" + d + "\tc=" + c + "\tb=" + b);
    }
    
    void common(){
        System.out.println("Class A: method common()");
    }
    
    static void execute(){
        System.out.println("Class A: method excute()");
    }
}

class B extends A {
    
    float a = 3.0f;
    static int b = 30;
    int c = 5;
    String d = "Java program.";

    void show() {
        super.show();
        System.out.println("Class B: a=" + a + "\td=" + d + "\tc=" + c + "\tb=" +b);
    }
    
    void common(){
        System.out.println("Class B: method common()");
    }
    
    static void execute(){
        System.out.println("Class B: method execute()");
    }
    
    public static void main(String[] args) {
        A a = new B();
        a.show();
        System.out.println("----------------------");
        a.common();
        System.out.println("----------------------");
        a.execute();
    }
}

執(zhí)行輸出

Class A: a=1    d=2.0    c=3    b=20
Class B: a=3.0    d=Java program.    c=5    b=30
----------------------
Class B: method common()
----------------------
Class A: method excute()
傳參時(shí)的多態(tài)

【面試題】Java中的參數(shù)傳遞是傳傳遞還是傳引用?

Java語言的方法調(diào)用只支持參數(shù)的值傳遞, 當(dāng)一個(gè)對象實(shí)例作為一個(gè)參數(shù)被傳遞到方法中時(shí),參數(shù)的值就是對該對象的引用。對象的屬性可以在被調(diào)用過程中被改變,但對對象引用的改變是不會影響到調(diào)用者的。

程序示例

public class Test {

    public static void invoke(int num, Person person){
        num = 222;
        person.setAge(20);
        person.setName("李四");
        System.out.println(num + "," + person.getName() + "," + person.getAge());
    }

    public static void main(String[] args) {
        int num = 111;
        Person person = new Person("張三", 10);
        invoke(num, person);
        System.out.println(num + "," + person.getName() + "," + person.getAge());
    }

    @Data
    static class Person{

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        private String name;

        private int age;

    }
}

程序輸出

222,李四,20
111,李四,20

七大原則

  1. 單一職責(zé)原則(一個(gè)類只做它該做的事情)、
  2. 開閉原則(更改性封閉,擴(kuò)展性開放)、
  3. 依賴倒轉(zhuǎn)原則(面向接口編程)、
  4. 里氏替換原則(任何時(shí)候都可以用子類型替換掉父類型)、
  5. 接口隔離原則(接口要小而專,絕不能大而全)、
  6. 合成復(fù)用原則(優(yōu)先使用聚合或合成關(guān)系復(fù)用代碼)、
  7. 迪米特原則(對象與對象之間應(yīng)該使用盡可能少的方法來關(guān)聯(lián))

Java程序初始化順序

Java程序初始化順序

Java語言中當(dāng)實(shí)例化對象時(shí),對象所在的類的所有成員變量首先要進(jìn)行實(shí)例化, 只有當(dāng)所有類成員實(shí)例化后,才會調(diào)用對象所在類的構(gòu)造函數(shù)創(chuàng)建對象; Java程序的初始化一般遵循3個(gè)原則(優(yōu)先級依次遞減):

  • 1). 靜態(tài)對象(變量)優(yōu)先于非靜態(tài)對象(變量)初始化.
  • 2). 分類優(yōu)先于子類進(jìn)行初始化.
  • 3). 按照成員變量的定義順序進(jìn)行初始化.即使變量定義散布于方法定義之中, 他們依然在任何方法(包括構(gòu)造函數(shù))被調(diào)用之前先初始化.

程序示例

class B {

    static {
        System.out.println("B with static ");
    }

    {
        System.out.println("B with code block");
    }

    public B(){
        System.out.println("B with construct");
    }

}

class A extends B {

    static {
        System.out.println("A with static");
    }

    {
        System.out.println("A with code block");
    }

    public A(){
        System.out.println("A with construct");
    }
}

場景1用例:

public class Test {

    public static void main(String[] args) {
        new B();
        System.out.println("------");
        new B();
    }
}

場景1輸出:

B with static 
B with code block
B with construct
------
B with code block
B with construct

場景2用例:

public class Test {

    public static void main(String[] args) {
        new A();
    }
}

場景2輸出:

B with static 
A with static
B with code block
B with construct
A with code block
A with construct

場景3用例:

public class Test {

    public static void main(String[] args) {
        new A();
        System.out.println("------");
        new A();
    }
}

場景3輸出:

B with static 
A with static
B with code block
B with construct
A with code block
A with construct
------
B with code block
B with construct
A with code block
A with construct    

Java語言的變量類型

Java語言的變量類型

在Java語言中,變量的類型只要有3種: 成員變量、靜態(tài)變量局部變量.

成員變量

類成員變量的作用范圍與類的實(shí)例化對象的作用范圍相同, 當(dāng)類被實(shí)例化時(shí), 成員變量就會在內(nèi)存中分配空間并初始化,直到這個(gè)被實(shí)例化對象的生命周期結(jié)束時(shí)成員變量的生命周期才結(jié)束.

類成員變量作用域有4種,訪問權(quán)限范圍由大到小依次為: public > protected > default > private.

  • public: 表明該成員變量或方法對所有類或?qū)ο蠖际强梢姷?,所有類或?qū)ο蠖伎梢灾苯釉L問.
  • protected: 表明該成員變量或方法對自己及其子類是可見的,除此之外的其他類或?qū)ο蠖紱]有訪問權(quán)限.
  • default: 表明該成員變量或方法只有自己和與其位于同一包內(nèi)的類可見, 若父類與子類位于不同的包內(nèi),則無訪問權(quán)限.
  • private: 表明該成員變量或方法是私有的,只有當(dāng)前類對其具有訪問權(quán)限,除此之外 的其他類或者對象都沒有訪問權(quán)限.

注意: 這些修飾符只能修飾成員變量,不能用來修飾局部變量, private與protected不能用來修飾類(只有 public、 abstract或 final 能用來修飾類).

靜態(tài)變量

被static修飾的成員變量稱為靜態(tài)變量或全局變量,與成員變量不同的是,靜態(tài)變量不依賴于特定的實(shí)例,而是被所有實(shí)例所共享; 也就是說: 只要一個(gè)類被加載, JVM就會給類的靜態(tài)變量分配存儲空間. 因此就可以通過類名和變量名來訪問靜態(tài)變量.

局部變量

局部變量的作用域與可見性為它所在的花括號內(nèi).

理解Java-位運(yùn)算

位運(yùn)算符主要針對二進(jìn)制,它包括了:“與”、“非”、“或”、“異或”。

數(shù)據(jù)基本單位

在了解運(yùn)算符前,先了解一下數(shù)據(jù)的基本單位

  • BIT(比特): 一個(gè)二進(jìn)制位(最小數(shù)據(jù)單位), 比如:0
  • BYTE(字節(jié)): 1BYTE = 8BIT, 比如:01010110
  • KB(千字節(jié)): 1KB = 1024BYTE
  • MB(兆字節(jié)): 1MB = 1024KB
  • GB(吉字節(jié)): 1GB = 1024MB
  • TB(太字節(jié)): 1TB=1024GB

Java中的基本數(shù)據(jù)類型

不同語言中的數(shù)據(jù)長度可能不一樣, 這里介紹一下Java語言中的基礎(chǔ)數(shù)據(jù)長度

數(shù)據(jù)類型與對應(yīng)長度

類型 長度
boolean -
char 16bit
byte 8bit
short 16bit
int 32bit
long 64bit
float 32bit
double 64bit

程序示例

@Test
public void test(){
   // 輸出: 1
   System.out.println(Byte.BYTES);
   // 輸出: 2
   System.out.println(Short.BYTES);
   // 輸出: 4
   System.out.println(Integer.BYTES);
   // 輸出: 8
   System.out.println(Long.BYTES);
   // 輸出: 4
   System.out.println(Float.BYTES);
   // 輸出: 8
   System.out.println(Double.BYTES);
   // 輸出: 11111111111111111111111111111110
   System.out.println(Integer.toBinaryString(-2));
   // 輸出: 1111111111111111111111111111111111111111111111111111111111111110
   System.out.println(Long.toBinaryString(-2L));
   char c = '蘇';
   // 輸出: 蘇
   System.out.println(c);
}

^(異或)

參與運(yùn)算的兩個(gè)數(shù), 如果兩個(gè)相應(yīng)位相同(二進(jìn)制),則結(jié)果為0,否則為1.
即:0^0=0, 1^0=1, 0^1=1, 1^1=0

運(yùn)算說明

操作 二進(jìn)制 十進(jìn)制
- 000000000000000000000000000000001 1
- 000000000000000000000000000000010 2
^ 000000000000000000000000000000011 3

程序測試

@Test
public void test(){
   // 輸出: 3
   System.out.println(1^2);
}

&(與)

參與運(yùn)算的兩個(gè)數(shù), 如果兩個(gè)相應(yīng)位都為1(二進(jìn)制),結(jié)果才為1,否則結(jié)果為0.
即:0^0=0, 1^0=1, 0^1=1, 1^1=1

運(yùn)算說明

操作 二進(jìn)制 十進(jìn)制
- 000000000000000000000000000000001 1
- 000000000000000000000000000000010 2
& 000000000000000000000000000000000 0

程序測試

@Test
public void test(){
   // 輸出: 0
   System.out.println(1&2);
}

| (或)

參與運(yùn)算的兩個(gè)數(shù), 如果兩個(gè)相應(yīng)位只要有一個(gè)為1(二進(jìn)制),那么結(jié)果就是1,否則就為0.
即:0^0=0, 1^0=1, 0^1=1, 1^1=1

運(yùn)算說明

操作 二進(jìn)制 十進(jìn)制
- 000000000000000000000000000000001 1
- 000000000000000000000000000000010 2
000000000000000000000000000000011 3

程序測試

@Test
public void test(){
   // 輸出: 3
   System.out.println(1|2);
}

~(非)

參數(shù)計(jì)算的數(shù),如果位為0(二進(jìn)制),結(jié)果是1,如果位為1,結(jié)果是0.

運(yùn)算說明

二進(jìn)制 十進(jìn)制 運(yùn)算 二進(jìn)制 十進(jìn)制
000000000000000000000000000000001 1 ~ 11111111111111111111111111111110 -2
000000000000000000000000000000010 2 ~ 11111111111111111111111111111101 -3

程序測試

@Test
public void test(){

   // 輸出: -2
   System.out.println(~1);
   // 輸出: 11111111111111111111111111111110
   System.out.println(Integer.toBinaryString(~1));

   // 輸出: -3
   System.out.println(~2);
   // 輸出: 11111111111111111111111111111101
   System.out.println(Integer.toBinaryString(~2));
}

關(guān)于位運(yùn)算的面試題

【面試題】實(shí)現(xiàn)兩個(gè)數(shù)的交換

思路: 兩個(gè)數(shù)做異或,會計(jì)算出來一個(gè)中間數(shù)(即便這兩個(gè)數(shù)相同,那么計(jì)算結(jié)果為0也滿足),用這個(gè)中間數(shù)做交換時(shí)的中間引用停留即可.

代碼實(shí)現(xiàn)

@Test
public void find(){
   int a = 1, b=2;
   System.out.println("交換前: a = " + a + ", b = " + b);
   a = a ^ b;
   b = a ^ b;  // ((a^b) ^ b) = a
   a = a ^ b;  // ((a^b) ^ b) = a
   System.out.println("交換后: a = " + a + ", b = " + b);
}

【面試題】實(shí)現(xiàn)字符串翻轉(zhuǎn)

思路: 使用異或進(jìn)行高低位轉(zhuǎn)換

代碼實(shí)現(xiàn)

/**
 * @param str 待返轉(zhuǎn)的字符串
 * @return 翻轉(zhuǎn)后的字符串
 */
public static String reverse(String str){
   char[] chars = str.toCharArray();
   int low = 0;
   int top = chars.length - 1;
   while (low < top){
       chars[low] ^= chars[top];
       chars[top] ^= chars[low];
       chars[low] ^= chars[top];
       low++;
       top--;
   }
   return new String(chars);
}

【面試題】一堆數(shù), 除過一個(gè)數(shù)只出現(xiàn)了一次, 其它數(shù)均出現(xiàn)了2n次(n>=1), 找出該數(shù).

分析: 因?yàn)橄嗤膬蓚€(gè)數(shù)做^結(jié)果為0, 0^任何數(shù)=任何數(shù). 將這堆數(shù)從第一個(gè)開始,一直到最后一個(gè)進(jìn)行異或運(yùn)算, 那么最后結(jié)果值就是那個(gè)只出現(xiàn)了一次的數(shù).

代碼實(shí)現(xiàn)

@Test
public void findOne(){
   int[] arr = new int[] {1, 1, 2, 2, 3, 5, 5, 7, 7, 6, 6};
   // 從第一個(gè)數(shù)標(biāo)開始
   int result = arr[0];
   // 以此進(jìn)行后面的數(shù)據(jù)的異或運(yùn)算
   for(int i = 1; i < arr.length; i++){
       result = result ^ arr[i];
   }
   // 程序輸出: 3
   System.out.println(result);
}

【面試題】java中 ||| , &&& 有什么區(qū)別

  • &(與): 位運(yùn)算符,也兼有邏輯預(yù)算符的功能.

  • &&(短路與): 只是邏輯運(yùn)算符. &與&&作為邏輯運(yùn)算符時(shí)有如下區(qū)別:

  • &: 無論&左邊是否為false,他都會繼續(xù)檢驗(yàn)右邊的boolean值。

  • &&: 只要檢測到左邊值為false時(shí), 就會直接判斷結(jié)果,不會在檢驗(yàn)右邊的值(因?yàn)?與"有一個(gè)false最后結(jié)果就是false了),所以&&的執(zhí)行效率更高,所以邏輯運(yùn)算時(shí)一般都是使用&&.

  • |(或)與||(短路或) 區(qū)別 同&(與)和&&(短路與)相似.

Java中的Object類.

Java中的Object類

Object有哪些公用方法

  • 1.clone方法

保護(hù)方法, 創(chuàng)建并返回此對象的一個(gè)副本, 用于實(shí)現(xiàn)對象的淺復(fù)制, 只有實(shí)現(xiàn)了Cloneable接口才可以調(diào)用該方法,否則拋出CloneNotSupportedException異常;
JAVA里除了8種基本類型傳參數(shù)是值傳遞,其他的類對象傳參數(shù)都是引用傳遞,我們有時(shí)候不希望在方法里將參數(shù)改變,這時(shí)就需要在類中復(fù)寫clone方法.

  • 2.getClass方法

final方法,返回一個(gè)對象的運(yùn)行時(shí)類。

  • 3.toString方法

返回該對象的字符串表示, 該方法用得比較多,一般子類都有覆蓋。

  • 4.finalize方法

該方法用于釋放資源。當(dāng)垃圾回收器確定不存在對該對象的更多引用時(shí),由對象的垃圾回收器調(diào)用此方法。子類重寫 finalize 方法,以配置系統(tǒng)資源或執(zhí)行其他清除。

在啟用某個(gè)對象的 finalize 方法后,將不會執(zhí)行進(jìn)一步操作,直到 Java 虛擬機(jī)再次確定尚未終止的任何線程無法再通過任何方法訪問此對象,其中包括由準(zhǔn)備終止的其他對象或類執(zhí)行的可能操作,在執(zhí)行該操作時(shí),對象可能被丟棄。

對于任何給定對象,Java 虛擬機(jī)最多只調(diào)用一次 finalize 方法。

  • 5.equals方法

該方法是非常重要的一個(gè)方法。一般equals和==是不一樣的,但是在Object中兩者是一樣的。子類一般都要重寫這個(gè)方法。

  • 6.hashCode方法

該方法用于哈希查找,可以減少在查找中使用equals的次數(shù),重寫了equals方法一般都要重寫hashCode方法。這個(gè)方法在一些具有哈希功能的Collection中用到。

一般必須滿足obj1.equals(obj2)==true1??梢酝瞥?/code>obj1.hashCode()==obj2.hashCode()`,但是hashCode相等不一定就滿足equals。不過為了提高效率,應(yīng)該盡量使上面兩個(gè)條件接近等價(jià)。

如果不重寫hashCode(),在HashSet中添加兩個(gè)equals的對象,會將兩個(gè)對象都加入進(jìn)去。

  • 7.wait方法

wait方法就是使當(dāng)前線程等待該對象的鎖,當(dāng)前線程必須是該對象的擁有者,也就是具有該對象的鎖。wait()方法一直等待,直到獲得鎖或者被中斷。wait(long timeout)設(shè)定一個(gè)超時(shí)間隔,如果在規(guī)定時(shí)間內(nèi)沒有獲得鎖就返回。

調(diào)用該方法后當(dāng)前線程進(jìn)入睡眠狀態(tài),直到以下事件發(fā)生。

    1. 其他線程調(diào)用了該對象的notify方法。
  • 2)其他線程調(diào)用了該對象的notifyAll方法。
  • 3)其他線程調(diào)用了interrupt中斷該線程。
  • 4)時(shí)間間隔到了。

此時(shí)該線程就可以被調(diào)度了,如果是被中斷的話就拋出一個(gè)InterruptedException異常。

  • 8.notify方法

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

  • 9.notifyAll方法

該方法喚醒在該對象上等待的所有線程。

面試延伸

【面試題】Java是值傳遞還是引用傳遞

Java是指傳遞, 對于基本類型傳遞的是值, 對于引用類型傳遞的是指針的地址

  • 值傳遞:方法調(diào)用時(shí),實(shí)際參數(shù)把它的值傳遞給對應(yīng)的形式參數(shù),方法執(zhí)行中形式參數(shù)值的改變不影響實(shí)際參數(shù)的值。

  • 引用傳遞:也稱為傳地址, 方法調(diào)用時(shí)實(shí)際參數(shù)的引用(地址,而不是參數(shù)的值)被傳遞給方法中相對應(yīng)的形式參數(shù),在方法執(zhí)行中對形式參數(shù)的操作實(shí)際上就是對實(shí)際參數(shù)的操作,方法執(zhí)行中形式參數(shù)值的改變將會影響實(shí)際參數(shù)的值.

【面試題】闡述final、finallyfinalize的區(qū)別

  • final:修飾符(關(guān)鍵字)有三種用法:(1)修飾類:表示該類不能被繼承;(2)修飾方法:表示方法不能被重寫;(3)修飾變量:表示變量只能一次賦值以后值不能被修改(常量)

  • finally:通常放在try…catch…的后面構(gòu)造總是執(zhí)行代碼塊(try{}里的return語句,其后finally{}里的代碼會方法返回給調(diào)用者前執(zhí)行),這就意味著程序無論正常執(zhí)行還是發(fā)生異常,這里的代碼只要JVM不關(guān)閉都能執(zhí)行,可以將釋放外部資源的代碼寫在finally塊中。

  • finalize:Object類中定義的方法,Java中允許使用finalize()方法在垃圾收集器將對象從內(nèi)存中清除出去之前做必要的清理工作(如關(guān)閉連接、關(guān)閉文件)。這個(gè)方法一般不會顯示的調(diào)用, 通常是由垃圾收集器在銷毀對象時(shí)調(diào)用的,通過重寫finalize()方法可以整理系統(tǒng)資源或者執(zhí)行其他清理工作。

【面試題】equals== 的區(qū)別

  • == 比較的是變量(棧)內(nèi)存中存放的對象的(堆)內(nèi)存地址,用來判斷兩個(gè)對象的地址是否相同,即是否是指相同一個(gè)對象。比較的是真正意義上的指針操作.

  • equals 比較的是兩個(gè)對象的內(nèi)容是否相等,由于所有的類都是繼承自java.lang.Object類的,所以適用于所有對象,而equals()可以返回true或者false主要取決于重寫equals方法的實(shí)現(xiàn)邏輯.

代碼示例

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

【面試題】對象值相同(x.equals(y)為true,但卻可以有不同的hash值?

如果兩個(gè)對象滿足equals為true,既 x.equals(y)==true, 那么他的哈希碼(hash code)必然相同.

如果兩個(gè)對象的hashCode相同, 它們并不一定相同.

【面試題】如何解決Hash沖突

通過構(gòu)造性能良好的哈希函數(shù),可以減少沖突,但一般不可能完全避免沖突,因此解決沖突是哈希法的另一個(gè)關(guān)鍵問題。創(chuàng)建哈希表和查找哈希表都會遇到?jīng)_突,兩種情況下解決沖突的方法應(yīng)該一致。下面以創(chuàng)建哈希表為例,說明解決沖突的方法。常用的解決沖突方法有以下四種:

  • 開放定址法

又稱為開放散列法

基本思想是: 當(dāng)關(guān)鍵字key的哈希地址p=H(key)出現(xiàn)沖突時(shí),以p為基礎(chǔ),產(chǎn)生另一個(gè)哈希地址p1,如果p1仍然沖突,再以p為基礎(chǔ),產(chǎn)生另一個(gè)哈希地址p2, …;直到找出一個(gè)不沖突的哈希地址pi ,將相應(yīng)元素存入其中.

這種方法有一個(gè)通用的再散列函數(shù)形式: Hi = ( H(key) + di ) % m 其中 i=1,2,…,n

說明: H(key)為哈希函數(shù), m為表長, di稱為增量序列, 增量序列的取值方式不同,相應(yīng)的再散列方式也不同, 主要有以下三種:

  • 線性探測再散列

di i=1,2,3,…,m-1 這種方法的特點(diǎn)是: 沖突發(fā)生時(shí),順序查看表中下一單元,直到找出一個(gè)空單元或查遍全表。

  • 二次探測再散列

di = 12,-12,22,-22,…,k2,-k2 (k<=m/2) 這種方法的特點(diǎn)是: 沖突發(fā)生時(shí),在表的左右進(jìn)行跳躍式探測,比較靈活。

  • 偽隨機(jī)探測再散列

di = 偽隨機(jī)數(shù)序列

示例說明

例如: 已知哈希表長度m=11,哈希函數(shù)為:H(key) = key % 11,則H(47)=3,H(26)=4,H(60)=5,假設(shè)下一個(gè)關(guān)鍵字為69,則H(69)=3,與47沖突.

  • 用線性探測再散列處理沖突: 下一個(gè)哈希地址為H1=(3 + 1) % 11 = 4,仍然沖突,再找下一個(gè)哈希地址為H2= (3 + 2) % 11 = 5,還是沖突,繼續(xù)找下一個(gè)哈希地址為H3=(3 + 3)% 11 = 6,此時(shí)不再沖突,將69填入5號單元。

  • 用二次探測再散列處理沖突: 下一個(gè)哈希地址為H1=(3 + 12)% 11 = 4,仍然沖突,再找下一個(gè)哈希地址為H2= (3 - 12) % 11 = 2,此時(shí)不再沖突,將69填入2號單元。

  • 用偽隨機(jī)探測再散列處理沖突: 設(shè)偽隨機(jī)數(shù)序列為: 2,5,9,……..,則下一個(gè)哈希地址為H1=(3 + 2)% 11 = 5,仍然沖突,再找下一個(gè)哈希地址為H2=(3 + 5)% 11 = 8,此時(shí)不再沖突,將69填入8號單元。

  • 再哈希法

基本思想: 同時(shí)構(gòu)造多個(gè)不同的哈希函數(shù): Hi=RH1(key) i=1, 2, ..., k

當(dāng)哈希地址Hi=RH1(key)發(fā)生沖突時(shí),再計(jì)算Hi=RH2(key)..., 直到?jīng)_突不再產(chǎn)生。這種方法不易產(chǎn)生聚集,但增加了計(jì)算時(shí)間.

  • 鏈地址法

基本思想: 將所有哈希地址相同的記錄都鏈接在同一單鏈表中; 并將單鏈表的頭指針存在哈希表的第i個(gè)單元中,因而查找、插入和刪除主要在同義詞鏈中進(jìn)行.

鏈地址法適用于經(jīng)常進(jìn)行插入和刪除的情況.

  • 建立公共溢出區(qū)

基本思想: 將哈希表分為基本表和溢出表兩部分,凡是和基本表發(fā)生沖突的元素,一律填入溢出表

Java線程

Java線程

線程狀態(tài)

線程基本上有5種狀態(tài),分別是:新建、就緒運(yùn)行、 阻塞、死亡

image
  • 新建狀態(tài)(New)

當(dāng)線程對象對創(chuàng)建后,即進(jìn)入了新建狀態(tài),如:Thread t = new MyThread();

  • 就緒狀態(tài)(Runnable)

當(dāng)調(diào)用線程對象的start()方法,線程即進(jìn)入就緒狀態(tài)。處于就緒狀態(tài)的線程,只是說明此線程已經(jīng)做好了準(zhǔn)備,隨時(shí)等待CPU調(diào)度執(zhí)行,并不是說執(zhí)行了t.start()此線程立即就會執(zhí)行; 如: t.start();

  • 運(yùn)行狀態(tài)(Running)

當(dāng)CPU開始調(diào)度處于就緒狀態(tài)的線程時(shí),此時(shí)線程才得以真正執(zhí)行,即進(jìn)入到運(yùn)行狀態(tài)。注:就緒狀態(tài)是進(jìn)入到運(yùn)行狀態(tài)的唯一入口,也就是說,線程要想進(jìn)入運(yùn)行狀態(tài)執(zhí)行,首先必須處于就緒狀態(tài)中.

  • 阻塞狀態(tài)(Blocked)

處于運(yùn)行狀態(tài)中的線程由于某種原因,暫時(shí)放棄對CPU的使用權(quán),停止執(zhí)行,此時(shí)進(jìn)入阻塞狀態(tài),直到其進(jìn)入到就緒狀態(tài),才有機(jī)會再次被CPU調(diào)用以進(jìn)入到運(yùn)行狀態(tài)。根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:

  1. 等待阻塞

運(yùn)行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);

  1. 同步阻塞

線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用),它會進(jìn)入同步阻塞狀態(tài);

  1. 其他阻塞

通過調(diào)用線程的sleep()或join()或發(fā)出了I/O請求時(shí),線程會進(jìn)入到阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。

  • 死亡狀態(tài)(Dead)

線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。

線程生命周期

一個(gè)線程的聲明周期一般從新建狀態(tài)(New)開始,到死亡狀態(tài)(Dead)結(jié)束,中間可以存在許多中可能。

image

如上圖所示,一般情況下會有4個(gè)分支情況:

  • 正常情況(紅色箭頭),

  • 發(fā)生鎖阻塞(同步阻塞)的情況(藍(lán)色箭頭),

  • 發(fā)生等待阻塞的情況(黃色箭頭),

  • 發(fā)生其他阻塞的情況(黑色箭頭),分別對應(yīng)上圖4個(gè)不同顏色的流向

  • 正常情況

如上圖中紅色箭頭所示,正常狀態(tài)下線程的聲明周期是這樣的:NEW -> RUNNABLE -> RUNNING -> DEAD。

  • 發(fā)生鎖阻塞(同步阻塞)

如上圖中藍(lán)色箭頭所示,當(dāng)線程進(jìn)入 RUNNING 狀態(tài)時(shí)進(jìn)入了 synchronized 方法塊,這時(shí)就會發(fā)生鎖阻塞,線程進(jìn)入一個(gè) Lock Pool 鎖池中。當(dāng)其獲得鎖之后,又進(jìn)入到可運(yùn)行狀態(tài)。當(dāng)CPU分片輪詢到它的時(shí)候,它就再次運(yùn)行,直至 DEAD 狀態(tài)。

  • 發(fā)生等待阻塞

如上圖中藍(lán)色箭頭所示,當(dāng)線程進(jìn)入 RUNNING 狀態(tài)時(shí)遇到了 wait() 方法,這時(shí)就會發(fā)生等待阻塞,它會等待其他線程調(diào)用 notify() 方法釋放鎖之后才可恢復(fù)到可運(yùn)行狀態(tài)。當(dāng)CPU分片輪詢到它的時(shí)候,它就再次運(yùn)行,直至 DEAD 狀態(tài)。等待阻塞和鎖阻塞其實(shí)是同一類型的,都是因?yàn)闋帄Z鎖而發(fā)生的線程等待,唯一的不同是因?yàn)樗鼈冋{(diào)用的是不同的方式實(shí)現(xiàn),但底層原理相同。要注意的是執(zhí)行 wait() 方法的時(shí)候,線程一定要獲得鎖,所以 wait() 方法一般都在 synchronized 方法或代碼塊中。當(dāng)其獲得鎖之后進(jìn)入等待池(wait pool)并釋放鎖。收到 notify() 通知之后等待獲取鎖,獲取鎖之后才可以運(yùn)行。

  • 發(fā)生其他阻塞(如:IO讀取等)

當(dāng)線程需要去讀取文件,而此時(shí)文件又被其他線程占用,那么就會發(fā)生阻塞。這時(shí)候線程需要等待其他線程讀取完之后才能繼續(xù)進(jìn)行,這可以稱為 IO 阻塞。當(dāng)然了還有很多種情況,如網(wǎng)絡(luò)阻塞等等

CountDownLatch應(yīng)用

CountDownLatch是一個(gè)同步輔助類,這個(gè)類能夠使一個(gè)線程等待其他線程完成各自的工作后再執(zhí)行。例如,應(yīng)用程序的主線程希望在負(fù)責(zé)啟動框架服務(wù)的線程已經(jīng)啟動所有的框架服務(wù)之后再執(zhí)行。

CountDownLatch實(shí)現(xiàn)概要

CountDownLatch是通過一個(gè)計(jì)數(shù)器來實(shí)現(xiàn)的,計(jì)數(shù)器的初始值為線程的數(shù)量。每當(dāng)一個(gè)線程完成了自己的任務(wù)后,計(jì)數(shù)器的值就會減1。當(dāng)計(jì)數(shù)器值到達(dá)0時(shí),它表示所有的線程已經(jīng)完成了任務(wù),然后在閉鎖上等待的線程就可以恢復(fù)執(zhí)行任務(wù)。

CountDownLatch多線程應(yīng)用

列表循環(huán)遍歷(耗時(shí)8697ms)

@Test
public void optimizeListV1(){
    long start = System.currentTimeMillis();
    try {
        final List<String> lists = Arrays.asList("aa", "bb", "cc", "dd", "ee");
        for(int i=0; i<lists.size(); i++){
            if(i == 2){
                Thread.sleep(3000);
            }
            Thread.sleep(1000);
        }
        System.out.println("聚合完成");
    }catch (Exception e){

    }finally {
        MockTimeUtil.mockInvokeTime("循環(huán)列表場景模擬:", start);
    }
}

多線程聚合列表(耗時(shí)4671ms)

@Test
public void optimizeList(){
    long start = System.currentTimeMillis();
    try {
       ExecutorService ex = Executors.newFixedThreadPool(5);
       final List<String> lists = Arrays.asList("aa", "bb", "cc", "dd", "ee");
       final CountDownLatch latch =  new CountDownLatch(lists.size());
       for(int i=0; i<lists.size(); i++){
           final int tmp = i;
           ex.submit(new Callable<Object>() {
               @Override
               public Object call() throws Exception {
                   if(tmp == 2){
                       Thread.sleep(3000);
                   }
                   Thread.sleep(1000);
                   latch.countDown();
                   return null;
               }
           });
       }
       //latch.await();
       latch.await(3500, TimeUnit.MILLISECONDS);
       System.out.println("聚合完成");
   }catch (Exception e){

   }finally {
       MockTimeUtil.mockInvokeTime("線程列表場景模擬:", start);
   }
}

CountDownLatch方法說明

CountDownLatch源碼

public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

    private final Sync sync;

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public void countDown() {
        sync.releaseShared(1);
    }

    public long getCount() {
        return sync.getCount();
    }

    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

方法說明:

  • countDown():當(dāng)前線程調(diào)用此方法,則計(jì)數(shù)減一
  • await(): 調(diào)用此方法會一直阻塞當(dāng)前線程,直到計(jì)時(shí)器的值為0

CompleteService應(yīng)用

CompleteService使用場景

當(dāng)我們需要批量任務(wù)處理,但是并不關(guān)心任務(wù)完成的先后順序,我們異步的提交任務(wù),等待有任務(wù)執(zhí)行完成之后然后對該完成結(jié)果處理,如此循環(huán)直到該批量任務(wù)完成.
我們遵循異步處理完成后的操作原則時(shí),誰先完成收割誰.

基于集合Future遍歷處理

針對這種場景,我們很可能會想到把所有的異步任務(wù)收集到一個(gè)集合,然后遍歷這個(gè)集合(Future),調(diào)用future.get()獲取處理結(jié)果,進(jìn)行后續(xù)操作,然后我們就會寫出如下代碼

ExecutorService pool = Executors.newFixedThreadPool(5);
final List<String> dList = Arrays.asList("aa", "bb", "cc", "dd", "ee");
List<Future> fList= new ArrayList<Future>();
for(int i=0; i<dList.size(); i++){
    final int tmp = i;
    Future future =  pool.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            if (tmp == 2) {
                Thread.sleep(3000);
            }
            Thread.sleep(1000);
            return "線程" + Thread.currentThread().getName() + "處理數(shù)據(jù)元素list(" +  + tmp +") = " + dList.get(tmp) ;
        }
    });
    fList.add(future);
}
System.out.println("聚合完成");
for (int i = 0; i < fList.size(); i++) {
    System.out.println(fList.get(i).get());
}

執(zhí)行這段代碼,會發(fā)現(xiàn)執(zhí)行結(jié)果并沒有按照我們所預(yù)期的來

聚合完成
線程pool-1-thread-1處理數(shù)據(jù)元素list(0) = aa
線程pool-1-thread-2處理數(shù)據(jù)元素list(1) = bb
線程pool-1-thread-3處理數(shù)據(jù)元素list(2) = cc
線程pool-1-thread-4處理數(shù)據(jù)元素list(3) = dd
線程pool-1-thread-5處理數(shù)據(jù)元素list(4) = ee

可見,上面的執(zhí)行結(jié)果并不是我們想要的,明顯cc元素的執(zhí)行比較耗時(shí),但是我們的處理結(jié)果卻是按照循環(huán)遍歷的順序來的,原因如下:

從list中遍歷的每個(gè)Future對象并不一定處于完成狀態(tài),這時(shí)調(diào)用get()方法就會被阻塞住,如果系統(tǒng)是設(shè)計(jì)成每個(gè)線程完成后就能根據(jù)其結(jié)果繼續(xù)做后面的事,這樣對于處于list后面的但是先完成的線程就會增加了額外的等待時(shí)間。

基于CompletionService完成并行聚合

ExecutorService pool = Executors.newFixedThreadPool(5);
CompletionService<Object> cs = new ExecutorCompletionService<Object>(pool);
final List<String> dList = Arrays.asList("aa", "bb", "cc", "dd", "ee");
for(int i=0; i<dList.size(); i++){
    final int tmp = i;
    cs.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            if (tmp == 2) {
                Thread.sleep(3000);
            }
            Thread.sleep(1000);
            return "線程" + Thread.currentThread().getName() + "處理數(shù)據(jù)元素list(" +  + tmp +") = " + dList.get(tmp);
        }
    });
}
System.out.println("聚合完成");
for (int i = 0; i < dList.size(); i++) {
    System.out.println(cs.take().get());
}

執(zhí)行會發(fā)現(xiàn)這種結(jié)果才是我們真正要的

聚合
完成
線程pool-1-thread-2處理數(shù)據(jù)元素list(1) = bb
線程pool-1-thread-1處理數(shù)據(jù)元素list(0) = aa
線程pool-1-thread-4處理數(shù)據(jù)元素list(3) = dd
線程pool-1-thread-5處理數(shù)據(jù)元素list(4) = ee
線程pool-1-thread-3處理數(shù)據(jù)元素list(2) = cc

我們能得到想要的結(jié)果,是因?yàn)镃ompleteService中內(nèi)部維護(hù)著一個(gè)BlockingQueue.原理如下:

CompletionService的實(shí)現(xiàn)是維護(hù)一個(gè)保存Future對象的BlockingQueue。只有當(dāng)這個(gè)Future對象狀態(tài)是結(jié)束的時(shí)候,才會加入到這個(gè)Queue中,take()方法其實(shí)就是Producer-Consumer中的Consumer。它會從Queue中取出Future對象,如果Queue是空的,就會阻塞在那里,直到有完成的Future對象加入到Queue中。

CompleteService中take()和poll()的區(qū)別

查看CompleteService的接口定義

public interface CompletionService<V> {

    Future<V> submit(Callable<V> task);

    Future<V> submit(Runnable task, V result);

    Future<V> take() throws InterruptedException;

    Future<V> poll();

    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}

從接口中我們可以看到CompletionService中定義的summit相關(guān)的方法用來載入線程體(分別處理實(shí)現(xiàn)Callable或Runable的線程), poll()和take()用來獲取返回結(jié)果集.

關(guān)于poll()和take()的區(qū)別

poll()是非阻塞的,若目前無結(jié)果,返回一個(gè)null,線程繼續(xù)運(yùn)行不阻塞。take()是阻塞的,若當(dāng)前無結(jié)果,則線程阻塞,直到產(chǎn)生一個(gè)結(jié)果

示例poll()和take()的區(qū)別
ExecutorService pool = Executors.newFixedThreadPool(5);
CompletionService<Object> cs = new ExecutorCompletionService<Object>(pool);
final List<String> dList = Arrays.asList("aa", "bb", "cc", "dd", "ee");
for(int i=0; i<dList.size(); i++){
    final int tmp = i;
    cs.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            if (tmp == 2) {
                Thread.sleep(3000);
            }
            Thread.sleep(1000);
            return "線程" + Thread.currentThread().getName() + "處理數(shù)據(jù)元素list(" +  + tmp +") = " + dList.get(tmp);
        }
    });
}
System.out.println("聚合完成");
AtomicInteger index = new AtomicInteger(0);
while(index.get()<dList.size()) {
    Future<Object> f = cs.poll();
    if(f == null) {
        System.out.println("沒發(fā)現(xiàn)有完成的任務(wù)");
    }else {
        System.out.println(f.get());
        index.incrementAndGet();
    }
    Thread.sleep(500);
}

程序運(yùn)行結(jié)果

聚合完成
沒發(fā)現(xiàn)有完成的任務(wù)
沒發(fā)現(xiàn)有完成的任務(wù)
線程pool-1-thread-1處理數(shù)據(jù)元素list(0) = aa
線程pool-1-thread-4處理數(shù)據(jù)元素list(3) = dd
線程pool-1-thread-2處理數(shù)據(jù)元素list(1) = bb
線程pool-1-thread-5處理數(shù)據(jù)元素list(4) = ee
沒發(fā)現(xiàn)有完成的任務(wù)
沒發(fā)現(xiàn)有完成的任務(wù)
線程pool-1-thread-3處理數(shù)據(jù)元素list(2) = cc

JavaIO模型

JavaIO模型

在高性能的IO體系設(shè)計(jì)中,有幾個(gè)名詞概念需要先做個(gè)概述.

同步和異步、阻塞和非阻塞

同步和異步是針對應(yīng)用程序與內(nèi)核的交互而言的,阻塞和非阻塞是針對于進(jìn)程在訪問數(shù)據(jù)的時(shí)候,根據(jù)IO操作的就緒狀態(tài)來采取的不同方式,說白了是一種讀取或者寫入操作函數(shù)的實(shí)現(xiàn)方式,阻塞方式下讀取或者寫入函數(shù)將一直等待,而非阻塞方式下,讀取或者寫入函數(shù)會立即返回一個(gè)狀態(tài)值.
簡而言之: 同步和異步是目的,阻塞和非阻塞是實(shí)現(xiàn)方式。

  • 同步: 指的是用戶進(jìn)程觸發(fā)IO操作并等待或者輪詢的去查看IO操作是否就緒.
  • 異步: 指用戶進(jìn)程觸發(fā)IO操作以后便開始做自己的事情,而當(dāng)IO操作已經(jīng)完成的時(shí)候會得到IO完成的通知(異步的特點(diǎn)就是通知)告訴朋友自己合適衣服的尺寸,大小,顏色,讓朋友委托去賣,然后自己可以去干別的事.
  • 阻塞: 所謂阻塞方式的意思是指, 當(dāng)試圖對該文件描述符進(jìn)行讀寫時(shí), 如果當(dāng)時(shí)沒有東西可讀,或者暫時(shí)不可寫, 程序就進(jìn)入等待 狀態(tài), 直到有東西可讀或者可寫為止. 比如:去公交站充值,發(fā)現(xiàn)這個(gè)時(shí)候,充值員不在(可能上廁所去了),然后我們就在這里等待,一直等到充值員回來為止.
  • 非阻塞: 非阻塞狀態(tài)下,如果沒有東西可讀或者不可寫, 讀寫函數(shù)馬上返回而不會等待. 比如: 銀行里取款辦業(yè)務(wù)時(shí),領(lǐng)取一張小票,領(lǐng)取完后我們自己可以玩玩手機(jī),或者與別人聊聊天,當(dāng)輪我們時(shí),銀行的喇叭會通知,這時(shí)候我們就可以去了.

Java中的IO模型

  • Java中主要的三種IO模型: 阻塞IO(BIO)、非阻塞IO(NIO)和 異步IO(AIO).

  • Java中提供的IO有關(guān)的API,在文件處理的時(shí)候,其實(shí)依賴操作系統(tǒng)層面的IO操作實(shí)現(xiàn)的。比如在Linux 2.6以后,Java中NIO和AIO都是通過epoll來實(shí)現(xiàn)的,而在Windows上,AIO是通過IOCP來實(shí)現(xiàn)的.

同步阻塞IO(JAVA BIO)

同步并阻塞,服務(wù)器實(shí)現(xiàn)模式為一個(gè)連接一個(gè)線程,即客戶端有連接請求時(shí)服務(wù)器端就需要啟動一個(gè)線程進(jìn)行處理,如果這個(gè)連接不做任何事情會造成不必要的線程開銷,當(dāng)然可以通過線程池機(jī)制改善.

同步非阻塞IO(Java NIO)

同步非阻塞,服務(wù)器實(shí)現(xiàn)模式為一個(gè)請求一個(gè)線程,即客戶端發(fā)送的連接請求都會注冊到多路復(fù)用器上,多路復(fù)用器輪詢到連接有I/O請求時(shí)才啟動一個(gè)線程進(jìn)行處理。用戶進(jìn)程也需要時(shí)不時(shí)的詢問IO操作是否就緒,這就要求用戶進(jìn)程不停的去詢問.

異步阻塞IO(Java NIO)

此種方式下是指應(yīng)用發(fā)起一個(gè)IO操作以后,不等待內(nèi)核IO操作的完成,等內(nèi)核完成IO操作以后會通知應(yīng)用程序,這其實(shí)就是同步和異步最關(guān)鍵的區(qū)別,同步必須等待或者主動的去詢問IO是否完成,那么為什么說是阻塞的呢?因?yàn)榇藭r(shí)是通過select系統(tǒng)調(diào)用來完成的,而select函數(shù)本身的實(shí)現(xiàn)方式是阻塞的,而采用select函數(shù)有個(gè)好處就是它可以同時(shí)監(jiān)聽多個(gè)文件句柄(如果從UNP的角度看,select屬于同步操作。因?yàn)閟elect之后,進(jìn)程還需要讀寫數(shù)據(jù))從而提高系統(tǒng)的并發(fā)性!

異步非阻塞IO(Java AIO(NIO.2))

在此種模式下,用戶進(jìn)程只需要發(fā)起一個(gè)IO操作然后立即返回,等IO操作真正的完成以后,應(yīng)用程序會得到IO操作完成的通知,此時(shí)用戶進(jìn)程只需要對數(shù)據(jù)進(jìn)行處理就好了,不需要進(jìn)行實(shí)際的IO讀寫操作,因?yàn)檎嬲腎O讀取或者寫入操作已經(jīng)由內(nèi)核完成了.

BIO、NIO、AIO應(yīng)用場景

  • BIO方式適用于連接數(shù)目比較小且固定的架構(gòu),這種方式對服務(wù)器資源要求比較高,并發(fā)局限于應(yīng)用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解.
  • NIO方式適用于連接數(shù)目多且連接比較短(輕操作)的架構(gòu),比如聊天服務(wù)器,并發(fā)局限于應(yīng)用中,編程比較復(fù)雜,JDK1.4開始支持.
  • AIO方式使用于連接數(shù)目多且連接比較長(重操作)的架構(gòu),比如相冊服務(wù)器,充分調(diào)用OS參與并發(fā)操作,編程比較復(fù)雜,JDK7開始支持.

Linux中的IO模型

  • 在Linux(UNIX)中共有五種IO模型: 阻塞IO模型、非阻塞IO模型、信號驅(qū)動IO模型、IO復(fù)用模型 和 異步IO模型.

阻塞IO模型

阻塞 I/O 是最簡單的 I/O 模型,一般表現(xiàn)為進(jìn)程或線程等待某個(gè)條件,如果條件不滿足,則一直等下去。條件滿足,則進(jìn)行下一步操作.

image

應(yīng)用進(jìn)程通過系統(tǒng)調(diào)用 recvfrom 接收數(shù)據(jù),但由于內(nèi)核還未準(zhǔn)備好數(shù)據(jù)報(bào),應(yīng)用進(jìn)程就會阻塞住,直到內(nèi)核準(zhǔn)備好數(shù)據(jù)報(bào),recvfrom 完成數(shù)據(jù)報(bào)復(fù)制工作,應(yīng)用進(jìn)程才能結(jié)束阻塞狀態(tài)。

非阻塞IO模型

非阻塞的IO模型。應(yīng)用進(jìn)程與內(nèi)核交互,目的未達(dá)到之前,不再一味的等著,而是直接返回。然后通過輪詢的方式,不停的去問內(nèi)核數(shù)據(jù)準(zhǔn)備有沒有準(zhǔn)備好。如果某一次輪詢發(fā)現(xiàn)數(shù)據(jù)已經(jīng)準(zhǔn)備好了,那就把數(shù)據(jù)拷貝到用戶空間中。

image

應(yīng)用進(jìn)程通過 recvfrom 調(diào)用不停的去和內(nèi)核交互,直到內(nèi)核準(zhǔn)備好數(shù)據(jù)。如果沒有準(zhǔn)備好,內(nèi)核會返回error,應(yīng)用進(jìn)程在得到error后,過一段時(shí)間再發(fā)送recvfrom請求。在兩次發(fā)送請求的時(shí)間段,進(jìn)程可以先做別的事情.

信號驅(qū)動IO模型

應(yīng)用進(jìn)程在讀取文件時(shí)通知內(nèi)核,如果某個(gè) socket 的某個(gè)事件發(fā)生時(shí),請向我發(fā)一個(gè)信號。在收到信號后,信號對應(yīng)的處理函數(shù)會進(jìn)行后續(xù)處理。

image

應(yīng)用進(jìn)程預(yù)先向內(nèi)核注冊一個(gè)信號處理函數(shù),然后用戶進(jìn)程返回,并且不阻塞,當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備就緒時(shí)會發(fā)送一個(gè)信號給進(jìn)程,用戶進(jìn)程便在信號處理函數(shù)中開始把數(shù)據(jù)拷貝的用戶空間中。

IO復(fù)用模型

多個(gè)進(jìn)程的IO可以注冊到同一個(gè)管道上,這個(gè)管道會統(tǒng)一和內(nèi)核進(jìn)行交互。當(dāng)管道中的某一個(gè)請求需要的數(shù)據(jù)準(zhǔn)備好之后,進(jìn)程再把對應(yīng)的數(shù)據(jù)拷貝到用戶空間中。

image

IO多路轉(zhuǎn)接是多了一個(gè)select函數(shù),多個(gè)進(jìn)程的IO可以注冊到同一個(gè)select上,當(dāng)用戶進(jìn)程調(diào)用該select,select會監(jiān)聽所有注冊好的IO,如果所有被監(jiān)聽的IO需要的數(shù)據(jù)都沒有準(zhǔn)備好時(shí),select調(diào)用進(jìn)程會阻塞。當(dāng)任意一個(gè)IO所需的數(shù)據(jù)準(zhǔn)備好之后,select調(diào)用就會返回,然后進(jìn)程在通過recvfrom來進(jìn)行數(shù)據(jù)拷貝。

這里的IO復(fù)用模型,并沒有向內(nèi)核注冊信號處理函數(shù),所以,他并不是非阻塞的。進(jìn)程在發(fā)出select后,要等到select監(jiān)聽的所有IO操作中至少有一個(gè)需要的數(shù)據(jù)準(zhǔn)備好,才會有返回,并且也需要再次發(fā)送請求去進(jìn)行文件的拷貝。

異步IO模型

異步IO模型。應(yīng)用進(jìn)程把IO請求傳給內(nèi)核后,完全由內(nèi)核去操作文件拷貝。內(nèi)核完成相關(guān)操作后,會發(fā)信號告訴應(yīng)用進(jìn)程本次IO已經(jīng)完成.

image

用戶進(jìn)程發(fā)起aio_read操作之后,給內(nèi)核傳遞描述符、緩沖區(qū)指針、緩沖區(qū)大小等,告訴內(nèi)核當(dāng)整個(gè)操作完成時(shí),如何通知進(jìn)程,然后就立刻去做其他事情了。當(dāng)內(nèi)核收到aio_read后,會立刻返回,然后內(nèi)核開始等待數(shù)據(jù)準(zhǔn)備,數(shù)據(jù)準(zhǔn)備好以后,直接把數(shù)據(jù)拷貝到用戶控件,然后再通知進(jìn)程本次IO已經(jīng)完成。

5種IO模型對比

說明:

  • 阻塞IO模型、非阻塞IO模型、IO復(fù)用模型和信號驅(qū)動IO模型都是同步的IO模型。原因是因?yàn)椋瑹o論以上那種模型,真正的數(shù)據(jù)拷貝過程,都是同步進(jìn)行的。
  • 雖然信號驅(qū)動IO模型,內(nèi)核是在數(shù)據(jù)準(zhǔn)備好之后通知進(jìn)程,然后進(jìn)程再通過recvfrom操作進(jìn)行數(shù)據(jù)拷貝。我們可以認(rèn)為數(shù)據(jù)準(zhǔn)備階段是異步的,但是,數(shù)據(jù)拷貝操作是同步的。所以,整個(gè)IO過程也不能認(rèn)為是異步的。
image

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容