JAVA基礎
1、8種基本數據類型及其字節數
2、i++與++i的異同之處
同:
(1)i++與++i都是變量自增1,等價於i=i+1;
(2)i++與++i的使用僅僅針對變量,如 final int i=0;++i; 編譯報錯; ++2; 編譯報錯;
異:
i++:先運算后增1 ;
@Test public void test() { int x = 1; int y = x++; System.out.println("x=" + x + ",y=" + y); // x=2,y=1 }
++i:先增1后運算;
@Test public void test() { int x = 1; int y = ++x; System.out.println("x=" + x + ",y=" + y); // x=2,y=2 }
3、&和&&的區別和聯系,|和||的區別和聯系
與運算分為普通與(&)和短路與(&&)兩種。
(1) 普通與(&):所有的判斷條件都要判斷
(2)短路與(&&): 如果前面的判斷返回了false,后面不再判斷,最終結果就是false
或運算分為普通或(|)和短路或(||)兩種。
(1)普通或(|):所有的判斷條件都要判斷
(2) 短路或(||): 如果前面的判斷返回了true,后面不再判斷,最終結果就是true
4、private/默認/protected/public權限修飾符的區別
5、關鍵字
this和super關鍵字:
this:當前對象;super:直接父類對象。
this():當前類的無參構造方法,也可以指定有參的如:this(a);
super():直接父類的無參構造方法,也可以指定有參的如:super(a)。
默認順序:如果有個變量b,調用時為向上遍歷,b—>this.b—>super.b,如果未找到,編譯器將顯式提示錯誤信息;
static關鍵字:
主要用途是方便在沒有創建對象的情況下來進行調用(方法/變量)。
static可以用來修飾類的成員方法、類的成員變量,修飾代碼塊。
來源:https://www.cnblogs.com/dolphin0520/p/3799052.html
final和abstract關鍵字:
final可以聲明成員變量、方法、類以及本地變量。

package xcj.homepage.demo; public class FinalDemo { static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static void main(String[] args) { threadLocal.set("1"); // 被final修飾的引用變量指向的對象內容可變 System.out.println(threadLocal.get()); String a = "hello2"; final String b = "hello"; // final修飾的編譯期知道其值 String d = "hello"; String c = b + 2; String e = d + 2; System.out.println((a == c)); System.out.println((a == e)); final String f = getHello(); // final修飾的運行期才能知道其值 String g = f + 2; System.out.println((a == g)); String h = getHello1(); // 運行期才能知道其值 String i = h + 2; System.out.println((a == i)); new FinalDemo().changeValue(1); // final修飾的參數是不可改變的 StringBuffer buffer1 = new StringBuffer("hello"); new FinalDemo().changeValue(buffer1); System.out.println(buffer1); // final修飾引用類型的參數,不能再讓其指向其他對象,但是對其所指向的內容是可以更改的。 StringBuffer buffer2 = new StringBuffer("hello"); new FinalDemo().changeValue1(buffer2); System.out.println(buffer2); } public static String getHello() { return "hello"; } public final static String getHello1() { return "hello"; } // final修飾成員變量、成員方法、類,還可以修飾參數、若某個參數被final修飾了,則代表了該參數是不可改變的 public void changeValue(final int i) { // i++; // The final local variable i cannot be assigned. It must be blank and not using a compound System.out.println(i); } public void changeValue(final StringBuffer buffer) { // final修飾引用類型的參數,不能再讓其指向其他對象,但是對其所指向的內容是可以更改的。 // buffer = new StringBuffer("hi"); buffer.append("world"); System.out.println(buffer); } public void changeValue1(StringBuffer buffer) { // buffer重新指向另一個對象 buffer = new StringBuffer("hi"); buffer.append("world"); System.out.println(buffer); } }
abstract修飾方法和類。抽象方法只有方法的聲明,沒有方法的實現。抽象類不能被實例化,不能通過new關鍵字創建類的對象。如果子類(非抽象類)繼承了抽象類,抽象類可以指向子類的引用。
final、finally、finalize的區別:
final關鍵字可以用來修飾屬性,方法和類。還有就是如果內部類要訪問局部變量的話,那么對應的局部變量也必須為final關鍵字修飾的。
finally關鍵字一般也用在處理try{}catch{}finally{}的異常中,finally里面的內容一定會執行。
finalize在垃圾回收中會使用。finalize是Object類中的方法。在垃圾回收器執行的時候會調用被回收對象的此方法。
6、==和equals的區別和聯系
(1)==:比較的是值是否相等;
對於基本數據類型,則直接比較其存儲的 “值”是否相等;
對於引用類型,則比較的是所指向的對象的地址。
(2)equals:equals方法不能作用於基本數據類型的變量,會編譯報錯。equals繼承Object類,比較的是是否是同一個對象。
如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;
像String,Double,Date,Integer等類對equals方法進行了重寫的話,比較的是所指向的對象的內容。
如String重寫了equals方法,代碼如下:

public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
------------------
Integer重寫了equals方法,為值比較;Integer重寫的equals方法,代碼如下:

public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
[-128,127]范圍的兩個相同值使用==或equals都為true;非[-128,127]范圍的兩個Integer使用==為false,因為比較的是地址;使用equals為true。
原因:在-128~127的Integer值並且以Integer x = value;的方式賦值的Integer值在進行==和equals比較時,都會返回true,因為Java里面對處在-128~127之間的Integer值,用的是原生數據類型int,此時調用的是Integer.valueOf()方法,會在內存里供重用,也就是說這之間的Integer值進行==比較時只是進行int原生數據類型的數值比較,而超出-128~127的范圍,進行==比較時是進行地址及數值比較。
int、Integer使用==和equals代碼示例:

@Test public void test() { int i1 = 1,i2 = 1; System.out.println("兩個int == " + (i1 == i2)); // true // System.out.println("兩個int equals " + (i1.equals(i2))); // 基本類型無equals方法,這樣寫會編譯報錯 Integer i3 = 1,i4 = 1; System.out.println("[-128,127]范圍的兩個Integer == " + (i3 == i4)); // true System.out.println("[-128,127]范圍的兩個Integer equals " + i3.equals(i4)); // true System.out.println("一個Integer,一個int == " + (i3 == i1)); // true System.out.println("Integer equals int" + (i3.equals(i1))); // true,i3為引用類型,有equals方法,不會報錯 // System.out.println("int equals Integer" + (i1.equals(i3))); // 基本類型無equals方法,這樣寫會編譯報錯 Integer i5 = 128,i6 = 128; System.out.println("非[-128,127]范圍的兩個Integer == " + (i5 == i6)); // false System.out.println("非[-128,127]范圍的兩個Integer equals " + (i5.equals(i6))); // true int i7 = 128,i8 = 128; System.out.println("非[-128,127]范圍的兩個int == " + (i7 == i8)); // true }
執行結果:

兩個int == true [-128,127]范圍的兩個Integer == true [-128,127]范圍的兩個Integer equals true 一個Integer,一個int == true Integer equals inttrue 非[-128,127]范圍的兩個Integer == false 非[-128,127]范圍的兩個Integer equals true 非[-128,127]范圍的兩個int == true
--------------------
對於String示例: String s1 = "ABC",s2 = "ABC"; String s7 = "A" + "BC",s8 = "ABC"; 使用==均為true,因為像這種寫法產生的這種"常量"就會被放到常量池;
String s3 = new String("ABC"),s4 = new String("ABC"); String s5 = "ABC",s6 = new String("ABC"); String s9 = "A" + new String("BC"),s10 = "ABC"; 使用==均為false,因為new的對象是向堆申請新的空間存儲。

@Test public void test() { String s1 = "ABC",s2 = "ABC"; System.out.println("兩個String == " + (s1 == s2)); // true System.out.println("兩個String equals " + (s1.equals(s2))); // true String s3 = new String("ABC"),s4 = new String("ABC"); System.out.println("兩個String == " + (s3 == s4)); // false System.out.println("兩個String equals " + (s3.equals(s4))); // true String s5 = "ABC",s6 = new String("ABC"); System.out.println("兩個String == " + (s5 == s6)); // false System.out.println("兩個String equals " + (s5.equals(s6))); // true String s7 = "A" + "BC",s8 = "ABC"; System.out.println("兩個String == " + (s7 == s8)); // true System.out.println("兩個String equals " + (s7.equals(s8))); // true String s9 = "A" + new String("BC"),s10 = "ABC"; System.out.println("兩個String == " + (s9 == s10)); // false System.out.println("兩個String equals " + (s9.equals(s10))); // true }
繼承中,類初始化順序:父類靜態成員和靜態方法塊 -> 子類靜態成員和靜態方法塊 -> 父類普通成員和普通方法塊 -> 父類構造函數 -> 子類普通成員和普通方法塊 -> 子類構造函數
public class TestB extends MyTestA { public TestB() { System.out.println("子類構造函數"); } {System.out.println("子類普通代碼塊");} static { System.out.println("子類靜態代碼塊"); } public static void main(String[] args) { new TestB(); } } class MyTestA { public MyTestA() { System.out.println("父類構造函數"); } {System.out.println("父類普通代碼塊");} static { System.out.println("父類靜態代碼塊"); } }
父類靜態代碼塊
子類靜態代碼塊
父類普通代碼塊
父類構造函數
子類普通代碼塊
子類構造函數
7、 類和對象的關系
對象是對客觀事物的抽象,類是對對象的抽象,類是一種抽象的數據類型。
它們的關系是,對象是類的實例,類是對象的模板。
類是抽象的,不占用內存,而對象是具體的,占用存儲空間。
8、面向過程和面向對象的區別
面向過程
優點:性能比面向對象高,因為類調用時需要實例化,開銷比較大,比較消耗資源;比如單片機、嵌入式開發、 Linux/Unix等一般采用面向過程開發,性能是最重要的因素。
缺點:沒有面向對象易維護、易復用、易擴展
面向對象
優點:易維護、易復用、易擴展,由於面向對象有封裝、繼承、多態性的特性,可以設計出低耦合的系統,使系統 更加靈活、更加易於維護
缺點:性能比面向過程低
參考https://blog.csdn.net/jerry11112/article/details/79027834
面向對象的思想就是把一切都看成對象,而對象一般都由屬性+方法組成!
屬性屬於對象靜態的一面,用來描述具體某個對象的特征,方法屬於對象動態的一面。
類:具有同種屬性的對象稱為類,是個抽象的概念。
面向對象有三大特性,分別是封裝性、繼承性和多態性。
封裝,就是把客觀事物封裝成抽象的類,並且類可以把自己的數據和方法只讓可信的類或對象操作,對不可信的進行信息隱藏。
public,protected,default,private修飾的這四個成員屬性的訪問權限依次降低。
繼承,可以使用現有類的所有功能,並在無需重新編寫原來的類的情況下對這些功能進行擴展。
多態,是允許你將父對象設置為和一個或多個他的子對象相等的技術,父對象可以根據當前賦值給它的子對象特性以不同的方式運作。
簡而言之,允許將自類類型的指針賦值給父類類型的指針。
實現多態方式,覆蓋和重載。
9、談談Java的多態,Java中如何實現多態
多態是同種類的多個對象,在接收到同一消息時產生了不同反應和效果。
多態可以表現在子父類上或者通過接口實現。
多態有編譯時多態(通過方法重載實現)和運行時多態(通過方法覆蓋實現(子類覆蓋父類方法)即動態綁定,使用父類引用指向子類對象,再調用某一父類中的方法時,不同子類會表現出不同結果)。
對象類型的轉換分為以下兩種:
(1)向上轉型(類型的自動轉換):子類的對象可以直接賦給父類的對象變量。
(2)向下轉型: 將父類的引用強制轉換成子類的引用。它必須強制轉換。
10、簡述Java的垃圾回收機制
(1)JVM虛擬機通過可達性分析算法,來確實對象是否會被回收;
可達性分析算法:設立若干根對象(GC Root),每個對象都是一個子節點,當一個對象找不到根時,就認為該對象不可達,可以被回收。
(2)使用標記-清除算法、復制算法、標記-整理算法、分代收集算法進行垃圾回收。
①標記-清除算法:遍歷所有的GC Root,分別標記可達的對象和不可達的對象,標記完成后就將不可達的對象回收。但是效率低、回收得到的空間不連續(空間問題,標記清除之后會產生大量不連續的內存碎片,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而造成內存空間浪費)。
②復制算法:將內存分為大小相等的兩塊,每次只使用一塊。當這一塊內存滿了,就將還存活的對象復制到另一塊上,並且嚴格按照內存地址排列,然后把已使用的那塊內存統一回收。這樣能夠得到連續的內存空間,每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,但是,內存縮小為原來的一半,浪費了一半內存。
③標記-整理算法:與標記-清除算法類似;但標記-整理算法除了會不可達的對象回收外,還會對剩余的存活對象進行重新整理,不會產生內存碎片。
④分代收集算法:jvm使用最多的一種算法,它會在具體的場景(JVM三個區域:新生代、老年代、永久代)自動選擇以上三種算法進行垃圾對象回收。
新生代:有大批對象死亡,少量存活,JVM選用復制算法。(新生代是生命周期較短的對象,所有新生成的對象都先放在新生代中,存放滿時會移到老年代)
老年代:對象存活率高,沒有額外空間對其分配擔保,使用標記-清除或者標記-整理。(注:老年代都是生命周期較長的對象,在新生代中經歷N次垃圾回收后仍存在的會放到老年代中)
永久代:主要存放靜態文件,如JAVA類、方法等,不會回收。
注:新生代和老年代存儲在java虛擬機堆上 ;永久代存儲在方法區上。
11、java程序編譯后會生成字節碼文件(byte code),就是.class文件。
12、 繼承條件下構造方法的執行過程
先執行父類無參構造方法,再執行子類構造方法。
13、存在i+1<i嗎?
存在,i=2147483647時,i+1<1。2147483647 + 1為負數。
@Native public static final int MAX_VALUE = 0x7fffffff; Integer的最大值為2147483647,超出后,就為負數了。
計算機中用補碼來運算加減法,用補碼計算-2147483647-1和2147483647+1都得到1000 0000 0000 0000 0000 0000 0000 0000,而加法溢出的結果在范圍[-214748368,2147483647]中,故得到-214748368。
參考https://www.cnblogs.com/zakers/p/6739708.html
測試代碼如下:

@Test public void test() { int i = 0; while(true) { if (i+1 < i) { System.out.println(i); // 2147483647 break; } i++; } System.out.println(2147483647 + 1); // -2147483648 }
14、二進制、八進制、十進制、十六進制的相互轉換
(1)十進制轉二進制:十進制數除2取余法,即十進制數除2,余數為權位上的數,得到的商值繼續除2,依此步驟繼續向下運算直到商為0為止。
(2)二進制轉十進制:把二進制數按權展開、相加即得十進制數。
(3)二進制轉八進制:3位二進制數按權展開相加得到1位八進制數。(注意事項,3位二進制轉成八進制是從右到左開始轉換,不足時補0)。
(4)八進制轉成二進制:八進制數通過除2取余法,得到二進制數,對每個八進制為3個二進制,不足時在最左邊補零。
(5)二進制轉十六進制:與二進制轉八進制方法近似,八進制是取三合一,十六進制是取四合一。(注意事項,4位二進制轉成十六進制是從右到左開始轉換,不足時補0)。
(6)十六進制轉二進制:十六進制數通過除2取余法,得到二進制數,對每個十六進制為4個二進制,不足時在最左邊補零。
(7)十進制轉八進制或者十六進制有兩種方法:
①間接法:把十進制轉成二進制,然后再由二進制轉成八進制或者十六進制。
②直接法:把十進制轉八進制或者十六進制按照除8或者16取余,直到商為0為止。
(8)八進制或者十六進制轉成十進制:把八進制、十六進制數按權展開、相加即得十進制數。
(9)八進制與十六進制之間的轉換有兩種方法:
①他們之間的轉換可以先轉成二進制然后再相互轉換。
②他們之間的轉換可以先轉成十進制然后再相互轉換。
參考 https://zhidao.baidu.com/question/2144662682878005588.html
JAVA集合
15、Java集合體系結構(List、Set、Collection、Map的區別和聯系)
Java 集合可分為 Collection 和 Map 兩種體系。簡化后結構如下:
(1)Collection接口:
Set:不可重復的集合。
①HashSet:無序,可以放一個null,線程不安全;
②TreeSet:有序,線程不安全;默認情況下,元素不允許為null值,元素必須是相同類型。
③LinkedHashSet:有序,可以放一個null,線程不安全;
List:可重復的集合。
①ArrayList:動態數組,線程不安全;
②LinkedList:雙向鏈表,線程不安全;
③Vector:動態數組,線程安全;
④Stack:動態數組,線程安全。
Queue:元素有序,先進先出。
(2)Map接口:具有映射關系“key-value對”的集合。Map中的集合不能包含重復的鍵,值可以重復;每個鍵只能對應一個值。
①HashMap:鍵唯一,無序,key可為null,線程不安全;
②HashTable:鍵唯一,無序,key和value不能為null,線程安全;
③TreeMap:鍵唯一,有序,key不能為null,線程不安全;
④LinkedHashMap:鍵唯一,有序,key可為null,線程不安全。
16、HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓后找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。
當兩個不同的鍵對象的hashcode相同時會發生什么? 它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對。
來源 https://www.cnblogs.com/ITtangtang/p/3948798.html
17、Vector、ArrayList、LinkedList的使用、原理、異同、存儲性能和特性
18、HashMap、LinkedHashMap、Hashtable、TreeMap的使用、原理、異同、存儲性能和特性
19、HashSet、TreeSet的使用、原理、異同、存儲性能和特性
20、List里面如何剔除相同的對象?
21、Collections工具類中的sort()方法如何比較元素?
JAVA IO流
22、Java 中有幾種類型的流?
根據處理數據類型的不同分為:字符流(Reader和Writer為基類)和字節流(InputStream和OutputStream為基類);
根據數據流向不同分為:輸入流(InputStream和Reader為基類)和輸出流(OutputStream和Writer為基類);
字節流是 8 位通用字節流,字符流是16位Unicode字符流。
23、字符流和字節流聯系區別;什么時候使用字節流和字符流?
字符流的由來: 因為數據編碼的不同,而有了對字符進行高效操作的流對象。本質其實就是基於字節流讀取時,去查了指定的碼表。
字節流和字符流的區別:
(1)讀寫單位不同:字節流以字節(8bit)為單位,字符流以字符為單位,根據碼表映射字符,一次可能讀多個字節。
(2)處理對象不同:字節流能處理所有類型的數據(如圖片、avi等),而字符流只能處理字符類型的數據。
(3)字節流在操作的時候本身是不會用到緩沖區的,是文件本身的直接操作的;而字符流在操作的時候下后是會用到緩沖區的,是通過緩沖區來操作文件。
結論:優先選用字節流。首先因為硬盤上的所有文件都是以字節的形式進行傳輸或者保存的,包括圖片等內容。但是字符只是在內存中才會形成的,所以在開發中,字節流使用廣泛。
24、列舉常用字節輸入流和輸出流並說明其特點。
字節輸入流:InputStream -> {FileInputStream}、{FilterInputStream -> BufferedInputStream}、{ByteArrayInputStream}、{StringBufferInputStream}、{PipedInputStream}、{ObjectInputStream}
字節輸出流:OutputStream -> {FileOutputStream}、{FilterOutputStream -> BufferedOutputStream}、{ByteArrayOutputStream}、{PipedOutputStrea}、{ObjectOutputStream}
字符輸入流:Reader -> {BufferedReader}、{InputStreamReader -> FileReader} 、{CharArrayReader -> CharReader}、{StringReader}、{PipedReader}、{FilterReader -> PushbackReader}
字符輸出流:Writer -> {BufferedWriter}、{OutputStreamWriter -> FileWriter}、{CharArrayWriter -> CharWriter}、{StringWriter}、{PipedWriter}、{PrintWriter}、{FileWriter}
使用FileInputStream的read方法,讀取文件內容,代碼示例如下:

@Test public void test() { InputStream is = null; try { is = new FileInputStream("F://test.txt"); byte[] buf = new byte[1024]; int length = 0; //循環讀取文件內容,輸入流中將最多buf.length個字節的數據讀入一個buf數組中,返回類型是讀取到的字節數。 //當文件讀取到結尾時返回 -1,循環結束。 while((length = is.read(buf)) != -1){ System.out.print(new String(buf,0,length)); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 關閉流 if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用BufferedReader的readLine方法,讀取文件內容,代碼示例如下:

@Test public void test() { BufferedReader br = null; try { br = new BufferedReader(new FileReader("F://test.txt")); String line; while((line = br.readLine()) != null){ System.out.print(line + "\n"); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 關閉流 if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用FileOutputStream將字符串(字符串需先使用getByte方法轉為字節)寫出到文件,代碼示例如下:

@Test public void test() { OutputStream outputStream = null; try { outputStream = new FileOutputStream("F:\\log.txt", true); try { outputStream.write("test 您好".getBytes("utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用BufferedWriter將字符串寫出到文件,代碼示例如下:

@Test public void test() { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter("F:\\log.txt")); try { bw.write("test 您好"); bw.newLine(); bw.write("再見"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (bw != null) { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
25、說說BIO、NIO和AIO的區別。
BIO:同步阻塞IO,位於java.io包,基於流模型實現。在讀寫動作完成前,一直阻塞。調用時有可靠的線性順序。代碼簡單直觀,但IO的效率和擴展性低。如:InputStream、OutputStream、Reader、Writer。
NIO:同步非阻塞IO,位於java.nio包,基於Channel和Buffer實現。
AIO:NIO升級版,提供了異步非阻塞IO,異步IO是基於事件和回調機制實現的。
來源 http://www.imooc.com/article/265871
示例 https://blog.csdn.net/anxpp/article/details/51512200
26、JAVA中如何解析xml,不同方式有何優缺點?
HTTP
27、TCP為何采用三次握手來建立連接,若釆用二次握手可以嗎,請說明理由?(TCP三次握手,四次揮手)
三次握手(假設有A和B兩端要進行通信):
第一次:首先A發送一個(SYN)到B,意思是A要和B建立連接進行通信;
第二次:B收到A要建立連接的請求之后,發送一個確認(SYN+ACK)給A,意思是收到A的消息了,B這里也是通的,表示可以建立連接;
第三次:A如果收到了B的確認消息之后,再發出一個確認(ACK)消息,意思是告訴B,這邊是通的,然后A和B就可以建立連接相互通信了;
如果只有兩次通信的話,沒有第三次確認消息,這時候B不確定A是否收到了確認消息,有可能這個確認消息由於某些原因丟了。
如果采用二次握手,那么只要B端發出確認報文就會認為新的連接已經建立了,但是A端並沒有發出建立連接的請求,因此不會去向B端發送數據,B端沒有收到數據就會一直等待,這樣B端就會白白浪費掉很多資源。
而三次握手可以保證信道數據傳輸的可靠性。
28、HTTP協議工作原理及其特點
HTTP協議(超文本傳輸協議)是指計算機通信網絡中兩台計算機之間進行通信所必須共同遵守的規定或規則,是一種通信協議,它允許將超文本標記語言(HTML)文檔從Web服務器傳送到客戶端的瀏覽器。
通常,由HTTP客戶端發起一個請求,建立一個到服務器指定端口(默認是80端口)的TCP連接。HTTP服務器則在那個端口監聽客戶端發送過來的請求。一旦收到請求,服務器(向客戶端)發回一個狀態行,比如"HTTP/1.1 200 OK",和(響應的)消息,消息的消息體可能是請求的文件、錯誤消息、或者其它一些信息。
HTTP使用TCP而不是UDP的原因在於(打開)一個網頁必須傳送很多數據,而TCP協議提供傳輸控制,按順序組織數據,和錯誤糾正。
工作原理:輸入URL后,瀏覽器給Web服務器發送了一個Request, Web服務器接到Request后進行處理,生成相應的Response,然后發送給瀏覽器, 瀏覽器解析Response中的HTML,這樣我們就看到了網頁。
主要特點:
(1)支持客戶/服務器模式。
(2)簡單快速:客戶向服務器請求服務時,只需傳送請求方法和路徑。請求方法常用的有GET、HEAD、POST。每種方 法規定了客戶與服務器聯系的類型不同。由於HTTP協議簡單,使得HTTP服務器的程序規模小,因而通信速度很快。
(3)靈活:HTTP允許傳輸任意類型的數據對象。正在傳輸的類型由Content-Type加以標記。
(4)無連接:無連接的含義是限制每次連接只處理一個請求。服務器處理完客戶的請求,並收到客戶的應答后,即斷開連接。采用這種方式可以節省傳輸時間。
(5)無狀態:HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。缺少狀態意味着如果后續處理需要前面的信息,則它必須重傳,這樣可能導致每次連接傳送的數據量增大。另一方面,在服務器不需要先前信息時它的應答就 、較快。客戶端與服務器進行動態交互的 Web 應用程序出現之后,HTTP 無狀態的特性嚴重阻礙了這些應用程序的實現,畢竟交互是需要承前啟后的,兩種用於保持 HTTP 連接狀態的技術就應運而生了,一個是 Cookie,而另一個則是Session。
注:瀏覽器分析Response中的 HTML,發現其中引用了很多其他文件,比如圖片,CSS文件,JS文件。瀏覽器會自動再次發送Request去獲取圖片,CSS文件,或者JS文件。
來源:https://www.cnblogs.com/youzaijiang/p/10710200.html
JS
29、JavaScript常用數據類型有哪些
按數據類型的復雜方式划分為基本類型、特殊類型和復雜類型。
基本數據類型:number(數字類型)、string(字符串類型)、boolean(布爾類型);
復雜數據類型:array(數組類型)、object:對象數據類型;
特殊數據類型:null(空對象數據);underfined(未定義的任何數據類型)。
寫法:
array數組類型:簡單的由中括號包括起來,元素間用逗號分隔。如
一維數組 var Province =new Array('江蘇','浙江','安徽'); var Province = ['江蘇','浙江','安徽'];
二維數組 var City =[['南京','蘇州'],['杭州','寧波']]; 或者

var Province=new Array(); Province[0]=new Array(); Province[0][0]="南京"; Province[0][1]="蘇州"; Province[1]=new Array(); Province[1][0]="杭州"; Province[1][1]="寧波";
對象數據類型: var Province = {name:'江蘇',id:'A'}; 對象Province 有兩個屬性:name和id。
一個對象包括屬性和方法的示例如下:

var person = { firstName: "Bill", lastName : "Gates", id : 678, fullName : function() { return this.firstName + " " + this.lastName; } };
來源:https://www.gyzgl.com/jsnews/309.html
擴展:
JSON:是JavaScript Object Notation的縮寫,它是一種數據交換格式。
使用 JSON.stringify(Object); 可將對象序列化成JSON格式的字符串。如下:

var xiaoming = { name: '小明', age: 14, gender: true, height: 1.65, grade: null, 'middle-school': '\"W3C\" Middle School', skills: ['JavaScript', 'Java', 'Python', 'Lisp'] }; // 對象 var s = JSON.stringify(xiaoming); // 對象序列化成JSON格式的字符串
使用 JSON.parse() 可將JSON格式的字符串反序列化為JS對象。如下:

JSON.parse('[1,2,3,true]'); // [1, 2, 3, true] JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14} JSON.parse('true'); // true JSON.parse('123.45'); // 123.45
30、Javascript是面向對象的,怎么體現Javascript的繼承關系?
1、原型鏈繼承:將父類的實例作為子類的原型。(使用prototype實現繼承)

function Person (name) { this.name = name; }; Person.prototype.getName = function () { //對原型進行擴展 return this.name; }; function Parent (age) { this.age = age; }; Parent.prototype = new Person('老明'); //這一句是關鍵 //通過構造器函數創建出一個新對象,把老對象的東西都拿過來。 Parent.prototype.getAge = function () { return this.age; }; // Parent.prototype.getName = function () { //可以重寫從父類繼承來的方法,會優先調用自己的。 // console.log(222); // }; var result = new Parent(22); console.log(result.getName()); //老明 //調用了從Person原型中繼承來的方法(繼承到了當前對象的原型中) console.log(result.getAge()); //22 //調用了從Parent原型中擴展來的方法
2、構造繼承:使用父類的構造函數來增強子類實例,等於是復制父類的實例屬性給子類(沒用到原型),就是把父類中通過this指定的屬性和方法復制(借用)到子類創建的實例中(利用call或者apply)。

function Person (name) { this.name = name; this.friends = ['小李','小紅']; this.getName = function () { return this.name; } }; // Person.prototype.geSex = function () { //對原型進行擴展的方法就無法復用了 // console.log("男"); // }; function Parent = (age) { Person.call(this,'老明'); //這一句是核心關鍵 //這樣就會在新parent對象上執行Person構造函數中定義的所有對象初始化代碼, // 結果parent的每個實例都會具有自己的friends屬性的副本 this.age = age; }; var result = new Parent(23); console.log(result.name); //老明 console.log(result.friends); //["小李", "小紅"] console.log(result.getName()); //老明 console.log(result.age); //23 console.log(result.getSex()); //這個會報錯,調用不到父原型上面擴展的方法
3、組合繼承: 通過調用父類構造,繼承父類的屬性並保留傳參的優點,然后再通過將父類實例作為子類原型,實現函數復用。(使用prototype和call)

function Person (name) { this.name = name; this.friends = ['小李','小紅']; }; Person.prototype.getName = function () { return this.name; }; function Parent (age) { Person.call(this,'老明'); //這一步很關鍵 this.age = age; }; Parent.prototype = new Person('老明'); //這一步也很關鍵 var result = new Parent(24); console.log(result.name); //老明 result.friends.push("小智"); // console.log(result.friends); //['小李','小紅','小智'] console.log(result.getName()); //老明 console.log(result.age); //24 var result1 = new Parent(25); //通過借用構造函數都有自己的屬性,通過原型享用公共的方法 console.log(result1.name); //老明 console.log(result1.friends); //['小李','小紅']
4、寄生組合繼承:通過寄生方式,砍掉父類的實例屬性。

function Person(name) { this.name = name; this.friends = ['小李','小紅']; } Person.prototype.getName = function () { return this.name; }; function Parent(age) { Person.call(this,"老明"); this.age = age; } (function () { var Super = function () {}; // 創建一個沒有實例方法的類 Super.prototype = Person.prototype; Parent.prototype = new Super(); //將實例作為子類的原型 })(); var result = new Parent(23); console.log(result.name); console.log(result.friends); console.log(result.getName()); console.log(result.age);
來源 https://www.cnblogs.com/chaixiaozhi/p/8515087.html
31、談談JS的閉包
閉包是函數和聲明該函數的詞法環境的組合。
參考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
JSP
32、JSP九大內置對象
(1)輸入輸出對象:out對象、response對象、request對象
(2)通信控制對象:pageContext對象、session對象、application對象
(3)Servlet對象: page對象、config對象
(4)錯誤處理對象:exception對象
來源 https://blog.csdn.net/qq_42806915/article/details/82717694
33、列舉JSP的四大作用域
(1)page:只在當前頁面有效;
(2)request:在當前請求中有效;
(3)session:他在當前回話中有效;
(4)application:在所有的應用程序中都有效。
34、JSP的執行過程
來源:https://cloud.tencent.com/developer/article/1400348
35、JSP和Servlet的區別和聯系
1、JSP編譯后就變成了Servlet。JSP是Servlet的一種簡化,使用JSP只需要完成程序員需要輸出到客戶端的內容,JSP中的Java腳本如何鑲嵌到一個類中,由JSP容器完成。
而Servlet則是個完整的Java類,這個類的Service方法用於生成對客戶端的響應。
2、JSP更擅長表現於頁面顯示,Servlet更擅長於邏輯控制。
3、Servlet中沒有內置對象,JSP中的內置對象都是必須通過HttpServletRequest對象,HttpServletResponse對象以及HttpServlet對象得到。
框架
36、談談過濾器原理及其作用?談談攔截器原理和作用?
過濾器:在客戶端到服務器的過程中,當發送請求時,如果有不符合的信息將會被filter進行攔截,如果符合則會進行放行,在服務器給客戶端響應時也會進行判斷,如果有不符合的信息將會被filter進行攔截,如果符合則會進行放行。簡而言之,filter負責攔截請求和放行。
過濾器原理:Filter 接口中有一個doFilter 方法,我們可以自定義類實現Filter接口;並在web.xml中配置過濾器設置對哪些資源進行攔截;web服務器每次調用 web 資源的 service 方法之前,都會先調用一下 Filter 的 doFilter 方法 doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain){} ,做一些預處理后,執行 filterChain.doFilter(request, response); 代碼進行放行,或者對不符合的請求進行攔截;如果放行(執行了doFilter方法),則調用鏈完成后, web 服務器就會調用 web 資源的 service 方法。
過濾器作用:過濾器主要的作用是過濾請求,可以通過Filter技術,對web服務器管理的所有web資源(如:JSP、Servlet、靜態圖片文件、或靜態HTML文件)進行攔截,或者實現URL級別的權限控制、過慮敏感詞匯、壓縮響應信息等一些高級功能。
代碼示例:

package xcj.homepage.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("這里可以做一些處理"); // if (false) { // return ; // 也可以加一些校驗,如URL權限攔截等,不符合則return,則不會放行該次請求 // } chain.doFilter(request, response); // doFilter放行 } @Override public void destroy() { System.out.println("destroy"); } }

<filter> <filter-name>MyFilter</filter-name> <filter-class>xcj.homepage.filter.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>MyFilter</filter-name> <url-pattern>/user/*</url-pattern> </filter-mapping>
參考:
圖解 https://blog.csdn.net/pinnuli/article/details/81296969
原理與使用 https://blog.csdn.net/qq_35246620/article/details/69230454
攔截器:主要作用也是在服務端真正處理請求前后進行一些相關的操作。 例如初始化資源,權限監控,會話設置,菜單獲取,資源清理等。
Struts2攔截器是在訪問某個Action或Action的某個方法,字段之前或之后實施攔截,並且Struts2攔截器是可插拔的,攔截器是AOP的一種實現。Struts2規定用戶自定義攔截器必須實現com.opensymphony.xwork2.interceptor.Interceptor接口。該接口聲明了3個方法init,destroy,intercept方法。

public class LoginInterceptor extends AbstractInterceptor{ @Override public String intercept(ActionInvocation invocation) throws Exception { //得到攔截到的action的名稱,看是否是login,當是login的時候,不用進行下面的檢測了,直接執行下一個攔截器 String actionName=invocation.getProxy().getActionName(); if("login".equals(actionName)){ return invocation.invoke(); } //如果不是login.則判斷是否已登錄,及檢測session中key為user的值是否存在,如果不存在,跳回到登錄頁面 String user=(String)invocation.getInvocationContext().getSession().get("user"); if(user==null){ System.out.println("未登錄"); return "login"; } //進行到這里.說明用戶已登錄,則跳轉到下一個攔截器 return invocation.invoke(); } }

<package name="default" namespace="/" extends="struts-default"> <interceptors> <!-- 配置自定義的攔截器--> <interceptor name="checkLogin" class="com.wang.interceptor.LoginInterceptor"/> <!--配置一個攔截器棧,里面包含自己定義的攔截器和defaultStack默認攔截器--> <interceptor-stack name="myStack"> <interceptor-ref name="defaultStack"></interceptor-ref> <interceptor-ref name="checkLogin"></interceptor-ref> </interceptor-stack> </interceptors> <!--引用默認的攔截器(棧)--> <default-interceptor-ref name="myStack"></default-interceptor-ref> <!--配置一個全局結果集--> <global-results> <result name="login">/login.jsp</result> </global-results> <action name="login" class="com.wang.action.LoginAction" > <result>/succ.jsp</result> <result name="error">/login.jsp</result> </action> </package>
Struts攔截器 https://www.jb51.net/article/128542.htm
Struts攔截器和過濾器 https://blog.csdn.net/a707819156/article/details/79659369
Spring攔截器(AOP):攔截器(Interceptor), 是 AOP (Aspect-Oriented Programming) 的另一種叫法。
SpringMVC的攔截器不同於Spring的攔截器,SpringMVC具有統一的入口DispatcherServlet,所有的請求都通過DispatcherServlet,所有的操作都是通過該servlet進行的順序操作,SpringMVC的攔截器一般繼承自HandlerInterceptorAdapter 或者實現 HandlerInterceptor 接口,對應提供了三個preHandle,postHandle,afterCompletion方法。

public class MyInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /** * preHandle方法是進行處理器攔截用的,顧名思義,該方法將在Controller處理之前進行調用, * SpringMVC中的Interceptor攔截器是鏈式的,可以同時存在多個Interceptor, * 然后SpringMVC會根據聲明的前后順序一個接一個的執行, * 而且所有的Interceptor中的preHandle方法都會在Controller方法調用之前調用。 * SpringMVC的這種Interceptor鏈式結構也是可以進行中斷的, * 這種中斷方式是令preHandle的返回值為false,當preHandle的返回值為false的時候整個請求就 結束了。 這種方式,如果返回false,一般情況需要重定向或者轉發到其他頁面, 采用request的轉發或者response的重定向即可 */ } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { /** * 這個方法只會在當前這個Interceptor的preHandle方法返回值為true的時候才會執行。 postHandle是進行處理器攔截用的,它的執行時間是在處理器進行處理之 后, 也就是在Controller的方法調用之后執行,但是它會在DispatcherServlet進行視圖的渲染之前執行,也就是說在這個方法中你可以對ModelAndView進行操作。這個方法的鏈式結構跟正常訪問的方向是相反的,也就是說先聲明的Interceptor攔截器該方法反而會后調用,這跟Struts2里面的攔截器的執行過程有點像,只是Struts2里面的intercept方法中要手動的調用ActionInvocation的invoke方法,Struts2中調用ActionInvocation的invoke方法就是調用下一個Interceptor或者是調用action,然后要在Interceptor之前調用的內容都寫在調用invoke之前,要在Interceptor之后調用的內容都寫在調用invoke方法之后。 */ } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { /** * 該方法也是需要當前對應的Interceptor的preHandle方法的返回值為true時才會執行。 * 該方法將在整個請求完成之后,也就是DispatcherServlet渲染了視圖執行, 這個方法的主要作用是用於清理資源的, */ } }

<mvc:interceptors> <!-- 寫在外面,表示攔截所有鏈接 --> <bean id="" class=""/> <mvc:interceptor> <mvc:mapping path="/**" /> <!-- 排除攔截的鏈接 --> <mvc:exclude-mapping path="/static/**" /> <bean class="攔截器java代碼路徑" /> </mvc:interceptor> </mvc:interceptors>
SpringMVC攔截器 https://www.cnblogs.com/kevinShaw/p/9179446.html
監聽器:TODO...
JavaWeb三大組件(Servlet、Filter、Listener):https://www.cnblogs.com/kefir/p/9426754.html
Java三大器(Filter、Listener、Interceptor)。
37、Ajax含義及其主要技術、工作原理
38、web項目從瀏覽器發起交易響應緩慢,請簡述從哪些方面如數分析
39、設計模式
鏈接:23種設計模式 http://c.biancheng.net/design_pattern/
設計原則:
(1)封裝變化:找出應用中可能需要變化之處,把他們獨立出來,不要和那些不需要變化的代碼混合在一起。或者理解為“把會變化的部分取出並封裝起來,以便以后可以輕易地擴充此部分,而不影響不需要變化的其他部分”。
(2)面向接口編程:針對接口編程,不針對實現編程:指的是針對超類型編程,即變量的聲明類型是超類型,通常是一個抽象類或一個接口,關鍵在於多態。
(3)多用組合,少用繼承:可以在允許時動態。
(4)迪米特原則:為交互對象之間的松耦合設計而努力。
(5)開放-關閉原則:類應該對擴展開放,對修改關閉。
(6)依賴倒置原則:要依賴抽象,不要依賴具體類。
含義:不能讓高層組件依賴底層組件,高層和底層組件都應該依賴於抽象。
指導方針:變量不可以持有具體類的引用;不要讓類派生自具體類;不要覆蓋基類中已實現的方法。
(7)最少知識原則(迪米特原則):減少對象間的交互,只留幾個密友。也就是不讓太多的類耦合在一起,免得修改系統中的一部分,會影響到其他部分。
(8)好萊塢原則:高層組件對待底層組件的方式是“別調用我們,我們會調用你”。
(9)單一原則:一個類應該只有一個引起變化的原因。
參考書籍《HeadFirst設計模式》
示例:
(1)單例模式

public class SingletonDemo implements Serializable { // final域修飾的私有靜態成員 private transient static final SingletonDemo INSTANCE = new SingletonDemo(); // 私有的構造器 private SingletonDemo() {} // 公有的靜態工廠方法 public static SingletonDemo getInstance() { return INSTANCE; } private Object readResolve() { return INSTANCE; } }
40、Maven和ANT的區別
41、什么是RPC遠程過程調用?
42、什么是Dubbo?Dubbo中有哪些角色?Dubbo執行流程是什么?Dubbo支持的協議有哪些?Dubbo支持的注冊中心有哪些?
調用關系說明:
(1)服務容器 Container
負責啟動,加載,運行服務提供者。
(2)服務提供者 Provider
在啟動時,向注冊中心注冊自己提供的服務。
(3)服務消費者 Consumer
在啟動時,向注冊中心訂閱自己所需的服務。
(4)注冊中心 Registry
返回服務提供者地址列表給消費者,如果有變更,注冊中心將基於長連接推送變更數據給消費者。
(5)服務消費者 Consumer
,從提供者地址列表中,基於軟負載均衡算法,選一台提供者進行調用,如果調用失敗,再選另一台調用。
(6)服務消費者 Consumer
和提供者 Provider
,在內存中累計調用次數和調用時間,定時每分鍾發送一次統計數據到監控中心 Monitor
。
dubbo支持的通信協議:dubbo、rmi、hessian、http、webservice、thrift、memcached、redis等。
Dubbo支持的注冊中心:ZooKeeper注冊中心、Redis注冊中心、Multicast注冊中心。
代碼示例:https://www.jianshu.com/p/cb9373c1ddba
服務提供方:

<!-- 提供方應用信息,用於計算依賴關系 --> <dubbo:application name="gw-service-user" /> <!-- 使用zookeeper注冊中心暴露服務地址 --> <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" /> <!-- 用dubbo協議在20880端口暴露服務 --> <dubbo:protocol name="dubbo" port="20880" /> <!-- 用戶服務接口 --> <dubbo:service interface="wusc.edu.facade.user.service.PmsUserFacade" ref="pmsUserFacade" />
服務消費方:

<!-- 消費方應用名,用於計算依賴關系,不是匹配條件,不要與提供方一樣 --> <dubbo:application name="edu-web-boss" /> <!-- 使用zookeeper注冊中心暴露服務地址 --> <!-- 注冊中心地址 --> <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" /> <!-- 用戶服務接口 --> <dubbo:reference interface="wusc.edu.facade.user.service.PmsUserFacade" id="pmsUserFacade" check="false" />
43、ZooKeeper的作用是什么?
(1)master節點選舉,主節點掛了以后,從節點就會接受工作,並保證這個節點是唯一的,這也是所謂的首腦模式,從而保證我們的集群是高可用的。
(2)統一配置文件管理,即只需要不熟一台服務器,則可以把相同配置文件同步更新到其他所喲與的服務器,次操作在雲計算中用的特別的多(假設修改了redis統一配置)。
(3)發布與訂閱,類似消息隊列MQ(amq,rmq。。),dubbo發布者把數據存在znode上,訂閱者會讀取這個數據。
(4)提供分布式鎖,分布式環境不同進程之間爭奪資源,類似於多線程中的鎖。
(5)集群管理,集群保證數據的強一致性。 主節點數據會同步到附屬節點 客戶端不管鏈接任何客戶端的時候都會保證讀取數據的一致性。
dubbo中zookeeper做注冊中心,如果注冊中心集群都掛掉,那發布者和訂閱者還能通信嗎?(面試高頻題)
zookeeper的信息會緩存到服務器本地作為一個cache緩存文件,並且轉換成properties對象方便使用,每次調用時,按照本地存儲的地址進行調用,但是無法從注冊中心去同步最新的服務列表,短期的注冊中心掛掉是不要緊的,但一定要盡快修復。所以掛掉是不要緊的,但前提是你沒有增加新的服務,如果你要調用新的服務,則是不能辦到的。44、什么是跨域?怎么解決
跨域,指的是瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源(域名,協議,端口均相同)策略造成的,是瀏覽器對JS施加的安全限制。
解決辦法:(1)JSONPP只支持GET請求;(2)代理:寫一個接口www.123.com/server,由這個接口在后端去調用www.456.com/server並拿到返回值,然后再返回給index.htm;這樣就繞過了瀏覽器端,不存在跨域問題。
來源:https://www.cnblogs.com/wennice/p/7150461.html
45、什么是JMS?JMS有哪些模型?
JMS 全稱:Java Message Service,JMS是java的消息服務,JMS的客戶端之間可以通過JMS服務進行異步的消息傳輸。
來源:https://www.cnblogs.com/xinhuaxuan/p/6104274.html
46、為什么要使用連接池?數據庫連接池的原理?項目中使用的數據庫連接池?
(1)數據庫連接是一件費時的操作,連接池可以使多個操作共享一個連接,提高對數據庫連接資源的管理。
(2)基本思想就是為數據庫連接建立一個“緩沖池”。預先在緩沖池中放入一定數量的連接,當需要建立數據庫連接時,只需從“緩沖池”中取出一個,使用完畢之后再放回去。我們可以通過設定連接池最大連接數來防止系統無盡的與數據庫連接。更為重要的是我們可以通過連接池的管理機制監視數據庫的連接的數量、使用情況,為系統開發,測試及性能調整提供依據。
常用數據庫連接池:DBCP、C3P0、druid、Proxool。
參考:https://www.cnblogs.com/nuccch/p/8120349.html
47、Struts2的執行流程
(1)客戶端初始化一個指向Servlet容器(例如Tomcat)的請求。
(2)這個請求經過一系列的過濾器(Filter)(這些過濾器中有一個叫做ActionContextCleanUp的可選過濾器,這個過濾器對於Struts2和其他框架的集成很有幫助,例如:SiteMesh Plugin)。
(3)接着FilterDispatcher被調用,FilterDispatcher詢問ActionMapper來決定這個請求是否需要調用某個Action。
(4)如果ActionMapper決定需要調用某個Action,FilterDispatcher把請求的處理交給ActionProxy。
(5)ActionProxy通過ConfigurationManager詢問框架的配置文件,找到需要調用的Action類 ,這里,我們一般是從struts.xml配置中讀取。
(6)ActionProxy創建一個ActionInvocation的實例。
(7)ActionInvocation實例使用命名模式來調用,在調用Action的過程前后,涉及到相關攔截器(Intercepter)的調用。
(8)一旦Action執行完畢,ActionInvocation負責根據struts.xml中的配置找到對應的返回結果。返回結果通常是(但不總是,也可能是另外的一個Action鏈)一個需要被表示的JSP或者FreeMarker的模版。在表示的過程中可以使用Struts2 框架中繼承的標簽。在這個過程中需要涉及到ActionMapper。
48、SpringMVC的執行流程
(1)用戶向服務器發送請求,請求被Spring 前端控制Servelt DispatcherServlet捕獲;
(2)DispatcherServlet對請求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執行完成后,向DispatcherServlet 返回一個ModelAndView對象;
(6)根據返回的ModelAndView,選擇一個適合的ViewResolver(必須是已經注冊到Spring容器中的ViewResolver)返回給DispatcherServlet ;
(7)ViewResolver 結合Model和View,來渲染視圖;
(8)將渲染結果返回給客戶端。
49、說一下Spring中的兩大核心
兩大核心:(1)IOC(Inversion of Control, 控制反轉)把創建對象的操作交給框架,亦被稱為 DI(Dependency Injection, 依賴注入);(2)AOP(面向切面編程)。
IOC:主要是對bean的注冊以及對bean中參數的初始化。
(1)控制反轉:
最基礎的調用的對象是通過new一個對象出來,例如: People p=new People();
我們Spring框架中的IOC即改變這種方式的調用,將后面“new People”轉換為xml文件去調用,即使用第三者調用
(2)依賴注入
實現依賴注入的三種方式:setter方式注入,構造注入,使用P命名實現注入
AOP:
面向切面編程,簡單地說就是在不改變原程序的基礎上為代碼段增加新功能,對代碼段進行增強處理
前置增強、后置增強、最終增強、異常增強、環繞增強(定義一個用於增強的FirstAop類)
(1)使用XML配置:

<!-- 配置切面 --> <aop:config> <!-- 聲明目標點 --> <aop:pointcut expression="execution(* xcj.homepage.service..*Service*.*(..))" id="target"/> <!-- 連接切點跟被切點 --> <aop:aspect ref="first"> <!-- 前置增強 --> <aop:before method="pre_study" pointcut-ref="target"/> <!-- 后置增強 --> <aop:after-returning method="do_work" returning="result" pointcut-ref="target"/> <!-- 最終增強 --> <aop:after method="do_final" pointcut-ref="target"/> <!-- 異常增強 --> <aop:after-throwing method="do_exception" throwing="e" pointcut-ref="target"/> <!-- 環繞增強 --> <aop:around method="around" pointcut-ref="target"/> </aop:aspect> </aop:config>
(2)使用注解:
TODO..(要單獨寫一個SpringAOP相關知識點的博客)
Spring AOP詳解:https://www.cnblogs.com/hongwz/p/5764917.html
Spring AOP實現原理:https://baijiahao.baidu.com/s?id=1615034709376787673&wfr=spider&for=pc
https://blog.csdn.net/u010452388/article/details/80868392
50、Spring的事務的傳播特性
Spring事務傳播特性: 該特性是保證事務是否開啟,業務邏輯是否使用同一個事務的保證。當事務在傳播過程中會受其影響。其傳播特性包括
(1)PROPAGATION_REQUIRED 如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇。
(2)PROPAGATION_SUPPORTS 支持當前事務,如果當前沒有事務,就以非事務方式執行。
(3)PROPAGATION_MANDATORY 使用當前的事務,如果當前沒有事務,就拋出異常。
(4)PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。
(5)PROPAGATION_NOT_SUPPORTED 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
(6)PROPAGATION_NEVER 以非事務方式執行,如果當前存在事務,則拋出異常。
(7)PROPAGATION_NESTED 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與 PROPAGATION_REQUIRED 類似的操作。
Spring提供了兩種事務管理方式, 編程式事務和聲明式事務。
編程式事務指的是通過編碼方式實現事務控制;
聲明式事務基於 AOP,將具體業務邏輯與事務處理邏輯解耦。
Spring中聲明式事務處理有兩種方式,一種是在配置文件(xml)中做相關的事務規則聲明,另一種是基於@Transactional 注解的方式。
事務的四大特性ACID:原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持久性(durability)。
(1)原子性:一個事務必須被視為一個不可分割的最小工作單元,要么全部提交成功,要么全部失敗回滾(一個事務中的多個操作要么都成功要么都失敗)。
(2)一致性:數據庫總是從一個一致性的狀態轉換到另一個一致性的狀態(例如存錢操作,存之前和存之后的錢數應該是一致的)。
(3)隔離性:一個事務所做的修改在最終提交以前,對其他事務是不可見的(事務與事務應該是相互隔離的)。
(4)持久性:一旦事務提交,則其所做的修改就會永久保存到數據庫中(事務一旦提交,數據要持久保存)。
數據庫
51、數據庫MySQL,Oracle分頁時用的語句
MySQL:
SELECT * FROM admin_company LIMIT 0,10; SELECT * FROM admin_company ORDER BY id LIMIT 0,10;
Oracle:
SELECT * FROM (SELECT ROWNUM rn ,* FROM admin_company WHERE rn <= 10) WHERE rn > 0;
沒有ORDER BY,兩層查詢
rownum偽列產生的序號是按照數據被查詢出來的順序添加上去的,第一條是1,第二條是2,依次加1。
在ORACLE中使用rownum偽列分頁時,需要多加一層查詢,以保證rownum序號的連續性。
SELECT * FROM (SELECT ROWNUM rn , c.* FROM (SELECT * FROM admin_company ORDER BY companyno) c WHERE rn <= 10 ) WHERE rn > 0;
有ORDER BY,三層查詢
Orcale排序后分頁查詢,需要多加一層查詢原因:
參考 https://www.cnblogs.com/lgzslf/archive/2010/05/30/1747469.html
52、SQL怎么優化執行效率更高、SQL優化經驗
(1)SELECT子句中避免使用‘*’:Oracle在解析的過程中, 會將‘*’依次轉換成所有的列名, 這個工作是通過查詢數據字典完成的, 這意味着將耗費更多的時間。
(2)使用表的別名(Alias): 當在SQL語句中連接多個表時, 請使用表的別名並把別名前綴於每個Column上。這樣一來,就可以減少解析的時間並減少那些由Column歧義引起的語法錯誤。
(3)用IN來替換OR、用UNION替換OR (適用於索引列)、用EXISTS替代IN、用NOT EXISTS替代NOT IN。
(4)如果不需要去重,用UNION-ALL 替換UNION:UNION 將對結果集合進行合並和排序,這個操作會使用到SORT_AREA_SIZE這塊內存,UNION ALL 將重復輸出兩個結果集合中相同記錄,排序也不是必要的,效率就會因此得到提高。
(5)優化GROUP BY:提高GROUP BY 語句的效率,可以通過將不需要的記錄在GROUP BY 之前過濾掉。
(6)使用DECODE函數來減少處理時間。【TODO】
(7)用Where子句替換HAVING子句:HAVING 只會在檢索出所有記錄之后才對結果集進行過濾。這個處理需要排序,總計等操作。
(8)合理使用索引:
①避免在索引列上使用NOT,當Oracle“遇到”NOT,他就會停止使用索引轉而執行全表掃描。
②避免在索引列使用 !=、||、+,WHERE子句中,優化器將不使用索引而使用全表掃描。
③避免在索引列上使用計算。WHERE子句中,如果索引列是函數的一部分。優化器將不使用索引而使用全表掃描。
④避免在索引中使用任何可以為空的列,Oracle將無法使用該索引。對於單列索引,如果列包含空值,索引中將不存在此記錄。對於復合索引,如果每個列都為 空,索引中同樣不存在此記錄。如果至少有一個列不為空,則記錄存在於索引中。
⑤總是使用索引的第一個列:如果索引是建立在多個列上,只有在它的第一個列(leading column)被where子句引用時,優化器才會選擇使用該索引。這也是一條簡單而重要的規則,當僅引用索引的第二個列時,優化器使用了全表掃描而忽略 了索引。
⑥避免對WHERE子句的列名使用函數(避免改變索引列的類型):當比較不同數據類型的數據時,如:
SELECT * FROM EMP WHERE EMPNO = TO_NUMBER(‘123') ; 類型轉換沒有發生在索引列上,索引的用途沒有被改變。
SELECT * FROM EMP WHERETO_NUMBER(EMP_TYPE)=123; 因為內部發生的類型轉換, 這個索引將不會被用到。
SQL優化大全 https://blog.csdn.net/hguisu/article/details/5731629
53、數據庫索引的了解
作用:提高查詢速度、確保數據的唯一性、可以加速表和表之間的連接,實現表和表之間的參照完整性、使用分組和排序子句進行數據檢索時,可以減少分組和排序的時間、全文檢索字段進行搜素優化。
分類:主鍵索引(PRIMAY KEY)、唯一索引(UNIQUE)、常規索引(INDEX)、全文索引(FULLTEXT)。
主鍵索引的幾種創建方式:確保數據記錄的唯一性,主鍵索引只能有一個。(以下為MYSQL示例)
CREATE TABLE mytable ( ID INT(11) AUTO_INCREMENT PRIMARY KEY, username VARCHAR (16) NOT NULL #或 PRIMARY KEY(`ID`) ) ;
唯一索引的幾種創建方式:避免同一個表中某數據列中的值重復,唯一索引可有多個。(以下為MYSQL示例)
(1)創建索引: CREATE UNIQUE INDEX indexName ON mytable(username(length));
(2)修改表結構: ALTER table mytable ADD UNIQUE [indexName] (username(length));
(3)創建表時指定:
CREATE TABLE mytable ( ID INT NOT NULL, username VARCHAR (16) NOT NULL, UNIQUE [ indexName ] (username (LENGTH)) # 或者username VARCHAR(16) NOT NULL UNIQUE ) ;
常規索引的幾種創建方式:快速定位特定數據,應加在查詢條件的字段,不易添加太多常規索引,影響數據的插入,刪除和修改操作,使用KEY或INDEX關鍵字設置。(以下為MYSQL示例)
(1)創建表時添加:
CREATE TABLE mytable ( ID INT NOT NULL, userno VARCHAR (16) NOT NULL, username VARCHAR (16) NOT NULL, loginname VARCHAR (16) NOT NULL, INDEX `index1` (userno, username), KEY `index2` (userno, loginname) ) ;
(2)創建后追加: ALTER TABLE `mytable` ADD INDEX `ind` (`userno`,`username`);
全文索引的幾種創建方式:快速定位特定數據,只能用於MyISAM類型的數據表,只能用於CHAR ,VARCHAR,TEXT數據列類型。(以下為MYSQL示例)
(1)創建表時添加:
CREATE TABLE mytable( username VARCHAR (16) NOT NULL, FULLTEXT(`username`) )ENGINE=MYISAM;
(2)創建后追加: ALTER TABLE mytable ADD FULLTEXT(`username`);
JAVA虛擬機
54、簡述Java內存管理機制,以及垃圾回收的原理和使用過Java調優工具,垃圾回收器可以馬上回收內存嗎?如何通知虛擬機進行垃圾回收?
內存管理主要包括內存分配和內存回收兩個部分。
JAVA程序執行過程
JAVA內存模型划分:
類成員初始化順序總結:先靜態后普通再構造, 先父類后子類,同級看書寫順序。
垃圾回收機制:同上面[10、簡述Java的垃圾回收機制]知識點。
JAVA監控工具:
jconsole
: 提供JVM活動的圖形化視圖,包括線程的使用,類的使用和GC活動。
jvisualvm
: 監視JVM的GUI工具,可以用來剖析運行的應用,分析JVM堆轉儲。
Java語言規范並不保證GC一定會執行。強制執行垃圾回收(不符合規范):System.gc()。Runtime.getRuntime().gc(),手動執行System.gc(),通知GC運行。
調用了System.gc(),也只是通知虛擬機要回收垃圾,至於虛擬機什么時候運行回收器就不知道了。
來源:https://www.cnblogs.com/KingIceMou/p/6967129.html
《深入理解Java虛擬機》https://www.cnblogs.com/scorpio-cat/p/jvm.html
55、描述JVM加載class文件的原理機制
類加載機制的流程:包括了加載、連接(驗證、准備、解析)、初始化五個階段。
(1)加載:查找裝載二進制文件,通過一個類的全限定名獲取類的二進制字節流,並將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構;在 Java 堆中生成一個代表這個類的 java.lang.Class 對象,作為對方法區中這些數據的訪問入口。
(2)驗證:為了確保Class文件中的字節流包含的信息符合當前虛擬機的要求,完成以下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
(3)准備:准備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。
(4)解析:解析階段是虛擬機將常量池中的符號引用轉化為直接引用的過程,可選。
(5)初始化:初始化階段是根據程序員通過程序指定的主觀計划去初始化類變量和其他資源,也就是執行類構造器()方法的過程。
56、說說JVM原理?內存泄漏與溢出的區別?何時產生內存泄漏?
JVM原理:
JVM是Java Virtual Machine(Java虛擬機)的縮寫,它是整個java實現跨平台的最核心的部分,所有的Java程序會首先被編譯為.class的類文件,這種類文件可以在虛擬機上執行,也就是說class並不直接與機器的操作系統相對應,而是經過虛擬機間接與操作系統交互,由虛擬機將程序解釋給本地系統執行。JVM是Java平台的基礎,和實際的機器一樣,它也有自己的指令集,並且在運行時操作不同的內存區域。JVM通過抽象操作系統和CPU結構,提供了一種與平台無關的代碼執行方法,即與特殊的實現方法、主機硬件、主機操作系統無關。JVM的主要工作是解釋自己的指令集(即字節碼)到CPU的指令集或對應的系統調用,保護用戶免被惡意程序騷擾。JVM對上層的Java源文件是不關心的,它關注的只是由源文件生成的類文件(.class文件)。
內存泄漏與溢出的區別:
1) 內存泄漏是指分配出去的內存無法回收了。
2) 內存溢出是指程序要求的內存,超出了系統所能分配的范圍,從而發生溢出。比如用byte類型的變量存儲10000這個數據,就屬於內存溢出。
3) 內存溢出是提供的內存不夠;內存泄漏是無法再提供內存資源。
何時產生內存泄漏:
(1)靜態集合類:在使用Set、Vector、HashMap等集合類的時候需要特別注意,有可能會發生內存泄漏。當這些集合被定義成靜態的時候,由於它們的生命周期跟應用程序一樣長,這時候,就有可能會發生內存泄漏。
(2)監聽器:在Java中,我們經常會使用到監聽器,如對某個控件添加單擊監聽器addOnClickListener(),但往往釋放對象的時候會忘記刪除監聽器,這就有可能造成內存泄漏。好的方法就是,在釋放對象的時候,應該記住釋放所有監聽器,這就能避免了因為監聽器而導致的內存泄漏。
(3)各種連接:Java中的連接包括數據庫連接、網絡連接和io連接,如果沒有顯式調用其close()方法,是不會自動關閉的,這些連接就不能被GC回收而導致內存泄漏。一般情況下,在try代碼塊里創建連接,在finally里釋放連接,就能夠避免此類內存泄漏。
(4)外部模塊的引用:調用外部模塊的時候,也應該注意防止內存泄漏。如模塊A調用了外部模塊B的一個方法,如:public void register(Object o)。這個方法有可能就使得A模塊持有傳入對象的引用,這時候需要查看B模塊是否提供了去除引用的方法,如unregister()。這種情況容易忽略,而且發生了內存泄漏的話,比較難察覺,應該在編寫代碼過程中就應該注意此類問題。
(5)單例模式:使用單例模式的時候也有可能導致內存泄漏。因為單例對象初始化后將在JVM的整個生命周期內存在,如果它持有一個外部對象(生命周期比較短)的引用,那么這個外部對象就不能被回收,而導致內存泄漏。如果這個外部對象還持有其它對象的引用,那么內存泄漏會更嚴重,因此需要特別注意此類情況。這種情況就需要考慮下單例模式的設計會不會有問題,應該怎樣保證不會產生內存泄漏問題。
57、Java的類加載器都有哪些,每個類加載器都有加載哪些類,什么是雙親委派模型,是做什么的?
Java的類加載器有三個,對應Java的三種類:
Bootstrap Loader // 負責加載系統類 (指的是內置類,像是String,對應於C#中的System類和C/C++標准庫中的類)
|
- - ExtClassLoader // 負責加載擴展類(就是繼承類和實現類)
|
- - AppClassLoader // 負責加載應用類(程序員自定義的類)。
雙親委派模型過程:某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
使用雙親委派模型的好處在於Java類隨着它的類加載器一起具備了一種帶有優先級的層次關系。
參考:https://blog.csdn.net/xu768840497/article/details/79175335
58、Java 中會存在內存泄漏嗎,請簡單描述。
存在,JAVA中內存泄露的發生場景,通俗地說,就是程序員可能創建了一個對象,以后一直不再使用這個對象,這個對象卻一直被引用,即這個對象無用但是卻無法被垃圾回收器回收的,這就是JAVA中的內存泄露。
來源:https://www.cnblogs.com/Berryxiong/p/6220890.html
多線程
59、啟動一個線程是調用 run() 還是 start() 方法?start() 和 run() 方法有什么區別?
start():它的作用是啟動一個新線程,新線程處於就緒狀態,拿到cpu執行權就會執行相應的run()方法。start()不能被重復調用。
run():run()就和普通的成員方法一樣,可以被重復調用。單獨調用run()的話,會在當前線程中執行run(),而並不會啟動新線程。
60、Thread類的方法sleep()、yield()與Object的方法wait()、notify()比較。
61、用Thread和Runnable哪種方式更好?區別是什么?
顯然是用Runnable更好,實現Runnable接口比繼承Thread類所具有的優勢:
(1)適合多個相同的程序代碼的線程去處理同一個資源。
(2)可以避免java中的單繼承的限制。
(3)增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨立。
(4)線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類。
實現線程幾種方式:(1)繼承java.lang.Thread類;(2)實現java.lang.Runnable接口;(3)實現java.util.concurrent.Callable接口。

public class ThreadDemo { //進程:每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1--n個線程。(進程是資源分配的最小單位) //線程:同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。(線程是cpu調度的最小單位) //多線程的好處:發揮多核CPU的優勢、防止阻塞、便於建模 //單核CPU上所謂的”多線程”那是假的多線程,同一時間處理器只會處理一段邏輯,只不過線程之間切換得比較快,看着像多個線程”同時”運行罷了。多核CPU上的多線程才是真正的多線程,它能讓你的多段邏輯同時工作 //如果單核CPU使用單線程,那么只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那么你的整個程序在數據返回回來之前就停止運行了。多線程可以防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行 //假設有一個大的任務A,單線程編程,那么就要考慮很多,建立整個程序模型比較麻煩。但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程序模型,並通過多線程分別運行這幾個任務,那就簡單很多了 class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; System.out.println(name); } @Override public void run() { for (int i=0;i<3;i++) { System.out.println("--線程"+ name); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Test public void testMyThread() { Thread thread1 = new MyThread("1"); Thread thread2 = new MyThread("2"); Thread thread3 = new MyThread("3"); Thread thread4 = new MyThread("4"); Thread thread5 = new MyThread("5"); Thread thread6 = new MyThread("6"); System.out.println("====thread start===="); System.out.println("====thread1.run()===="); thread1.run(); // 直接調用run則為普通方法,按順序執行 System.out.println("====thread2.run()===="); thread2.run(); System.out.println("====thread3.start()===="); thread3.start(); System.out.println("====thread4.start()===="); thread4.start(); System.out.println("====thread5.run()===="); thread5.run(); System.out.println("====thread6.run()===="); thread6.run(); System.out.println("====thread end===="); } class MyRunnable implements Runnable { private String name; public MyRunnable(String name) { this.name = name; System.out.println(name); } @Override public void run() { for (int i=0;i<3;i++) { System.out.println("--線程"+ name); try { Thread.sleep((int) Math.random() * 10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Test public void testMyRunnable() { Thread thread1 = new Thread(new MyRunnable("1")); Thread thread2 = new Thread(new MyRunnable("2")); Thread thread3 = new Thread(new MyRunnable("3")); Thread thread4 = new Thread(new MyRunnable("4")); Thread thread5 = new Thread(new MyRunnable("5")); Thread thread6 = new Thread(new MyRunnable("6")); System.out.println("====thread start===="); System.out.println("====thread1.run()===="); thread1.run(); // 直接調用run則為普通方法,按順序執行 System.out.println("====thread2.run()===="); thread2.run(); System.out.println("====thread3.start()===="); thread3.start(); System.out.println("====thread4.start()===="); thread4.start(); System.out.println("====thread5.run()===="); thread5.run(); System.out.println("====thread6.run()===="); thread6.run(); System.out.println("====thread end===="); } class Mycallable implements Callable<String> { @Override public String call() throws Exception { String str = ""; str += "A"; System.out.println(str); return str; } } @Test public void testCallable() { Mycallable mycallable = new Mycallable(); // 1.執行 Callable 方式,需要 FutureTask 實現類的支持 FutureTask<String> futureTask = new FutureTask<String>(mycallable); Thread thread = new Thread(futureTask); System.out.println("--start--"); thread.start(); System.out.println("--end--"); try { System.out.println("11" + futureTask.get()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
62、線程安全理解
線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。線程不安全就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據。
還有一種通俗的解釋:如果你的代碼在多線程下執行和在單線程下執行永遠都能獲得一樣的結果,那么你的代碼就是線程安全的。
這個問題有值得一提的地方,就是線程安全也是有幾個級別的:
(1)不可變
像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新創建一個,因此這些不可變對象不需要任何同步手段就可以直接在多線程環境下使用
(2)絕對線程安全
不管運行時環境如何,調用者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標注自己是線程安全的類,實際上絕大多數都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相對線程安全
相對線程安全也就是我們通常意義上所說的線程安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限於此,如果有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制。
(4)線程非安全
這個就沒什么好說的了,ArrayList、LinkedList、HashMap等都是線程非安全的類。
Vector 和 ArrayList 實現了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 關鍵修飾。但對於復合操作,Vector仍然需要進行同步處理。
63、ThreadLocal
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲。變量值的共享可以使用public static變量的形式,所有的線程都使用同一個public static變量,但是如果每一個線程都有自己的變量該如何共享呢,就是通過ThreadLocal,ThreadLocal為變量在每個線程中都創建了一個副本,那么每個線程可以訪問自己內部的副本變量。
最常見的ThreadLocal使用場景為用來解決數據庫連接、Session管理等。
ThreadLocal方法:
get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本;
set()用來設置當前線程中變量的副本;
remove()用來移除當前線程中變量的副本;
initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法。
代碼示例:

public class ThreadLocalDemo { private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static void setValue(ThreadLocalValue value) { threadLocal.set(value); } public static ThreadLocalValue getValue() { return (ThreadLocalValue) threadLocal.get(); } public static void remove() { threadLocal.remove(); } @Test public void test() { ThreadLocalDemo.setValue(ThreadLocalValue.VALUE_1); System.out.println(ThreadLocalDemo.getValue()); ThreadLocalDemo.remove(); System.out.println(ThreadLocalDemo.getValue()); } } enum ThreadLocalValue { VALUE_1, VALUE_2; }
64、synchronized
synchronized:當一個線程獲取了對應的鎖,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而獲取鎖的線程釋放鎖會有三種情況:
(1)獲取鎖的線程執行完該代碼塊,然后線程釋放對鎖的占有;
(2)線程執行發生異常,此時JVM會讓線程自動釋放鎖;
(3)調用wait方法,在等待的時候立即釋放鎖,方便其他的線程使用鎖。
synchronized作用:
(1)可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操作);
(2)可保證一個線程的變化(主要是共享數據的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能)。
synchronized3種應用方式:
(1)修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
(2)修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖。
(3)修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
修飾實例方法代碼錯誤示例:synchronized修飾實例方法,獲得當前實例的鎖;啟用兩個線程,兩個線程分別擁有兩個不同的對象實例鎖,故不會產生互斥,得不到期望的結果。

public class SynchronizedDemo implements Runnable { // 共享資源 static int no = 0; public synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾實例方法,獲得當前實例的鎖;兩個線程兩個實例,得到的數量可能少於2000 } }
上述代碼,increase()改為靜態方法加synchronized,則可以得到期望的結果,因為靜態方法同步鎖,鎖定的是當前類對象。

/** * synchronized同步不僅可以阻止一個線程看到對象處於不一致的狀態之中, * 它還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。 * */ public class SynchronizedDemo implements Runnable { // 共享資源 static int no = 0; public static synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾靜態方法,獲得當前類對象的鎖;得到結果都為2000 } }
如果使用實例方法,想產生互斥,則需要兩個線程擁有的是同一個對象實例,如下:

public class SynchronizedDemo implements Runnable { static SynchronizedDemo instance = new SynchronizedDemo(); // 共享資源 static Integer no = 0; public synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo = new SynchronizedDemo(); Thread t1 = new Thread(demo); Thread t2 = new Thread(demo); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾普通方法,兩個線程擁有同一對象實例;得到結果都為2000 } }
修飾代碼塊代碼錯誤示范:synchronized修飾代碼塊,鎖為this-當前實例對象鎖;鎖定的是兩個不同的實例,故得到結果可能少於2000

public class SynchronizedDemo implements Runnable { // 共享資源 static int no = 0; public void increase() { no++; } @Override public void run() { // this,當前實例對象鎖 synchronized(this){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾代碼塊,鎖為this-當前實例對象鎖;得到結果可能少於2000 } }
如果改成鎖定SynchronizedDemo.class,則可以得到期望的結果

public class SynchronizedDemo implements Runnable { // 共享資源 static int no = 0; public void increase() { no++; } @Override public void run() { synchronized(SynchronizedDemo.class){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾代碼塊,獲得class對象鎖;得到結果都為2000 } }
或者改成鎖定同一對象實例,也可以得到正常結果

public class SynchronizedDemo implements Runnable { static SynchronizedDemo instance = new SynchronizedDemo(); // 共享資源 static Integer no = 0; public void increase() { no++; } @Override public void run() { synchronized(instance){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // join含義:當前線程等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修飾代碼塊,獲得是同一個對象實例;得到結果都為2000 } }
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
synchronized的可重入性:synchronized是基於原子性的內部鎖機制,是可重入的。
鎖重入:一個線程得到一個對象鎖后再次請求該對象鎖。也就是,當一個線程得到一個對象鎖后,再次請求此對象鎖時是可以再次得到該對象的鎖的。

public class Student { public static void main(String[] args) { Student student = new Student(); student.doA(); } public synchronized void doA() { System.out.println("do a"); doB(); } public synchronized void doB() { System.out.println("do b"); } }
65、Lock
Lock是位於java.util.concurrent.locks.Lock包下的一個接口。主要提供方法有:

public interface Lock { /** * 獲取鎖 */ void lock(); /** * 獲取鎖 * @throws InterruptedException */ void lockInterruptibly() throws InterruptedException; /** * 獲取鎖 * @return */ boolean tryLock(); /** * 獲取鎖 * 在時間期限之內如果還拿不到鎖,就返回false;如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true * @param time * @param unit * @return * @throws InterruptedException */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * 釋放鎖 */ void unlock(); /** * * @return */ Condition newCondition(); }
獲取鎖:lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()
釋放鎖:unLock()
使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。

package xcj.homepage.concurrent; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { public void lockTest() { Lock lock = new ReentrantLock(); lock.lock(); // 獲取鎖 try { System.out.println("處理任務"); } finally { lock.unlock(); // 釋放鎖 } } public void tryLockTest() { Lock lock = new ReentrantLock(); if (lock.tryLock()) { // 獲取到鎖 try { System.out.println("處理任務"); } finally { lock.unlock(); // 釋放鎖 } } else { System.out.println("未獲取到鎖,做其他事情"); } } public void lockInterruptiblyTest() throws InterruptedException { Lock lock = new ReentrantLock(); lock.lockInterruptibly(); // 獲取鎖 try { System.out.println("處理任務"); } finally { lock.unlock(); // 釋放鎖 } } }
當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的,調用thread.interrupt()即可中斷阻塞過程中的線程。
66、ReentrantLock
ReentrantLock實現了Lock接口,也是可重入鎖。
使用lock()示例:

package xcj.homepage.concurrent; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock(); // 注意這個地方 public static void main(String[] args) { final ReentrantLockDemo test = new ReentrantLockDemo(); new Thread() { public void run() { test.insert(Thread.currentThread()); }; }.start(); new Thread() { public void run() { test.insert(Thread.currentThread()); }; }.start(); } public void insert(Thread thread) { lock.lock(); try { System.out.println(thread.getName() + "得到了鎖"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { // TODO: handle exception } finally { System.out.println(thread.getName() + "釋放了鎖"); lock.unlock(); } } }
執行結果:

Thread-0得到了鎖 Thread-0釋放了鎖 Thread-1得到了鎖 Thread-1釋放了鎖
使用lockInterruptibly()響應中斷,通過調用thread.interrupt()示例:

package xcj.homepage.concurrent; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private Lock lock = new ReentrantLock(); public static void main(String[] args) { ReentrantLockDemo demo = new ReentrantLockDemo(); MyThread thread1 = new MyThread(demo); MyThread thread2 = new MyThread(demo); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); // 中斷 } public void insert(Thread thread) throws InterruptedException { lock.lockInterruptibly(); // 注意,如果需要正確中斷等待鎖的線程,必須將獲取鎖放在外面,然后將InterruptedException拋出 try { System.out.println(thread.getName() + "得到了鎖"); long startTime = System.currentTimeMillis(); for (;;) { if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) break; // 插入數據 } } finally { System.out.println(Thread.currentThread().getName() + "執行finally"); lock.unlock(); System.out.println(thread.getName() + "釋放了鎖"); } } } class MyThread extends Thread { private ReentrantLockDemo demo = null; public MyThread(ReentrantLockDemo demo) { this.demo = demo; } @Override public void run() { try { demo.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中斷"); } } }
執行結果:

Thread-0得到了鎖
Thread-1被中斷
67、Lock和synchronized的不同點
(1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現;
(2)synchronized在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
(3)Lock可以讓等待鎖的線程響應中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
(4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
(5)Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
參考鏈接:
synchronized:https://blog.csdn.net/zjy15203167987/article/details/82531772
synchronized-鎖重入:https://blog.csdn.net/zjy15203167987/article/details/80558515
面試synchronized: https://www.cnblogs.com/noKing/p/9190673.html
Lock:https://www.cnblogs.com/lucky_dai/p/5498295.html
volatile:https://www.cnblogs.com/kubidemanong/p/9505944.html
ThreadLocal:https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/zjy15203167987/article/details/80480947
queue:https://blog.csdn.net/zjy15203167987/article/details/80400658
線程池之ThreadPoolExecutor使用:https://www.jianshu.com/p/f030aa5d7a28