Java面試必備八股文整理
JAVA基礎(chǔ)八股文
Switch能支持哪些類型?
jdk5之前,switch能夠作用在byte,short,char,int(實際上都是提升為int)等四個基本類型,jdk5引入了enum(也是int),jdk7引入了字符串(實際上是調(diào)用了string的hashcode,因此本質(zhì)上還是int),但是不能使用long,因為switch 對應(yīng)的 JVM 字節(jié)碼 lookupswitch、tableswitch 指令只支持 int 類型,而long沒法轉(zhuǎn)換為int。
內(nèi)部類有哪幾種?有什么優(yōu)點?
成員內(nèi)部類,局部內(nèi)部類,匿名內(nèi)部類和靜態(tài)內(nèi)部類。
- 靜態(tài)內(nèi)部類。 類內(nèi)部的靜態(tài)類,可以訪問外部類的所有靜態(tài)變量,可以通過new 外部類.靜態(tài)內(nèi)部類()來創(chuàng)建。
- 成員內(nèi)部類。 類內(nèi)部的,成員位置上的非靜態(tài)類??梢栽L問外部類所有變量和方法。創(chuàng)建方式為外部類實例.new 內(nèi)部類()。
- 局部內(nèi)部類。定義在方法內(nèi)的內(nèi)部類,就是局部內(nèi)部類。定義在實例方法中的局部類可以訪問外部類所有的變量和方法,定義在靜態(tài)方法中的局部類只能訪問外部類的靜態(tài)變量和方法。在對應(yīng)方法中new 內(nèi)部類()來創(chuàng)建。
- 匿名內(nèi)部類。沒有名字的內(nèi)部類。其必須繼承一個抽象類或者實現(xiàn)一個接口;不能定義任何靜態(tài)成員和靜態(tài)方法;匿名內(nèi)部類不能訪問外部類未加final修飾的變量(注意:JDK1.8即使沒有用final修飾也可以訪問);匿名內(nèi)部類不能是抽象的。
內(nèi)部類的優(yōu)點:
- 內(nèi)部類可以訪問外部類的內(nèi)容(即使是private)。
- 內(nèi)部類可以模擬出一種“多重繼承”。
- 匿名內(nèi)部類可以方便的定義回調(diào)。
為什么匿名內(nèi)部類不能訪問外部類未加final的變量
匿名內(nèi)部類在生成字節(jié)碼階段,會把涉及的變量作為構(gòu)造函數(shù)的參數(shù),這樣使得在匿名內(nèi)部類中修改的數(shù)據(jù)無法傳遞到外部,因此不能是final。
String,StringBuffer,StringBuilder
- String底層是final char value[]。因此其不可變。
- StringBuffer和StringBuilder都是繼承自AbstractStringBuilder,其底層使用的是char value[](無final),所以可變。StringBuffer是線程安全的(使用synchronized保證),StringBuilder不是。
- 每次對String類型進行改變時,會生成一個新的String對象,然后把指針指向新的String,但是其他兩個都是操作自身。
Java中的異常處理簡介
所有的異常類都有個共同祖先 Throwable。
Error一般是指程序無法處理的錯誤或者不應(yīng)該處理的錯誤。
Exception是程序可以處理的異常。
Exception分為runtime exception和非運行時異常;或者分為受檢異常和非受檢異常。受檢異常是指編譯器必須處理的異常
JAVA反射獲取類的三種方式
- 調(diào)用對象的.getClass()方法
- 調(diào)用某個類的.class屬性
- 使用Class.forName+類的全路徑
JAVA的四種標準元注解
- @Target 修飾對象范圍,例如types, packages
- @Retention 定義保留的時間長短,例如SOURCE,RUNTIME
- @Document描述javadoc
- @Inherited闡述某個被標注的類型是被繼承的。作用:如果一個類用上了@Inherited修飾的注解,那么其子類也會繼承這個注解。但是如果接口用上個@Inherited修飾的注解,其實現(xiàn)類不會繼承這個注解;或者父類的方法用了@Inherited修飾的注解,子類也不會繼承這個注解。這樣操作后,子類可以獲取父類的注解,字段等信息。
collection和map的關(guān)系
Java容器分為collection和map兩大類。
collection里面的子類
主要包括list*包括ArrayList,LinkedList,Vector)和set(主要有HashSet,LinkedSet和TreeSet)
java 容器的快速失?。╢ast-fail)機制
這是通過modCount實現(xiàn)的。遍歷器在遍歷訪問集合中的內(nèi)容時,會維護modCount和expectedModCount。當modCount不等于expoectedModCount時,就會拋出異常。迭代器在調(diào)用next()、remove()方法時都是調(diào)用checkForComodification()方法來進行檢測modCount是否等于expectedModCount。具體來說:在創(chuàng)建迭代器的時候會把對象的modCount的值傳遞給迭代器的expectedModCount,如果創(chuàng)建多個迭代器對一個集合對象進行修改的話,那么就會有一個modCount和多個expectedModCount,且modCount的值之間也會不一樣,這就導(dǎo)致了moCount和expectedModCount的值不一致,從而產(chǎn)生異常。
那么為什么在下列代碼中即使單線程,也會出現(xiàn)快速失敗呢:
for( Integer i:list){ list.remove(i); }
因為當使用for each時,會生成一個iterator來來遍歷該list,同時這個list正在被iterator.remove修改。
解決辦法:
1.在遍歷過程中,所有改變modCount值的地方都加上synchronized。
2.使用CopyOnWriteArrayList來替換ArrayList。CopyOnWriteArrayList是在有寫操作的時候會copy一份數(shù)據(jù),然后寫完再設(shè)置成新的數(shù)據(jù)。
ArrayList擴容機制
簡單來說就是當前容量*1.5+1
Vector和ArrayList
Vector與ArrayList一樣,也是通過數(shù)組實現(xiàn)的,不同的是它支持線程的同步,即某一時刻只有一個線程能夠?qū)慥ector,避免多線程同時寫而引起的不一致性,但實現(xiàn)同步需要很高的花費,因此,訪問它比訪問ArrayList慢。
###LinkedList
LinkedList是用雙向鏈表結(jié)構(gòu)存儲數(shù)據(jù)的,并且通過first和last引用分別指向鏈表的第一個和最后一個元素,很適合數(shù)據(jù)的動態(tài)插入和刪除,隨機訪問和遍歷速度比較慢。另外,他還提供了List接口中沒有定義的方法,專門用于操作表頭和表尾元素,可以當作堆棧、隊列和雙向隊列使用。
HashSet判斷元素相等性的方法
HashSet首先判斷兩個元素的哈希值,如果哈希值一樣,接著會比較equals方法 如果 equls結(jié)果為true ,HashSet就視為同一個元素。如果equals 為false就不是同一個元素。
什么時候需要復(fù)寫hashcode()和compartTo方法
HashMap中實現(xiàn)了一個Entry[]數(shù)組,數(shù)組的每個item是一個單項鏈表的結(jié)構(gòu),當我們put(key, value)的時候,HashMap首先會newItem.key.hashCode()作為該newItem在Entry[]中存儲的下標,要是對應(yīng)的下標的位置上沒有任何item,則直接存儲上去,要是已經(jīng)有oldItem存儲在了上面,那就會判斷oldItem.key.equals(newItem.key),那么要是我們把上面的Person作為key進行存儲的時候,重寫了equals()方法,但沒重寫hashCode()方法,當我們?nèi)ut()的時候,首先會通過hashCode() 計算下標,由于沒有重寫hashCode(),那么實質(zhì)是完整的Object的hashCode(),會受到Object多個屬性的影響,本來equals的兩個Person對象,反而得到了兩個不同的下標。
同樣的,HashMap在get(key)的過程中,也是首先調(diào)用hashCode()計算item的下標,然后在對應(yīng)下標的地方找,要是為null,就返回null,要是 != null,會去調(diào)用equals()方法,比較key是否一致,只有當key一致的時候,才會返回value,要是我們沒有重寫hashCode()方法,本來有的item,反而會找不到,返回null結(jié)果。
所以,要是你重寫了equals()方法,而你的對象可能會放入到散列(HashMap,HashTable或HashSet等)中,那么還必須重寫hashCode(), 如果你的對象有可能放到有序隊列(實現(xiàn)了Comparable)中,那么還需要重寫compareTo()的方法。
TreeSet是什么
TreeSet()是使用二叉樹的原理對新add()的對象按照指定的順序排序(升序、降序),每增加一個對象都會進行排序,將對象插入的二叉樹指定的位置。Integer和String對象都可以進行默認的TreeSet排序,而自定義類的對象是不可以的,自己定義的類必須實現(xiàn)Comparable接口,并且覆寫相應(yīng)的compareTo()函數(shù),才可以正常使用。在覆寫compare()函數(shù)時,要返回相應(yīng)的值才能使TreeSet按照一定的規(guī)則來排序。比較此對象與指定對象的順序。如果該對象小于、等于或大于指定對象,則分別返回負整數(shù)、零或正整數(shù)。
TreeMap
與HashSet和HashMap關(guān)系類似,TreeSet是基于TreeMap實現(xiàn)的。他采用紅黑樹來保存map的每一個Entry。
HashMap的實現(xiàn)
HashMap根據(jù)鍵的hashCode值存儲數(shù)據(jù),大多數(shù)情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導(dǎo)致數(shù)據(jù)的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。我們用下面這張圖來介紹 HashMap 的結(jié)構(gòu)。
JDK7:
大方向上,HashMap 里面是一個數(shù)組,然后數(shù)組中每個元素是一個單向鏈表。上圖中,每個綠色的實體是嵌套類 Entry 的實例,Entry 包含四個屬性:key, value, hash 值和用于單向鏈表的 next。
1. capacity:當前數(shù)組容量,始終保持 2^n,可以擴容,擴容后數(shù)組大小為當前的 2 倍。
2. loadFactor:負載因子,默認為 0.75(負載因子設(shè)置為0.75是為了在大量空間浪費和大量hash沖突之間取得一個平衡)。計算HashMap的實時裝載因子的方法為:size/capacity
3. threshold:擴容的閾值,等于 capacity * loadFactor
4. size:size表示HashMap中存放KV的數(shù)量(為鏈表和樹中的KV的總和)
JDK8:
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 數(shù)組+鏈表+紅黑樹 組成。 根據(jù) Java7 HashMap 的介紹,我們知道,查找的時候,根據(jù) hash 值我們能夠快速定位到數(shù)組的具體下標,但是之后的話,需要順著鏈表一個個比較下去才能找到我們需要的,時間復(fù)雜度取決于鏈表的長度,為 O(n)。為了降低這部分的開銷,在 Java8 中,當鏈表中的元素超過了 8 個以后,會將鏈表轉(zhuǎn)換為紅黑樹 (將鏈表轉(zhuǎn)換成紅黑樹前會判斷,如果當前數(shù)組的長度小于 64,那么會選擇先進行數(shù)組擴容,而不是轉(zhuǎn)換為紅黑樹),在這些位置進行查找的時候可以降低時間復(fù)雜度為 O(logN)。
HashMap是線程不安全的,其主要體現(xiàn):
1.在jdk1.7中,在多線程環(huán)境下,擴容時會造成環(huán)形鏈或數(shù)據(jù)覆蓋。
2.在jdk1.8中,在多線程環(huán)境下,會發(fā)生數(shù)據(jù)覆蓋的情況。
HashMap擴容的時候為什么是2的n次冪?
數(shù)組下標的計算方法是(n - 1) & hash,取余(%)操作中如果除數(shù)是2的冪次則等價于與其除數(shù)減一的與(&)操作(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二進制位操作 &,相對于%能夠提高運算效率,這就解釋了 HashMap 的長度為什么是2的冪次方。
HashMap的put方法:
1.根據(jù)key通過哈希算法與與運算得出數(shù)組下標
2.如果數(shù)組下標元素為空,則將key和value封裝為Entry對象(JDK1.7是Entry對象,JDK1.8是Node對象)并放入該位置。
3.如果數(shù)組下標位置元素不為空,則要分情況
? (i)如果是在JDK1.7,則首先會判斷是否需要擴容,如果要擴容就進行擴容,如果不需要擴容就生成Entry對象,并使用頭插法添加到當前鏈表中。
(ii)如果是在JDK1.8中,則會先判斷當前位置上的TreeNode類型,看是紅黑樹還是鏈表Node
? (a)如果是紅黑樹TreeNode,則將key和value封裝為一個紅黑樹節(jié)點并添加到紅黑樹中去,在這個過程中會判斷紅黑樹中是否存在當前key,如果存在則更新value。
? (b)如果此位置上的Node對象是鏈表節(jié)點,則將key和value封裝為一個Node并通過尾插法插入到鏈表的最后位置去,因為是尾插法,所以需要遍歷鏈表,在遍歷過程中會判斷是否存在當前key,如果存在則更新其value,當遍歷完鏈表后,將新的Node插入到鏈表中,插入到鏈表后,會看當前鏈表的節(jié)點個數(shù),如果大于8,則會將鏈表轉(zhuǎn)為紅黑樹
? (c)將key和value封裝為Node插入到鏈表或紅黑樹后,在判斷是否需要擴容,如果需要擴容,就結(jié)束put方法。
HashMap源碼中在計算hash值的時候為什么要右移16位?
當數(shù)組的長度很短時,只有低位數(shù)的hashcode值能參與運算。而讓高16位參與運算可以更好的均勻散列,減少碰撞,進一步降低hash沖突的幾率。并且使得高16位和低16位的信息都被保留了。
而在這里采用異或運算而不采用& ,| 運算的原因是 異或運算能更好的保留各部分的特征,如果采用&運算計算出來的值的二進制會向1靠攏,采用|運算計算出來的值的二進制會向0靠攏。
計算方式:hash實際上是object.hashCode() ^ object.hashCode() >> 16。數(shù)組下標index = (table.length-1)&hash。table.length-1的目的是把結(jié)果限制在0-table.length-1里面,因為這個值通常不大,所以會丟失高位信息(因為是&操作)。
ConcurrentHashMap
Segment段:
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因為它支持并發(fā)操作,所以要復(fù)雜一些。整個 ConcurrentHashMap 由一個個 Segment 組成,Segment 代表”部分“或”一段“的意思,所以很多地方都會將其描述為分段鎖。注意,行文中,我很多地方用了“槽”來代表一個 segment。
線程安全(Segment 繼承 ReentrantLock 加鎖)
簡單理解就是,ConcurrentHashMap 是一個 Segment 數(shù)組,Segment 通過繼承 ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每個 Segment 是線程安全的,也就實現(xiàn)了全局的線程安全。
concurrencyLevel: 并行級別、并發(fā)數(shù)、Segment 數(shù)。默認是 16,也就是說 ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,最多可以同時支持 16 個線程并發(fā)寫,只要它們的操作分別分布在不同的 Segment 上。這個值可以在初始化的時候設(shè)置為其他值,但是一旦初始化以后,它是不可以擴容的。再具體到每個 Segment 內(nèi)部,其實每個 Segment 很像之前介紹的 HashMap,不過它要保證線程安全,所以處理起來要麻煩些。
和HashMap一樣,ConcurrentHashMap的JDK7和JDK8實現(xiàn)也有區(qū)別。如下圖所示:
可以看到Java8中的ConcurrentHashMap和Java8的HashMap很像,那么他是怎么保證線程安全的呢。java8實現(xiàn)了粒度更細的加鎖,去掉了segment數(shù)組,直接使用synchronized鎖住hash后得到的數(shù)組下標位置中的第一個元素 ,如下圖,這樣加鎖比segment加鎖能支持更高的并發(fā)量。
HashTable
Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,并且是線程安全的,任一時間只有一個線程能寫Hashtable,并發(fā)性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。
各種集合實現(xiàn)的擴容倍數(shù)
名稱 | 倍數(shù) | 備注 |
HashMap | 2倍 | jdk1.7中,擴容后重新計算hash,1.8中,根據(jù),是否仍然在同一個桶中判斷,即e.hash()& oldCap,oldCap為舊容量,當你給定了初始容量值時,會將其擴充到2的冪 |
HashTable | 2倍+1 | 無 |
哪些Map支持key為null
HashMap 、LinkedHashMap 的 key 和 value 都允許為 null。
而ConcurrentHashMap、ConcurrentSkipListMap、Hashtable、TreeMap 的 key 不允許為 null。
select和epoll的區(qū)別
當需要讀兩個以上的I/O的時候,如果使用阻塞式的I/O,那么可能長時間的阻塞在一個描述符上面,另
外的描述符雖然有數(shù)據(jù)但是不能讀出來,這樣實時性不能滿足要求,大概的解決方案有以下幾種:
1.使用多進程或者多線程,但是這種方法會造成程序的復(fù)雜,而且對與進程與線程的創(chuàng)建維護也需要
很多的開銷(Apache服務(wù)器是用的子進程的方式,優(yōu)點可以隔離用戶);
2.用一個進程,但是使用非阻塞的I/O讀取數(shù)據(jù),當一個I/O不可讀的時候立刻返回,檢查下一個是否
可讀,這種形式的循環(huán)為輪詢(polling),這種方法比較浪費CPU時間,因為大多數(shù)時間是不可
讀,但是仍花費時間不斷反復(fù)執(zhí)行read系統(tǒng)調(diào)用;
3.異步I/O,當一個描述符準備好的時候用一個信號告訴進程,但是由于信號個數(shù)有限,多個描述符
時不適用;
4.一種較好的方式為I/O多路復(fù)用,先構(gòu)造一張有關(guān)描述符的列表(epoll中為隊列),然后調(diào)用一個
函數(shù),直到這些描述符中的一個準備好時才返回,返回時告訴進程哪些I/O就緒。select和epoll這
兩個機制都是多路I/O機制的解決方案,select為POSIX標準中的,而epoll為Linux所特有的。
它們的區(qū)別主要有三點:
- select的句柄數(shù)目受限,在linux/posix_types.h頭文件有這樣的聲明: #define __FD_SETSIZE1024 表示select最多同時監(jiān)聽1024個fd。而epoll沒有,它的限制是最大的打開文件句柄數(shù)目;
- epoll的最大好處是不會隨著FD的數(shù)目增長而降低效率,在selec中采用輪詢處理,其中的數(shù)據(jù)結(jié)構(gòu)類似一個數(shù)組的數(shù)據(jù)結(jié)構(gòu),而epoll是維護一個隊列,直接看隊列是不是空就可以了。epoll只會對“活躍”的socket進行操作—這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有”活躍”的socket才會主動的去調(diào)用 callback函數(shù)(把這個句柄加入隊列),其他idle狀態(tài)句柄則不會,在這點上,epoll實現(xiàn)了一個”偽”AIO。但是如果絕大部分的I/O都是“活躍的”,每個I/O端口使用率很高的話,epoll效率不一定比select高(可能是要維護隊列復(fù)雜);
- 使用mmap加速內(nèi)核與用戶空間的消息傳遞。無論是select,poll還是epoll都需要內(nèi)核把FD消息通知給用戶空間,如何避免不必要的內(nèi)存拷貝就很重要,在這點上,epoll是通過內(nèi)核于用戶空間mmap同一塊內(nèi)存實現(xiàn)的。
wait和sleep的共同點和區(qū)別
共同點 :
他們都是在多線程的環(huán)境下,都可以在程序的調(diào)用處阻塞指定的毫秒數(shù),并返回。wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態(tài) ,從而使線程立刻拋出InterruptedException。
如果線程A希望立即結(jié)束線程B,則可以對線程B對應(yīng)的Thread實例調(diào)用interrupt方法。如果此刻線程B正在wait/sleep/join,則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結(jié)束線程。
需要注意的是,InterruptedException是線程自己從內(nèi)部拋出的,并不是interrupt()方法拋出的。對某一線程調(diào)用 interrupt()時,如果該線程正在執(zhí)行普通的代碼,那么該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到 wait()/sleep()/join()后,就會立刻拋出InterruptedException 。
不同點 :
1.每個對象都有一個鎖來控制同步訪問。Synchronized關(guān)鍵字可以和對象的鎖交互,來實現(xiàn)線程的同步。
sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
2.wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用
3.sleep必須捕獲異常,而wait,notify和notifyAll不需要捕獲異常
4.sleep是線程類(Thread)的方法,導(dǎo)致此線程暫停執(zhí)行指定時間,給執(zhí)行機會給其他線程,但是監(jiān)控狀態(tài)依然保持,到時后會自動恢復(fù)。調(diào)用sleep不會釋放對象鎖。
5.wait是Object類的方法,對此對象調(diào)用wait方法導(dǎo)致本線程放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象發(fā)出notify方法(或notifyAll)后本線程才進入對象鎖定池準備獲得對象鎖進入就緒狀態(tài)。
threadLocal簡介
ThreadLocal為變量在每個線程都創(chuàng)建了一個副本。每個線程可以訪問自己內(nèi)部的副本變量。首先,在每個線程Thread內(nèi)部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。
初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
然后在當前線程里面,如果要使用副本變量,就可以通過get方法在threadLocals里面查找。
threadLocaMap為什么是弱引用呢?
每個Thread內(nèi)部都維護一個ThreadLocalMap字典數(shù)據(jù)結(jié)構(gòu),字典的Key值是ThreadLocal,那么當某個ThreadLocal對象不再使用(沒有其它地方再引用)時,每個已經(jīng)關(guān)聯(lián)了此ThreadLocal的線程怎么在其內(nèi)部的ThreadLocalMap里做清除此資源呢?JDK中的ThreadLocalMap又做了一次精彩的表演,它沒有繼承java.util.Map類,而是自己實現(xiàn)了一套專門用來定時清理無效資源的字典結(jié)構(gòu)。其內(nèi)部存儲實體結(jié)構(gòu)Entry<ThreadLocal, T>繼承自java.lan.ref.WeakReference,這樣當ThreadLocal不再被引用時,因為弱引用機制原因,當jvm發(fā)現(xiàn)內(nèi)存不足時,會自動回收弱引用指向的實例內(nèi)存,即其線程內(nèi)部的ThreadLocalMap會釋放其對ThreadLocal的引用從而讓jvm回收ThreadLocal對象。這里是重點強調(diào)下,是回收對ThreadLocal對象,而非整個Entry,所以線程變量中的值T對象還是在內(nèi)存中存在的,所以內(nèi)存泄漏的問題還沒有完全解決。接著分析JDK的實現(xiàn),會發(fā)現(xiàn)在調(diào)用ThreadLocal.get()或者ThreadLocal.set(T)時都會定期執(zhí)行回收無效的Entry操作。
ThreadLocal是如何做到為每一個線程維護變量的副本的呢?
其實實現(xiàn)的思路很簡單:在ThreadLocal類中有一個static聲明的Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應(yīng)線程的變量副本。
synchronized和volatile區(qū)別
1.volatile主要應(yīng)用在多個線程對實例變量更改的場合,刷新主內(nèi)存共享變量的值從而使得各個線程可以獲得最新的值,線程讀取變量的值需要從主存中讀??;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。另外,synchronized還會創(chuàng)建一個內(nèi)存屏障,內(nèi)存屏障指令保證了所有CPU操作結(jié)果都會直接刷到主存中(即釋放鎖前),從而保證了操作的內(nèi)存可見性,同時也使得先獲得這個鎖的線程的所有操作
2.volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的。 volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞,比如多個線程爭搶 synchronized鎖對象時,會出現(xiàn)阻塞。
3.volatile僅能實現(xiàn)變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性,因為線程獲得鎖才能進入臨界區(qū),從而保證臨界區(qū)中的所有語句全部得到 執(zhí)行。
4.volatile標記的變量不會被編譯器優(yōu)化,可以禁止進行指令重排;synchronized標記的變量可以被編譯器優(yōu)化。
java8的新特性
- 接口的默認實現(xiàn)
- lambda表達式
- 注意lambda表達式不能訪問接口的默認方法。
- 函數(shù)式接口 常見的函數(shù)式接口有Predicate,F(xiàn)unction,Optional,Stream,F(xiàn)ilter,Sort,Map,Match,Count,Reduce等
- 方法與構(gòu)造函數(shù)引用
- Date的API
- annotation
java9的新特性
- 模塊系統(tǒng)
- java.net.http,提供了良好的http1和http2的支持
- jshell
- 不可變集合工廠方法
- 私有接口方法
- I/O流新特性(增加了新的方法來讀取和復(fù)制InputStream的內(nèi)容)
- 多版本兼容jar
- 統(tǒng)一JVM日志
- 默認垃圾處理器從Parallel GC切換到了G1
compareTo接口規(guī)范
小于返回<0,等于返回0,大于返回大于0
為什么要用元空間來代替永久代
1、字符串存在永久代中,容易出現(xiàn)性能問題和內(nèi)存溢出。
2、類及方法的信息等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現(xiàn)永久代溢出,太大則容易導(dǎo)致老年代溢出。
3、永久代會為 GC 帶來不必要的復(fù)雜度,并且回收效率偏低。
Java I/O
1.堵塞IO模型。
最傳統(tǒng)的一種 IO 模型,即在讀寫數(shù)據(jù)過程中會發(fā)生阻塞現(xiàn)象。當用戶線程發(fā)出 IO 請求之后,內(nèi)核會去查看數(shù)據(jù)是否就緒,如果沒有就緒就會等待數(shù)據(jù)就緒,而用戶線程就會處于阻塞狀態(tài),用戶線程交出 CPU。當數(shù)據(jù)就緒之后,內(nèi)核會將數(shù)據(jù)拷貝到用戶線程,并返回結(jié)果給用戶線程,用戶線程才解除block 狀態(tài)。典型的阻塞 IO 模型的例子為:data = socket.read();如果數(shù)據(jù)沒有就緒,就會一直阻塞在 read 方法。
2.非堵塞IO模型。
當用戶線程發(fā)起一個 read 操作后,并不需要等待,而是馬上就得到了一個結(jié)果。如果結(jié)果是一個error 時,它就知道數(shù)據(jù)還沒有準備好,于是它可以再次發(fā)送 read 操作。一旦內(nèi)核中的數(shù)據(jù)準備好了,并且又再次收到了用戶線程的請求,那么它馬上就將數(shù)據(jù)拷貝到了用戶線程,然后返回。所以事實上,在非阻塞 IO 模型中,用戶線程需要不斷地詢問內(nèi)核數(shù)據(jù)是否就緒,也就說非阻塞 IO不會交出 CPU,而會一直占用 CPU。但是對于非阻塞 IO 就有一個非常嚴重的問題,在 while 循環(huán)中需要不斷地去詢問內(nèi)核數(shù)據(jù)是否就緒,這樣會導(dǎo)致 CPU 占用率非常高,因此一般情況下很少使用 while 循環(huán)這種方式來讀取數(shù)據(jù)。
3.多路復(fù)用 IO 模型
多路復(fù)用 IO 模型是目前使用得比較多的模型。Java NIO 實際上就是多路復(fù)用 IO。在多路復(fù)用 IO模型中,會有一個線程不斷去輪詢多個 socket 的狀態(tài),只有當 socket 真正有讀寫事件時,才真正調(diào)用實際的 IO 讀寫操作。因為在多路復(fù)用 IO 模型中,只需要使用一個線程就可以管理多個socket,系統(tǒng)不需要建立新的進程或者線程,也不必維護這些線程和進程,并且只有在真正有socket 讀寫事件進行時,才會使用 IO 資源,所以它大大減少了資源占用。在 Java NIO 中,是通過 selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那里,因此這種方式會導(dǎo)致用戶線程的阻塞。多路復(fù)用 IO 模式,通過一個線程就可以管理多個 socket,只有當socket 真正有讀寫事件發(fā)生才會占用資源來進行實際的讀寫操作。因此,多路復(fù)用 IO 比較適合連接數(shù)比較多的情況。
另外多路復(fù)用 IO 為何比非阻塞 IO 模型的效率高?因為在非阻塞 IO 中,不斷地詢問 socket 狀態(tài)時通過用戶線程去進行的,而在多路復(fù)用 IO 中,輪詢每個 socket 狀態(tài)是內(nèi)核在進行的,這個效率要比用戶線程要高的多。
不過要注意的是,多路復(fù)用 IO 模型是通過輪詢的方式來檢測是否有事件到達,并且對到達的事件逐一進行響應(yīng)。因此對于多路復(fù)用 IO 模型來說,一旦事件響應(yīng)體很大,那么就會導(dǎo)致后續(xù)的事件遲遲得不到處理,并且會影響新的事件輪詢。
一般的,IO多路復(fù)用機制都依賴于一個事件多路分離器(Event Demultiplexer)。分離器對象可將來自事件源的IO事件分離出來,并分發(fā)到對應(yīng)的read/write事件處理器。
Reactor模型和Proactor模型
Reactor模型和Proactor模型都是用來處理IO復(fù)用的模型。Reactor采用同步IO,Proactor采用異步IO。
在Reactor模式中,事件分離器負責風帶文件描述符或者socket為讀寫操作準備就緒,并將就緒事件傳遞給對應(yīng)的處理器,最后由處理器負責完成實際的讀寫工作。
在Proactor模式中,處理器或者兼任處理器的事件分離器,只負責發(fā)起異步讀寫操作。IO是由系統(tǒng)完成的。
以讀操作為例,介紹兩者差異:
在reactor中實現(xiàn)讀:
step1: 注冊讀就緒事件和相應(yīng)的事件處理器。
step2:事件分離器等待事件。
step3:事件到來,激活分離器,分離器調(diào)用時間對應(yīng)的處理器。
step4:處理器完成讀操作,注冊新的事件,返還控制權(quán)。
在Proactor中實現(xiàn)讀:
step1:處理器發(fā)出異步讀請求,這時候處理器無視IO就緒事件,關(guān)注的是完成事件。
step2:事件分離器等待操作完成事件。
step3:在分離器等待過程中,操作系統(tǒng)完成讀操作,并將結(jié)果存入用戶自定義緩沖區(qū),通知時間分離器讀操作完成。
step4:事件分離器呼喚處理器。
step5:事件處理器處理用戶自定義緩沖區(qū)中的數(shù)據(jù),然后啟動一個新的異步操作,并將控制權(quán)返回事件分類器。
4.信號驅(qū)動IO模型
在信號驅(qū)動 IO 模型中,當用戶線程發(fā)起一個 IO 請求操作,會給對應(yīng)的 socket 注冊一個信號函數(shù),然后用戶線程會繼續(xù)執(zhí)行,當內(nèi)核數(shù)據(jù)就緒時會發(fā)送一個信號給用戶線程,用戶線程接收到信號之后,便在信號函數(shù)中調(diào)用 IO 讀寫操作來進行實際的 IO 請求操作。
5.異步 IO 模型
異步 IO 模型才是最理想的 IO 模型,在異步 IO 模型中,當用戶線程發(fā)起 read 操作之后,立刻就可以開始去做其它的事。而另一方面,從內(nèi)核的角度,當它受到一個 asynchronous read 之后,它會立刻返回,說明 read 請求已經(jīng)成功發(fā)起了,因此不會對用戶線程產(chǎn)生任何 block。然后,內(nèi)核會等待數(shù)據(jù)準備完成,然后將數(shù)據(jù)拷貝到用戶線程,當這一切都完成之后,內(nèi)核會給用戶線程發(fā)送一個信號,告訴它 read 操作完成了。也就說用戶線程完全不需要實際的整個 IO 操作是如何進行的,只需要先發(fā)起一個請求,當接收內(nèi)核返回的成功信號時表示 IO 操作已經(jīng)完成,可以直接去使用數(shù)據(jù)了。
也就說在異步 IO 模型中,IO 操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內(nèi)核自動完成,然后發(fā)送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調(diào)用 IO 函數(shù)進行具體的讀寫。這點是和信號驅(qū)動模型有所不同的,在信號驅(qū)動模型中,當用戶線程接收到信號表示數(shù)據(jù)已經(jīng)就緒,然后需要用戶線程調(diào)用 IO 函數(shù)進行實際的讀寫操作;而在異步 IO 模型中,收到信號表示 IO 操作已經(jīng)完成,不需要再在用戶線程中調(diào)用 IO 函數(shù)進行實際的讀寫操作。
JAVA NIO
NIO基于Reactor。
NIO主要有三大核心部分:Channel(通道),Buffer(緩沖區(qū)), Selector。傳統(tǒng)IO基于字節(jié)流和字符流進行操作,而NIO基于Channel和Buffer(緩沖區(qū))進行操作,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中,或者從緩沖區(qū)寫入到通道中。Selector(選擇區(qū))用于監(jiān)聽多個通道的事件(比如:連接打開,數(shù)據(jù)到達)。因此,單個線程可以監(jiān)聽多個數(shù)據(jù)通道。如下圖所示:
NIO和傳統(tǒng)IO之間第一個最大的區(qū)別是,IO是面向流的,NIO是面向緩沖區(qū)的。
Java IO面向流意味著每次從流中讀一個或多個字節(jié),直至讀取所有字節(jié),它們沒有被緩存在任何地方。此外,它不能前后移動流中的數(shù)據(jù)。如果需要前后移動從流中讀取的數(shù)據(jù),需要先將它緩存到一個緩沖區(qū)。NIO的緩沖導(dǎo)向方法不同。數(shù)據(jù)讀取到一個它稍后處理的緩沖區(qū),需要時可在緩沖區(qū)中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區(qū)中包含所有您需要處理的數(shù)據(jù)。而且,需確保當更多的數(shù)據(jù)讀入緩沖區(qū)時,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)。
IO的各種流是阻塞的。這意味著,當一個線程調(diào)用read() 或 write()時,該線程被阻塞,直到有一些數(shù)據(jù)被讀取,或數(shù)據(jù)完全寫入。該線程在此期間不能再干任何事情了。 NIO的非阻塞模式,使一個線程從某通道發(fā)送請求讀取數(shù)據(jù),但是它僅能得到目前可用的數(shù)據(jù),如果目前沒有數(shù)據(jù)可用時,就什么都不會獲取。而不是保持線程阻塞,所以直至數(shù)據(jù)變的可以讀取之前,該線程可以繼續(xù)做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數(shù)據(jù)到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用于在其它通道上執(zhí)行IO操作,所以一個單獨的線程現(xiàn)在可以管理多個輸入和輸出通道(channel)。
下面說明下一些名詞的含義:
1.Channel。Channel和IO中的Stream(流)是差不多一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream,而Channel是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。
2.Buffer,故名思意,緩沖區(qū),實際上是一個容器,是一個連續(xù)數(shù)組。Channel提供從文件、網(wǎng)絡(luò)讀取數(shù)據(jù)的渠道,但是讀取或?qū)懭氲臄?shù)據(jù)都必須經(jīng)由Buffer。
客戶端發(fā)送數(shù)據(jù)時,必須先將數(shù)據(jù)存入Buffer中,然后將Buffer中的內(nèi)容寫入通道。服務(wù)端這邊接收數(shù)據(jù)必須通過Channel將數(shù)據(jù)讀入到Buffer中,然后再從Buffer中取出數(shù)據(jù)來處理。
下圖描述了一個從一個客戶端向服務(wù)端發(fā)送數(shù)據(jù),然后服務(wù)端接收數(shù)據(jù)的過程。
3.Selector。Selector類是NIO的核心類,Selector能夠檢測多個注冊的通道上是否有事件發(fā)生,如果有事件發(fā)生,便獲取事件然后針對每個事件進行相應(yīng)的響應(yīng)處理。這樣一來,只是用一個單線程就可以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發(fā)生時,才會調(diào)用函數(shù)來進行讀寫,就大大地減少了系統(tǒng)開銷,并且不必為每個連接都創(chuàng)建一個線程,不用去維護多個線程,并且避免了多線程之間的上下文切換導(dǎo)致的開銷。
Java AIO(NIO.2)
與NIO不同,當進行讀寫操作時,只需要調(diào)用API的Read和Write方法即可這兩個都是異步的,完成后會調(diào)用回調(diào)函數(shù)。
主要在java.nio.channels包下增加了下面四個異步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
其中,對于AsynchronousSocketChannel而言,linux和windows實現(xiàn)方式并不一致,windows上通過IOCP實現(xiàn),即WindowsAsynchronousSocketChannelImpl,實現(xiàn)接口為Iocp.OverlappedChannel;而在Linux上是UnixAsynchronousSocketChannelImpl,實現(xiàn)接口為Port.PollableChannel。
AIO不需要對selector進行輪詢。
Zero copy
例如從文件中讀取數(shù)據(jù)并將其通過網(wǎng)絡(luò)傳輸給其他應(yīng)用程序的的操作需要經(jīng)歷四次內(nèi)核態(tài)和用戶態(tài)的切換。
步驟如下:
1.read()的調(diào)用引起了從用戶態(tài)到內(nèi)核態(tài)的切換(看圖二),內(nèi)部是通過sys_read()(或者類似的方
法)發(fā)起對文件數(shù)據(jù)的讀取。數(shù)據(jù)的第一次復(fù)制是通過DMA(直接內(nèi)存訪問)將磁盤上的數(shù)據(jù)復(fù)制
到內(nèi)核空間的緩沖區(qū)中;
2.數(shù)據(jù)從內(nèi)核空間的緩沖區(qū)復(fù)制到用戶空間的緩沖區(qū)后,read()方法也就返回了。此時內(nèi)核態(tài)又切換
回用戶態(tài),現(xiàn)在數(shù)據(jù)也已經(jīng)復(fù)制到了用戶地址空間的緩存中;
3.socket的send()方法的調(diào)用又會引起用戶態(tài)到內(nèi)核的切換,第三次數(shù)據(jù)復(fù)制又將數(shù)據(jù)從用戶空間緩
沖區(qū)復(fù)制到了內(nèi)核空間的緩沖區(qū),這次數(shù)據(jù)被放在了不同于之前的內(nèi)核緩沖區(qū)中,這個緩沖區(qū)與數(shù)
據(jù)將要被傳輸?shù)降膕ocket關(guān)聯(lián);
4.send()系統(tǒng)調(diào)用返回后,就產(chǎn)生了第四次用戶態(tài)和內(nèi)核態(tài)的切換。隨著DMA單獨異步的將數(shù)據(jù)從內(nèi)
核態(tài)的緩沖區(qū)中傳輸?shù)絽f(xié)議引擎發(fā)送到網(wǎng)絡(luò)上,有了第四次數(shù)據(jù)復(fù)制。
Java中的java.nio.channels.FilleChannel中定義了兩個方法:transferTo和transferFrom,該方法允許將一個通道交叉連接到另一個通道,而不需要通過一個中轉(zhuǎn)緩沖區(qū)來傳遞數(shù)據(jù)。
使用transferTo()方式所經(jīng)歷的步驟:
5.transferTo調(diào)用會引起DMA將文件內(nèi)容復(fù)制到讀緩沖區(qū)(內(nèi)核空間的緩沖區(qū)),然后數(shù)據(jù)從這個緩沖
區(qū)復(fù)制到另一個與socket輸出相關(guān)的內(nèi)核緩沖區(qū)中;
6.第三次數(shù)據(jù)復(fù)制就是DMA把socket關(guān)聯(lián)的緩沖區(qū)中的數(shù)據(jù)復(fù)制到協(xié)議引擎上發(fā)送到網(wǎng)絡(luò)上。
這次改善,是通過將內(nèi)核、用戶態(tài)切換的次數(shù)從四次減少到兩次,將數(shù)據(jù)的復(fù)制次數(shù)從四次減少到
三次(只有一次用到cpu資源)。但這并沒有達到我們零復(fù)制的目標。如果底層網(wǎng)絡(luò)適配器支持收集操作的
話,我們可以進一步減少內(nèi)核對數(shù)據(jù)的復(fù)制次數(shù)。在內(nèi)核為2.4或者以上版本的linux系統(tǒng)上,socket緩
沖區(qū)描述符將被用來滿足這個需求。這個方式不僅減少了內(nèi)核用戶態(tài)間的切換,而且也省去了那次需要
cpu參與的復(fù)制過程。從用戶角度來看依舊是調(diào)用transferTo()方法,但是其本質(zhì)發(fā)生了變化:
7.調(diào)用transferTo方法后數(shù)據(jù)被DMA從文件復(fù)制到了內(nèi)核的一個緩沖區(qū)中;
8.數(shù)據(jù)不再被復(fù)制到socket關(guān)聯(lián)的緩沖區(qū)中了,僅僅是將一個描述符(包含了數(shù)據(jù)的位置和長度等信
息)追加到socket關(guān)聯(lián)的緩沖區(qū)中。DMA直接將內(nèi)核中的緩沖區(qū)中的數(shù)據(jù)傳輸給協(xié)議引擎,消除了
僅剩的一次需要cpu周期的數(shù)據(jù)復(fù)制。
Java多線程
Java線程池
為什么要用線程池
1.降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建、銷毀線程造成的消耗。
2.提高響應(yīng)速度。當任務(wù)到達時,任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。
3.提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一的分配、調(diào)優(yōu)和監(jiān)控。
4.提供更多更強大的功能:線程池具備可拓展性,允許開發(fā)人員向其中增加更多的功能。比如延時定時線程池ScheduledThreadPoolExecutor,就允許任務(wù)延期執(zhí)行或定期執(zhí)行。
線程池的創(chuàng)建方式
線程池的創(chuàng)建方法總共有 7 種,但總體來說可分為 2 類:
一類是通過 ThreadPoolExecutor 創(chuàng)建的線程池;另一個類是通過 Executors 創(chuàng)建的線程池。
線程池的創(chuàng)建方式總共包含以下 7 種(其中 6 種是通過 Executors 創(chuàng)建的,1 種是通過ThreadPoolExecutor 創(chuàng)建的):
Executors.newFixedThreadPool:創(chuàng)建一個固定大小的線程池,可控制并發(fā)的線程數(shù),超出的線程會在隊列中等待;
Executors.newCachedThreadPool:創(chuàng)建一個可緩存的線程池,若線程數(shù)超過處理所需,緩存一段時間后會回收,若線程數(shù)不夠,則新建線程;
Executors.newSingleThreadExecutor:創(chuàng)建單個線程數(shù)的線程池,它可以保證先進先出的執(zhí)行順序;
Executors.newScheduledThreadPool:創(chuàng)建一個可以執(zhí)行延遲任務(wù)的線程池;
Executors.newSingleThreadScheduledExecutor:創(chuàng)建一個單線程的可以執(zhí)行延遲任務(wù)的線程池;
Executors.newWorkStealingPool:創(chuàng)建一個搶占式執(zhí)行的線程池(任務(wù)執(zhí)行順序不確定)【JDK 1.8 添加】。
ThreadPoolExecutor:最原始的創(chuàng)建線程池的方式,它包含了 7 個參數(shù)可供設(shè)置。
參數(shù) | 說明 |
corePoolSize | 核心線程數(shù)量,線程池維護線程的最少數(shù)量 |
maximumPoolSize | 線程池維護線程的最大數(shù)量 |
keepAliveTime | 線程池除核心線程外的其他線程的最長空閑時間,超過該時間的空閑線程會被銷毀 |
unit | keepAliveTime的單位,TimeUnit中的幾個靜態(tài)屬性:NANOSECONDS(納秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒)、MINUTES、HOURS、DAYS |
workQueue | 線程池所使用的任務(wù)緩沖隊列,有如下幾種: 1. ArrayBlockingQueue:一個由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。2.- LinkedBlockingQueue:一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列。3. SynchronousQueue:一個不存儲元素的阻塞隊列,即直接提交給線程不保持它們。 4. PriorityBlockingQueue:一個支持優(yōu)先級排序的無界阻塞隊列。5 . DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列,只有在延遲期滿時才能從中提取元素。6. LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。與SynchronousQueue類似,還含有非阻塞方法。7. LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。 |
threadFactory | 線程工廠,用于創(chuàng)建線程,一般用默認的即可 |
handler | 線程池對拒絕任務(wù)的處理策略,包括如下幾種:1. AbortPolicy:拒絕并拋出異常。2.CallerRunsPolicy:使用當前調(diào)用的線程來執(zhí)行此任務(wù)。3. DiscardOldestPolicy:拋棄隊列頭部(最舊)的一個任務(wù),并執(zhí)行當前任務(wù)。4. DiscardPolicy:忽略并拋棄當前任務(wù)。 |
一般來說,推薦使用ThreadPoolExecutor創(chuàng)建線程池(因為Executor創(chuàng)建的很多線程池,Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導(dǎo)致 OOM。
2)CachedThreadPool:允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導(dǎo)致 OOM。
Callable、Runable、Future、FutureTash
四者之間的關(guān)系:
Callable是Runnable封裝的異步運算任務(wù)。
Future用來保存Callable異步運算的結(jié)果
FutureTask封裝Future的實體類
1.Callable與Runnbale的區(qū)別
a、Callable定義的方法是call,而Runnable定義的方法是run。
b、call方法有返回值,而run方法是沒有返回值的。
c、call方法可以拋出異常,而run方法不能拋出異常。Future
2.Future表示異步計算的結(jié)果,主要是判斷任務(wù)是否完成、中斷任務(wù)、獲取任務(wù)執(zhí)行結(jié)果。
3.FutureTask
可取消的異步計算,此類提供了對Future的基本實現(xiàn),僅在計算完成時才能獲取結(jié)果,如果計算尚未完
成,則阻塞get方法。
FutureTask不僅實現(xiàn)了Future接口,還實現(xiàn)了Runnable接口,所以不僅可以將FutureTask當成一個任務(wù)交給Executor來執(zhí)行,還可以通過Thread來創(chuàng)建一個線程。
線程池工作流程
簡單來說,提交一個任務(wù)到線程池中,線程池的處理流程如下:
1、判斷線程池里的核心線程是否都在執(zhí)行任務(wù),如果不是(核心線程空閑或者還有核心線程沒有被創(chuàng)建)則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果核心線程都在執(zhí)行任務(wù),則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務(wù)存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
3、判斷線程池里的線程是否都處于工作狀態(tài),如果沒有,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果已經(jīng)滿了,則交給飽和策略來處理這個任務(wù)。
線程的生命周期
線程的五個生命周期
新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)五種。
線程的實現(xiàn)有兩種方式,一是繼承Thread類,二是實現(xiàn)Runnable接口,但無論如何,當我們new了這個對象后。線程就進入了初始狀態(tài);當該對象調(diào)用了start()方法,就進入可執(zhí)行狀態(tài);進入可執(zhí)行狀態(tài)后,當該對象被操作系統(tǒng)選中。獲得CPU時間片就會進入執(zhí)行狀態(tài);進入執(zhí)行狀態(tài)后情況就比較復(fù)雜了
4.1. run()方法或main()方法結(jié)束后,線程就進入終止狀態(tài);
4.2. 當線程調(diào)用了自身的sleep()方法或其它線程的join()方法,就會進入堵塞狀態(tài)(該狀態(tài)既停止當前線程,但并不釋放所占有的資源)。當sleep()結(jié)束或join()結(jié)束后。該線程進入可執(zhí)行狀態(tài),繼續(xù)等待OS分配時間片;
4.3. 線程調(diào)用了yield()方法,意思是放棄當前獲得的CPU時間片,回到可執(zhí)行狀態(tài),這時與其它進程處于同等競爭狀態(tài),OS有可能會接著又讓這個進程進入執(zhí)行狀態(tài)。
4.4. 當線程剛進入可執(zhí)行狀態(tài)(注意,還沒執(zhí)行),發(fā)現(xiàn)將要調(diào)用的資源被synchroniza(同步),獲取不到鎖標記。將會馬上進入鎖池狀態(tài),等待獲取鎖標記(這時的鎖池里或許已經(jīng)有了其它線程在等待獲取鎖標記,這時它們處于隊列狀態(tài),既先到先得),一旦線程獲得鎖標記后,就轉(zhuǎn)入可執(zhí)行狀態(tài)。等待OS分配CPU時間片。
4.5. 當線程調(diào)用wait()方法后會進入等待隊列(進入這個狀態(tài)會釋放所占有的全部資源,與堵塞狀態(tài)不同)。進入這個狀態(tài)后。是不能自己主動喚醒的,必須依靠其它線程調(diào)用notify()或notifyAll()方法才干被喚醒(因為notify()僅僅是喚醒一個線程,但我們由不能確定詳細喚醒的是哪一個線程?;蛟S我們須要喚醒的線程不可以被喚醒,因此在實際使用時,一般都用notifyAll()方法,喚醒有所線程),線程被喚醒后會進入鎖池。等待獲取鎖標記。
僵死進程
什么是僵死進程:
僵死進程就是指子進程退出時,父進程并未對其發(fā)出的SIGCHLD信號進行適當處理,導(dǎo)致子進程停留在僵死狀態(tài)等待其父進程,這個狀態(tài)下的子進程就是僵死進程。這個僵死進程不占有內(nèi)存,也不會執(zhí)行代碼,更不能被調(diào)用,他只是在進程列表中占了個地位而已。
如何結(jié)束僵死進程:
他只需要父進程調(diào)用wait()函數(shù)來替他收尸然后就完整的結(jié)束這一生。否則會一直保存這個僵死的狀態(tài)。把他父進程給kill了,這樣他就變成了一個孤兒進程,父親沒了沒人替他收拾,這時候僵死進程就會被過繼給一個名叫init()進程,這個進程會給他收尸。
JAVA多線程并發(fā)
JAVA的鎖
1.互斥同步鎖
1)Synchorized
2)ReentrantLock
互斥同步鎖也叫做阻塞同步鎖,特征是會對沒有獲取鎖的線程進行阻塞。
要理解互斥同步鎖,首選要明白什么是互斥什么是同步。簡單的說互斥就是非你即我,同步就是順序訪問?;コ馔芥i就是以互斥的手段達到順序訪問的目的。操作系統(tǒng)提供了很多互斥機制比如信號量,互斥量,臨界區(qū)資源等來控制在某一個時刻只能有一個或者一組線程訪問同一個資源。
Java里面的互斥同步鎖就是Synchorized和ReentrantLock,前者是由語言級別實現(xiàn)的互斥同步鎖,理解和寫法簡單但是機制笨拙,在JDK6之后性能優(yōu)化大幅提升,即使在競爭激烈的情況下也能保持一個和ReentrantLock相差不多的性能,所以JDK6之后的程序選擇不應(yīng)該再因為性能問題而放棄synchorized。
ReentrantLock是API層面的互斥同步鎖,需要程序自己打開并在finally中關(guān)閉鎖,和synchorized相比更加的靈活,體現(xiàn)在三個方面:等待可中斷,公平鎖以及綁定多個條件。但是如果程序猿對
ReentrantLock理解不夠深刻,或者忘記釋放lock,那么不僅不會提升性能反而會帶來額外的問題。另外synchorized是JVM實現(xiàn)的,可以通過監(jiān)控工具來監(jiān)控鎖的狀態(tài),遇到異常JVM會自動釋放掉鎖。而ReentrantLock必須由程序主動的釋放鎖。
互斥同步鎖都是可重入鎖,好處是可以保證不會死鎖。但是因為涉及到核心態(tài)和用戶態(tài)的切換,因此比較消耗性能。JVM開發(fā)團隊在JDK5-JDK6升級過程中采用了很多鎖優(yōu)化機制來優(yōu)化同步無競爭情況下鎖的性能。比如:自旋鎖和適應(yīng)性自旋鎖,輕量級鎖,偏向鎖,鎖粗化和鎖消除。
2.非堵塞同步
原子類(CAS)
非阻塞同步鎖也叫樂觀鎖,相比悲觀鎖來說,它會先進行資源在工作內(nèi)存中的更新,然后根據(jù)與主存中舊值的對比來確定在此期間是否有其他線程對共享資源進行了更新,如果舊值與期望值相同,就認為沒有更新,可以把新值寫回內(nèi)存,否則就一直重試直到成功。它的實現(xiàn)方式依賴于處理器的機器指令:
CAS(Compare And Swap)
JUC中提供了幾個Automic類以及每個類上的原子操作就是樂觀鎖機制
不激烈情況下,性能比synchronized略遜,而激烈的時候,也能維持常態(tài)。激烈的時候,Atomic的性能會優(yōu)于ReentrantLock一倍左右。但是其有一個缺點,就是只能同步一個值,一段代碼中只能出現(xiàn)一個 Atomic的變量,多于一個同步無效。因為他不能在多個Atomic之間同步。
非阻塞鎖是不可重入的,否則會造成死鎖。
3.無同步方案
1)可重入代碼
在執(zhí)行的任何時刻都可以中斷-重入執(zhí)行而不會產(chǎn)生沖突。特點就是不會依賴堆上的共享資源
2)ThreadLocal/Volaitile
線程本地的變量,每個線程獲取一份共享變量的拷貝,單獨進行處理。
4.線程本地存儲
如果一個共享資源一定要被多線程共享,可以盡量讓一個線程完成所有的處理操作,比如生產(chǎn)者消費者模式中,一般會讓一個消費者完成對隊列上資源的消費。典型的應(yīng)用是基于請求-應(yīng)答模式的web服務(wù)器的設(shè)計
SynchronousQueue原理
SynchronousQueue 是一個隊列,但它的特別之處在于它內(nèi)部沒有容器。其中的一個生產(chǎn)線程,當它生產(chǎn)產(chǎn)品(即put的時候),如果當前沒有人想要消費產(chǎn)品(即當前沒有線程執(zhí)行take),此生產(chǎn)線程必須阻塞,等待一個消費線程調(diào)用take操作,take操作將會喚醒該生產(chǎn)線程,同時消費線程會獲取生產(chǎn)線程的產(chǎn)品(即數(shù)據(jù)傳遞),這樣的一個過程稱為一次配對過程(當然也可以先take后put,原理是一樣的)。SynchronousQueue的實現(xiàn)并不依賴AQS(AbstractQueuedSynchronizer)而是使用CAS。
SynchronousQueue的內(nèi)部實現(xiàn)了兩個類,一個是TransferStack類,使用LIFO順序存儲元素,這個類用于非公平模式;還有一個類是TransferQueue,使用FIFI順序存儲元素,這個類用于公平模式。這兩個類繼承自"Nonblocking Concurrent Objects with Condition Synchronization"算法,此算法是由W. N. Scherer III 和 M. L. Scott提出的,關(guān)于此算法的理論內(nèi)容在這個網(wǎng)站中:http://www.cs.rochester.edu/u/scott/synchronization/pseudocode/duals.html。兩個類的性能差不多,F(xiàn)IFO通常用于在競爭下支持更高的吞吐量,而LIFO在一般的應(yīng)用中保證更高的線程局部性。
JVM
JVM相關(guān)知識
JVM內(nèi)存模型
JVM包括五塊數(shù)據(jù)區(qū)域:
參數(shù) | 說明 |
方法區(qū) | 方法區(qū)是各個線程共享的內(nèi)存區(qū)域,用于存儲被虛擬機加載的類型信息,常量,靜態(tài)變量,即時編譯器編譯后的代碼緩存等數(shù)據(jù),其中包括運行時常量池(用于存放編譯器生成的各種字面量和符號引用 ) |
虛擬機棧 | 線程私有,描述的是Java方法執(zhí)行的線程內(nèi)存模型,每個方法被執(zhí)行的時候,Java虛擬機都會同步創(chuàng)建一個棧幀用于存儲局部變量,操作數(shù)棧,動態(tài)連接,方法出口等信息。每一個方法被調(diào)用直到執(zhí)行完畢的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程,其中含有局部變量表,存放Java基本數(shù)據(jù)類型,對象引用和returnAddress(指向了一條字節(jié)碼指令的地址) |
本地方法棧 | 結(jié)構(gòu)與虛擬機棧相似,但是是服務(wù)于本地方法的 |
程序計數(shù)器 | 線程私有,可以看成當前線程所執(zhí)行的字節(jié)碼的行號指示器。 |
JAVA堆 | 線程共享,此內(nèi)存區(qū)域用于存放對象實例 |
直接內(nèi)存 | 直接內(nèi)存并不是 JVM 運行時數(shù)據(jù)區(qū)的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存, 然后使用DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在 Java堆和 Native 堆中來回復(fù)制數(shù)據(jù), 因此在一些場景中可以顯著提高性能。 |
其中,JVM堆從GC角度看看能被分為新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年代。如下圖所示:
名詞解釋:
1.新生代。是用來存放新生的對象。一般占據(jù)堆的 1/3 空間。由于頻繁創(chuàng)建對象,所以新生代會頻繁觸發(fā)MinorGC 進行垃圾回收。新生代又分為 Eden 區(qū)、ServivorFrom、ServivorTo 三個區(qū)。
2.Eden區(qū)。Java 新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大,則直接分配到老年代)。當 Eden 區(qū)內(nèi)存不夠的時候就會觸發(fā) MinorGC,對新生代區(qū)進行一次垃圾回收。
3.ServivorFrom。 上一次 GC 的幸存者,作為這一次 GC 的被掃描者。
4.ServivorTo。保留了一次 MinorGC 過程中的幸存者。
5.老年代。主要存放應(yīng)用程序中生命周期長的內(nèi)存對象。老年代的對象比較穩(wěn)定,所以 MajorGC 不會頻繁執(zhí)行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導(dǎo)致空間不夠用時才觸發(fā)。當無法找到足夠大的連續(xù)空間分配給新創(chuàng)建的較大對象時也會提前觸發(fā)一次 MajorGC 進行垃圾回收騰出空間。 MajorGC 采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產(chǎn)生內(nèi)存碎片,為了減少內(nèi)存損耗,我們一般需要進行合并或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。
6.永久代。指內(nèi)存的永久保存區(qū)域,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時候被放入永久區(qū)域,它和存放實例的區(qū)域不同,GC 不會在主程序運行期對永久區(qū)域進行清理。所以這也導(dǎo)致了永久代的區(qū)域會隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常。在 Java8 中,永久代已經(jīng)被移除,被一個稱為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代。元空間的本質(zhì)和永久代類似,元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機中,而是使用本地內(nèi)存。因此,默認情況下,元空間的大小僅受本地內(nèi)存限制。類的元數(shù)據(jù)放入 native memory, 字符串池和類的靜態(tài)變量放入 java 堆中(從1.7開始),這樣可以加載多少類的元數(shù)據(jù)就不再由MaxPermSize 控制, 而由系統(tǒng)的實際可用空間來控制。除了OOM的原因,元空間取代永久代的原因是為了合并Hotspot和JRockit的代碼(http://openjdk.java.net/jeps/122Motivation)。
元數(shù)據(jù)的定義:
- It’s the model of the loaded class base that Java retains at runtime in order to dynamically load, link, JIT compile, and execute Java code.
- Different design choices you make when writing your code can significantly expand or contract the amount of metadata Java needs to retain.
- The JVM can give you a breakdown of metadata storage costs for individual structures that model each loaded class, allowing you to weigh and compare the costs of alternative designs.
這部分參考資料:https://developers.redhat.com/blog/2018/02/14/java-class-metadata
GC過程:
新生代GC采用復(fù)制算法。稱為Minor GC。
1.首先,把 Eden 和 ServivorFrom 區(qū)域中存活的對象復(fù)制到 ServicorTo 區(qū)域(如果對象的年齡已經(jīng)達到了老年的標準,則賦值到老年代區(qū)),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區(qū))。
2.清空 eden、servicorFrom,以及互換這兩個區(qū)域名字。
JVM線程模型
一般來說,線程模型有三種,分別是:
1.內(nèi)核線程模型。
完全由操作系統(tǒng)內(nèi)核提供的內(nèi)核線程(Kernel-Level Thread ,KLT)來實現(xiàn)多線程。在此模型下,線程的切換調(diào)度由系統(tǒng)內(nèi)核完成,系統(tǒng)內(nèi)核負責將多個線程執(zhí)行的任務(wù)映射到各個CPU中去執(zhí)行。操作系統(tǒng)內(nèi)核提供的內(nèi)核線程(Kernel-Level Thread ,KLT)來實現(xiàn)多線程。在此模型下,線程的切換調(diào)度由系統(tǒng)內(nèi)核完成,系統(tǒng)內(nèi)核負責將多個線程執(zhí)行的任務(wù)映射到各個CPU中去執(zhí)行。由于內(nèi)核線程的支持,每個輕量級進程都成為一個獨立的調(diào)度單元,即使有一個輕量級進程在系統(tǒng)調(diào)用中阻塞了,也不會影響整個進程繼續(xù)工作,但是輕量級進程具有它的局限性:
首先,由于是基于內(nèi)核線程實現(xiàn)的,所以各種線程操作,如創(chuàng)建、析構(gòu)及同步,都需要進行系統(tǒng)調(diào)用。而系統(tǒng)調(diào)用的代價相對較高,需要在用戶態(tài)(User Mode)和內(nèi)核態(tài)(Kernel Mode)中來回切換。
其次,每個輕量級進程都需要有一個內(nèi)核線程的支持,因此輕量級進程要消耗一定的內(nèi)核資源(如內(nèi)核線程的??臻g),因此一個系統(tǒng)支持輕量級進程的數(shù)量是有限的。
2.用戶線程模型。
用戶線程指的是完全建立在用戶空間的線程庫上,系統(tǒng)內(nèi)核不能感知線程存在的實現(xiàn)。用戶線程的建立、同步、銷毀和調(diào)度完全在用戶態(tài)中完成,不需要內(nèi)核的幫助。如果程序?qū)崿F(xiàn)得當,這種線程不需要切換到內(nèi)核態(tài),因此操作可以是非常快速且低消耗的,也可以支持規(guī)模更大的線程數(shù)量,部分高性能數(shù)據(jù)庫中的多線程就是由用戶線程實現(xiàn)的。這種進程與用戶線程之間1:N的關(guān)系稱為一對多的線程模型。使用用戶線程的優(yōu)勢在于不需要系統(tǒng)內(nèi)核支援,劣勢也在于沒有系統(tǒng)內(nèi)核的支援,所有的線程操作都需要用戶程序自己處理。線程的創(chuàng)建、切換和調(diào)度都是需要考慮的問題,而且由于操作系統(tǒng)只把處理器資源分配到進程,那諸如“阻塞如何處理”、“多處理器系統(tǒng)中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。因而使用用戶線程實現(xiàn)的程序一般都比較復(fù)雜,此處所講的“復(fù)雜”與“程序自己完成線程操作”,并不限制程序中必須編寫了復(fù)雜的實現(xiàn)用戶線程的代碼,使用用戶線程的程序,很多都依賴特定的線程庫來完成基本的線程操作,這些復(fù)雜性都封裝在線程庫之中,除了以前在不支持多線程的操作系統(tǒng)中(如DOS)的多線程程序與少數(shù)有特殊需求的程序外,現(xiàn)在使用用戶線程的程序越來越少了,Java、Ruby等語言都曾經(jīng)使用過用戶線程,最終又都放棄使用它。
3.混合線程模型
線程除了依賴內(nèi)核線程實現(xiàn)和完全由用戶程序自己實現(xiàn)之外,還有一種將內(nèi)核線程與用戶線程一起使用的實現(xiàn)方式。在這種混合實現(xiàn)下,既存在用戶線程,也存在輕量級進程。用戶線程還是完全建立在用戶空間中,因此用戶線程的創(chuàng)建、切換、析構(gòu)等操作依然廉價,并且可以支持大規(guī)模的用戶線程并發(fā)。而操作系統(tǒng)提供支持的輕量級進程則作為用戶線程和內(nèi)核線程之間的橋梁,這樣可以使用內(nèi)核提供的線程調(diào)度功能及處理器映射,并且用戶線程的系統(tǒng)調(diào)用要通過輕量級線程來完成,大大降低了整個進程被完全阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數(shù)量比是不定的,即為N:M的關(guān)系。許多UNIX系列的操作系統(tǒng),如Solaris、HP-UX等都提供了N:M的線程模型實現(xiàn)。
對于Sun JDK來說,它的Windows版與Linux版都是使用一對一的線程模型(即內(nèi)核線程模型)實現(xiàn)的,一條Java線程就映射到一條輕量級進程之中,因為Windows和Linux系統(tǒng)提供的線程模型就是一對一的。
JVM 允許一個應(yīng)用并發(fā)執(zhí)行多個線程。Hotspot JVM 中的 Java 線程與原生操作系統(tǒng)線程有直接的映射關(guān)系。當線程本地存儲、緩沖區(qū)分配、同步對象、棧、程序計數(shù)器等準備好以后,就會創(chuàng)建一個操作系統(tǒng)原生線程。Java 線程結(jié)束,原生線程隨之被回收。操作系統(tǒng)負責調(diào)度所有線程,并把它們分配到任何可用的 CPU 上。當原生線程初始化完畢,就會調(diào)用 Java 線程的 run() 方法。當線程結(jié)束時,會釋放原生線程和 Java 線程的所有資源。
Hotspot JVM 后臺運行的系統(tǒng)線程主要有下面幾個:
線程名 | 功能 |
虛擬機線程(VM thread) | 這個線程等到 JVM 到達安全點操作出現(xiàn)。這些操作的類型有:stop-the-world 垃圾回收、線程棧 dump、線程暫停、線程偏向鎖(biased locking)解除。具體來說:The VMThread spends its time waiting for operations to appear in the VMOperationQueue, and then executing those operations. Typically these operations are passed on to the VMThread because they require that the VM reach a safepoint before they can be executed. In simple terms, when the VM is at safepoint all threads inside the VM have been blocked, and any threads executing in native code are prevented from returning to the VM while the safepoint is in progress. This means that the VM operation can be executed knowing that no thread can be in the middle of modifying the Java heap, and all threads are in a state such that their Java stacks are unchanging and can be examined.The most familiar VM operation is for garbage collection, or more specifically for the “stop-the-world” phase of garbage collection that is common to many garbage collection algorithms. But many other safepoint based VM operations exist, for example: biased locking revocation, thread stack dumps, thread suspension or stopping (i.e. The java.lang.Thread.stop() method) and numerous inspection/modification operations requested through JVMTI.Many VM operations are synchronous, that is the requestor blocks until the operation has completed, but some are asynchronous or concurrent, meaning that the requestor can proceed in parallel with the VMThread (assuming no safepoint is initiated of course).Safepoints are initiated using a cooperative, polling-based mechanism. In simple terms, every so often a thread asks “should I block for a safepoint?”. Asking this question efficiently is not so simple. One place where the question is often asked is during a thread state transition. Not all state transitions do this, for example a thread leaving the VM to go to native code, but many do. The other places where a thread asks are in compiled code when returning from a method or at certain stages during loop iteration. Threads executing interpreted code don’t usually ask the question, instead when the safepoint is requested the interpreter switches to a different dispatch table that includes the code to ask the question; when the safepoint is over, the dispatch table is switched back again. Once a safepoint has been requested, the VMThread must wait until all threads are known to be in a safepoint-safe state before proceeding to execute the VM operation. During a safepoint the Threads_lock is used to block any threads that were running, with the VMThread finally releasing the Threads_lock after the VM operation has been performed. |
周期性任務(wù)線程 | 這線程負責定時器事件(也就是中斷),用來調(diào)度周期性操作的執(zhí)行。 |
GC線程 | 這些線程支持 JVM 中不同的垃圾回收活動。 |
編譯器線程 | 這些線程在運行時將字節(jié)碼動態(tài)編譯成本地平臺相關(guān)的機器碼。 |
信號分發(fā)線程 | 這個線程接收發(fā)送到 JVM 的信號并調(diào)用適當?shù)?JVM 方法處理。 |
此部分參考資料: https://openjdk.java.net/groups/hotspot/docs/RuntimeOverview.html
垃圾回收
GC Roots
可達性分析中GC Roots對象包括下列幾種:
- 虛擬機棧引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI引用的對象。
- 虛擬機內(nèi)部的引用,比如基本數(shù)據(jù)類型對應(yīng)的class對象。
- 所有同步鎖持有的對象。
- 反應(yīng)虛擬機內(nèi)部情況的JMXBean、JVMTI注冊的回調(diào),本地代碼緩存等。
- 局部回收時,某個區(qū)域中的對象完全可能被其他區(qū)域的對象所引用,所以關(guān)聯(lián)區(qū)域的對象也一并加入GC Roots。
由于目前主流Java虛擬機使用的都是準確式垃圾收集(指虛擬機可以準確知道一段值是代碼還是數(shù)據(jù)),因此虛擬機并不需要一個不漏的檢查完所有執(zhí)行上下文和全局的位置,而是有辦法直接得到哪些地方存著對象的引用。Hotspot使用OopMap來完成任務(wù),一旦類加載完成,HotSpot就會把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來。
OopMap 記錄了棧上本地變量到堆上對象的引用關(guān)系。其作用是:垃圾收集時,收集線程會對棧上的內(nèi)存進行掃描,看看哪些位置存儲了 Reference 類型。如果發(fā)現(xiàn)某個位置確實存的是 Reference 類型,就意味著它所引用的對象這一次不能被回收。但問題是,棧上的本地變量表里面只有一部分數(shù)據(jù)是 Reference 類型的(它們是我們所需要的),那些非 Reference 類型的數(shù)據(jù)對我們而言毫無用處,但我們還是不得不對整個棧全部掃描一遍,這是對時間和資源的一種浪費。
一個很自然的想法是,能不能用空間換時間,在某個時候把棧上代表引用的位置全部記錄下來,這樣到真正 gc 的時候就可以直接讀取,而不用再一點一點的掃描了。事實上,大部分主流的虛擬機也正是這么做的,比如 HotSpot ,它使用一種叫做 OopMap 的數(shù)據(jù)結(jié)構(gòu)來記錄這類信息。
我們知道,一個線程意味著一個棧,一個棧由多個棧幀組成,一個棧幀對應(yīng)著一個方法,一個方法里面可能有多個安全點。 gc 發(fā)生時,程序首先運行到最近的一個安全點停下來,然后更新自己的 OopMap ,記下棧上哪些位置代表著引用。枚舉根節(jié)點時,遞歸遍歷每個棧幀的 OopMap ,通過棧中記錄的被引用對象的內(nèi)存地址,即可找到這些對象( GC Roots )。
可以把oopMap簡單理解成是調(diào)試信息。在源代碼里面每個變量都是有類型的,但是編譯之后的代碼就只有變量在棧上的位置了。oopMap就是一個附加的信息,告訴你棧上哪個位置本來是個什么東西。 這個信息是在JIT編譯時跟機器碼一起產(chǎn)生的。因為只有編譯器知道源代碼跟產(chǎn)生的代碼的對應(yīng)關(guān)系。 每個方法可能會有好幾個oopMap,就是根據(jù)safepoint把一個方法的代碼分成幾段,每一段代碼一個oopMap,作用域自然也僅限于這一段代碼。 循環(huán)中引用多個對象,肯定會有多個變量,編譯后占據(jù)棧上的多個位置。那這段代碼的oopMap就會包含多條記錄。
通過上面的解釋,我們可以很清楚的看到使用 OopMap 可以避免全棧掃描,加快枚舉根節(jié)點的速度。但這并不是它的全部用意。它的另外一個更根本的作用是,可以幫助 HotSpot 實現(xiàn)準確式 GC 。
但是隨著而來的又有一個問題,就是在方法執(zhí)行的過程中, 可能會導(dǎo)致引用關(guān)系發(fā)生變化,那么保存的OopMap就要隨著變化。如果每次引用關(guān)系發(fā)生了變化都要去修改OopMap的話,這又是一件成本很高的事情。所以這里就引入了安全點的概念。
什么是安全點?OopMap的作用是為了在GC的時候,快速進行可達性分析,所以O(shè)opMap并不需要一發(fā)生改變就去更新這個映射表。只要這個更新在GC發(fā)生之前就可以了。所以O(shè)opMap只需要在預(yù)先選定的一些位置上記錄變化的OopMap就行了。這些特定的點就是SafePoint(安全點)。由此也可以知道,程序并不是在所有的位置上都可以進行GC的,只有在達到這樣的安全點才能暫停下來進行GC。
既然安全點決定了GC的時機,那么安全點的選擇就至為重要了。安全點太少,會讓GC等待的時間太長,太多會浪費性能。所以安全點的選擇是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準的,所以我們這里了解一下結(jié)果就行了。一般會在如下幾個位置選擇安全點:
- 循環(huán)的末尾
- 方法臨返回前 / 調(diào)用方法的call指令后
- 可能拋異常的位置
安全點另一方面問題:如何讓垃圾收集時所有線程都到達安全點:主要有兩種方案:搶先式中斷和主動式中斷。
搶斷式中斷就是在GC的時候,讓所有的線程都中斷,如果這些線程中發(fā)現(xiàn)中斷地方不在安全點上的,就恢復(fù)線程,讓他們重新跑起來,直到跑到安全點上。
主動式中斷在GC的時候,不會主動去中斷線程,僅僅是設(shè)置一個標志,當程序運行到安全點時就去輪訓(xùn)該位置,發(fā)現(xiàn)該位置被設(shè)置為真時就自己中斷掛起。所以輪訓(xùn)標志的地方是和安全點重合的,另外創(chuàng)建對象需要分配內(nèi)存的地方也需要輪詢該位置。
但是安全點也不是完美的,對于處于Sleep或者block的線程,無法到達安全點,因此引入了安全區(qū)域進行處理。
四種引用類型
1.強引用。即傳統(tǒng)引用。
2.軟引用。用來描述還有用,但非必須的對象。僅在系統(tǒng)內(nèi)存即將溢出時回收。SoftReference類。常用于緩存等場景。
3.弱引用。必須程度小于軟引用。在下一次垃圾收集時回收。WeakReference類。ThreadLocal類。
4.虛引用。最弱的引用,一個對象是否有虛引用的存在,不會對其生存時間構(gòu)成影響,也無法通過虛引用獲取一個對象實例。虛引用一般用于在一個對象被回收時收到一個系統(tǒng)通知。虛引用一般和一個隊列綁定。PhantomReference類。虛引用主要用來跟蹤對象被垃圾回收的活動。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象之前,把這個虛引用加入到與之關(guān)聯(lián)的引用隊列中。程序如果發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動。
常見垃圾回收算法
1.標記清除算法(mark-sweep)。標記所有需要回收的對象,并在標記完成后,統(tǒng)一回收所有被標記的對象。會產(chǎn)生內(nèi)存碎片化嚴重,后續(xù)可能發(fā)生大對象不能找到可利用空間的問題,并且如果Java堆中包含大量需要回收的對象,必須進行大量標記和清除的動作。
2.復(fù)制算法。 為了解決 Mark-Sweep 算法內(nèi)存碎片化的缺陷而被提出的算法。按內(nèi)存容量將內(nèi)存劃分為等大小的兩塊。每次只使用其中一塊,當這一塊內(nèi)存滿后將尚存活的對象復(fù)制到另一塊上去,把已使用的內(nèi)存清掉。這種算法雖然實現(xiàn)簡單,內(nèi)存效率高,不易產(chǎn)生碎片,但是最大的問題是可用內(nèi)存被壓縮到了原本的一半。且存活對象增多的話,Copying 算法的效率會大大降低。
3.標記整理算法(Mark-Compact)。結(jié)合了以上兩個算法,為了避免缺陷而提出。標記階段和 Mark-Sweep 算法相同,標記后不是清理對象,而是將存活對象移向內(nèi)存的一端。然后清除端邊界外的對象。
4.分代收集算法。分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根據(jù)對象存活的不同生命周期將內(nèi)存劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據(jù)不同區(qū)域選擇不同的算法。
目前大部分 JVM 的 GC 對于新生代都采取 Copying 算法,因為新生代中每次垃圾回收都要回收大部分對象,即要復(fù)制的操作比較少,但通常并不是按照 1:1 來劃分新生代。一般將新生代劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Survivor , To Survivor ),每次使用Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的對象復(fù)制到另一塊 Survivor 空間中,Eden:From Survivor:To Survivor =8:1:1。而老年代因為每次只回收少量對象,因而采用 Mark-Compact 算法。當對象在 Survivor 區(qū)躲過一次 GC 后,其年齡就會+1。默認情況下年齡到達 15 的對象會被移到老生代中。
目前的分代回收器中,新生代占總空間的1/3,老年代占2/3.。
實際上,并不是內(nèi)存被耗空的時候才拋出OutOfMemoryException,而是JVM98%的時間都花費在內(nèi)存回收,每次回收的內(nèi)存小于2%滿足時就拋出異常。
常見的垃圾回收器
Serial
用于新生代,單線程,STW時間長。
SerialOld
用于老年代,單線程的。一樣有STW。
ParallelScavenger
在Serial基礎(chǔ)上,使用了多線程。
ParallelOld
應(yīng)用于老年代,多線程的。一樣有STW。
ParNew
與ParallelScavenger的區(qū)別就是,PN能更好的和CMS配合使用,PN的響應(yīng)時間優(yōu)先,PS的吞吐量優(yōu)先。
CMS
三色標記:
黑色標記:自己已經(jīng)標記,直接引用的對象區(qū)域已經(jīng)標記
灰色標記:自己標記完成,但引用區(qū)域沒來得及標記
白色標記:沒有遍歷到的區(qū)域,可以理解為沒有標記
三色標記的漏標問題:
原本A引用B,B引用C,在A被標記為黑色后,A建立了指向C的引用,且B指向C的引用斷開。此時A已經(jīng)被標記為黑色,則不會再便利A的引用,且B對C的引用已經(jīng)斷開。這樣會造成C未被標記,被垃圾回收,造成空指針問題。
CMS解決方式:
當發(fā)生A引用C<黑色對象指向了白色對象>的情況。此時將A標記為灰色。并且在全部標記結(jié)束后,會有一個remark階段,必須從頭掃描一遍。發(fā)生在重新標記階段,所以在這個階段必須STW。
CMS有4個主要階段:初始標記階段、并發(fā)標記階段、重新標記階段、并發(fā)清理階段。
初始標記階段:開始一個短暫的STW,然后進行初始標記,初始標記只標記根節(jié)點,所以這個STW時間很短。這個階段是單線程的。
并發(fā)標記階段:據(jù)說GC大部分時間都浪費在這,所以這一步是和工作線程同時運行的。一邊產(chǎn)生垃圾,一邊標記垃圾。
重新標記階段:開始一個短暫的STW,把在并發(fā)標記階段,工作線程產(chǎn)生的垃圾重新標記一下,這個STW時間也很短。這個階段是多線程的。
并發(fā)清理階段:工作線程和垃圾回收線程同時運行,把這個時候工作線程產(chǎn)生的垃圾叫做浮動垃圾,浮動垃圾就要等到下一次CMS清理了。CMS標記的一個實例:
CMS是老年代的垃圾回收器,在老年代分配不下的時候,觸發(fā)CMS。
CMS的最大問題:CMS會使內(nèi)存碎片化,老年代產(chǎn)生了很多的碎片,然后從年輕代過來的對象無法找到空間,造成了promotion failed。這時候,CMS既沒有機會進行垃圾回收,又放不下新來的對象,在這種情況下,CMS會調(diào)用SerialOld來進行垃圾回收。
G1
G1在邏輯上分代,在物理上不分代。G1引入了分而治之的思想,把內(nèi)存分為一個一個的小塊(region)。每個region邏輯上屬于下面四種分代中的一種。
四種分代:
a.Old區(qū):老對象
b.Survivor區(qū):存活對象
c.Eden區(qū):新生對象
d.Humongous區(qū):大對象,如果這個對象特別大,可能會跨兩個region。
針對三色標記漏標問題,G1解決方案是:
當發(fā)生B指向C的引用<指針>斷開的時候。將這個引用<指針>記錄在一個特定區(qū)域。垃圾回收線程在掃描的時候會查看這個區(qū)域的增量數(shù)據(jù)。發(fā)現(xiàn)有增量數(shù)據(jù)。會看C此時還有沒有對象指向他,如果有則將區(qū)域C標記為灰色。否則不標記<視為可回收垃圾>。
ZGC
邏輯和物理上都不分代,在并發(fā)標記的時候采用的是顏色指針。42位記錄地址,4位代表狀態(tài),18位的空閑。
垃圾回收器的選擇
按內(nèi)存大小分:
類加載機制
上圖為JVM加載Java類的過程。
加載
加載是類加載過程中的一個階段,這個階段會在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的入口。注意這里不一定非得要從一個Class文件獲取,這里既可以從ZIP包中讀?。ū热鐝膉ar包和war包中讀?。?,從網(wǎng)絡(luò)中獲取(Web Applet),可以在運行時計算生成(動態(tài)代理),也可以由其它文件生成(比如將JSP文件轉(zhuǎn)換成對應(yīng)的Class類),從數(shù)據(jù)庫讀?。ɡ缒承┲虚g件服務(wù)器),從加密文件中獲取(用來防止Class文件被窺探)。用戶可以通過自定義的ClassLoader來完成類的加載。
對于數(shù)據(jù)類來說,其空間是Java直接在內(nèi)存中動態(tài)構(gòu)造的,但是類加載器還是得加載其中的元素類型。其遵循一下規(guī)律。
如果數(shù)組的組件類型是引用類型。遞歸采用本節(jié)定義的加載過程去加載組件類型。數(shù)組C將會標識再加載該組件類型的類加載器的類名稱空間上(因為一個類型必須與類加載器一起確定其唯一性)。
如果數(shù)組的組件類型不是引用類型,例如(int),Java虛擬機會把數(shù)組C標記為與引導(dǎo)類加載器管理。
數(shù)組類的可訪問行與它的組件類型的可訪問行一致。如果組件類型不是引用類型,他的數(shù)組類的可訪問行將默認為public,可被所有的類和接口訪問到,
驗證
這一階段的主要目的是為了確保Class文件的字節(jié)流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
驗證主要包括:
文件格式驗證。主要是字節(jié)流是否符合Class文件格式的規(guī)范。
元數(shù)據(jù)驗證。對字節(jié)碼所描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范。
字節(jié)碼驗證。通過數(shù)據(jù)流分析和控制流分析,確認程序語義是合法的,符合邏輯的,不會危害虛擬機。
符號引用驗證。發(fā)生在虛擬機將符號引用轉(zhuǎn)換為直接引用的時候。
準備
準備階段是正式為類變量(被static修飾的變量)分配內(nèi)存并設(shè)置類變量的初始值階段,即在方法區(qū)中分配這些變量所使用的內(nèi)存空間。
解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。符號引用主要有CONSTANT_Class_info, Constant_Fieldref_Info,Constant_Methodref_Info等類型的常量。
符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現(xiàn)。符號引用與虛擬機的內(nèi)存布局無關(guān),引用的目標并不一定加載到內(nèi)存中。在Java中,一個java類將會編譯成一個class文件。在編譯時,java類并不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時People類并不知道Language類的實際內(nèi)存地址,因此只能使用符號org.simple.Language(假設(shè)是這個,當然實際中是由類似于CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機實現(xiàn)的內(nèi)存布局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規(guī)范的Class文件格式中。
初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導(dǎo)。到了初始階段,才開始真正執(zhí)行類中定義的Java程序代碼。類初始化主要執(zhí)行類構(gòu)造器<clinit>方法。
<clinit>方法是由編譯器自動收集類中所有類變量的賦值語句和靜態(tài)語句(也就是static{}中的語句)合并產(chǎn)生的,收集順序就是文件中出現(xiàn)的順序。靜態(tài)語句塊可以為出現(xiàn)在其后的靜態(tài)變量賦值,但是不能訪問。
以下幾種情況不會進行初始化:
通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。
定義對象數(shù)組,不會觸發(fā)該類的初始化。
常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類。
通過類名獲取Class對象,不會觸發(fā)類的初始化。
通過Class.forName加載指定類時,如果指定參數(shù)initialize為false時,也不會觸發(fā)類初始化,其實這個參數(shù)是告訴虛擬機,是否要對類進行初始化。
通過ClassLoader默認的loadClass方法,也不會觸發(fā)初始化動作。
類加載器和雙親委派模型
啟動類加載器(Bootstrap ClassLoader)。C++實現(xiàn),負責加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
擴展類加載器(Extension ClassLoader)。負責加載 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。
應(yīng)用程序類加載器(Application ClassLoader)。負責加載用戶路徑(classpath)上的類庫。
當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類去完成,每一個層次類加載器都是如此,因此所有的加載請求都應(yīng)該傳送到啟動類加載其中,只有當父類加載器反饋自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器才會嘗試自己去加載。 采用雙親委派的一個好處是比如加載位于rt.jar包中的類java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個Object對象。
雙親委派模型的好處:
安全性,避免了核心類被替換避免了類的重復(fù)加載,因為同一個類可以被不同的classLoader加載。
一些特點:
1.全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入。
2.父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時,才使用本類加載器從自己的類路徑中加載該類。
3.緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效。
java對象內(nèi)存相關(guān)
內(nèi)存分配機制
類加載完成后會在java堆中劃分區(qū)域分配給對象。分配包含兩種方式:
指針碰撞。如果Java堆的內(nèi)存是規(guī)整,即所有用過的內(nèi)存放在一邊,而空閑的放在另一邊。分配內(nèi)存時將位于中間的指針指示器向空閑的內(nèi)存移動一段與對象大小相等的距離,這樣便完成分配內(nèi)存工作??臻e列表。如果Java堆的內(nèi)存不是規(guī)整的,則需要由虛擬機維護一個列表來記錄哪些內(nèi)存是可用的,這樣在分配的時候可以從列表中查詢到足夠大的內(nèi)存分配給對象,并在分配后更新列表記錄。
選擇哪種分配方式是由 Java 堆是否規(guī)整來決定的,而 Java 堆是否規(guī)整又由所 采用的垃圾收集器是否帶有壓縮整理功能決定。
那怎么解決內(nèi)存分配的并發(fā)問題呢?
一方面對分配內(nèi)存空間的行為進行同步處理(采用CAS+失敗重試來保證同步);
另一方面給每個進程在java堆中預(yù)分配一小塊空間(TLAB,Thread Local Allocation Buffer),對象現(xiàn)在tlab上分配空間而TLAB的分配才需要用到同步鎖。
從分區(qū)角度看,內(nèi)存一般在eden區(qū)分配,如果空間不夠進行一次minor GC,如果還不夠則啟用擔保機制在老年代分配。特別的,對于大對象,直接進入老年代。
訪問內(nèi)存中的對象
目前有兩種方式:句柄訪問和直接指針。
句柄訪問是指:Java堆中劃分出一塊內(nèi)存來作為句柄池,引用中存儲對象的句柄地址,而句柄中包含了對象實例數(shù)據(jù)(實例池)與對象類型數(shù)據(jù)(方法區(qū))各自的具體地址信息。這種方法的優(yōu)勢:引用中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而引用本身不需要修改。
直接指針:如果使用直接指針訪問,引用中存儲的直接就是對象地址,那么Java堆對象內(nèi)部的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息。優(yōu)勢:速度更快,節(jié)省了一次指針定位的時間開銷。由于對象的訪問在Java中非常頻繁,因此這類開銷積少成多后也是非??捎^的執(zhí)行成本。HotSpot中采用的就是這種方式。
JVM調(diào)優(yōu)
JVM調(diào)優(yōu)可以考慮在以下幾個方面進行:
線程池:解決用戶響應(yīng)時間長的問題
連接池
JVM啟動參數(shù):調(diào)整各代的內(nèi)存比例和垃圾回收算法,提高吞吐量
程序算法:改進程序邏輯算法提高性能
GC調(diào)優(yōu)
在GC調(diào)優(yōu)之前,我們需要記住下面的原則:
- 多數(shù)的Java應(yīng)用不需要在服務(wù)器上進行GC優(yōu)化;
- 多數(shù)導(dǎo)致GC問題的Java應(yīng)用,都不是因為我們參數(shù)設(shè)置錯誤,而是代碼問題;
- 在應(yīng)用上線之前,先考慮將機器的JVM參數(shù)設(shè)置到最優(yōu)(最適合);
- 減少創(chuàng)建對象的數(shù)量;
- 減少使用全局變量和大對象;
- GC優(yōu)化是到最后不得已才采用的手段;
- 在實際使用中,分析GC情況優(yōu)化代碼比優(yōu)化GC參數(shù)要多得多;
GC優(yōu)化的目的有兩個:
- 將轉(zhuǎn)移到老年代的對象數(shù)量降低到最??;
- 減少full GC的執(zhí)行時間;
為了達到上面的目的,一般地,你需要做的事情有:
- 減少使用全局變量和大對象;
- 調(diào)整新生代的大小到最合適;
- 設(shè)置老年代的大小為最合適;
- 選擇合適的GC收集器;
到此這篇關(guān)于Java面試必備八股文整理的文章就介紹到這了,更多相關(guān)Java面試八股文內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 并發(fā)編程:volatile的使用及其原理解析
下面小編就為大家?guī)硪黄狫ava 并發(fā)編程:volatile的使用及其原理解析。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-05-05IntelliJ Idea 2020.1 正式發(fā)布,官方支持中文(必看)
這篇文章主要介紹了IntelliJ Idea 2020.1 正式發(fā)布,官方支持中文了,本文通過截圖的形式給大家展示,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04SpringCloud集成Eureka并實現(xiàn)負載均衡的過程詳解
這篇文章主要給大家詳細介紹了SpringCloud集成Eureka并實現(xiàn)負載均衡的過程,文章通過代碼示例和圖文講解的非常詳細,對大家的學(xué)習(xí)或工作有一定的參考價值,需要的朋友可以參考下2023-11-11BigDecimal的toString()、toPlainString()和toEngineeringString()區(qū)
使用BigDecimal進行打印的時候,經(jīng)常會對BigDecimal提供的三個toString方法感到好奇,以下整理3個toString方法的區(qū)別及用法,需要的朋友可以參考下2023-08-08hibernate關(guān)于session的關(guān)閉實例解析
這篇文章主要介紹了hibernate關(guān)于session的關(guān)閉實例解析,分享了相關(guān)代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-02-02Java之jdbc連接mysql數(shù)據(jù)庫的方法步驟詳解
這篇文章主要介紹了Java之jdbc連接mysql數(shù)據(jù)庫的方法步驟詳解,JCBC技術(shù)是java開發(fā)必備的只是,jdbc連接mysql數(shù)據(jù)庫,這是一個比較簡單的方法,有興趣的可以了解一下2020-07-07