1、談談對面向對象思想的理解
首先,談談“面向過程”vs“面向對象”
我覺得這兩者是思考角度的差異,面向過程更多是以“執行者”的角度來思考問題,而面向對象更多是以“組織者”的角度來思考問題,舉個例子,比如我要產生一個0-10之間的隨機數,如果以“面向過程”的思維,那我更多是關注如何去設計一個算法,然後保證比較均衡產生0-10的隨機數,而面向對象的思維會更多關注,我找誰來幫我們做這件事,比如Random類,調用其中提供的方法即可。
所以,面向對象的思維更多的是考慮如何去選擇合適的工具,然後組織到一起幹一件事。
好比一個導演,要拍一場電影,那麼首先要有男豬腳和女豬腳,然後還有其他等等,最後把這些資源組織起來,拍成一場電影。
再說回我們的程序世界,這個組織者的思維無處不在,比如,我們要開發項目,以三層架構的模式來開發,那麼這個時候,我們不需要重複造輪子,只需要選擇市面上主流的框架即可,比如SpringMVC,Spring,MyBatis,這些都是各層的主流框架。
2、JDK、JRE和JVM有什麼區別?
JDK:Java Development Kit,Java開發工具包,提供了Java的開發環境和運行環境。
包含了編譯Java源文件的編譯器Javac,還有調試和分析的工具。
JRE:Java Runtime Environment,Java運行環境,包含Java虛擬機及一些基礎類庫
JVM:Java Virtual Machine,Java虛擬機,提供執行字節碼文件的能力
所以,如果只是運行Java程序,只需要安裝JRE即可。
另外注意,JVM是實現Java跨平臺的核心,但JVM本身並不是跨平臺的,不同的平臺需要安裝不同的JVM
3、Java的基本數據類型
boolean,byte,char,short,int,long,float,double
注意:String是引用類型
4、==和equals的區別
== 比較的是值
比較基本的數據類型,比較的是數值
比較引用類型:比較引用指向的值(地址)
equals
默認比較也是地址,因爲這個方法的最初定義在Object上,默認的實現就是比較地址
自定義的類,如果需要比較的是內容,那麼就要學String,重寫equals方法
代碼案例:測試以下的每道題,你是否能夠正確得到答案?
String s1 = new String("zs");
String s2 = new String("zs");
System.out.println(s1 == s2);//false
String s3 = "zs";
String s4 = "zs";
System.out.println(s3 == s4);//true
System.out.println(s3 == s1);//false
String s5 = "zszs";
String s6 = s3+s4;
System.out.println(s5 == s6);//false
final String s7 = "zs";
final String s8 = "zs";
String s9 = s7+s8;
System.out.println(s5 == s9);//true
final String s10 = s3+s4;
System.out.println(s5 == s10);//false
5、final的作用
final修飾類,表示類不可變,不可繼承。比如,String,不可變性
final修飾方法,表示該方法不可重寫。比如模板方法,可以固定我們的算法
final修飾變量,這個變量就是常量
注意:
修飾的是基本數據類型,這個值本身不能修改
修飾的是引用類型,引用的指向不能修改
比如下面的代碼是可以的
final Student student = new Student(1,"Andy");
student.setAge(18);//注意,這個是可以的!
6、String s = "java"與String s = new String("java")
String s = "java";
String s = new String("java");
這兩者的內存分配方式是不一樣的。
第一種方式,JVM會將其分配到常量池,而第二種方式是分配到堆內存
7、String,StringBuffer,StringBuilder區別
String 跟其他兩個類的區別是
String是final類型,每次聲明的都是不可變的對象,
所以每次操作都會產生新的String對象,然後將指針指向新的String對象。
StringBuffer,StringBuilder都是在原有對象上進行操作
所以,如果需要經常改變字符串內容,則建議採用這兩者。
StringBuffer vs StringBuilder
前者是線程安全的,後者是線程不安全的。
線程不安全性能更高,所以在開發中,優先採用StringBuilder.
StringBuilder > StringBuffer > String
8、接口和抽象類的區別
這個問題,要分JDK版本來區分回答:
-
JDK1.8之前:
-
-
語法:
-
- 抽象類:方法可以有抽象的,也可以有非抽象, 有構造器
- 接口:方法都是抽象,屬性都是常量,默認有public static final修飾
-
設計:
-
- 抽象類:同一類事物的抽取,比如針對Dao層操作的封裝,如,BaseDao,BaseServiceImpl
- 接口:通常更像是一種標準的制定,定製系統之間對接的標準
- 例子:
- 1,單體項目,分層開發,interface作爲各層之間的紐帶,在controller中注入IUserService,在Service注入IUserDao
- 2,分佈式項目,面向服務的開發,抽取服務service,這個時候,就會產生服務的提供者和服務的消費者兩個角色
- 這兩個角色之間的紐帶,依然是接口
-
-
JDK1.8之後:
-
- 接口裏面可以有實現的方法,注意要在方法的聲明上加上default或者static
最後區分幾個概念:
-
多繼承,多重繼承,多實現
-
- 多重繼承:A->B->C(爺孫三代的關係)
- 多實現:Person implements IRunable,IEatable(符合多項國際化標準)
- 多繼承:接口可以多繼承,類只支持單繼承
9、算法題-求N的階乘(手寫)
這道算法題一般考查的遞歸的編程技能,那麼我們回顧下遞歸程序的特點:
1,什麼是遞歸?
遞歸,就是方法內部調用方法自身
遞歸的注意事項:
找到規律,編寫遞歸公式
找到出口(邊界值),讓遞歸有結束邊界
注意:如果遞歸太多層,或者沒有正確結束遞歸,則會出現“棧內存溢出Error”!
問題:爲什麼會出現棧內存溢出,而不是堆內存溢出?
2,這道題該怎麼寫?
規律:N !=(n-1)!*n;
出口:n == 1或n == 0 return 1;
public static int getResult(int n){
if(n<0){
throw new ValidateException("非法參數");
}
if(n==1 || n==0){
return 1;
}
return getResult(n-1)*n;
}
10、算法題-求解斐波那切數列的第N個數是幾?(手寫)
如何實現遞歸求斐波那切數列第N個數字的值(傳說中的不死神兔就是這個問題)
數字的規律:1,1,2,3,5,8,13,21....
所以,我們可以分析編寫如下:
/**
* 規律:每個數等於前兩個數之和
* 出口:第1項和第2項都等於1
*/
public static int getFeiBo(int n) {
if (n < 0){
return -1;
}
if (n == 1 || n == 2) {
return 1;
} else {
return getFeiBo(n - 1) + getFeiBo(n - 2);
}
}
11、什麼是向上轉型?什麼是向下轉型?
這道題目一般出現在(筆試-選擇題)
舉例說明即可:
向上轉型:Person person = new Student(); 安全的
向下轉型:Teacher teacher = (Teacher)person; 不安全的
12、Int和Integer的區別(重點)
1,來,先來一道考題,你看做對了嗎?
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);
System.out.println(i1 == i2);//false
Integer i3 = 126;
Integer i4 = 126;
int i5 = 126;
System.out.println(i3 == i4);//true
System.out.println(i3 == i5);//true
Integer i6 = 128;
Integer i7 = 128;
int i8 = 128;
System.out.println(i6 == i7);//false
System.out.println(i6 == i8);//true
以上這些輸出的答案是什麼?true or false? why?
你可以自己先思考,再看後面的答案分析。
答案揭曉
分情況來比較
- 都定義爲Integer的比較:
new:一旦new,就是開闢一塊新內存,結果肯定是false
不new:
看範圍
Integer做了緩存,-128至127,當你取值在這個範圍的時候,會採用緩存的對象,所以會相等
當不在這個範圍,內部創建新的對象,此時不相等
- Integer和int的比較:
實際比較的是數值,Integer會做拆箱的動作,來跟基本數據類型做比較
此時跟是否在緩存範圍內或是否new都沒關係
源碼分析:
當我們寫Integer i = 126,實際上做了自動裝箱:Integer i = Integer.valueOf(126);
分析這段源碼
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//IntegerCache是Integer的內部類
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
13、方法的重寫和重載的區別
一般出現在(筆試題-選擇題),下面我們說下重點
- 重載:發生在一個類裏面,方法名相同,參數列表不同(混淆點:跟返回類型沒關係)
以下不構成重載
public double add(int a,int b)
public int add(int a,int b)
- 重寫:發生在父類子類之間的,方法名相同,參數列表相同
14、算法題-冒泡排序
冒泡排序原理:
-
比較相鄰的兩個元素,如果前者大於後者則交換位置;
-
這樣對數組第0個數據到N-1個數據進行遍歷比較一次後,最大的數據會移動到最後一位。
-
N=N-1,如果N=0則排序完成;
public void bubbleSort(int[] array){
if(array.length <= 1){
return;
}
for(int i=0;i<array.length;i++){
for(int j=0;j<array.length-i-1;j++){
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
關於上面的優化思考
其實,當某次冒泡操作沒有數據交換時,說明已經達到了完全有序,
不用再繼續後續的冒泡操作。
public void bubbleSort(int[] array){
if(array.length <= 1){
return;
}
//重複n次冒泡
for(int i=0;i<array.length;i++){
//是否可以提交退出冒泡的標記
boolean flag = false;
//相鄰之間兩兩比較,並且每次減少一位參與比較
for(int j=0;j<array.length-i-1;j++){
if(array[j] > array[j+1]){
//需要交換
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
//
flag = true;//有數據交換,不能提前退出
}
}
if(!flag){
//沒有數據交換,提前退出冒泡比較
break;
}
}
}
15、List和Set的區別
-
List(有序,可重複)
-
Set(無序,不可重複)
16、談談ArrayList和LinkedList的區別
1,底層數據結構的差異
ArrayList,數組,連續一塊內存空間
LinkedList,雙向鏈表,不是連續的內存空間
2,一個常規的結論
雖然不嚴謹,但也可以應付很多面試了
ArrayList,查找快,因爲是連續的內存空間,方便尋址,但刪除,插入慢,因爲需要發生數據遷移
LinkedList,查找慢,因爲需要通過指針一個個尋找,但刪除,插入塊,因爲只要改變前後節點的指針指向即可。
3,ArrayList細節分析
1,增加
-
-
添加到末尾,正常不需要做特別的處理,除非現有的數組空間不夠了,需要擴容
-
-
數組初始化容量多大?10,當你知道需要存儲多少數據時,建議在創建的時候,直接設置初始化大小
-
怎麼擴容?
-
- 當發現容量不夠之後,就進行擴容
- 按原先數組容量的1.5倍進行擴容,位運算,下面是關鍵的源碼
-
-
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
-
-
- 再將原先數組的元素複製到新數組,Arrays
-
-
elementData = Arrays.copyOf(elementData, newCapacity)
-
- 添加到其他位置,這個時候需要做整體的搬遷
-
2,刪除
-
- 刪除末尾,並不需要遷移
- 刪除其他的位置,這個時候也需要搬遷
-
3,修改
-
- 修改之前,必須先定位
- 定位-查找-ArrayList(數組是一段連續的內存空間,定位會特別快)
-
4,查找
-
- 如上所述
4,LinkedList細節分析
1,提供了的兩個引用(first,last)
2,增加
添加到末尾,創建一個新的節點,將之前的last節點設置爲新節點的pre,新節點設置爲last
我們看下源碼:
void linkLast(E e) {
//獲取到最後一個節點
final Node<E> l = last;
//構建一個新節點,將當前的last作爲這個新節點的pre
final Node<E> newNode = new Node<>(l, e, null);
//把last指向新節點
last = newNode;
//如果原先沒有最後一個節點
if (l == null)
//將first指向新節點
first = newNode;
else
//否則,將原先的last的next指向新節點
l.next = newNode;
size++;
modCount++;
}
Node節點的定義:內部類
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加到其他位置,這個時候,就需要調整前後節點的引用指向
3,如何去定義一個雙向鏈表的節點,如上述的源碼所示
4,修改
修改最後一個節點或者第一個節點,那麼就很快(first,last)
修改其他位置,如果是按座標來定位節點,則會按照二分查找法,源碼如下:
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
5,一個思考題,假如我們可以確定要存儲1000個元素,那麼採用ArrayList和LinkedList,
哪個更耗內存,爲什麼?
6,LinkedList,要實現在A和B之間插入C,該如何實現,編寫僞代碼即可
17、如何在雙向鏈表A和B之間插入C?
可以使用僞代碼的方式來實現,你的答案是什麼?
假設我們定位到了A節點,那麼A.next就是B節點,這個是前提。
你的答案是?可以思考過後,再看答案
C.pre = A;
C.next = A.next;
A.next.pre = C;
A.next = C;
18、談談HashSet的存儲原理
HashSet的存儲原理或者工作原理,主要是從如何保證唯一性來說起。
這裏面主要有3個問題,需要回答?
第一,爲什麼要採用Hash算法?有什麼優勢,解決了什麼問題?
第二,所謂哈希表是一張什麼表?
第三,HashSet如何保證保存對象的唯一性?會經歷一個什麼樣的運算過程?
大家可以先思考,晚些再補充答案!
首先,我們要明確一點,HashSet底層採用的是HashMap來實現存儲,其值作爲HashMap的key
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
具體關於hashmap的細節再說
第一,爲什麼要採用Hash算法?有什麼優勢,解決了什麼問題?
解決的問題是唯一性
存儲數據,底層採用的是數組
當我們往數組放數據的時候,你如何判斷是否唯一?
可以採用遍歷的方式,逐個比較,但是這種效率低,尤其是數據很多的情況下
所以,爲了解決這個效率低的問題,我們採用新的方式
採用hash算法,通過計算存儲對象的hashcode,然後再跟數組長度-1做位運算,得到我們要存儲在數組的哪個下標下,如果此時計算的位置沒有其他元素,直接存儲,不用比較。
此處,我們只會用到hashCode
但是隨着元素的不斷添加,就可能出現“哈希衝突”,不同的對象計算出來的hash值是相同的,這個時候,我們就需要比較,才需要用到equals方法
如果equals相同,則不插入,不相等,則形成鏈表
第二,所謂哈希表是一張什麼表?
本質是一個數組,而且數組的元素是鏈表
JDK1.7的版本實現
JDK1.8做了優化
隨着元素不斷添加,鏈表可能會越來越長,會優化紅黑樹
19、談談LinkedHashMap和HashMap的區別(重點)
此處,我們好好談談HashMap
主要關注幾個點:
1,初始化大小是16,如果事先知道數據量的大小,建議修改默認初始化大小。 減少擴容次數,提高性能 ,這是我一直會強調的點
2,最大的裝載因子默認是0.75,當HashMap中元素個數達到容量的0.75時,就會擴容。 容量是原先的兩倍
3,HashMap底層採用鏈表法來解決衝突。 但是存在一個問題,就是鏈表也可能會過長,影響性能
於是JDK1.8,對HashMap做了進一步的優化,引入了紅黑樹。
當鏈表長度超過8,且數組容量大於64時,鏈表就會轉換爲紅黑樹
當紅黑樹的節點數量小於6時,會將紅黑樹轉換爲鏈表。
因爲在數據量較小的情況下,紅黑樹要維護自身平衡,比鏈表性能沒有優勢。
這3點非常重要!
其次,LinkedHashMap就是鏈表+散列表的結構,其底層採用了Linked雙向鏈表來保存節點的訪問順序,所以保證了有序性。
20、談談ConcurrentHashMap,HashMap,Hashtable的區別
1,首先,來看看其他幾個相關的類
Hashtable是線程安全的,但效率低
HashMap是線程不安全的,但效率高
Collections.synchronizedMap(),工具類提供了同步包裝器的方法,來返回具有線程安全的集合對象
性能依然有問題
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
//在這個類的內部方法實現上,也只是單純加上了鎖
public V put(K key, V value) {
synchronized (mutex) {
return m.put(key, value);
}
}
爲解決這樣的矛盾問題,所以JDK提供了併發包,來平衡這樣的問題(java.util.concurrent)
2,ConcurrentHashMap(重點)
- 兼顧了線程安全和效率的問題
分析:HashTable鎖了整段數據(用戶操作是不同的數據段,依然需要等待)
解決方案:把數據分段,執行分段鎖(分離鎖),核心把鎖的範圍變小,這樣出現併發衝突的概率就變小
在保存的時候,計算所存儲的數據是屬於哪一段,只鎖當前這一段
- 注意:分段鎖(分離鎖)是JDK1.8之前的一種的方案,JDK1.8之後做了優化。
JDK1.7跟JDK1.8在ConcurrentHashMap的實現上存在以下區別:
1,數據結構
JDK1.7採用鏈表的方式,而JDK1.8則採用鏈表+紅黑樹的方式
2,發生hash碰撞之後
JDK1.7發生碰撞之後,會採用鏈表的方式來解決
JDK1.8發生碰撞之後,默認採用鏈表,但當鏈表的長度超過8,且數組容量超過64時,會轉換爲紅黑樹存儲
3,保證併發安全
JDK1.7採用分段鎖的方式,而JDK1.8採用CAS和synchronized的組合模式
4,查詢複雜度
JDK1.7採用鏈表的方式,時間複雜度爲O(n),而JDK1.8在採用紅黑樹的方式時,時間複雜度爲O(log(n))
題外話:
不過紅黑樹其實是一種兜底方案,因爲當鏈表數量達到8個的時候,其發生的概率是千萬分之幾,所以作者考慮到這種極端情況下,需要用紅黑樹的方式來優化
21、ArrayList VS Vector
說句實話,對這種古老的Vector,之所以你在筆試題會遇到,我感覺是面試官偷懶了。
來吧,我們看看
ArrayList:線程不安全,效率高,常用
Vector:線程安全的,效率低
我們看Vector的源碼:
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
modCount++;
ensureCapacityHepler(minCapacity);
}
}
22、談談IO流的分類及選擇
1,分類
按方向分:輸入流,輸出流
(注意,是站在程序的角度來看方向),輸入流用於讀文件,輸出流用於寫文件
按讀取的單位分:字節流,字符流
按處理的方式分:節點流,處理流
比如,FileInputStream和BufferedInputStream(後者帶有緩存區功能-byte[])
IO流的4大基類:InputStream,OutputStream,Reader,Writer
2,選擇
字節流可以讀取任何文件
讀取文本文件的時候:選擇字符流(假如有解析文件的內容的需求,比如逐行處理,則採用字符流,比如txt文件)
讀取二進制文件的時候,選擇字節流(視頻,音頻,doc,ppt)
23、serialVersionUID的作用是什麼
當執行序列化時,我們寫對象到磁盤中,會根據當前這個類的結構生成一個版本號ID
當反序列化時,程序會比較磁盤中的序列化版本號ID跟當前的類結構生成的版本號ID是否一致,如果一致則反序列化成功,否則,反序列化失敗
加上版本號,有助於當我們的類結構發生了變化,依然可以之前已經序列化的對象反序列化成功
24、請描述下Java的異常體系
Error是虛擬機內部錯誤
棧內存溢出錯誤:StackOverflowError(遞歸,遞歸層次太多或遞歸沒有結束)
堆內存溢出錯誤:OutOfMemoryError(堆創建了很多對象)
Exception是我們編寫的程序錯誤
RuntimeException:也稱爲LogicException
爲什麼編譯器不會要求你去try catch處理?
本質是邏輯錯誤,比如空指針異常,這種問題是編程邏輯不嚴謹造成的
應該通過完善我們的代碼編程邏輯,來解決問題
非RuntimeException:
編譯器會要求我們try catch或者throws處理
本質是客觀因素造成的問題,比如FileNotFoundException
寫了一個程序,自動閱卷,需要讀取答案的路徑(用戶錄入),用戶可能錄入是一個錯誤的路徑,所以我們要提前預案,寫好發生異常之後的處理方式,這也是java程序健壯性的一種體現
25、羅列常見的5個運行時異常
此類異常,編譯時沒有提示做異常處理,因此通常此類異常的正確理解應該是“邏輯錯誤”
算數異常,
空指針,
類型轉換異常,
數組越界,
NumberFormateException(數字格式異常,轉換失敗,比如“a12”就會轉換失敗)
26、羅列常見的5個非運行時異常
IOException,
SQLException,
FileNotFoundException,
NoSuchFileException,
NoSuchMethodException
27、throw跟throws的區別
throw,作用於方法內,用於主動拋出異常
throws, 作用於方法聲明上,聲明該方法有可能會拋些某些異常針對項目中,異常的處理方式,我們一般採用層層往上拋,最終通過異常處理機制統一處理(展示異常頁面,或返回統一的json信息),自定義 異常一般繼承RunntimeException,我們去看看Hibernate等框架,他們的異常體系都是最終繼承自RunntimeException
28、一道關於try catch finally返回值的問題
以下這道題,在實際開發中,並不會這麼寫。
這個是面試官爲了考察大家對finally的認識,而苦思冥想出來,我猜的。
結果是多少?你可以先想下。。。。。。。。
答案是:2,因爲finally是無論如何都會執行,除非JVM關閉了
29、創建線程的方式
我們常說的方式有以下三種:
繼承Thread
實現Runable接口
實現Callable接口(可以獲取線程執行之後的返回值)
但實際後兩種,更準確的理解是創建了一個可執行的任務,要採用多線程的方式執行,
還需要通過創建Thread對象來執行,比如 new Thread(new Runnable(){}).start();這樣的方式來執行。
在實際開發中,我們通常採用線程池的方式來完成Thread的創建,更好管理線程資源。
案例:如何正確啓動線程
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running.....");
}
}
public static void main(String[] args){
MyThread thread = new MyThread();
//正確啓動線程的方式
//thread.run();//調用方法並非開啓新線程
thread.start();
}
案例:實現runnable只是創建了一個可執行任務,並不是一個線程
class MyTask implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":running....");
}
}
public static void main(String[] args){
MyTask task = new MyTask();
//task.start(); //並不能直接以線程的方式來啓動
//它表達的是一個任務,需要啓動一個線程來執行
new Thread(task).start();
}
案例三:runnable vs callable
class MyTask2 implements Callable<Boolean>{
@Override
public Boolean call() throws Exception {
return null;
}
}
明確一點:
本質上來說創建線程的方式就是繼承Thread,就是線程池,內部也是創建好線程對象來執行任務。
30、一個普通main方法的執行,是單線程模式還是多線程模式?爲什麼?
因爲java有個重要的特性,叫垃圾自動回收機制,所以答案是多線程,這裏面有兩部分,主線程(用戶線程),垃圾回收線程GC(守護線程)同時存在。
31、請描述線程的生命週期
一圖勝千言!
靈魂畫家出品。
上述的圖有些簡略,下面詳細說明下,線程共有6種狀態:
new,runnable,blocked,waiting,timed waiting,terminated
1,當進入synchronized同步代碼塊或同步方法時,且沒有獲取到鎖,線程就進入了blocked狀態,直到鎖被釋放,重新進入runnable狀態
2,當線程調用wait()或者join時,線程都會進入到waiting狀態,當調用notify或notifyAll時,或者join的線程執行結束後,會進入runnable狀態
3,當線程調用sleep(time),或者wait(time)時,進入timed waiting狀態,
當休眠時間結束後,或者調用notify或notifyAll時會重新runnable狀態。
4,程序執行結束,線程進入terminated狀態
案例篇
/**
* @author huangguizhao
* 測試線程的狀態
*/
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Task());
System.out.println(thread.getState());//NEW
thread.start();
System.out.println(thread.getState());//RUNNABLE
//保險起見,讓當前主線程休眠下
Thread.sleep(10);
System.out.println(thread.getState());//terminated
}
}
class Task implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class ThreadStateTest {
public static void main(String[] args) throws InterruptedException {
BlockTask task = new BlockTask();
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
//從嚴謹的角度來說,t1線程不一定會先執行,此處是假設t1先執行
System.out.println(t1.getState());//RUNNABLE
System.out.println(t2.getState());//BLOCKED
Thread.sleep(10);
System.out.println(t1.getState());//TIMED_WAITING
Thread.sleep(1000);
System.out.println(t1.getState());//WAITING
}
}
class BlockTask implements Runnable{
@Override
public void run() {
synchronized (this){
//另一個線程會進入block狀態
try {
//目的是讓線程進入waiting time狀態
Thread.sleep(1000);
//進入waiting狀態
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
blocked,waiting,timed waiting 我們都稱爲阻塞狀態
上述的就緒狀態和運行狀態,都表現爲runnable狀態
32、談談Sleep和wait的區別
1,所屬的類不同:
sleep方法是定義在Thread上
wait方法是定義在Object上
2,對於鎖資源的處理方式不同
sleep不會釋放鎖
wait會釋放鎖
3,使用範圍:
sleep可以使用在任何代碼塊
wait必須在同步方法或同步代碼塊執行
4,與wait配套使用的方法
- void notify()
Wakes up a single thread that is waiting on this object’s monitor.
譯:喚醒在此對象監視器上等待的單個線程
- void notifyAll()
Wakes up all threads that are waiting on this object’s monitor.
譯:喚醒在此對象監視器上等待的所有線程
- void wait( )
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object.
譯:導致當前的線程等待,直到其他線程調用此對象的notify( ) 方法或 notifyAll( ) 方法
生命週期
1,當線程調用wait()或者join時,線程都會進入到waiting狀態,當調用notify或notifyAll時,或者join的線程執行結束後,會進入runnable狀態
2,當線程調用sleep(time),或者wait(time)時,進入timed waiting狀態,
最後,留下一個思考題,爲什麼wait要定義在Object中,而不定義在Thread中?
來解釋下,我們回想下,在同步代碼塊中,我們說需要一個對象鎖來實現多線程的互斥效果,也就是說,Java的鎖是對象級別的,而不是線程級別的。
爲什麼wait必須寫在同步代碼塊中?
原因是避免CPU切換到其他線程,而其他線程又提前執行了notify方法,那這樣就達不到我們的預期(先wait再由其他線程來喚醒),所以需要一個同步鎖來保護
33、JDK提供的線程池有哪些?實際開發我們該怎麼使用?
1,JDK通過接口ExecutorService來表示線程池,通過工具類Executors來創建多種線程池對象
ExecutorService service1 = Executors.newSingleThreadExecutor();
ExecutorService service2 = Executors.newFixedThreadPool(2);
ExecutorService service3 = Executors.newCachedThreadPool();
ExecutorService service4 = Executors.newScheduledThreadPool(2);
2,各種線程池的特點如下:
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
3,在實際開發中,我們是怎麼使用的?(重點)
實際開發中,線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程
使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。
如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題
實際開發中,線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式
FixedThreadPool 和 SingleThreadPool,允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
CachedThreadPool 和 ScheduledThreadPool,允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM
所以,綜上所述,我們都會採用底層的方式來創建線程池,大家自己查閱各種線程池的源代碼就可以看到他們都是採用了同一個類來創建。
自己查看,印象更深刻。
34、談談你對線程安全的理解?
如果這個是面試官直接問你的問題,你會怎麼回答?
一個專業的描述是,當多個線程訪問一個對象時,如果不用進行額外的同步控制或其他的協調操作,調用這個對象的行爲都可以獲得正確的結果,我們就說這個對象是線程安全的
那麼我們如何做到線程安全?
實現線程安全的方式有多種,其中在源碼中常見的方式是,採用synchronized關鍵字給代碼塊或方法加鎖,比如StringBuffer
查看StringBuffer的源碼,你會看到是這樣的:
@Override
public synchronized int length() {
return count;
}
@Override
public synchronized int capacity() {
return value.length;
}
那麼,我們開發中,如果需要拼接字符串,使用StringBuilder還是StringBuffer?
場景一:
如果是多個線程訪問同一個資源,那麼就需要上鎖,才能保證數據的安全性。
這個時候如果使用的是非線程安全的對象,比如StringBuilder,那麼就需要藉助外力,給他加synchronized關鍵字。或者直接使用線程安全的對象StringBuffer
場景二:
如果每個線程訪問的是各自的資源,那麼就不需要考慮線程安全的問題,所以這個時候,我們可以放心使用非線程安全的對象,比如StringBuilder
比如在方法中,創建對象,來實現字符串的拼接。
看場景,如果我們是在方法中使用,那麼建議在方法中創建StringBuilder,這時候相當是每個線程獨立佔有一個StringBuilder對象,不存在多線程共享一個資源的情況,所以我們可以安心使用,雖然StringBuilder本身不是線程安全的。
什麼時候需要考慮線程安全?
1,多個線程訪問同一個資源
2,資源是有狀態的,比如我們上述講的字符串拼接,這個時候數據是會有變化的
35、談談你對ThreadLocal的理解
ThreadLocal解決了什麼問題?內部源碼是怎麼樣的?
作用:
爲每個線程創建一個副本
實現在線程的上下文傳遞同一個對象,比如connection
第一個問題:證明ThreadLocal爲每個線程創建一個變量副本
public class ThreadLocalTest {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
//開啓多個線程來執行任務
Task task = new Task();
new Thread(task).start();
Thread.sleep(10);
new Thread(task).start();
}
static class Task implements Runnable{
@Override
public void run() {
Long result = threadLocal.get();
if(result == null){
threadLocal.set(System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"->"+threadLocal.get());
}
}
}
}
輸出的結果是不同的
問題二:爲什麼可以給每個線程保存一個不同的副本
那我們來分析源碼
Long result = threadLocal.get();
public T get() {
//1.獲取當前線程
Thread t = Thread.currentThread();
//2,獲取到當前線程對應的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3.以threadLocal爲key,獲取到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.獲取對應entry的value,就是我們存放到裏面的變量的副本
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
我們需要結合set方法的源碼分析,纔可以更好理解
threadLocal.set(System.currentTimeMillis());
public void set(T value) {
//1.獲取到當前線程
Thread t = Thread.currentThread();
//2.獲取當前線程對應的map
ThreadLocalMap map = getMap(t);
if (map != null)
//3.往map存放一個鍵值對
//this ThreadLocal
//value 保存的副本
map.set(this, value);
else
createMap(t, value);
}
所以,我們得到結論:
每個線程都會有對應的map,map來保存鍵值對。
問題三:ThreadLocal這種特性,在實際開發中解決了什麼問題?
比如:hibernate管理session,mybatis管理sqlsession,其內部都是採用ThreadLocal來實現的。
前提知識:不管是什麼框架,最本質的操作都是基於JDBC,當我們需要跟數據庫打交道的時候,都需要有一個connection。
那麼,當我們需要在業務層實現事務控制時,該如何達到這個效果?
我們構建下代碼如下:
public class UserService {
//省略接口的聲明
private UserDao userDao = new UserDao();
private LogDao logDao = new LogDao();
//事務的邊界放在業務層
//JDBC的封裝,connection
public void add(){
userDao.add();
logDao.add();
}
}
public class UserDao {
public void add() {
System.out.println("UserDao add。。。");
//創建connection對象
//connection.commit();
//connection.rollback();
}
}
public class LogDao {
public void add() {
System.out.println("LogDao add。。。");
//創建connection對象
//connection.commit();
//connection.rollback();
}
}
如果代碼按上面的方式來管理connection,我們還可以保證service的事務控制嗎?
這是不行的,假設第一個dao操作成功了,那麼它就提交事務了,而第二個dao操作失敗了,它回滾了事務,但不會影響到第一個dao的事務,因爲上面這麼寫是兩個獨立的事務
那麼怎麼解決。
上面的根源就是兩個dao操作的是不同的connection
所以,我們保證是同個connection即可
//事務的邊界放在業務層
//JDBC的封裝,connection
public void add() {
Connection connection = new Connection();
userDao.add(connection);
logDao.add(connection);
}
上面的方式代碼不夠優雅
public class ConnectionUtils {
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
public static Connection getConnection() {
Connection connection = threadLocal.get();
if(connection == null){
connection = new Connection();
threadLocal.set(connection);
}
return connection;
}
}
public class UserDao {
public void add() {
System.out.println("UserDao add。。。");
//創建connection對象
//connection.commit();
//connection.rollback();
Connection connection = ConnectionUtils.getConnection();
System.out.println("UserDao->"+connection);
}
}
到此,我們可以保證兩個dao操作的是同一個connection
36、我們來談談類的加載機制
面試場景
面試官第一問:
請問,我現在編寫一個類,類全名如下:java.lang.String,
我們知道JDK也給我們聽過了一個java.lang.String,
那麼,我們編寫的這個String類能否替換到JDK默認提供,也就是說程序實際運行的時候,會加載我們的String還是JDK的String?爲什麼?
如果,你無法確定?那麼第二問:
瞭解類的加載機制嗎?知道JDK的類加載器嗎?雙親委託機制說說看
如果,你還不瞭解,那麼我們聊聊今天的天氣吧!
1,首先,什麼是類的加載機制?
JVM使用Java類的流程如下:
1,Java源文件----編譯---->class文件
2,類加載器ClassLoader會讀取這個.class文件,並將其轉化爲java.lang.Class的實例。有了該實例,JVM就可以使用他來創建對象,調用方法等操作了。
那麼ClassLoader是以一種什麼機制來加載Class的?
這就是我們要談的類的加載機制!
2,搞清楚這個問題,首先要知道,我們用到的Class文件都有哪些來源?
1,Java內部自帶的核心類,位於$JAVA_HOME/jre/lib,其中最著名的莫過於rt.jar
2,Java的擴展類,位於$JAVA_HOME/jre/lib/ext目錄下
3,我們自己開發的類或項目開發用到的第三方jar包,位於我們項目的目錄下,比如WEB-INF/lib目錄
3,那麼,針對這些Class,JDK是怎麼分工的?誰來加載這些Class?
針對不同的來源,Java分了不同的ClassLoader來加載
1,Java核心類,這些Java運行的基礎類,由一個名爲BootstrapClassLoader加載器負責加載。這個類加載器被稱爲“根加載器或引導加載器”
注意:BootstrapClassLoader不繼承ClassLoader,是由JVM內部實現。法力無邊,所以你通過java程序訪問不到,得到的是null。
2,Java擴展類,是由ExtClassLoader負責加載,被稱爲“擴展類加載器”。
3,項目中編寫的類,是由AppClassLoader來負責加載,被稱爲“系統類加載器”。
4, 那憑什麼,我就知道這個類應該由老大BootStrapClassLoader來加載?
這裏面就要基於雙親委託機制?
所謂雙親委託機制,就是加載一個類,會先獲取到一個系統類加載器AppClassLoader的實例,然後往上層層請求,先由BootstarpClassLoader去加載,
如果BootStrapClassLoader發現沒有,再下發給ExtClassLoader去加載,還是沒有,才由AppClassLoader去加載。
如果還是沒有,則報錯
5,所以,上述問題的答案你清楚了嗎?
JDK提供java.lang.String類,默認在rt.jar這個包裏面,所以,默認會由BootstarpClassLoader加載,
所以,我們自己編寫的java.lang.String,都沒有機會被加載到
6,給兩段程序看看,類加載器的關係
案例1:創建一個自己的類,然後打印其類加載器
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("com.huangguizhao.thread.client.Programmer");
ClassLoader classLoader = clazz.getClassLoader();
System.out.println(classLoader.getClass().getSimpleName());
}
}
案例2:打印其雙親類加載器信息
while(classLoader.getParent() != null) {
classLoader = classLoader.getParent();
System.out.println("-->"+classLoader.getClass().getSimpleName());
}
37、談談對CSS盒子模型的理解
來,一圖勝千言
padding:內邊距
border:邊框
margin:外邊距
div+css,通常就用於佈局。
38、JavaScript提供了哪些定時器
定時器在js中的應用非常廣泛,比如首頁的輪播圖效果,網頁的時鐘,秒殺倒計時等等
就是採用定時器來實現的。
那麼主要提供了兩種定時器:
<script type="text/javascript">
//一次性的
window.setTimeout(function () {
alert(1);
},1000)
//週期性的
window.setInterval(function () {
alert(2);
},1000)
</script>
39、JavaScript如何處理兼容性問題?
什麼是兼容性問題:
因爲歷史原因,不同瀏覽器支持的方法或屬性有差異
解決辦法:
1,判斷當前是哪款瀏覽器內核,然後調用這個內核支持的方法,但獲取內核的方式,通常會有誤差
2,存在性檢查的方式(推薦)
如果當前的對象或方法存在,則會是true,進入if,否則進入false
案例:比如判斷當前瀏覽器是否支持trim()方法
if(email.value.trim) {
if(email.value.trim() == "") {
email.value = "請輸入郵箱地址";
}
} else {
//正則表達式-去除前後空格
if(email.value.replace(/(^\s+)|(\s+$)/g,"") == "") {
email.value = "請輸入郵箱地址";
}
}
40、談談Ajax的工作原理
談這個問題的關鍵三要素,異步交互,XMLHttpRequest對象,回調函數。
下面,看圖,傳統模式跟Ajax工作模式的對比:
早期,預計是以XML爲主要的傳輸數據格式,所以Ajax的最後一個字母就是代表XML的意思,不過現在基本是json爲主。
41、淺談JavaScript的原型機制
JavaScript的原型有一個關鍵的作用就是來擴展其原有類的特性,比如下面這段代碼,給String擴展了hello方法。很多的框架就是採用這種方式來進行擴展,從而讓框架更易用。
42、談談Servlet的生命週期
首先,要明確一點,Servlet是單實例的,這個很重要!
生命週期的流程:
創建對象-->初始化-->service()-->doXXX()-->銷燬
創建對象的時機:
1,默認是第一次訪問該Servlet的時候創建
2,也可以通過配置web.xml,來改變創建時機,比如在容器啓動的時候去創建,DispatcherServlet(SpringMVC前端控制器)就是一個例子
1
執行的次數
對象的創建只有一次,單例
初始化一次
銷燬一次
關於線程安全
構成線程不安全三個因素:
1,多線程的環境(有多個客戶端,同時訪問Servlet)
2,多個線程共享資源,比如一個單例對象(Servlet是單例的)
3,這個單例對象是有狀態的(比如在Servlet方法中採用全局變量,並且以該變量的運算結果作爲下一步操作的判斷依據)
僞代碼,演示線程不安全的操作方式
public class MyServlet extends HttpServlet{
private int ticket = 100;
public void doXXX() {
if(ticket > 0) {
//......
ticket--;
}
}
}
所以,我們要避免在Servlet中做上述類似的操作!
分析Servlet內部源碼,關於Service對請求的分發處理邏輯,會調用相應的doXXX方法
43、描述JSP和Servlet的區別
技術的角度:
JSP本質就是一個Servlet
JSP的工作原理:JSP->翻譯->Servlet(java)->編譯->Class(最終跑的文件)
應用的角度:
JSP=HTML+Java
Servlet=Java+HTML
各取所長,JSP的特點在於實現視圖,Servlet的特點在於實現控制邏輯
44、描述Session跟Cookie的區別(重要)
1,存儲的位置不同
Session:服務端
Cookie:客戶端
存儲的數據格式不同
Session:value爲對象,Object類型
Cookie:value爲字符串,如果我們存儲一個對象,這個時候,就需要將對象轉換爲JSON
存儲的數據大小
Session:受服務器內存控制
Cookie:一般來說,最大爲4k
生命週期不同
Session:服務器端控制,默認是30分鐘,注意,當用戶關閉了瀏覽器,session並不會消失
Cookie:客戶端控制,其實是客戶端的一個文件,分兩種情況
1,默認的是會話級的cookie,這種隨着瀏覽器的關閉而消失,比如保存sessionId的cookie
2,非會話級cookie,通過設置有效期來控制,比如這種“7天免登錄”這種功能,
就需要設置有效期,setMaxAge
cookie的其他配置
httpOnly=true:防止客戶端的XSS攻擊
path="/" :訪問路徑
domain="":設置cookie的域名
cookie跟session之間的聯繫
http協議是一種無狀態協議,服務器爲了記住用戶的狀態,我們採用的是Session的機制
而Session機制背後的原理是,服務器會自動生成會話級的cookie來保存session的標識,如下圖所示:
45、轉發和重定向的區別
1,轉發:
發生在服務器內部的跳轉,所以,對於客戶端來說,至始至終就是一次請求,所以這期間,保存在request對象中的數據可以傳遞
2,重定向:
發生在客戶端的跳轉,所以,是多次請求,這個時候,如果需要在多次請求之間傳遞數據,就需要用session對象
3,面試官的問題:
在後臺程序,想跳轉到百度,應該用轉發還是重定向?
答案:重定向,因爲轉發的範圍限制在服務器內部
46、談談三層架構
1、JavaEE將企業級軟件架構分爲三個層次:
Web層:負責與用戶交互並對外提供服務接口
業務邏輯層:實現業務邏輯模塊
數據存取層:將業務邏輯層處理的結果持久化,方便後續查詢
2、每個層都有各自的框架
WEB層:SpringMVC,Struts2,Struts1
業務邏輯層:Spring
數據持久層:Hibernate,MyBatis,SpringDataJPA,SpringJDBC
47、談談對MVC的理解(重要)
MVC是對Web層做了進一步的劃分,更加細化
Model(模型) - 模型代表一個存取數據的對象或 JAVA POJO。
View(視圖) - 視圖代表模型包含的數據的可視化,比如HTML,JSP,Thymeleaf,FreeMarker等等
Controller(控制器) - 控制器作用於模型和視圖上。它控制數據流向模型對象,並在數據變化時更新視圖。它使視圖與模型分離開,目前的技術代表是Servlet,Controller
常見的MVC框架有,Struts1,Struts2,SpringMVC
比如,SpringMVC分爲兩個控制器
DispatchServlet:前端控制器,由它來接收客戶端的請求,再根據客戶端請求的URL的特點,分發到對應的業務控制器,比如UserController
48、Iterator和ListIterator的區別?
Iterator可以遍歷Set和List,而ListIterator只能遍歷List
Iterator只能單向遍歷,而ListIterator可以雙向遍歷
ListIterator繼承與Iterator接口
49、併發和並行的區別
併發:
同一個CPU執行多個任務,按細分的時間片交替執行
並行:
在多個CPU上同時處理多個任務
50、什麼是序列化?
序列化是爲了保持對象在內存中的狀態,並且可以把保存的對象狀態再讀出來。
什麼時候需要用到java序列化?
1,需要將內存的對象狀態保存到文件中
2,需要通過socket通信進行對象傳輸時
3,我們將系統拆分成多個服務之後,服務之間傳輸對象,需要序列化
51、談談數據庫設計的三大範式及反範式
1,數據庫的三大範式
第一範式:列不可分
第二範式:要有主鍵
第三範式:不可存在傳遞依賴
比如商品表裏面關聯商品類別表,那麼只需要一個關聯字段product_type_id即可,其他字段信息可以通過表關聯查詢即可得到
如果商品表還存在一個商品類別名稱字段,如product_type_name,那就屬於存在傳遞依賴的情況,第三範式主要是從空間的角度來考慮,避免產生冗餘信息,浪費磁盤空間
2、反範式設計:(第三範式)
爲什麼會有反範式設計?
原因一:提高查詢效率(讀多寫少)
比如上述的描述中,顯示商品信息時,經常需要伴隨商品類別信息的展示,
所以這個時候,爲了提高查詢效率,可以通過冗餘一個商品名稱字段,這個可以將原先的表關聯查詢轉換爲單表查詢
原因二:保存歷史快照信息
比如訂單表,裏面需要包含收貨人的各項信息,如姓名,電話,地址等等,這些都屬於歷史快照,需要冗餘保存起來,
不能通過保存用戶地址ID去關聯查詢,因爲用戶的收貨人信息可能會在後期發生變更
52、說說常用的聚合函數有哪些及作用?
序號 | 函數名稱 | 描述 |
---|---|---|
1 | COUNT(*|列) | 求出全部記錄數 |
2 | SUM(列) | 求出總和 |
3 | AVG(列) | 平均值 |
4 | MAX(列) | 最大值 |
5 | MIN(列) | 最小值 |
基本使用語法:
select max(age) from t_student;
select min(age) from t_student;
53、左連接,右連接,內連接,如何編寫SQL,他們的區別是什麼?
左連接:以左表爲主
select a.,b. from a left join b on a.b_id = b.id;
右連接:以右表爲主
select a.,b. from a right join b on a.b_id = b.id;
內連接:只列出兩張表關聯查詢符合條件的記錄
select a.,b. from a inner join b on a.b_id = b.id;
案例:
select t.id t_id,t.name
t_name,c.id c_id,c.name
c_name
from t_teacher t LEFT JOIN t_class c on t.id=c.t_id; #4條,以老師表爲主
select t.id t_id,t.name
t_name,c.id c_id,c.name
c_name
from t_teacher t RIGHT JOIN t_class c on t.id=c.t_id; #4條,以班級表爲主
select t.id t_id,t.name
t_name,c.id c_id,c.name
c_name
from t_teacher t INNER JOIN t_class c on t.id=c.t_id; #3條,只展示匹配條件的記錄
54、如何解決SQL注入?
1,SQL注入,是指通過字符串拼接的方式構成了一種特殊的查詢語句
比如:select * from t_user where usename='' and password=''
' or 1=1 #
select * from t_user where usename='' or 1=1 # ' and password=''
2,解決方案
採用預處理對象,採用PreparedStatement對象,而不是Statement對象
可以解決SQL注入的問題
另外也可以提高執行效率,因爲是預先編譯執行
SQL執行過程(語法校驗->編譯->執行)
延伸
MyBatis如何解決了SQL注入的問題?採用#
MyBatis的#和$的差異,#可以解決SQL注入,而?號不能解決
55、JDBC如何實現對事務的控制及事務邊界
JDBC對事務的操作是基於Connection來進行控制的,具體代碼如下:
try {
//開啓事務
connection.setAutoCommit(false);
//做業務操作
//doSomething();
//提交事務
connection.commit();
} catch(Exception e) {
//回滾事務
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
但,注意,事務的邊界我們是放在業務層進行控制,因爲業務層通常包含多個dao層的操作。
56、談談事務的特點
原子性是基礎,隔離性是手段,一致性 是約束條件,而持久性是我們的目的。
簡稱,ACID
原子性( Atomicity )、一致性( Consistency )、隔離性( Isolation )和持久性( Durability )
原子性:
事務是數據庫的邏輯工作單位,事務中包含的各操作要麼都完成,要麼都不完成
(要麼一起成功,要麼一起失敗)
一致性:
事務一致性是指數據庫中的數據在事務操作前後都必須滿足業務規則約束。
比如A轉賬給B,那麼轉賬前後,AB的賬戶總金額應該是一致的。
隔離性:
一個事務的執行不能被其它事務干擾。即一個事務內部的操作及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。
(設置不同的隔離級別,互相干擾的程度會不同)
持久性:
事務一旦提交,結果便是永久性的。即使發生宕機,仍然可以依靠事務日誌完成數據的持久化。
日誌包括回滾日誌(undo)和重做日誌(redo),當我們通過事務修改數據時,首先會將數據庫變化的信息記錄到重做日誌中,然後再對數據庫中的數據進行修改。這樣即使數據庫系統發生奔潰,我們還可以通過重做日誌進行數據恢復。
57、談談事務的隔離級別
有以下4個級別:
l READ UNCOMMITTED 讀未提交,髒讀、不可重複讀、幻讀有可能發生。
l READ COMMITTED 讀已提交,可避免髒讀的發生,但不可重複讀、幻讀有可能發生。
l REPEATABLE READ 可重複讀,可避免髒讀、不可重複讀的發生,但幻讀有可能發生。
l SERIALIZABLE 串行化,可避免髒讀、不可重複讀、幻讀的發生,但性能會影響比較大。
特別說明:
幻讀,是指在本地事務查詢數據時只能看到3條,但是當執行更新時,卻會更新4條,所以稱爲幻讀
來一張彙總表:
58、synchronized和volatile的區別
1、作用的位置不同
synchronized是修飾方法,代碼塊
volatile是修飾變量
2、作用不同
synchronized,可以保證變量修改的可見性及原子性,可能會造成線程的阻塞
volatile僅能實現變量修改的可見性,但無法保證原子性,不會造成線程的阻塞
59、synchronized和lock的區別
1,作用的位置不同
synchronized可以給方法,代碼塊加鎖
lock只能給代碼塊加鎖
2,鎖的獲取鎖和釋放機制不同
synchronized無需手動獲取鎖和釋放鎖,發生異常會自動解鎖,不會出現死鎖。
lock需要自己加鎖和釋放鎖,如lock()和unlock(),如果忘記使用unlock(),則會出現死鎖,
所以,一般我們會在finally裏面使用unlock().
補充:
//明確採用人工的方式來上鎖
lock.lock();
//明確採用手工的方式來釋放鎖
lock.unlock();
synchronized修飾成員方法時,默認的鎖對象,就是當前對象
synchronized修飾靜態方法時,默認的鎖對象,當前類的class對象,比如User.class
synchronized修飾代碼塊時,可以自己來設置鎖對象,比如
synchronized(this){
//線程進入,就自動獲取到鎖
//線程執行結束,自動釋放鎖
}
60、深拷貝和淺拷貝的區別是什麼?
深拷貝:除了對象本身被複制外,對象所包含的所有成員變量都會被複制,包括引用類型的成員對象
淺拷貝:只複製對象其中包含的值類型的成員變量,而引用類型的成員對象沒有被複制
61、什麼是XSS攻擊?
XSS攻擊,俗稱跨站點腳本攻擊,
其原理是往網頁添加惡意的執行腳本,比如js腳本。
當用戶瀏覽該網頁時,嵌入其中的腳本就會被執行,從而達到攻擊用戶的目的。
比如盜取客戶的cookie,重定向到其他有毒的網站等等。
比如寫一段js腳本(這還是很有善意的腳本)
for(var i=1;i<100;i++) {
alert("努力不一定成功,但不努力一定很舒服!");
}
這個時候的解決辦法,是採用攔截器或過濾器對輸入的信息做過濾處理。比如將執行腳本的符號做一些替換處理。
62、說說TCP和UDP的區別
首先,兩者都是傳輸層的協議。
其次,
tcp提供可靠的傳輸協議,傳輸前需要建立連接,面向字節流,傳輸慢
udp無法保證傳輸的可靠性,無需創建連接,以報文的方式傳輸,效率高
63、什麼是死鎖?如何防止死鎖?
1、什麼是死鎖
死鎖最初由一個悲慘的故事說起,話說一羣哲學家一起聚餐,然後在每個人的左邊和右邊分別放着一根筷子,而只有同時抓到兩根筷子,才能正常喫飯,於是,不幸的故事發生了,每位哲學家都只抓到一根筷子,且都不願意釋放手中的筷子,於是,最終一桌的飯菜就這麼浪費了。
不知道這個故事是誰發明的,但確實形象說明了死鎖的情況。
轉換到線程的場景,就是線程A持有獨佔鎖資源a,並嘗試去獲取獨佔鎖資源b
同時,線程B持有獨佔鎖資源b,並嘗試去獲取獨佔鎖資源a
這樣線程A和線程B相互持有對方需要的鎖,從而發生阻塞,最終變爲死鎖。
public class Deadlock {
private static final Object a = new Object();
private static final Object b = new Object();
public static void main(String[] args) {
new Thread(new Task(true)).start();
new Thread(new Task(false)).start();
}
static class Task implements Runnable {
private boolean flag;
public Task(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"->獲取到a資源");
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"->獲取到b資源");
}
}
} else {
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"->獲取到b資源");
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"->獲取到a資源");
}
}
}
}
}
}
//有可能會出現死鎖,如果第一個線程已經走完,第二個線程才獲取到執行權限,那麼就不會出現死鎖
2、如何防止死鎖?(重點)
減少同步代碼塊嵌套操作
降低鎖的使用粒度,不要幾個功能共用一把鎖
儘量採用tryLock(timeout)的方法,可以設置超時時間,這樣超時之後,就可以主動退出,防止死鎖(關鍵)
64、什麼是反射?可以解決什麼問題?
反射是指程序在運行狀態中,
1,可以對任意一個類,都能夠獲取到這個類的所有屬性和方法。
2,對於任意一個對象,都可以調用它的任意一個方法和屬性
反射是一種能力
一種在程序運行時,動態獲取當前類對象的所有屬性和方法的能力,可以動態執行方法,給屬性賦值等操作的能力
Class代表的就是所有的字節碼對象的抽象,類
反射,讓我們的java程序具備動態性
這種動態獲取類信息及調用對象方法的功能稱爲反射
在java中,Class類就是關鍵API
public class Reflection {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
//1.以class對象爲基礎
Class<?> clazz = Class.forName("com.hgz.reflection.Student");
System.out.println(clazz);
//2.類中每一部分,都有對應的類與之匹配
//表示屬性的類
Field nameField =
clazz.getField("name");
//表示方法的類
Method helloMethod = clazz.getDeclaredMethod("hello", String.class);
//表示構造方法的類
Constructor<?>[] constructors = clazz.getConstructors();
}
}
這種能力帶來很多的好處,在我們的許多框架的背後實現上,都採用了反射的機制來實現動態效果。
框架是提供一種編程的約定
比如@Autowrie 就能實現自動注入
@Autowrie
private IUserService userService;
註解的解析程序,來掃描當前的包下面有哪些屬性加了這個註解,一旦有這個註解,就要去容器裏面獲取對應的類型的實現,然後給這個屬性賦值。
思考題:如何實現一個IOC容器?
65、如何實現動態代理?
SpringAOP(面向切面編程),AOP分離核心業務邏輯和非核心業務邏輯,其背後動態代理的思想,
主要的實現手段有兩種
1,JDK的動態代理,是基於接口的實現
2,基於CGLIB的動態代理,是基於繼承當前類的子類來實現的(所以,這個類不能是final)。我們項目結構是沒有接口的情況下,如果實現動態代理,那麼就需要使用這種方法。
所以,我們的Spring默認會在以上兩者根據代碼的關係自動切換,當我們採用基於接口的方式編程時,則默認採用JDK的動態代理實現。如果不是接口的方式,那麼會自動採用CGLIB。
SpringAOP的背後實現原理就是動態代理機制。
如何去驗證這個結論:
1,搭建一個Spring項目
2,創建有接口的方式
3,創建無接口的方式
4,打印輸出動態生成的代理對象(完整類名)
@Autowire
private IMiaoShaService miaoShaService;
比如,miaoshaService真正運行的時候就是一個代理對象
66、談談你對Spring的認識
這類問題,非常寬,來吧我們說說看
1、概覽圖如下
2、說說上面的模塊
核心的IOC容器技術(控制反轉),幫助我們自動管理依賴的對象,不需要我們自己創建和管理依賴對象,從而實現了層與層之間的解耦,所以重點是解耦!
核心的AOP技術(面向切面編程),方便我們將一些非核心業務邏輯抽離,從而實現核心業務和非核心業務的解耦,比如添加一個商品信息,那麼核心業務就是做添加商品信息記錄這個操作,非核心業務比如,事務的管理,日誌,性能檢測,讀寫分離的實現等等
spring Dao,Spring web模塊,更方便集成各大主流框架,比如ORM框架,hibernate,mybatis,比如MVC框架,struts2,SpringMVC
67、Spring的bean作用域有哪些?
1、默認是singleton,即單例模式
2、prototype,每次從容器調用bean時都會創建一個新的對象,比如整合Struts2框架的時候,spring管理action對象則需要這麼設置。
3、request,每次http請求都會創建一個對象
4、session,同一個session共享一個對象
5、global-session
68、Spring的bean是線程安全的嗎?
大家可以回顧下線程不安全構成的三要素:
1、多線程環境
2、訪問同一個資源
3、資源具有狀態性
那麼Spring的bean模式是單例,而且後端的程序,天然就處於一個多線程的工作環境。
那麼是安全的嗎?
關鍵看第3點,我們的bean基本是無狀態的,所以從這個點來說,是安全的。
所謂無狀態就是沒有存儲數據,即沒有通過數據的狀態來作爲下一步操作的判斷依據
69、談談SpringMVC的工作流程
如圖所示:
1、首先,將請求分給前端控制器DispatcherServlet
2、DispatcherServlet查詢HandlerMapping(映射控制器),從而找到處理請求的Controller(處理器)
3、Controller執行業務邏輯處理後,返回一個ModelAndView(模型和視圖)
4、DispatcherServlet查詢一個或多個ViewResolver(視圖解析器),找到ModelAndView對應的視圖對象,視圖對象負責渲染返回給客戶端
70、SpringMVC+Spring的父子容器關係
SpringMVC+Spring這種開發模式的時候,會有兩個容器
- SpringMVC容器管理,controller,Handlermapping,ViewResolver
- Spring容器管理,service,datasource,mapper,dao
- Spring容器是父容器,SpringMVC容器是子容器
- 子容器可以訪問父容器上面的資源,所以我們會在看Controller可以注入Service
71、SpringMVC有哪些常用的註解?有什麼作用?
@RequestMapping:做請求的URL跟我們controller或者方法的映射關係
@RequestParam:做請求參數的匹配,當請求參數名稱跟我們方法的參數名不一致的時候,可以做匹配
@GetMapping: 請求方式爲GET
@PostMapping:請求方式爲POST
@PathVariable:獲取URL中攜帶的參數值,處理RESTful風格的路徑參數
@CookieValue:獲取瀏覽器傳遞cookie值
@RequestBody:接收請求中的參數信息,一般來說,接收一個集合或數組,或者以post方式提交的數據
@ResponseBody: 改變返回邏輯視圖的默認行爲,返回具體的數據,比如json
@Controller:Spring定義的,作用就是標明這是一個controller類
@RestController:@Controller+@ResponseBody的組合
72、什麼是事務的傳播特性及Spring支持的特性有哪些?
1,什麼是事務的傳播特性?
我們一般都是將事務的邊界設置在Service層,
那麼當我們調用Service層的一個方法的時,它能夠保證我們的這個方法中執行的所有的對數據庫的更新操作保持在一個事務中,
在事務層裏面調用的這些方法要麼全部成功,要麼全部失敗。那麼事務的傳播特性也是從這裏說起的。
如果你在你的Service層的這個方法中,還調用了本類的其他的Service方法,那麼在調用其他的Service方法的時候,這個事務是怎麼規定的呢?
必須保證在我方法裏調用的這個方法與我本身的方法處在同一個事務中,否則無法保證事物的一致性。
事務的傳播特性就是解決這個問題的
2,Spring支持的事務傳播特性
在Spring中,針對傳播特性的多種配置,我們大多數情況下只用其中的一種:PROPGATION_REQUIRED:
這個配置項的意思是說當我調用service層的方法的時候,開啓一個事務,
那麼在調用這個service層裏面的其他的方法的時候,如果當前方法產生了事務就用當前方法產生的事務,否則就創建一個新的事務。
這個工作是由Spring來幫助我們完成的。
3,Spring支持的事務傳播特性
PROPAGATION_REQUIRED:支持當前事務,如果當前沒有事務,就新建一個事務。這是最常見的選擇。
PROPAGATION_SUPPORTS:支持當前事務,如果當前沒有事務,就以非事務方式執行。
PROPAGATION_MANDATORY:支持當前事務,如果當前沒有事務,就拋出異常。
PROPAGATION_REQUIRES_NEW:新建事務,如果當前存在事務,把當前事務掛起
PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則拋出異常。
73、什麼是悲觀鎖,什麼是樂觀鎖?
1、悲觀鎖是利用數據庫本身的鎖機制來實現,會鎖記錄。
實現的方式爲:select * from t_table where id = 1 for update
2、樂觀鎖是一種不鎖記錄的實現方式,採用CAS模式,採用version字段來作爲判斷依據。
每次對數據的更新操作,都會對version+1,這樣提交更新操作時,如果version的值已被更改,則更新失敗。
3、樂觀鎖的實現爲什麼要選擇version字段,如果選擇其他字段,比如業務字段store(庫存),那麼可能會出現所謂的ABA問題
74、談談hibernate的緩存機制
一級緩存:session級別的緩存,也稱爲線程級別的緩存,只在session的範圍內有效
二級緩存:sessionFactory級別的緩存,也稱爲進程級別的緩存,在所有的session中都有效
一般需要配置第三方的緩存支持,比如EhCache
查詢緩存:依賴於二級緩存,在HQL的查詢語句中生效
75、談談MyBatis跟Hibernate的區別?
1、靈活性,MyBatis我們一般是自己寫SQL,所以更靈活,更方便做優化
2、可移植性,正因爲MyBatis我們是自己寫SQL,而每個數據庫都有自己的SQL擴展,所以在可移植性方面,MyBatis會較差
所以,在技術的選型上,其實,有時候就是一種取捨。
一般,我們在追求性能的方面會更傾向選擇MyBatis。
76、MyBatis-緩存機制,從一級緩存到二級緩存
緩存,主要作用是提高了查詢性能,減少了跟數據庫交互的次數,從而也減輕了數據庫承受的壓力。
適用於讀多寫少的場景,如果數據變化頻率非常高,則不適用。
MyBatis的緩存分爲一級緩存和二級緩存。
一級緩存總結:
1,一級緩存模式是開啓狀態
2,一級緩存作用域在於SqlSession(大家可以關閉SqlSession,然後創建一個新的,再獲取對象,觀察實驗結果)
3,如果中間有對數據的更新操作,則將清空一級緩存。
下面,我們來看二級緩存(重點)
要使用二級緩存,需要經歷兩個步驟
1、開啓二級緩存(默認處於關閉狀態)
2、在Mapper.xml中,配置二級緩存(也支持在接口配置)
在標籤下面添加 標籤即可
默認的二級緩存配置會有如下特點:
2.1 所有的Select語句將會被緩存
2.2 所有的更新語句(insert、update、delete)將會刷新緩存
2.3 緩存將採用LRU(Least Recently Used 最近最少使用)算法來回收
2.4 緩存會存儲1024個對象的引用
回收算法建議採用LRU,當然,還提供了FIFO(先進先出),SOFT(軟引用),WEAK(弱引用)等其他算法。
3、二級緩存作用域在於SqlSessionFactory
78、MyBatis的XML映射文件都有哪些標籤
這道題,主要是看看你是否知道常用的標籤,如果沒記住,不用慌,翻開XML文件看看即可。
來,我們羅列下:
1、基本的CRUD標籤,select|insert|updae|delete
2、
3、動態SQL標籤:trim | where | set | foreach | if | choose | when | otherwise | bind等,其中
79、從瀏覽器輸入URL到頁面加載完畢,都經歷了什麼?
首先,需要經過DNS(域名解析服務)將URL轉換爲對應的ip地址,實際上域名只是方便我們記憶,在網絡上的每臺主機交互的地址都是IP。
其次,我們需要通過這個ip地址跟服務器建立TCP網絡連接,隨後向我們的服務器發出http請求。注意,http協議是tcp的上層協議
最後,服務器接收到我們的請求,處理完畢之後,將響應數據放入到http的響應信息中,然後返回給客戶端。
客戶端瀏覽器完成對服務器響應信息的渲染,將信息展現在用戶面前。
常見的響應狀態碼:
200,500,404,400,405,301這些你知道什麼意思嗎?
80、說說synchronized底層原理(重要)
這個我們要分情況來分析:
1、JDK1.6之前
synchronized是由一對monitor-enter和monitor-exit指令實現的。
這對指令的實現是依靠操作系統內部的互斥鎖來實現的,期間會涉及到用戶態到內存態的切換,所以這個操作是一個重量級的操作,性能較低。
2、JDK1.6之後
JVM對synchronized進行了優化,改了三個經歷的過程
偏向鎖-》輕量級鎖-》重量級鎖
偏向鎖:
在鎖對象保存一個thread-id字段,剛開始初始化爲空,當第一次線程訪問時,則將thread-id設置爲當前線程id,此時,我們稱爲持有偏向鎖。
當再次進入時,就會判斷當前線程id與thread-id是否一致
如果一致,則直接使用此對象
如果不一致,則升級爲輕量級鎖,通過自旋鎖循環一定次數來獲取鎖
如果執行了一定次數之後,還是沒能獲取鎖,則會升級爲重量級鎖。
鎖升級是爲了降低性能的消耗。
81、MyBatis有哪些分頁方式?
正常人,一般使用物理分頁。
分爲邏輯分頁和物理分頁
所謂邏輯分頁,是指使用MyBatis自帶的RowBounds進行分頁,它會一次性查出多條數據,然後再檢索分頁中的數據,具體一次性查詢多少條數據,受封裝jdbc配置的fetch-size決定
而物理分頁,是從數據庫中查詢指定條數的數據,而我們用的分頁插件PageHelper實現的就是物理分頁
那麼問題來了,你清楚分頁插件背後的原理嗎?
82、說說MyBatis分頁插件的原理是什麼?
首先,在MyBatis內部定義了一個攔截器接口
所有的插件都要實現該接口,來,我們看看這個接口的定義
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
那麼其中一個關鍵的方法就是intercept,從而實現攔截
分頁插件的原理就是使用MyBatis提供的插件接口,實現自定義插件,在插件的攔截方法內,攔截待執行的SQL,然後根據設置的dialect(方言),和設置的分頁參數,重寫SQL ,生成帶有分頁語句的SQL,執行重寫後的SQL,從而實現分頁
所以原理還是基於攔截器
83、說說JVM的主要組成部分
主要分爲4部分:
1,類加載器
2,運行時數據區
3,執行引擎
4,本地庫接口
這幾個分別的作用,我們用一張圖來描述下:
84、說說JVM的運行時數據區
具體來說,每個虛擬機是實際實現時,略有不同。
不過基本都符合虛擬機的規範,虛擬機規範將這個區域劃分爲5部分:
1、Java虛擬機棧
存儲局部變量,操作數棧,方法出口等,爲每個被執行的方法創建一個棧幀,是線程私有的,這一點跟堆是不同的
2、java堆
java虛擬機中內存最大的一塊,所有new的對象,都在這裏分配內存,被所有線程共享。
3、程序計數器
保存當前線程執行的字節碼行號指示器,通過改變該值,來實現執行下一條字節碼指令。
分支,循環,線程恢復等操作,都需要依賴這個計數器來實現。
4、方法區
存儲類信息,常量,靜態變量,即時編譯的代碼等數據
5、本地方法棧
與java虛擬機棧類似,只不過java虛擬機棧是服務java方法的,本地方法區棧服務虛擬機調用Native方法的。
85、談談類的裝載步驟
總共分爲:加載,檢查,準備,解析,初始化五個步驟
來,看圖
引用孫衛琴《Java面向對象編程》的一段描述,幫助大家更好理解符號引用和直接引用的區別
在類的加載過程中的解析階段,Java虛擬機會把類的二進制數據中的符號引用 替換爲 直接引用,如Worker類中一個方法:
public void gotoWork() {
car.run(); //這段代碼在Worker類中的二進制表示爲符號引用
}
在Worker類的二進制數據中,包含了一個對Car類的run()方法的符號引用,它由run()方法的全名 和 相關描述符組成。在解析階段,Java虛擬機會把這個符號引用替換爲一個指針,該指針指向Car類的run()方法在方法區的內存位置,這個指針就是直接引用。
86、談談如何判斷一個對象是否可以被回收?
目前是兩種方式:
方式一:引用計數器:爲每個對象創建一個引用計數,當有對象引用時,計數器+1,
當引用釋放時,計數器-1,所以,當計數器爲0時,就認爲可以被回收。
但這種算法,存在一個問題,存在循環引用的問題。
來,看代碼,但一般是爲
public static void main(String[] args) {
One one = new One();
Tow tow = new Tow();
one.tow = tow;
tow.one = one;
one = null;
tow = null;
}
}
class One {
public Tow tow;
}
class Tow {
public One one;
}
方式二:可達性分析
從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。
當一個對象到GC Roots沒有任何引用鏈時,則認爲此對象可以被回收。
大家可以認爲就是一個樹的根節點開始計算引用情況。
87、談談java的垃圾回收機制
我們通常指的垃圾回收,指的就是回收堆的內存。
我們創建的對象都保存在堆中,java虛擬機通過垃圾自動回收機制,簡稱GC,簡化了程序員的工作。
在java中,我們可以調用System.gc()來表示要進行垃圾回收,不過不建議使用,因爲使用之後,雖然不會立即觸發Full GC(堆內存全掃描),而是由虛擬機來決定執行時機,但是一旦執行,還是會停止所有的活動(stop the world),對應用影響很大。
我們一般建議,在一個對象不需要再被使用時,將其設置爲null,這樣GC雖然不會立即回收該對象的內存,但是會在下一次GC循環中被回收。
最後,說說finalize()方法,它是在釋放對象內存前,由GC調用,該方法有且僅被調用一次,一般不建議重寫該方法
88、談談JVM的垃圾回收算法及JVM參數
1、如何判斷一個對象是垃圾
在談JVM的垃圾回收算法之前,我們再來回顧下兩個關鍵問題:
1、什麼是垃圾回收?
2、如何判斷一個對象是垃圾?
所謂的垃圾回收,是指回收哪些死亡的對象所佔據的堆空間。
而如何判斷一個對象已經死亡,有兩種方式,引用計數法和可達性分析算法;
引用計數法,需要額外的空間來存儲計數器,如果有一個引用指向某一個對象,則該對象的引用計數器+1,如果該引用指向另一個對象,則原先的對象計算器-1.
但這種算法,會存在循環引用的bug問題,存在內存溢出的風險。
可達性分析算法,是以GC Root作爲起點,能夠引用到的對象則是有用對象,反之則是死亡的。
那麼,什麼是GC Root,一般可以理解爲堆外指向堆內的引用,包括以下常見的兩種:
1、java方法棧幀中的局部變量
2、已被加載的類靜態變量
下面,我們開始來談垃圾回收算法!
1、標記清除算法
是現在垃圾算法的思想基礎,它將垃圾回收分爲兩個階段:
標記階段和清除階段。
首先,是通過根節點GC Root,標記所有從根節點開始的可達對象。
因此,未被標記的對象都是垃圾對象。
然後,在清除節點,則刪除所有未被標記的對象。
標記清除算法的缺點:
1、效率不高
2、該算法會產生不連續的內存碎片,當我們需要分配較大對象時,會因爲無法找到足夠的連續內存空間,而不得不再次提前觸發垃圾回收,如果內存還是不夠,則報內存不足異常。
2、標記壓縮算法
標記壓縮算法是老年代的一種回收算法
首先,標記階段跟“標記清除算法”一致
區別在於清理階段,爲了避免內存碎片產生,所有的存活對象會被壓縮到內存的一端
這個算法解決之前標記清除算法的碎片問題
但是標記和壓縮的效率依然不高
3、複製算法
複製算法是爲了解決效率問題,它將內存一分爲二,每次只使用其中一塊,
這樣,當這一塊內容用完了,就將存活的對象複製到另一個塊上,然後將另一塊內存一次清理掉,這樣回收的效率也就提升了,也不存在內存碎片的問題。
算法優點是回收效率高,不存在內存碎片,但是浪費內存一半的內存空間,另外在對象存活率高的情況下,採用複製算法,效率將會變低。
4、分代收集算法
目前,主流的虛擬機大都採用分代收集算法,它根據對象存活週期的不同,而將內存劃分爲多塊區域。一般就是我們耳熟能詳的新生代和老年代,然後再各自採用不同的回收算法。
新生代(Eden),對象的存活率低,所以採用複製算法
老年代(Old),對象的存活率高,所以採用標記清除或標記整理算法
對象會優先分配到新生代,如果長時間存活或者對象過大會直接分配到老年代(新生代空間不夠)。
算法細節:
1、對象新建,將存放在新生代的Eden區域,注意Suvivor區又分爲兩塊區域,FromSuv和ToSuv
2、當年輕代Eden滿時,將會觸發Minor GC,如果對象仍然存活,對象將會被移動到Fromsuvivor空間,對象還是在新生代
3、再次發生minor GC,對象還存活,那麼將會採用複製算法,將對象移動到ToSuv區域,此時對象的年齡+1
4、再次發生minor GC,對象仍然存活,此時Survivor中跟對象Object同齡的對象還沒有達到Surivivor區的一半,所以還是會繼續採用複製算法,將fromSuv和ToSuv的區域進行互換
5、當多次發生monorGC後,對象Object仍然存活,且此時,此時Survivor中跟對象Object同齡的對象達到Surivivor區的一半,那麼對象Object將會移動到老年代區域,或者對象經過多次的回收,年齡達到了15歲,那麼也會遷移到老年代。
5、JVM配置的相關參數
- -Xms2g:初始化推大小爲 2g;
- -Xmx4g:堆最大內存爲 4g;
- -XX:NewRatio=4:設置年輕的和老年代的內存比例爲 1:4;
- -XX:SurvivorRatio=8:設置新生代 Eden 和 Survivor 比例爲 8:2;
6、垃圾回收器有哪些?
做垃圾回收的時候,都有一個統一的特點,叫stop the world.
往回收效率越來越高的方向來走的,垃圾回收的時間(stop the world)在變短
1、單線程回收器
採用單個線程的方式來進行回收,效率一般。服務器是多核CPU,資源無法得到更好利用
2、多線程回收器
可以充分利用CPU資源
3、CMS回收器
3.1 初始化標記
GCRoot
public class Gc {
private static SomeObject = new SomeObject();
}
class SomeObject {
}
這個時候會stop the world,但是由於我們只是標記GCRoot,所以花費的時間很短
3.2 併發標記
一邊可以繼續往下跟蹤,做可達性分析,相比比較耗時 100
一邊可以讓程序繼續運行,可能重新創建對象,也可能創造垃圾 20
3.3 重新標記
處理在併發標記過程中,再次產生新的垃圾,stop the world 20
3.4 併發回收
一邊針對我們剛纔的垃圾對象進行回收
一邊程序繼續運行
4、G1垃圾回收器
將內存劃分多個塊 ,每個塊再獨立進行回收
89、談談SpringBoot的工作原理
對技術的探索,一切源於好奇心,保持好奇心,才能讓人更年輕。
至今,我們已經有了很多創建SpringBoot項目的經驗,比如我們要創建一個支持web開發的項目,我們只需要引入web-starter模塊即可。
那麼,SpringBoot爲什麼這麼神奇?引入的依賴變少了,配置文件也不見了,但項目卻可以正常運行。下面我們一起來探究這背後的邏輯:
1、爲什麼依賴的依賴變少了?SpringBoot是如何管理這些依賴的?
我們分兩個點來看起
1.1 從pom文件出發
首先,是有一個父工程的引用
我們繼續往裏面跟蹤,發現父工程又依賴於另一個父工程
繼續跟蹤,發現這是一個pom工程,統一控制版本
定義了一堆第三方jar包的版本
結論:
所有我們使用了SpringBoot之後,由於父工程有對版本的統一控制,所以大部分第三方包,我們無需關注版本,個別沒有納入SpringBoot管理的,才需要設置版本號
1.2 SpringBoot將所有的常見開發功能,分成了一個個場景啓動器(starter),這樣我們需要開發什麼功能,就導入什麼場景啓動器依賴即可。
比如,我們現在要開發web項目,所以我們導入了spring-boot-starter-web
我們來跟蹤看看,內部也複用一些starter
還有Springweb和SpringMVC,這也就是爲什麼,我們就可以開發SpringWeb程序的原因
結論:
- 大家會發現,SpringBoot是通過定義各種各樣的Starter來管理這些依賴的
- 比如,我們需要開發web的功能,那麼引入spring-boot-starter-web
- 比如,我們需要開發模板頁的功能,那麼引入spring-boot-starter-thymeleaf
- 我們需要整合redis,那麼引入spring-boot-starter-data-redis
- 我們需要整合amqp,實現異步消息通信機制,那麼引入spring-boot-starter-amqp
- 等等,就是這麼方便
2、爲什麼我們不需要配置?
我們來看看SpringBoot的啓動類代碼,除了一個關鍵的註解,其他都是普通的類和main方法定義
那麼,我們來觀察下這個註解背後的東西,發現,這個註解是一個複合註解,包含了很多的信息
其他註解都是一個註解的常規配置,所以關鍵看圈中的這兩個
我們來分析第一個關鍵註解:@SpringBootConfiguration
我們可以看到,內部是包含了@Configuration,這是Spring定義配置類的註解
而@Configuration實際上就是一個@Component,表示一個受Spring管理的組件
結論:@SpringBootConfiguration這個註解只是更好區分這是SpringBoot的配置註解,本質還是用了Spring提供的@Configuration註解
我們再來探討下一個註解:@EnableAutoConfiguration
這個註解的作用是告訴SpringBoot開啓自動配置功能,這樣就減少了我們的配置
那麼具體是怎麼實現自動配置的?
我們先來觀察這個註解背後的內容
所以,又到了分析圈中的兩個註解了
先來分析@AutoConfigurationPackage
觀察其內部實現,內部是採用了@Import,來給容器導入一個Registrar組件
所以,我們繼續往下跟蹤,來看Registrar內部是什麼情況?
我們可以跟蹤源碼看看這段是什麼信息
結論:
通過源碼跟蹤,我們知道,程序運行到這裏,會去加載啓動類所在包下面的所有類
這就是爲什麼,默認情況下,我們要求定義的類,比如controller,service必須在啓動類的同級目錄或子級目錄的原因
再來分析@Import(AutoConfigurationImportSelector.class)
這個的關鍵是來看AutoConfigurationImportSelector.class內部的細節
在這個類的內部,有一個關鍵的方法,我們可以調試來看看結果
發現默認加載了好多的自動配置類,這些自動配置類,會自動給我們加載每個場景所需的所有組件,並配置好這些組件,這樣就省去了很多的配置
90、談談自定義註解
註解的內容很多,本次的面試題講解主要是針對以下幾個問題:
1、註解是在編譯期生效還是運行期生效?
2、有沒有在編譯期生效的註解?
3、編譯期生效的註解和運行期生效的註解有什麼區別?
我們的分析如下:
首先,第一個問題是給面試者挖坑,兩者都存在,關鍵看註解的定義描述;
其次,第二個問題是有,比如我們JDK默認提供註解@Oerride
我們觀察其註解的定義如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
上面的兩個註解說明做下解釋:
@Target(ElementType.METHOD) :表示該註解可以放到方法的定義上
@Retention(RetentionPolicy.SOURCE) : 表示該註解在編譯期有效
那什麼是編譯期有效?
就好比我們說的@Oerride,它的作用就是在編譯期間,檢查我們重寫的代碼有沒有符合語法規則,如果不符合就會通過紅線報錯,編譯失敗,而真正到運行期間就沒有作用了
最後,說第三個問題
就是運行期的註解有什麼用,其實我們用過的很多框架,他們都會提供註解,這些都不是JDK提供的註解,我們統稱爲自定義註解
比如Springweb提供的
我們觀察其註解的定義說明如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
我們可以看到其關鍵點的描述爲:
@Retention(RetentionPolicy.RUNTIME) : 表示是運行期有效
爲什麼需要定義爲運行期有效?
大家想想,我們創建Controller是不是在服務運行期間才正式對外提供服務的,而Spring容器需要去檢查到底哪些Controller可以對外提供服務,那麼以這個自定義註解爲暗號,一看,咦,你小子有這個註解,行了,你就是可以對外提供服務的人,所以註解必須是在運行期間有效
91、談談單體架構和微服務架構的區別?一般依據怎樣的原則進行微服務的拆分?
這是一道高頻的面試題,下面,我們一起來看看如何回答會更好。
首先,微服務架構並非就一定比單體架構好,我一直反對這種沒有獨立思考的人云亦云的答案,每種架構都有其適用場景。
第一,我們來看看單體架構適用的場景
單體架構特別適合初創公司的初創項目,可以小成本快速試錯,且系統模塊之間的調用,是進程內的通信,所以整體的性能表現會非常好,所以這類型的項目,我推薦採用單體架構足以,在市場還沒有打開之前,採用各類看似高大上的技術,除非是爲了賣弄技術,否則毫無意義。
做產品,需要考慮MVP模式,架構除了考慮技術,更應該考慮成本,成本意識是很關鍵的。
第二,我們來看看,微服務架構適合的場景
當系統經過一段時間的運營之後,如果運氣不錯,用戶量有了一定的增量,業務也隨着市場需求有了擴展,從而慢慢的整個系統的業務變得複雜而龐大,這個時候一個系統的啓動時間,重新編譯的時間,都可能會非常耗時,一個功能的修改也需要做全盤的迴歸測試,所謂牽一髮而動全身,這個時候就適合對系統進行服務拆分,拆分成多個服務子系統,每個子系統可以更靈活做升級。注意!此時原先的模塊之間的通信,由原先的進程內通信變爲進程間的通信,所以其響應速度會有所影響。
第三,我們再來看看,微服務拆分的原則
一般我們根據業務的邊界來拆分,比如按照商品,購物車,訂單等等業務邊界進行服務的拆分,另外一個,系統中存在的共性基礎服務,比如像短信,郵件,日誌等等,我們也可以作爲單獨的服務進行拆分,作爲基礎服務層供上層服務複用。
92、談談互聯網常見的負載均衡
負載均衡是我們對應高併發流量的一種常見處理方式
我們分兩個方面來聊這個問題,一個是負載均衡的分類,一個是負載均衡的常見算法。
1,負載均衡的分類、
基本我們可以分爲客戶端負載均衡和服務端負載均衡
服務端負載均衡,表示其負載均衡算法是在服務端實現的,比如我們常見的nginx,通過nginx我們可以來管理背後的多臺tomcat服務器,從而實現多臺tomcat服務共同對外提供服務的效果,如圖所示:
客戶端負載均衡:
就是表示其負載均衡算法是由調用者來維護,比如Dubbo的Proxy,SpringCloud的Ribbon
2、負載均衡的常見方式
1、輪詢
即按照固定順序,順序循環訪問後臺的服務器,比如上述的tomcat1,tomcat2
2、權重
即可以根據後臺服務器的硬件差異,配置權重,讓性能好的服務器多處理請求
3、最小活躍數
根據服務器的壓力,動態調整對請求的處理
4、ip_hash
根據客戶端的ip地址做hash運算,找到對應的服務器進行處理
5、一致性hash
相同參數的請求總是發到同一提供者。
當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。
算法參見:https://en.wikipedia.org/wiki/Consistent_hashing
93、談談微服務註冊中心zookeeper&Eureka
首先,大家要明確一點微服務註冊中心是一個重要的組件,解決的是服務的註冊和發現的問題,而zookeeper,Eureka都只是其中一款落地實現的產品,再比如Nacos也是如此,所以關鍵是掌握註冊中心的工作原理,組件的使用,諸如配置,安裝,這些都是常規步驟,沒有什麼特別的。
那下面,我們來談談這兩個註冊中心的工作原理,如果對nacos剛興趣,可以直接查看官網即可。
1、zookeeper
zookeeper的核心主要是包含兩個部分:服務信息的管理和變更通知機制(watch)
所謂的服務註冊,就是在zookeeper的服務器上創建一個節點,而且是臨時節點,保存着服務的地址信息
爲什麼是臨時節點?
因爲一旦服務節點宕機,則zookeeper可以自動將該節點刪除
所謂的服務發現,就是去獲取zookeeper上面的節點信息,獲取到提供該服務的地址列表信息
這樣當消費者去調用服務提供者,就可以採用負載均衡策略,去訪問其中一個提供者。
所謂監聽機制,當服務提供者某個節點發生故障,這個時候服務端的臨時節點會被刪除,上層的父節點就相當發生了變化,所以可以基於監聽機制通知客戶端(服務消費者)當前服務列表發生變化了,客戶端再次去獲取最新的服務列表信息。
下面,我們以圖片來說明
2、Eureka
1、包含兩個組件
Eureka Server 註冊中心服務端,提供了服務的註冊和發現(相當於zookeeper的作用)
Eureka Client 註冊中心客戶端(相當於之前的生產者和消費者), 需要將本身提供的服務註冊到EurekaServer
2、兩個關鍵的時間參數
一個是每隔30s,客戶端會發送心跳包給EurekaServer,告知健康狀態,表示還活着;
一個是每隔30s,客戶端會去找EurekaServer拉取最新的註冊表信息,刷新本地的緩存列表;
3,兩者集羣模型的差別
註冊中心作爲微服務架構中非常關鍵的組件,所以其可用性非常重要,所以我們來簡單說說其集羣架構的區別
zookeeper,奇數臺做集羣,CP(強一致性)
eureka,只需要兩臺以上即可,AP(可用性)
CAP是分佈式系統的基本參考原則,如果你之前對這個原則不瞭解,我們後續會再一篇文章來談談CAP