Java開發面試高頻考點學習筆記(每日更新)
1.深拷貝和淺拷貝
2.接口和抽象類的差別
3.java的記憶體是怎麼配置設定的
4.java中的泛型是什麼?類型擦除是什麼?
5.Java中的反射是什麼
6.序列化與反序列化
7.Object有哪些方法?
8.JVM記憶體模型
9.類加載機制
10.對象的建立和對象的布局
11.Java的四種引用(強引用、軟引用、弱引用和虛引用)
12.記憶體洩露和記憶體溢出
13.List、Set和Map三者的差別和其底層資料結構
14.建立線程的四種方式
15.NIO、AIO和BIO
16.重寫和重載
17.final/finally/finalize與static
18.String、StringBuffer和StringBuilder的差別
19.如果判斷一個對象是否該被回收?
20.垃圾收集算法
21.Double與Float
22.垃圾收集器
23.線程池
24.線程同步和線程通訊
25.中斷線程
26.Synchronized的用法
27.Synchronized的原理
28.Synchronized的四種狀态
29.Synchronized與重入鎖ReentrantLock的差別
30.鎖優化
31.Java設計模式
Java:
1.深拷貝和淺拷貝
記憶體中有棧區和堆區,基本類型資料直接存在棧中,而引用類型(new出來的)是在堆中存儲,在棧中儲存堆中的位址。也就是說引用類型中在棧中存的不是資料,而是位址。指派其實就是拷貝。【參考資料】
在基本類型資料指派的時候,沒有深淺拷貝的差別,因為直接賦予的是資料。
但在引用類型資料指派的時候,實際上是把原來的位址複制給了新的,并沒有實際複制其中的資料,是以這是一個淺拷貝(拷貝的深度不夠),當使用新的變量操作位址中的值的時候,舊變量對應的值也會發生改變。Java中Object的clone方法預設是淺拷貝。
深拷貝會創造另外一個一模一樣的對象,新對象和原來的對象不共享記憶體,修改新對象不會影響舊對象。【參考資料】
2.接口和抽象類的差別
抽象類:被abstract關鍵字修飾。抽象方法也被abstract修飾,隻有方法聲明,沒有方法體。
抽象類不能被執行個體化,隻能被繼承
抽象類可以有屬性、方法和構造方法,但是構造方法不能用于執行個體化,主要用于被子類調用
子類繼承抽象類,必須實作抽象類抽象方法,否則子類必須也是抽象類
抽象類中的抽象方法隻能是public或protected
接口:被interface關鍵字修飾。
接口可以包含變量和方法;變量隐式設定為public static final,方法被隐式設定為public abstract
接口支援多繼承,一個接口可以extends多個接口
一個類可以實作多個接口
jdk1.8中增加了預設方法和靜态方法:default/static
接口隻能是功能的定義,而抽象類既可以為功能的定義也可以為功能的實作。
接口和抽象類都不能被執行個體化,接口的實作類和抽象類的子類隻有實作了接口中/抽象類中的方法才能執行個體化。
實作接口的關鍵字是implements,繼承抽象類的關鍵字是extends。一個類可以實作多個接口,但一個類隻能繼承一個抽象類。
接口強調特定功能的實作,而抽象類強調所屬關系。
3.java的記憶體是怎麼配置設定的【參考資料】
記憶體配置設定分為在棧上配置設定和在堆上配置設定,大多數都是引用類型,是以堆空間用的較多。
對象根據存活時間分為年輕代、年老代、永久代(方法區)
年輕代:對象被建立時,首先配置設定在年輕代。年輕代有三個區域:Eden區,survivor 0區和survive 1區,Eden區大多數對象消亡速度很快,Eden是連續的記憶體空間,配置設定記憶體很快。Eden區滿的時候執行Minor GC,清理消亡對象,将存活的對象放在survivor 0區中,每次執行Minor GC的時候,将剩餘存活對象都放在非空的survivor區中,survivor區滿之後,就會清理并轉移到另一個survivor區,也就是說總有一個survivor區是空的。HotSpot虛拟機中預設切換15次之後,仍然存活的對象放在年老代中。
年老代:年老代的空間一般比年輕代大,存放更多的對象,年老代記憶體不足的時候,執行Major GC(Full GC),如果對象比較大的情況,可能直接放在老年代上。有可能出現老年代引用新生代對象的情況,java維護一個512 byte的塊“card table”,記錄引用映射,進行Minor GC的時候直接查card table就可以了。【參考資料】
4.java中的泛型是什麼?類型擦除是什麼?
java源代碼要運作,首先要經過編譯器編譯出位元組碼,位元組碼存儲着能被JVM解釋運作的指令。java的泛型在運作時,無法獲得類型參數的真正類型,因為編譯器編譯生成的位元組碼不包括類型參數的具體類型。
泛型是java 1.5之後引入的,其本質是參數化類型,也就是說變量的類型是一個參數,在使用的時候再指定為具體類型,泛型可以用于類、接口和方法。
public class User {
private T name;
}//泛型實際上就是把類型當作參數傳入了
而類型擦除機制使得Java的泛型實際上是僞泛型,類型參數隻存在于編譯期,運作時,JVM并不知道泛型的存在。
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2); //代碼輸出是true
}
}
在C++、C#這些支援真泛型的語言中,它們代表着不同的類,但在JVM看來他們是同一個類。無論何時定義一個泛型,相應的原始類型都會被自動提供,類型變量擦除,并使用其限定類型(無限定的變量用 Object)替換。Java 編譯器是通過先檢查代碼中泛型的類型,然後在進行類型擦除,再進行編譯。當具體的類型确定後,泛型提供了一種類型檢測的機制,隻有相比對的資料才能正常的指派,否則編譯器就不通過。
5.Java中的反射是什麼
java反射就是把類中的各個成分映射成一個個java對象,在運作期間,對于任意一個類,都能夠知道這個類的屬性和方法,是一種動态擷取資訊、動态調用對象的方法。
優點:動态加載類,提高代碼靈活度
缺點:降低性能,可能引起安全問題
我們使用的Spring/hibernate中使用了反射機制,在使用JDBC連接配接資料庫使用class.forName()通過反射加載資料庫的驅動程式。
Spring架構的IOC(動态加載管理bean)建立對象,AOP(動态代理)都和反射有關系。
6.序列化與反序列化
序列化:将Java對象轉換成位元組序列的過程。
反序列化:将位元組序列轉換成java對象。
serializable接口是可以進行序列化的标志性接口,僅僅是告訴JVM該類對象可以進行序列化。
先讓需要序列化的類實作serializable接口;序列化對象建立輸出流ObjectOutputStream,然後調用writeObject()方法;反序列化對象建立輸入流Obje ctInputStream,然後調用readObject()方法,得到一個object對象。最後關閉流。
7.Object有哪些方法?
equals:比較對象是否相等,這裡實質是比較位址是否相等。
wait:調用wait方法會導緻線程阻塞,釋放該對象的鎖
notify:調用對象的notify方法會随機解除該對象阻塞的線程,該線程重新擷取該對象的鎖
notifyAll:喚醒所有正在等待對象的線程,全部進入鎖池競争擷取鎖
wait,notify,notifyAll必須在synchronized方法塊中使用。
toString:轉換為字元串表示
getClass:傳回對象運作時類,即反射機制。
hashCode: 對象在記憶體中的位址轉換為int值。
8.JVM記憶體模型
程式計數器(PC register):線程執行的位元組碼行号訓示器,線程私有,唯一一個沒有記憶體超出錯誤的區域。
Java虛拟機棧:每個線程建立時都會建立一個虛拟機棧,内部儲存一個個棧幀,對應每一次方法調用。生命周期與線程相同。儲存方法的局部變量和部分結果,參與方法的調用和傳回。如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflow異常;如果虛拟機棧可以動态擴充,當擴充到無法申請足夠記憶體時抛出OutOfMemoryError異常。
本地方法棧:與虛拟機棧類似,但隻為native方法服務。
Java堆:線程共享記憶體,用來存放對象執行個體,是垃圾回收的主要區域。java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上連續就可以了,就類似于磁盤空間。如果在堆中沒有記憶體完成執行個體配置設定,而且堆也無法再拓展的時候,将會抛出OutOfMemoryError的異常。【參考資料】
方法區:是線程共享記憶體,它用于存儲已被虛拟機加載的類資訊等資料。它可以叫做永久代也可以是元空間,在jdk1.8之後,永久代的資料被配置設定到堆和元空間中,元空間存儲類資訊,字元串常量和運作時常量池放入堆中。方法區無法滿足記憶體配置設定需求時,抛出OutOfMemoryError異常。
JVM調優參數
(1) -Xms:初始化堆記憶體。預設為實體記憶體的六十四分之一
(2) -Xmx: 最大堆記憶體。預設為實體記憶體的四分之一
(3) -Xss:單個線程棧的大小
(4) -Xmn:設定新生代的大小
(5) -XX:MetaspaceSize:設定元空間大小
(6) -XX:SurvivorRatio:調節新生代eden和S0、S1的空間比例 預設為8:1:1
JVM性能監控工具
(1)jps -l:檢視程序号
(2)jstack:java堆棧跟蹤工具 檢視死鎖和cpu占用過高的代碼
(3)jinfo -flag檢視運作的java程式參數屬性的詳情
9.類加載機制
類加載就是将類的資料從class檔案加載到記憶體,并且進行校驗解析和初始化,形成可以讓虛拟機使用的java類型。
類的生命周期:加載,連結,初始化,使用,解除安裝。
加載:通過類名擷取二進制位元組流(通過類加載器),把靜态資料結構放在方法區,記憶體中生成對應class對象,作為通路入口。
連結:確定目前位元組流包含的資訊符合虛拟機要求。正式配置設定記憶體,設定初始值(僅配置設定靜态變量),虛拟機将常量池内的符号引用替換成直接引用。
初始化:按照代碼邏輯,賦予屬性真正的初始值,初始化階段就是執行類構造器方法的過程。
類加載器:包括啟動類加載器、擴充類加載器和應用程式類加載器。
10.對象的建立和對象的布局
對象建立的方法:
用new語句建立
調用clone方法,需要實作cloneable接口
反射:class的newInstance()
反序列化:從檔案中擷取一個對象的二進制流,使用ObjectInputStream的readObject方法。
對象建立的過程:
類加載檢查:判斷這個類是不是已經被加載連結初始化了。
為對象配置設定記憶體:如果記憶體規整,虛拟機使用碰撞指針法(指針向空閑區前移對象大小的距離);如果不規整則使用空閑清單法。并發安全:虛拟機維護一個清單記錄哪些記憶體塊可用,再配置設定的時候從清單中找到一塊足夠大的空間劃分給對象執行個體,并更新清單内容。
初始化配置設定的空間:所有屬性初始化為零,保證對象執行個體字段在不指派的時候可以直接用
設定對象頭資訊
執行構造方法初始化
逃逸:方法體内建立的對象,方法體外被其他變量引用過。這樣在方法執行完畢之後,該方法中建立的對象不能被GC回收。開啟逃逸分析之後,如果對象的作用域僅在方法内,那對象可以建立在虛拟機棧上,随方法入棧建立,出棧銷毀,減少GC回收壓力。
對象的記憶體布局:包含三部分:對象頭,執行個體資料和對齊填充。
對象頭:運作時資料和類型指針。标記字段包含hashcode、GC分代年齡、鎖狀态标志、線程持有鎖等資訊;類中繼資料的指針:可以知道這個對象是哪個類的執行個體。
執行個體資料:存儲對象真正的資料,也包含父類的資料。
對齊填充:保證對象大小是8位元組的整數倍。
11.Java的四種引用(強引用、軟引用、弱引用和虛引用)
在jdk1.2之前,Java對引用的定義很傳統:如果reference類型的資料中存儲的數值是另一塊記憶體的起始位址,就稱這塊記憶體代表一個引用。
強引用:Java中預設聲明的引用為強引用,隻要強引用存在,垃圾回收器永遠不會回收被引用的對象,哪怕記憶體不足,JVM也隻會抛出OOM錯誤,不會去回收。
Object obj = new Object();
軟引用:用于描述一些非必需但仍有用的對象。記憶體足夠的時候,軟引用對象不會被回收,隻有在記憶體不足的時候,系統會回收軟引用對象,如果記憶體還是不夠才會抛出OOM異常。這種特性使他往往用于實作緩存技術。在
JDK1.2 之後,用java.lang.ref.SoftReference類來表示軟引用。
弱引用:弱引用的強度比軟引用更弱。無論記憶體是否足夠,隻要JVM開始垃圾回收,那些被弱引用關聯的對象都會被回收。在 JDK1.2
之後,用java.lang.ref.WeakReference來表示弱引用。
虛引用:最弱的引用關系。與其他幾種引用不同,虛引用不會決定對象的生命周期,如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,任何時期都可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收器回收的活動,且必須與引用隊列聯合使用。當垃圾回收器準備回收一個對象的時候,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。
12.記憶體洩露和記憶體溢出
記憶體洩漏:一個不再被線程所使用的對象或變量還在記憶體中占用空間。
記憶體溢出:程式無法申請到足夠的記憶體。
記憶體洩漏的原因
1.長生命周期的對象持有短生命周期對象的引用。
2.連接配接未正常關閉。
3.變量作用域設定過大
避免記憶體洩漏
1.避免在循環中建立對象
2.沒有用的對象盡早釋放
3.慎用靜态變量
4.字元串的拼接使用Stringbuffer/StringBuilder
5.增大xmx和xms的值
記憶體溢出的原因
1.加載資料過大
2.死循環或過多循環
3.啟動參數中記憶體值設定過小
棧溢出
原因:遞歸深度過大、局部變量過大
解決:遞歸不要太深,局部變量改為靜态變量
如果排查記憶體問題
1.JConsole:能看到記憶體用量的趨勢,确定是否有問題
2.GC日志:能看到年輕代和老年代等區域配置是否合理
3.代碼中列印記憶體使用量
4.分析dump檔案:針對性的看到發生OOM時候的記憶體使用量和線程情況
13.List、Set和Map三者的差別和其底層資料結構
List:有序的對象
(1)ArrayList:數組
(2)Vector:數組
(3)LinkedList:雙向連結清單
Set:不允許重複的集合
(1)HashSet(無序且唯一):基于HashMap
(2)LinkedHashSet:基于HashMap
(3)TreeSet(有序且唯一):基于紅黑樹
Map:使用鍵值對存儲
(1)HashMap:Jdk1.8之前HashMap由數組+連結清單組成,之後再連結清單長度大于門檻值(預設8)時将連結清單轉換為紅黑樹以減少搜尋時間。
(2)LinkedHashMap:繼承自 HashMap,是以它的底層仍然是基于拉鍊式散列結構即由數組和連結清單或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增加了一條雙向連結清單,使得上面的結構可以保持鍵值對的插入順序。
(3)HashTable:數組+連結清單組成,數組是HashMap的主體,連結清單為了解決哈希沖突
(4)TreeMap:紅黑樹
ArrayList、LinkedList、Vector的差別
存儲結構:ArrayList和Vector是基于數組實作的,而LinkedList是基于雙向連結清單實作的。
線程安全性:ArrayList不具有線程安全性(ArrayList添加元素的操作不是原子操作,可能會出現一個線程的值覆寫另一個線程添加的值的問題),在單線程的環境中,LinkedList也是不安全的。Vector實作了線程安全,它大部分的關鍵字都包含synchronized,但效率低。
擴容機制:ArrayList和Vector都是用數組來存儲,容量不足的時候可以擴容,ArrayList擴容後的容量是之前的1.5倍,Vector預設是2倍。Vector可以設定擴容增量capacityIncrement。可變長度數組的原理是當元素個數超過數組長度時,産生一個新的數組,将原數組的資料複制到新數組,再将新元素添加到新數組中。
增删改查效率:ArrayList和Vector中,從指定的位置檢索一個對象,或在末尾插入删除一個元素時間複雜度都是O(1),但是在其他位置增加和删除對象的時間是O(n);LinkedList,插入删除任何位置的時間都是O(1),但是檢索一個元素的時間是O(n)。
14.建立線程的四種方式
繼承Thread類,重寫run方法,繼承Thread類的線程類不能再繼承其他父類。
實作Runnable接口,重寫run方法
通過Callable接口和Future接口建立線程,執行call方法,有傳回值可以抛異常
線程池。前三種的線程如果建立關閉頻繁的話會消耗系統資源影響性能,而使用線程池可以不用線程的時候放回線程池,用的時候再從線程池取。
15.NIO、AIO和BIO
BIO:傳統的網絡通訊模型,同步阻塞IO。伺服器實作是一個連接配接一個線程,用戶端有連接配接請求的時候,服務端就要啟動一個線程去處理。線程數量可能會爆炸導緻崩潰。适用于連接配接數目小且固定的架構。
NIO:同步非阻塞。伺服器實作是一個請求一個線程,用戶端發送的連接配接請求都會注冊到多路複用器上,複用器輪詢到連接配接有IO請求才啟動線程。适用于連接配接數目多且連接配接比較短的架構,比如聊天伺服器。
AIO:異步非阻塞。使用者程序隻需要發起一個IO操作然後立即傳回,等IO操作真正完成之後,應用程式會得到IO操作完成的通知。适用于連接配接數目多且連接配接長的架構。
16.重寫和重載
重寫(Override):重寫是子類對父類允許通路的方法實作過程進行重新編寫,傳回值和形參都不能改變。重寫的好處是子類可以根據特定需要,定義特定行為。異常範圍可以減少,但是不能抛出新的或更廣的異常。
class Animal{
public void move(){
System.out.println("動物可以移動");
}
}
//加入Java開發交流君樣:756584822一起吹水聊天
class Dog extends Animal{
public void move(){
System.out.println("狗可以跑和走");
}
}
public class TestDog{
public static void main(String args[]){
Animal a = new Animal(); // Animal 對象
Animal b = new Dog(); // Dog 對象
//加入Java開發交流君樣:756584822一起吹水聊天
a.move();// 執行 Animal 類的方法
b.move();//執行 Dog 類的方法
}
}
雖然b屬于Animal類型,但是它運作的是Dog類的move方法。因為在編譯階段,隻是檢查參數的引用類型,運作時JVM指定對象的類型并運作該對象的方法。
方法重寫規則
(1)參數清單和被重寫方法的參數清單必須完全相同。
(2)通路權限不能比父類中被重寫的方法通路權限更低。
(3)父類的成員方法隻能被它的子類重寫。
(4)聲明為final的方法不能被重寫;聲明為static的方法不能被重寫,但是能被再次聲明。
(5)構造方法不能被重寫。
(6)子類和父類在同一個包中,那麼子類可以重寫父類中沒有聲明為private和final的方法;如果不在同一個包中,子類隻能重寫父類聲明為public和protected的非final方法。
當需要在子類中調用父類的被重寫方法時,使用super關鍵字。
重載(Overload):是在一個類裡面,方法名字相同,參數不同的兩個方法。傳回類型可以相同也可以不同。每個重載的方法(或者構造函數)必須有一個獨一無二的參數類型清單。常用于構造器重載。
重載規則
(1)被重載的方法必須改變參數清單。
(2)被重載的方法可以改變傳回類型,可以改變通路修飾符,可以聲明新的或更廣的異常檢查。
(3)方法能夠在同一個類中或者在一個子類中被重載。
public class Overloading {
public int test(){
System.out.println("test1");
return 1;
}
public void test(int a){
System.out.println("test2");
}
//加入Java開發交流君樣:756584822一起吹水聊天
//以下兩個參數類型順序不同
public String test(int a,String s){
System.out.println("test3");
return "returntest3";
}
public String test(String s,int a){
System.out.println("test4");
return "returntest4";
}
public static void main(String[] args){
Overloading o = new Overloading();
System.out.println(o.test());
o.test(1);
System.out.println(o.test(1,"test3"));
System.out.println(o.test("test4",1));
}
}
方法重載和方法重寫是java多态的不同表現。
17.final/finally/finalize與static
final:java中的關鍵字,修飾符。如果一個類被聲明為final,就意味着它不能再派生出新的子類,不能作為父類被繼承。一個類不能被同時聲明final和abstract抽象類。如果變量或方法被聲明為final,就能保證它們在使用中不被改變,變量必須在聲明時指派,以後的引用中隻讀,被聲明final的方法隻能使用,不能重載。
finally:java的一種異常處理機制。java異常處理模型的最佳補充,finally結構使代碼總會執行,而不管有無異常發生。使用finally可以維護對象的内部狀态,清理非記憶體資源。在關閉資料庫連接配接時,如果把資料庫連接配接的close()方法放到finally中,就會減少出錯的可能。
finalize:Java中的一個方法名,該方法是在垃圾收集器将對象從記憶體中清除出去前,做必要的清理工作。這個方法是由垃圾收集器确定這個對象沒被引用的時候調用的。它在Object類中定義,是以所有類都繼承了它。子類可以覆寫該方法來整理資源和清理。
static:static修飾的屬性在編譯器初始化,初始化之後能改變,final修飾的屬性可以在編譯器也可以在運作期初始化,但是不能被改變;static不能修飾局部變量,但是final可以。
18.String、StringBuffer和StringBuilder的差別
String是java程式設計中廣泛使用的,但它的底層實作實際是一個final類型的字元數組,其中的值不可變,每次對String進行操作就會生成一個新對象,造成記憶體浪費。
private final char value[];
StringBuffer/StringBuilder:它們的底層是可變的字元數組,都繼承AbstractStringBuilder抽象類,是以在進行頻繁的字元串操作的時候,盡量使用這兩個類,它們的差別是:StringBuilder是線程不安全的,但執行速度較快;StringBuffer線程安全,但執行速度慢。StringBuffer使用synchronized關鍵字進行同步鎖。
另外,String類型的比較,“==”是比較兩個記憶體位址是否一樣,而“equals”是比較兩個字元串的值是不是一樣的。
19.如果判斷一個對象是否該被回收?
引用計數算法:為對象增加一個引用計數器,當對象增加一個引用的時候+1,引用失效-1,引用計數為0的對象可以被回收。但是當兩個對象循環引用的情況下,計數器永遠不為0,是以JVM不使用引用計數算法。
可達性分析算法:以GC Roots為起點開始搜尋,可達的對象都是存活的,不可達的對象可以被回收,JVM使用該算法進行判斷。GC Roots中包含:虛拟機棧中引用的對象、本地方法棧中引用的對象,方法區中靜态成員或常量引用的對象。
20.垃圾收集算法
标記-清除算法(Mark-Sweep)
标記階段:标記的過程實際上就是可達性分析算法過程,周遊GC Roots對象,可達的對象都做好标記,在對象的header中将其記錄為可達。
清除階段:對堆進行周遊,如果發現有某個對象沒有可達對象标記,則回收。
缺點:兩次周遊,效率低;GC運作時需要停止整個程式;産生大量的碎片,需要維護一個空閑清單。
複制算法(Copying)
對象在Survivor區每經曆一次Minor GC,就将對象年齡+1,當對象年齡達到某個值時,對象複制到老年代,預設為15。JVM中Eden和Survivor區的預設比例為8:1:1,保證記憶體使用率為90%,如果每次回收有多于10%的對象存活,Survivor空間可能就不夠用了,此時借用老年代空間。
缺點:複制收集算法在對象存活率高的時候需要進行很多的複制操作,效率會變低,老年代一般不會用該算法。
标記-整理算法
第一階段和标記-清楚算法一樣,第二階段将所有存活的對象壓縮到記憶體的另一端,按順序排放。之後,清理邊界外所有的空間。
缺點:效率不高,不僅要标記存活對象,還要整理所有存活對象的引用位址;移動過程中,要全程暫停使用者應用程式。
分代收集算法
新生代:使用複制算法,因為大量對象需要回收。
老年代:回收的對象很少,是以采用标記清除或者标記整理算法。
21.Double與Float
java語言支援兩種基本的浮點類型:float和double。32位浮點數float用1位表示符号,8位表示指數,用23位表示尾數;64位浮點數double用一位表示符号,11位表示指數,52位表示尾數。在表示超過23位的時候,float就會自動四舍五入,這就是float的精度限制,是以會出現double可以表示而float會不精确的情況,如果要将這兩個浮點數進行轉型,java提供了Float.doubleValue()和Double.floatValue()方法。使用這個方法在單精度轉雙精度的時候,會出現偏差。
浮點運算很少是精确的,隻要超過精度表示範圍就會産生誤差。
解決方法:可以通過String結合BigDecimal或者通過使用long類型來轉換。
22.垃圾收集器
檢視預設垃圾收集器:-XX:+PrintCommandLineFlags
Serial串行收集器:單線程收集器,隻使用一個線程回收垃圾,需要停掉其他所有線程,Client模式下預設新生代垃圾收集器,新生代使用複制算法,老年代使用标記整理算法,Serial
Old也作為CMS收集器的後備垃圾收集方案。JVM參數:-XX:+UseSerialGC
ParNew收集器:Serial的多線程版本,對應的JVM參數:-XX:+UseParNewGC。開啟參數之後,會使用ParNew(新生代)複制算法+Serial
Old(老年代)标記整理算法的組合,Java8之後不再推薦使用這種組合。
Parallel scavenge收集器:新生代和老年代都使用并行,Parallel scavenge收集器可以使用自适應調節政策,把基本的記憶體資料設定好,然後設定是更關注最大停頓時間或者更關注吞吐量,給虛拟機設立一個優化目标。JVM參數是:-XX:+UseParallelGC。新生代使用複制算法,老年代使用标記-整理算法。
CMS收集器:一種以擷取最短回收停頓時間為目标的收集器。JVM參數:-XX:+UseConcMarkSweepGC。使用ParNew(新生代)+CMS(老年代)+Serial
Old(後備)的收集器組合。優點是并發收集,停頓少。缺點是并發會造成CPU的壓力,而且标記清除算法會産生大量空間碎片。
(1)初始标記:标記GC Roots能直接關聯到的對象,速度很快,需要停頓。
(2)并發标記:進行GC Roots Trancing的過程,不需要停頓。
(3)重新标記:修正并發标記期間因為使用者程式繼續運作而導緻變動的那一部分對象重新進行标記,需要停頓。
(4)并發清除:不需要停頓。
G1垃圾收集器:它使得Eden、Survivor和Tenured等記憶體區域不再連續,而變成一個個大小一樣的region,每個region從1M到32M不等。它不再采用CMS的标記清理算法,G1整體上使用标記整理算法,局部上看是基于複制算法。JVM參數:-XX:+UseG1GC。
降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可以預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片内。是因為G1收集器在背景維護了一個優先清單,每次根據允許的收集時間,優先選擇回收價值最大的region。
另:JVM設定參數的方法(win10):環境變量中建立變量JAVA_OPTS,在裡面設定。
23.線程池
我們使用線程的時候去建立一個線程,這種方法非常簡便,但是會導緻一個問題:如果并發的線程數量很多,并且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁的建立線程會大大降低系統效率。
Java中引入了線程池來使得線程可以複用,執行完一個任務不會被立刻銷毀,而是可以繼續執行其他任務。
ThreadPoolExecutor類是線程池技術最核心的類:
其構造器中的參數意義
corePoolSize:核心池大小。在建立線程池之後,預設線程池中是沒有線程的,除非調用prestartAllCoreThreads()或者prestartCoreThread()方法來預建立線程,就是沒有任務到來之前先建立corePoolSize個線程。當線程池中的線程數目到達corePoolSize個之後,就會把到達的任務放到緩存序列中。
maximumPoolSize:非常重要的參數,表示線程池中最多能建立多少個線程。
keepAliveTime:表示線程沒有任務執行時最多保持多久會終止。
unit:參數keepAliveTime的時間機關。
workQueue:阻塞隊列,用來存儲等待執行的任務,會對線程池的運作過程産生重大影響。有三個選擇:ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue,一般使用後兩者。
threadFactory:線程工廠,主要用來建立線程。
handler:表示拒絕處理任務的政策,有四種取值:
(1)ThreadPoolExecutor.AbortPolicy:丢棄任務抛出RejectedExecutionException異常;
(2)ThreadPoolExecutor.DiscardPolicy:丢棄任務,不抛異常
(3)ThreadPoolExecutor.DiscardOldestPolicy:丢棄隊列最前面的任務,然後重新嘗試執行任務(重複該過程)
(4)ThreadPoolExecutor.CallRunsPolicy:由調用線程處理該任務
ThreadPoolExecutor類的方法
execute()和submit():都是送出任務,execute方法用于送出不需要傳回值的任務,無法判斷任務是不是被線程池執行成功;submit送出需要傳回值的任務,線程池傳回future類型的對象以判斷是否執行成功,future對象具有的get()方法可以擷取傳回值。`
shutdown()和shutdownNow():都是關閉線程池,他們的原理是周遊線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,是以無法響應中斷的任務可能永遠無法終止。shutdownNow首先将線程池的狀态設定成STOP,然後嘗試停止所有正在執行或者暫停的線程,并傳回等待執行任務的清單;shutdown隻是将線程池的狀态設定為SHUTDOWN,然後中斷所有沒有執行任務的線程。
如何合理配置設定線程池的大小:CPU密集型任務,一般公式為:最大線程數 = CPU核數+1;IO密集型的最大線程數 = CPU核數 * 2;
實作一個線程池:
public class Test {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("線程池中線程數目:"+executor.getPoolSize()+",隊列中等待執行的任務數目:"+
executor.getQueue().size()+",已執行完别的任務數目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
線程池不允許使用Executors的靜态方法建立,必須通過ThreadPoolExecutor。
線程池的處理流程
當線程池送出一個任務的時候:
(1)線程池判斷核心線程池中的線程是不是都在執行任務,如果不是則建立一個新的工作線程執行任務,否則進入流程(2)
(2)線程池判斷工作隊列是否已滿,如果沒有滿則将新送出的任務存儲在這個任務隊列中,如果工作隊列滿了,則進入流程(3)
(3)線程池判斷池中的線程是否都處在工作狀态,如果沒有則建立一個新的工作線程來執行任務,如果已經滿了就交給拒絕政策(handler)來處理任務。
四種線程池:
(1)newCachedThreadPool 建立一個可以緩存的線程池。
(2)newFixedThreadPool 建立一個定長線程池,可以控制線程最大并發數。
(3)newScheduledThreadPool 建立一個定長線程池,支援定時和周期性任務執行。
(4)newSingleThreadExecutor 建立一個單線程化的線程池,他隻會用唯一的工作線程來執行任務,保證所有任務按照指定順序執行。
//可以緩存的線程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); //需要指定長度
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
詳細實作代碼
24.線程同步和線程通訊
線程同步的五種方式:synchronized的關鍵字修飾方法、靜态資源或者代碼塊;Lock(必須放在try-catch-finally中執行,finally釋放鎖以防止死鎖);wait和notify,必須在synchronized範圍内,被synchronized鎖住的對象就是wait和notify的調用對象;CAS;信号量(Semaphore)。
線程通訊的方式:
(1)wait()、notify()、nofityAll():等待/通知機制。線程A調用了對象O的wait方法進入等待狀态,另一個線程B調用了對象O的notify或notifyAll方法,線程A收到通知之後,從對象O的wait方法中傳回執行後續操作。調用對象的wait方法會導緻線程阻塞,釋放該對象的鎖;調用對象的notify方法會随機解除該對象阻塞的線程,該線程重新嘗試擷取該對象的鎖;從wait方法傳回的前提是獲得了調用對象的鎖;必須在synchronized塊或方法中使用。
(2)condition:Condition用await(),signal,singalAll方法代替wait和notify。notify隻能随機喚醒一個線程,但是用condition可以喚醒指定線程。
(3)管道
(4)volatile
(5)Thread.join:如果一個線程執行了Thread.join(),意味着目前線程A等待thread線程中止之後才從thread.join()傳回。
25.中斷線程
調用一個線程的interrupt()方法來中斷線程,如果該線程處于阻塞、限期等待或者無限期等待狀态,那麼就會抛出InterruptedException,進而提前結束該線程。
如果線程的run()執行一個死循環,并且沒有執行sleep()等會抛出InterruptedException的操作,那麼調用interrupt()方法無法使線程提前結束。但是調用interrupt方法會設定線程的中斷标記,此時調用Thread.interrupted()或Thread.currentThread().isInterrupted()方法會傳回true。是以可以在循環體中使用interrupted()方法判斷線程是否處于中斷狀态,進而提前結束線程。
26.Synchronized的用法
線程安全是Java并發程式設計中的重點,造成線程安全問題主要有兩個原因:一是存在共享資料,二是存在多條線程共同操作共享資料。是以,當存在多個線程操作共享資料的時候,需要保證同一時刻有且隻有線程在操作共享資料,其他線程必須等到該線程處理完才能進行,這種方式叫做互斥鎖。Java中,關鍵字synchronized可以保證在同一時刻,隻有一個線程可以執行某個方法或者某個代碼塊,同時它還可以保證一個線程(共享資料)的變化被其他線程所看到(可見性保證,完全可以替代Volatile功能)
synchronized是Java的關鍵字,是一種同步鎖。
Java的内置鎖(synchronized):每個java對象都可以用做一個實作同步的鎖,這些鎖稱為内置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,退出同步代碼塊的時候會釋放該鎖。獲得内置鎖的唯一途徑就是進入鎖保護的同步代碼塊/方法。
Java的對象鎖和類鎖:在鎖的概念上與内置鎖一緻,但對象鎖是用于對象執行個體方法或對象執行個體上的,類鎖是用于類的靜态方法或者一個類的class對象上的。
Java中每個對象都有一把鎖和兩個隊列,一個隊列用于挂起未獲得鎖的線程,一個隊列用于挂起條件不滿足而等待的線程。synchronized實際上是一個加鎖和釋放鎖的內建。JVM負責跟蹤對象被加鎖的次數。如果一個對象被解鎖,計數歸零。線程第一次給對象加鎖的時候,計數變成1。每當這個相同的線程在此對象上獲得鎖的時候,計數就會遞增。每當任務離開一個synchronized方法,計數就會遞減,為0的時候鎖被完全釋放。
Synchronized有三種應用方式:
修飾一個執行個體方法:被修飾的方法稱為執行個體同步方法,其作用範圍是整個方法,鎖定的事該方法所屬的對象(調用該方法的對象)。所有需要獲得該對象鎖的操作都會對該對象加鎖。
public synchronized void method(){}
//等同于
public void method(){
synchronized(this){
}
}
如果一個對象有多個synchronized方法,隻要一個線程通路了其中的一個synchronized方法,其他線程不能同時通路這個對象中任何一個synchronized方法。
當一個對象O1在不同的線程中執行這個同步方法的時候,會形成互斥。但是O1對象所屬類的另一對象O2是可以調用這個被加了synchronized關鍵字的方法的。其他線程調用O2中的相同方法時不會造成同步阻塞。程式可能在這種情況下擺脫同步機制的控制,造成資料混亂。注意:
(1)synchronized關鍵字不會被繼承:子類覆寫父類帶synchronized方法的時候,必須也要給子類的這個方法顯式的增加synchronized關鍵字。
(2)定義接口的時候不能使用synchronized關鍵字。
(3)構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊完成同步。
修飾一個靜态方法:被修飾的方法被稱為靜态同步方法,其作用域是整個靜态方法,鎖是靜态方法所屬的類。
public synchronized static void method(){}
修飾代碼塊:被修飾的代碼塊被稱為同步語句塊。synchronized的括号中必須傳入一個對象作為鎖,作用範圍是大括号中的代碼,鎖是synchronized括号中的内容,可以分為類鎖和對象鎖
//鎖對象為執行個體對象
public void method(Object o){
synchronized(o){
...
}
}//加入Java開發交流君樣:756584822一起吹水聊天
//鎖對象為類的Class對象
public class Demo{
public static void method(){
synchronized(Demo.class){
...
}
}
}
27.Synchronized的原理
實際上是通過monitor(螢幕)。Java中的同步代碼塊是使用monitorenter和monitorexit指令實作的,其中monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入同步代碼塊的結束位置。
JVM保證這兩個指令成對出現。
當執行monitorenter指令的時候,線程試圖擷取鎖也就是擷取monitor對象的所有權,當計數器為0的時候就可以成功擷取,擷取後将計數器加一。在執行monitorexit指令之後,将鎖計數器減一,表明鎖被釋放。
synchronized修飾方法的時候,沒有monitorenter和monitorexit指令,取而代之的是ACC_SYNCHRONIZED辨別,這個辨別指明這個方法是一個同步方法。
28.Synchronized的四種狀态
無鎖–>偏向鎖–>輕量級鎖–>重量級鎖(過程不可逆)
偏向鎖:大多數情況下,鎖不存在多線程競争,總是由同一線程多次獲得;如果一個線程獲得了鎖,鎖進入偏向模式,此時對象頭的Mark Word結構也變為偏向鎖結構。
對象頭在第十章節中提到過,另外這篇文章講的更詳細。
當該線程再次請求鎖的時候,隻需要檢查Mark Word鎖标記為是否為偏向鎖,以及目前線程ID是不是等于Mark Word的Thread Id即可,省去了大量有關鎖申請的操作。
偏向鎖隻适用于隻有一個線程通路同步塊的場景。
輕量級鎖:當鎖是偏向鎖的時候,被另外的線程所通路,偏向鎖就會更新為輕量級鎖,其他線程會通過自旋的形式嘗試擷取鎖,不會阻塞,進而提高性能。适用于追求響應時間,同步快執行速度非常快的情況。
代碼在進入同步塊的時候,如果同步對象鎖狀态是無鎖,虛拟機首先在目前線程的棧幀中建立鎖記錄(Lock Record)空間,拷貝對象頭的Mark Word複制到鎖記錄中。
之後虛拟機使用CAS操作嘗試将對象的Mark Word更新為指向Lock Record的指針,并将Lock Record的owner指針指向對象的Mark Word。如果這個動作成功了,那麼這個線程就有了該對象的鎖,對象的鎖标記為設定為“00”,說明處于輕量級鎖定狀态。
如果這個動作失敗了,JVM檢查對象的Mark Word是否指向目前線程的棧幀,是則說明目前線程已經擁有了這個對象的鎖,否則說明多個線程競争鎖。
如果有兩個以上的線程競争同一個鎖,輕量級鎖不再有效,膨脹為重量級鎖。
重量級鎖:多線程情況,線程阻塞響應時間緩慢,頻繁的釋放擷取鎖會帶來巨大的性能損耗。适用于追求吞吐量,同步快執行速度較長的情景。
29.Synchronized與重入鎖ReentrantLock的差別
相對與ReentrantLock而言,synchronized鎖是重量級的,而且是内置鎖,意味着JVM可以對synchronized鎖做優化。
在synchronized鎖上阻塞的線程是不可中斷的,而ReentrantLock鎖實作了可中斷的阻塞。
synchronized鎖釋放是自動的,而ReentrantLock需要顯式釋放(在try-finally塊中釋放)\
線程在競争synchronized鎖的時候是非公平的:如果synchronized鎖被線程A占有,線程B請求失敗,被放入隊列中,線程C此時來請求鎖,恰好A在此時釋放了,線程C會跳過隊列中等待的線程B直接獲得這個鎖。但是ReentrantLock可以實作鎖的公平性。
synchronized鎖是讀寫和讀讀都互斥,ReentrankWriteLock分為讀鎖和寫鎖,讀鎖可以同時被多個線程持有,适合于讀多寫少的并發場景。
ReentrantLock隻能鎖代碼塊,但是synchronized可以鎖方法和類。ReentrantLock可以知道線程有沒有拿到鎖,但是synchronized不行。
有關synchronized的參考文章
30.鎖優化
在28章節中,我們提到過重量級鎖,在重量級鎖中,JVM會阻塞未擷取到鎖的線程,在鎖被釋放的時候喚醒這些線程,阻塞和喚醒依賴于作業系統,需要從使用者态切換到核心态,開銷很大。monitor調用了OS底層的互斥量(mutex),切換成本很高。是以JVM引入了自旋的概念。
自旋鎖與自适應自旋鎖,CAS實作:
自旋鎖:很多情況下,共享資料的鎖定狀态持續時間短,切換線程不值得;通過讓線程執行忙循環等待鎖的釋放,不讓出CPU,缺點是如果鎖被其他線程長時間占用,帶來很多開銷。
自适應自旋鎖:自旋的次數不固定,由前一次在同一個鎖上的自旋時間和鎖的擁有者狀态來決定。
優點:自旋鎖不會使線程狀态發生改變,一直處于使用者态,不會使線程阻塞,執行速度快。
CAS(Compare And Swap) 樂觀鎖與悲觀鎖:synchronized操作就是悲觀鎖,這種情況線程一旦得到鎖,其他需要鎖的線程就挂起的情況是悲觀鎖;CAS操作實際上是樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作,如果失敗了就重試,直到成功為止。悲觀在認為程式中的并發情況嚴重,樂觀在于并發情況不那麼嚴重,可以多次嘗試。
鎖消除:虛拟機在即時編譯器運作時,對一些代碼上要求同步而被檢測到實際不可能存在共享資料競争的鎖進行消除。依據是:JVM會判斷一段程式中的同步明顯不會逃逸出去進而被其他線程通路,JVM就把它們當作棧上的資料對待,認為這些資料是線程獨有的。
鎖粗化:在加同步鎖的時候,我們盡量的把同步塊的作用範圍限制到盡量小的範圍。但是如果存在一連串的操作都對同一個對象反複加鎖解鎖,甚至加鎖出現在循環體内,即使沒有線程競争,頻繁的進行互斥同步也會導緻消耗。
public static String test04(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
```
上述連續的append操作就屬于這類情況,jvm檢測到一連串操作都是對同一個對象加鎖,就會把鎖同步範圍擴充(粗化)到整個一系列操作的外部,使得一連串append操作隻需要加一次鎖就可以了。
31.Java設計模式
設計模式是一套被反複使用,多數人知曉的,經過分類編目的,代碼設計經驗的總結。使用設計模式是為了可重用代碼,讓代碼更容易被他人了解。實際上就是在某些場景下,針對某類問題的某種通用的解決方案。
設計模式分為三類:[【參考資料】](https://shimo.im/docs/KRQvWy8QTtPXYhHV)
(1)建立型模式:對象執行個體化的模式,建立型模式用于解耦對象的執行個體化過程。包括單例模式、簡單工廠、抽象工廠等。
(2)結構型模式:把類和對象結合在一起形成一個更大的結構。包括擴充卡模式、組合模式、裝飾模式等。
(3)行為型模式:類和對象如何互動、及劃分責任和算法。包括模闆模式、解釋器模式、觀察者模式等。
單例模式:屬于建立型模式,主要有三種寫法:懶漢式、餓漢式和登記式。
單例模式的特點:
(1)單例類隻能有一個執行個體
(2)單例類必須自己建立自己的唯一執行個體
(3)單例類必須給所有其他對象提供這一執行個體
懶漢式:在第一次調用的時候就執行個體化自己。
public class Singleton{
private Singleton(){}
private static Singleton single = null;
//靜态工廠方法
private static Singleton getInstance(){
if(single == null) single = new Singleton();
}
return single;
}
懶漢式并不考慮線程安全問題,是以他是線程不安全的,并發情況下很可能出現多個Singleton執行個體,要實作線程安全,有以下三個方式:
在getInstance方法上加同步關鍵字:在并發環境下,多個一起進入getInstance裡,因為還沒有執行個體化單例模式,single都是null,就會建立多個Singleton執行個體化對象,破壞了單例模式想要的結果。我們可以在getInstance方法上加synchronized鎖。
public static synchronized Singleton getInstance(){
if(single == null) single = new Singleton();
return single;
}
雙重校驗鎖定:
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null) singleton = new Singleton();
}
}
return singleton;
}
雙重校驗鎖定的單例仍然需要再加上volatile確定線程安全。
靜态同步類:即實作了線程安全,又避免了同步帶來的性能影響。
public class Singleton{
private static class LazyHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return LazyHolder.INSTANCE;
}
}
餓漢式:餓漢式在類建立的同時就已經建立好了一個靜态的對象供系統使用,以後不再改變,是以天生是線程安全的。
public class Singleton1{
private Singleton1(){}
private static final Singleton1 single = new Singleton1();
//靜态工廠方法
public static Singleton1 getInstance(){
return single;
}
}
餓漢就是類一旦加載,就把單例初始化完成,保證getInstance的時候,單例已經存在了;而懶漢比較懶,隻有使用者調用getInstance的時候,才會初始化這個執行個體。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巅峰。
可以的話請給我一個三連支援一下我喲???