若是你們去面Android客戶端崗位,那麼必問Java基礎和Kotlin基礎,因此,我打算花3,4篇文章的樣子來給你們總結下Android面試中會問到的一些Java基礎知識。java
面向過程:面向過程性能比面向對象高。由於對象調用須要實例化,開銷比較大,較消耗資源,因此當性能是最重要的考量因素的時候,好比單片機、嵌入式開發、Linux/Unix 等,通常採用面向過程開發。可是,面向過程沒有面向對象易維護、易複用、易擴展。
面向對象:面向對象易維護、易複用、易擴展。由於面向對象有封裝、繼承、多態性的特性,因此可設計出低耦合的系統,使得系統更加靈活、更加易於維護。面試
那爲何,面向過程性能比面向對象高呢?
面向過程也須要分配內存,計算內存偏移量,Java 性能差的主要緣由並非由於它是面嚮對象語言,而是由於 Java 是半編譯語言,最終的執行代碼並非能夠直接被 CPU 執行的二進制機器碼。而面向過程語言大多都是直接編譯成機器碼在電腦上執行,而且其它一些面向過程的腳本語言性能也並不必定比 Java 好。算法
當 .class 字節碼文件經過 JVM 轉爲機器能夠執行的二進制機器碼時,JVM 類加載器首先加載字節碼文件,而後經過解釋器逐行進行解釋執行,這種方式的執行速度相對比較慢。並且有些方法和代碼塊是反覆被調用的(也就是所謂的熱點代碼),因此後面引進了 JIT 編譯器,而 JIT 屬於運行時編譯。當 JIT 編譯器完成一次編譯後,會將字節碼對應的機器碼保存下來,下次能夠直接調用。這也解釋了咱們爲何常常會說 Java 是編譯與解釋共存的語言。數據庫
Java虛擬機所管理的內存包含程序計數器、Java虛擬機棧、本地方法棧、Java堆和方法區5個部分,模型圖以下圖所示。
編程
因爲Java虛擬機的多線程是經過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器只會執行一條線程中的指令。爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各個線程之間的計數器互不影響,獨立存儲,這類內存區域爲【線程私有】的內存。數組
程序計數器具備以下的特色:緩存
Java虛擬機棧也是線程私有的,它的生命週期與線程的生命週期同步,虛擬機棧描述的是Java方法執行的線程內存模型。每一個方法被執行的時候,Java虛擬機都會同步建立一個內存塊,用於存儲在該方法運行過程當中的信息,每一個方法被調用的過程都對應着一個棧幀在虛擬機中從入棧到出棧的過程。
安全
Java虛擬機棧有以下的特色:服務器
本地方法棧與虛擬機所發揮的做用很類似,區別在於虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則是爲虛擬機使用到的本地方法服務。數據結構
Java堆是虛擬機所管理的內存中最大的一塊,Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。
此內存區域的惟一目的就是存放對象實例,java中「幾乎」全部的對象實例都在這裏分配內存。這裏使用「幾乎」是由於java語言的發展,及時編譯的技術發展,逃逸分析技術的日漸強大,棧上分配、標量替換等優化手段,使java對象實例都分配在堆上變得不那麼絕對。
Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法(G1以後開始變得不同,引入了region,可是依舊採用了分代思想),Java堆中還能夠細分爲:新生代和老年代。再細緻一點的有Eden空間、From Survivor空間、ToSurvivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,簡寫TLAB)。
OOM異常
Java堆的大小既能夠固定也能夠擴展,可是主流的虛擬機,堆的大小都是支持擴展的。若是須要線程請求分配內存,但堆已滿且內存已沒法再擴展時,就拋出 OutOfMemoryError 異常。好比:
/** * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOMTest { public static final int _1MB = 1024 * 1024; public static void main(String[] args) { List<Integer[]> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Integer[] ints = new Integer[2 * _1MB]; list.add(ints); } } }
方法區和Java堆同樣,是各個線程共享的內存區域,他用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。
在 HotSpot JVM 中,永久代(永久代實現方法區)中用於存放類和方法的元數據以及常量池,好比Class和Method。每當一個類初次被加載的時候,它的元數據都會放到永久代中。永久代是有大小限制的,所以若是加載的類太多,頗有可能致使永久代內存溢出,爲此咱們不得不對虛擬機作調優。
後來HotSpot放棄永久代(PermGen),jdk1.7版本中,HotSpot已經把本來放在永久代的字符串常量池、靜態變量等移出,到了jdk1.8,徹底廢棄了永久代,方法區移至元空間(Metaspace)。好比類元信息、字段、靜態屬性、方法、常量等都移動到元空間區。元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。
經常使用的JVM調參以下表:
參數 | 做用描述 |
---|---|
-XX:MetaspaceSize | 分配給Metaspace(以字節計)的初始大小。若是不設置的話,默認是20.79M,這個初始大小是觸發首次 Metaspace Full GC 的閾值,例如 -XX:MetaspaceSize=256M |
-XX:MaxMetaspaceSize | 分配給Metaspace 的最大值,超過此值就會觸發Full GC,此值默認沒有限制,但應取決於系統內存的大小。JVM會動態地改變此值。可是線上環境建議設置,例如-XX:MaxMetaspaceSize=256M |
-XX:MinMetaspaceFreeRatio | 最小空閒比,當 Metaspace 發生 GC 後,會計算 Metaspace 的空閒比,若是空閒比(空閒空間/當前 Metaspace 大小)小於此值,就會觸發 Metaspace 擴容。默認值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40 |
-XX:MaxMetaspaceFreeRatio | 最大空閒比,當 Metaspace 發生 GC 後,會計算 Metaspace 的空閒比,若是空閒比(空閒空間/當前 Metaspace 大小)大於此值,就會觸發 Metaspace 釋放空間。默認值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70 |
運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表,用於存放編譯期間生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。
方法區中存放:類信息、常量、靜態變量、即時編譯器編譯後的代碼。常量就存放在運行時常量池中。
當類被 Java 虛擬機加載後, .class文件中的常量就存放在方法區的運行時常量池中。並且在運行期間,能夠向常量池中添加新的常量。如String類的intern()方法就能在運行期間向常量池中添加字符串常量。
直接內存並非虛擬機運行時數據區的組成部分,在 NIO 中引入了一種基於通道和緩衝的 IO 方式。它能夠經過調用本地方法直接分配Java虛擬機以外的內存,而後經過一個存儲在堆中的DirectByteBuffer對象直接操做該內存,而無須先將外部內存中的數據複製到堆中再進行操做,從而提升了數據操做的效率。
因爲直接內存並不是Java虛擬機的組成部分,所以直接內存的大小不受 Java 虛擬機控制,但既然是內存,若是內存不足時仍是會拋出OutOfMemoryError異常。
下面是直接內存與堆內存的一些異同點:
服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制,從而致使動態擴展時出現OutOfMemoryError異常。
Java的類加載器能夠分爲BootstrapClassLoader、ExtClassLoader和AppClassLoader,它們的做用以下。
類加載會涉及一些加載機制。
Java的內存管理主要涉及三個部分:堆 ( Java代碼可及的 Java堆 和 JVM自身使用的方法區)、棧 ( 服務Java方法的虛擬機棧 和 服務Native方法的本地方法棧 ) 和 保證程序在多線程環境下可以連續執行的程序計數器。
Java堆是進行垃圾回收的主要區域,故其也被稱爲GC堆;而方法區的垃圾回收主要針對的是新生代和中生代。總的來講,堆 (包括Java堆 和 方法區)是 垃圾回收的主要對象,特別是Java堆。
引用計數
每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收。此方法雖然簡單,但沒法解決對象相互循環引用的問題。
可達性分析
從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。在Java中,GC Roots包括:
本地方法棧中 JNI 引用的對象。
標記清除法
如它的名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其缺點進行改進而獲得的。
標記複雜算法有兩個主要的缺點:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
複製算法
複製的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
它的優勢是每次只須要對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。而缺點也是顯而易見的,內存縮小爲原來的一半,持續複製長生存期的對象則致使效率下降。
標記整理法
複製收集算法在對象存活率較高時就要執行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。
根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
分代收集算法
分代收集算法,就是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或「標記-整理」算法來進行回收。
方法重寫
在Java程序中,類的繼承關係能夠產生一個子類,子類繼承父類,它具有了父類全部的特徵,繼承了父類全部的方法和變量。子類能夠定義新的特徵,當子類須要修改父類的一些方法進行擴展,增大功能,程序設計者經常把這樣的一種操做方法稱爲重寫,也叫稱爲覆寫或覆蓋。
方法重寫有以下一些特色:
方法重載
方法重載是讓類以統一的方式處理不一樣類型數據的一種手段。調用方法時經過傳遞給它們的不一樣個數和類型的參數來決定具體使用哪一個方法,這就是多態性。所謂方法重載是指在一個類中,多個方法的方法名相同,可是參數列表不一樣。參數列表不一樣指的是參數個數、參數類型或者參數的順序不一樣。
按值傳遞:值傳遞是指在調用函數時將實際參數複製一份傳遞到函數中,這樣在函數中若是對參數進行修改,將不會影響到實際參數。簡單來講就是直接複製了一份數據過去,由於是直接複製,因此這種方式在傳遞時若是數據量很是大的話,運行效率天然就變低了,因此Java在傳遞數據量很小的數據是值傳遞,好比Java中的各類基本類型:int、float、double、boolean等類型。
引用傳遞:引用傳遞其實就彌補了上面說的不足,若是每次傳參數的時候都複製一份的話,若是這個參數佔用的內存空間太大的話,運行效率會很底下,因此引用傳遞就是直接把內存地址傳過去,也就是說引用傳遞時,操做的其實都是源數據,這樣的話修改有時候會衝突,記得用邏輯彌補下就行了,具體的數據類型就比較多了,好比Object,二維數組,List,Map等除了基本類型的參數都是引用傳遞。
下面是使用hashCode()與equals()的相關規定:
爲何必需要重寫 hashcode 方法?其實就是爲了保證同一個對象,保證在 equals 相同的狀況下 hashcode 值一定相同,若是重寫了 equals 而未重寫 hashcode 方法,可能就會出現兩個沒有關係的對象 equals 相同的(由於 equals 都是根據對象的特徵進行重寫的),但 hashcode 確實不相同的。
相同點:
異同點:
HashMap底層採用了數組+鏈表的數據結構,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的。
若是定位到的數組位置不含鏈表,那麼執行查找、添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,不然新增;對於查找操做來說,仍需遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
HashMap有4個構造器,其餘構造器若是用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值initialCapacity默認爲16,loadFactory默認爲0.75。
public HashMap(int initialCapacity, float loadFactor) { //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230) if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現 }
加載因子存在的緣由,仍是由於減緩哈希衝突,若是初始桶爲16,等到滿16個元素才擴容,某些桶裏可能就有不止一個元素了。因此加載因子默認爲0.75,也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。
Put過程
Get過程
JDK 1.8的HashMap底層採用的是鏈表+紅黑樹,增長一個閾值進行判斷是否將鏈表轉紅黑樹,HashEntry 修改成 Node,目的是解決hash衝突形成的鏈表愈來愈長、查詢慢的問題。
Get過程
Get過程
JDK8中ConcurrentHashMap參考了JDK8 HashMap的實現,採用了數組+鏈表+紅黑樹的實現方式來設計,內部大量採用CAS操做,那什麼是CAS。
CAS是compare and swap的縮寫,中文稱爲【比較交換】。CAS是一種基於鎖的操做,並且是樂觀鎖。在Java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,性能較悲觀鎖有很大的提升。
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和A的值是同樣的,那麼就將內存裏面的值更新成B。CAS是經過無限循環來獲取數據的,若是在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程須要自旋,到下次循環纔有可能機會執行。
Java 按照鎖的實現分爲樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖並非一種真實存在的鎖,而是一種設計思想。
悲觀鎖
悲觀鎖是一種悲觀思想,它總認爲最壞的狀況可能會出現,它認爲數據極可能會被其餘人所修改,因此悲觀鎖在持有數據的時候總會把資源 或者 數據 鎖住,這樣其餘線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放爲止。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。悲觀鎖的實現每每依靠數據庫自己的鎖功能實現。
Java 中的 Synchronized 和 ReentrantLock 等獨佔鎖(排他鎖)也是一種悲觀鎖思想的實現,由於 Synchronzied 和 ReetrantLock 無論是否持有資源,它都會嘗試去加鎖,生怕本身心愛的寶貝被別人拿走。
樂觀鎖
樂觀鎖的思想與悲觀鎖的思想相反,它總認爲資源和數據不會被別人所修改,因此讀取不會上鎖,可是樂觀鎖在進行寫入操做的時候會判斷當前數據是否被修改過(具體如何判斷咱們下面再說)。樂觀鎖的實現方案通常來講有兩種: 版本號機制 和 CAS實現 。樂觀鎖多適用於多度的應用類型,這樣能夠提升吞吐量。
在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。
線程是進程中可獨立執行的最小單位,也是 CPU 資源(時間片)分配的基本單位,同一個進程中的線程能夠共享進程中的資源,如內存空間和文件句柄。線程有一些基本的屬性,如id、name、以及priority。
id:線程 id 用於標識不一樣的線程,編號可能被後續建立的線程使用,編號是隻讀屬性,不能修改。
name:線程的名稱,默認值是 Thread-(id)
daemon:分爲守護線程和用戶線程,咱們能夠經過 setDaemon(true) 把線程設置爲守護線程。守護線程一般用於執行不重要的任務,好比監控其餘線程的運行狀況,GC 線程就是一個守護線程。setDaemon() 要在線程啓動前設置,不然 JVM 會拋出非法線程狀態異常,可被繼承。
priority:線程調度器會根據這個值來決定優先運行哪一個線程(不保證),優先級的取值範圍爲 1~10,默認值是 5,可被繼承。Thread 中定義了下面三個優先級常量:
一個線程被建立後,會經歷從建立到消亡的狀態,下圖是線程狀態的變動過程。
下表是展現了線程的生命週期狀態變化:
狀態 | 說明 |
---|---|
New | 新建立了一個線程對象,但尚未調用start()方法。 |
Runnable | Ready 狀態 線程對象建立後,其餘線程(好比 main 線程)調用了該對象的 start() 方法。該狀態的線程位於可運行線程池中,等待被線程調度選中 獲取 cpu 的使用權。Running 緒狀態的線程在得到 CPU 時間片後變爲運行中狀態(running)。 |
Blocked | 線程由於某種緣由放棄了cpu 使用權(等待鎖),暫時中止運行。 |
Waiting | 線程進入等待狀態由於如下幾個方法: Object#wait()、 Thread#join()、 LockSupport#park() |
Terminated | 該線程已經執行完畢。 |
線程同步和併發一般會問到Synchronized、volatile、Lock的做用。其中,Lock是一個類,而其他兩個則是Java關鍵字。
Synchronized
Synchronized是Java的關鍵字,也是Java的內置特性,在JVM層面實現了對臨界資源的同步互斥訪問,經過對對象的頭文件來操做,從而達到加鎖和釋放鎖的目的。使用Synchronized修飾的代碼或方法,一般有以下特性:
正是由於上面的特性,因此Synchronized的缺點也是顯而易見的:即若是一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,並執行該代碼塊時,其餘線程便只能一直等待,所以效率很低。
volatile
保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。而且volatile是禁止進行指令重排序。
所謂指令重排序,指的是處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
volatile爲了保證原子性,必須具有如下條件:
按照做用的不一樣,Java的鎖能夠分爲以下:
悲觀鎖認爲本身在使用數據的時候必定有別的線程來修改數據,所以在獲取數據的時候會先加鎖,確保數據不會被別的線程修改。Java 中,synchronized 關鍵字和 Lock 的實現類都是悲觀鎖。悲觀鎖適合寫操做多的場景,先加鎖能夠保證寫操做時數據正確。
而樂觀鎖認爲本身在使用數據時不會有別的線程修改數據,因此不會添加鎖,只是在更新數據的時候去判斷以前有沒有別的線程更新了這個數據。若是這個數據沒有被更新,當前線程將本身修改的數據成功寫入。若是數據已經被其餘線程更新,則根據不一樣的實現方式執行不一樣的操做(例如報錯或者自動重試)。樂觀鎖在 Java 中是經過使用無鎖編程來實現,最常採用的是 CAS 算法,Java 原子類中的遞增操做就經過 CAS 自旋實現。樂觀鎖適合讀操做多的場景,不加鎖的特色可以使其讀操做的性能大幅提高。
這裏說到了CAS算法,那麼什麼是CAS算法呢?
一個線程失敗或掛起並不會致使其餘線程也失敗或掛起,那麼這種算法就被稱爲非阻塞算法。而CAS就是一種非阻塞算法實現,也是一種樂觀鎖技術,它能在不使用鎖的狀況下實現多線程安全,所以是一種無鎖算法。
CAS算法的定義:CAS的主要做用是不使用加鎖就能夠實現線程安全,CAS 算法又稱爲比較交換算法,是一種實現併發算法時經常使用到的技術,Java併發包中的不少類都使用了CAS技術。CAS具體包括三個參數:當前內存值V、舊的預期值A、即將更新的值B,當且僅當預期值A和內存值V相同時,將內存值修改成B並返回true,不然什麼都不作,並返回false。
原子更新的基本操做包括:
以AtomicInteger爲例,代碼以下:
public class AtomicInteger extends Number implements java.io.Serializable { //返回當前的值 public final int get() { return value; } //原子更新爲新值並返回舊值 public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //最終會設置成新值 public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue); } //若是輸入的值等於預期值,則以原子方式更新爲新值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //原子自增 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //原子方式將當前值與輸入值相加並返回結果 public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } }
再如,下面是使用多線程對一個int值進行自增操做的代碼,以下所示。
public class AtomicIntegerDemo { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args){ for (int i = 0; i < 5; i++){ new Thread(new Runnable() { public void run() { //調用AtomicInteger的getAndIncement返回的是增長以前的值 System.out.println(atomicInteger.getAndIncrement()); } }).start(); } System.out.println(atomicInteger.get()); } }
阻塞或喚醒一個 Java 線程須要操做系統切換 CPU 狀態來完成,這種狀態轉換須要耗費處理器時間。在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。若是物理機器有多個處理器,可以讓兩個或以上的線程同時並行執行,咱們就可讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。
而爲了讓當前線程【稍等一下】,咱們需讓當前線程進行自旋,若是在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就能夠沒必要阻塞而是直接獲取同步資源,從而避免切換線程的開銷,這就是自旋鎖。
當前線程擁有其餘線程須要的資源,當前線程等待其餘線程已擁有的資源,都不放棄本身擁有的資源。
所謂反射,指的是在運行狀態中,對於任意一個類,都可以獲取這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性,而這種動態獲取的信息以及動態調用對象的方法的功能就被稱爲Java語言的反射機制。
使用反射前須要事先獲取到的字節碼,在Java中,獲取字節碼的方式有三種:
Java 語言中的類、方法、變量、參數和包等均可以被標註。和 Javadoc 不一樣,Java 標註能夠經過反射獲取標註內容。根據做用時機的不一樣,Java的註解能夠分爲三種:
爲了保證只有一個對象存在,可使用單例模式,網上有,單例模式的七種寫法。咱們介紹一下常見的幾種:
懶漢式
懶漢式使用的是static關鍵字,所以是線程不安全的。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
若是要線程安全,那麼須要使用synchronized關鍵字。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
不過,使用synchronized鎖住以後,運行效率明顯下降。
靜態內部類
靜態內部類利用了classloder的機制來保證初始化instance時只有一個線程。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
雙重校驗鎖
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
雙重檢查鎖定是synchronized的升級的寫法,那爲何要使用volatile關鍵字呢,是爲了禁止初始化實例時的重排序。咱們知道,初始化一個實例在java字節碼中會有4個步驟:
然後兩步是有可能會重排序,而使用volatile能夠禁止指令重排序。