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<T> {
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
異常;如果虛擬機??梢詣討B擴展,當擴展到無法申請足夠內存時拋出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
??勺冮L度數組的原理是當元素個數超過數組長度時,產生一個新的數組,將原數組的數據復制到新數組,再將新元素添加到新數組中。 -
增刪改查效率:
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
不能修飾局部變量,但是fina
l可以。
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
,然后中斷所有沒有執行任務的線程。
如何合理分配線程池的大?。篊PU密集型任務,一般公式為:最大線程數 = 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即可,省去了大量有關鎖申請的操作。
偏向鎖只適用于只有一個線程訪問同步塊的場景。
輕量級鎖:當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。適用于追求響應時間,同步快執行速度非??斓那闆r。
代碼在進入同步塊的時候,如果同步對象鎖狀態是無鎖,虛擬機首先在當前線程的棧幀中創建鎖記錄(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不行。
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設計模式
設計模式是一套被反復使用,多數人知曉的,經過分類編目的,代碼設計經驗的總結。使用設計模式是為了可重用代碼,讓代碼更容易被他人理解。實際上就是在某些場景下,針對某類問題的某種通用的解決方案。
設計模式分為三類:
- (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的時候,才會初始化這個實例。
總結
生命不止堅毅魚奮斗,有夢想才是有意義的追求
給大家推薦一個免費的學習交流君樣:756584822
最后,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。
Java開發交流君樣:756584822