面向?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)算.
基本類型自動轉(zhuǎn)換為對應(yīng)的封裝類的操作叫做自動裝箱操作,反之叫自動拆箱操作,自動裝箱操作有一個(gè)自動裝箱池(范圍為-128~127).只要自動裝箱的數(shù)在自動裝箱池范圍內(nèi),則直接去池中找數(shù)據(jù).
方法的多態(tài): 重載、重寫.
-
重寫(overriding): 父類繼承過來的方法對子類不合適時(shí)子類可以改變該方法的實(shí)現(xiàn),這種操作叫做方法的重寫/覆蓋(繼承是重寫的前提條件); 重寫要求:
- 返回值、方法名和參數(shù)相同((5.0以后允許返回子類類型));
- 子類異常不能超出父類異常;
- 子類訪問級別不能低于父類訪問級別.
重載(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
七大原則
- 單一職責(zé)原則(一個(gè)類只做它該做的事情)、
- 開閉原則(更改性封閉,擴(kuò)展性開放)、
- 依賴倒轉(zhuǎn)原則(面向接口編程)、
- 里氏替換原則(任何時(shí)候都可以用子類型替換掉父類型)、
- 接口隔離原則(接口要小而專,絕不能大而全)、
- 合成復(fù)用原則(優(yōu)先使用聚合或合成關(guān)系復(fù)用代碼)、
- 迪米特原則(對象與對象之間應(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ā)生。
- 其他線程調(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、finally、finalize的區(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)行、 阻塞、死亡。
- 新建狀態(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)又可以分為三種:
- 等待阻塞
運(yùn)行狀態(tài)中的線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);
- 同步阻塞
線程在獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程所占用),它會進(jìn)入同步阻塞狀態(tài);
- 其他阻塞
通過調(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é)束,中間可以存在許多中可能。
如上圖所示,一般情況下會有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)行下一步操作.
應(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ù)拷貝到用戶空間中。
應(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ù)處理。
應(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ù)拷貝到用戶空間中。
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)完成.
用戶進(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)為是異步的。
參考
- http://www.cnblogs.com/zjc950516/p/7877511.html
- https://blog.csdn.net/qq_33098049/article/details/82664061
- https://www.cnblogs.com/wuchaodzxx/p/7396599.html
- https://blog.csdn.net/yeiweilan/article/details/73412438
- https://www.cnblogs.com/qqzy168/p/3246057.html
- https://jingyan.baidu.com/article/4f34706e0a1ba9e387b56dab.html
- https://www.cnblogs.com/moongeek/p/7631447.html
- http://www.itdecent.cn/p/25e243850bd2?appinstall=0
- https://mp.weixin.qq.com/s/fVpb1R0o2knXh3_uZeaQBA
- https://www.cnblogs.com/xingzc/p/5796413.html
- http://cmsblogs.com/?cat=183
- http://www.cnblogs.com/lwbqqyumidi/p/3804883.html
- http://www.cnblogs.com/mengdd/archive/2013/02/20/2917966.html