java核心技術36講

Java-Basic

談談final、 finally、 finalize有什麼不一樣?

典型回答:
final能夠用來修飾類、方法、變量,分別有不一樣的意義, final修飾的class表明不能夠繼承擴展, final的變量是不能夠修改的,而final的方法也是不能夠重寫的( override)。html

finally則是Java保證重點代碼必定要被執行的一種機制。咱們能夠使用try-finally或者try-catch-finally來進行相似關閉JDBC鏈接、保證unlock鎖等動做。java

finalize是基礎類java.lang.Object的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。 finalize機制如今已經不推薦使用,而且在JDK 9開始被標記
爲deprecated。node

強引用、軟引用、弱引用、幻象引用有什麼區別?具體使用場景是什麼?

不一樣的引用類型,主要體現的是對象不一樣的可達性( reachable)狀態和對垃圾收集的影響。web

所謂強引用( 「Strong」 Reference),就是咱們最多見的普通對象引用,只要還有強引用指向一個對象,就能代表對象還「活着」,垃圾收集器不會碰這種對象。對於一個普通的對
象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲null,就是能夠被垃圾收集的了,固然具體回收時機仍是要看垃圾收集策略。面試

軟引用( SoftReference),是一種相對強引用弱化一些的引用,可讓對象豁免一些垃圾收集,只有當JVM認爲內存不足時,纔會去試圖回收軟引用指向的對象。 JVM會確保在拋
出OutOfMemoryError以前,清理軟引用指向的對象。軟引用一般用來實現內存敏感的緩存,若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩
存的同時,不會耗盡內存。
andriod中的圖片緩存是軟引用的例子.算法

弱引用( WeakReference)並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就能夠用來構建一種沒有特定約束的關係,好比,維護一種非強制性
的映射關係,若是試圖獲取時對象還在,就使用它,不然重現實例化。它一樣是不少緩存實現的選擇。
ThreadLocal中entry的Key是弱引用的例子.docker

對於幻象引用,有時候也翻譯成虛引用,你不能經過它訪問對象。幻象引用僅僅是提供了一種確保對象被fnalize之後,作某些事情的機制,好比,一般用來作所謂的PostMortem清理機制,我在專欄上一講中介紹的Java平臺自身Cleaner機制等,也有人利用幻象引用監控對象的建立和銷燬。shell

理解Java的字符串, String、 StringBufer、 StringBuilder有什麼區別?

String是Java語言很是基礎和重要的類,提供了構造和管理字符串的各類基本邏輯。它是典型的Immutable類,被聲明成爲fnal class,全部屬性也都是fnal的。也因爲它的不可
變性,相似拼接、裁剪字符串等動做,都會產生新的String對象。因爲字符串操做的廣泛性,因此相關操做的效率每每對應用性能有明顯影響。數據庫

StringBufer是爲解決上面提到拼接產生太多中間對象的問題而提供的一個類,咱們能夠用append或者add方法,把字符串添加到已有序列的末尾或者指定位置。 StringBufer本
質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,因此除非有線程安全的須要,否則仍是推薦使用它的後繼者,也就是StringBuilder。編程

StringBuilder是Java 1.5中新增的,在能力上和StringBufer沒有本質區別,可是它去掉了線程安全的部分,有效減少了開銷,是絕大部分狀況下進行字符串拼接的首選。

String是Immutable類的典型實現,原生的保證了基礎線程安全,由於你沒法對它內部數據進行任何修改,這種便利甚至體如今拷貝構造函數中,因爲不可
變, Immutable對象在拷貝時不須要額外複製數據。

爲了實現修改字符序列的目的, StringBufer和StringBuilder底層都是利用可修改的( char, JDK 9之後是byte)數組,兩者都繼承了AbstractStringBuilder,裏面包含了基本
操做,區別僅在於最終的方法是否加了synchronized。

談談Java反射機制,動態代理是基於什麼原理?

典型回答:
反射機制是Java語言提供的一種基礎功能,賦予程序在運行時自省( introspect,官方用語)的能力。經過反射咱們能夠直接操做類或者對象,好比獲取某個對象的類定義,獲取類
聲明的屬性和方法,調用方法或者構造對象,甚至能夠運行時修改類定義。

動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,不少場景都是利用相似機制作到的,好比用來包裝RPC調用、面向切面的編程( AOP)。

實現動態代理的方式不少,好比JDK自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其餘的實現方式,好比利用傳說中更高性能的字節碼操做機制,類
似ASM、 cglib(基於ASM)、 Javassist等。

咱們知道Spring AOP支持兩種模式的動態代理, JDK Proxy或者cglib,若是咱們選擇cglib方式,你會發現對接口的依賴被克服了。

cglib動態代理採起的是建立目標類的子類的方式,由於是子類化,咱們能夠達到近似使用被調用者自己的效果。

int和Integer有什麼區別?談談Integer的值緩存範圍。

典型回答:
int是咱們常說的整形數字,是Java的8個原始數據類型( Primitive Types, boolean、 byte 、 short、 char、 int、 foat、 double、 long)之一。 Java語言雖然號稱一切都是對象,
但原始數據類型是例外。

Integer是int對應的包裝類,它有一個int類型的字段存儲數據,而且提供了基本操做,好比數學運算、 int和字符串之間轉換等。在Java 5中,引入了自動裝箱和自動拆箱功能
( boxing/unboxing), Java能夠根據上下文,自動進行轉換,極大地簡化了相關編程。

關於Integer的值緩存,這涉及Java 5中另外一個改進。構建Integer對象的傳統方式是直接調用構造器,直接new一個對象。可是根據實踐,咱們發現大部分數據操做都是集中在有
限的、較小的數值範圍,於是,在Java 5中新增了靜態工廠方法valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照Javadoc, 這個值默認緩存
是-128到127之間。

這種緩存機制並非只有Integer纔有,一樣存在於其餘的一些包裝類,好比:

  • Boolean,緩存了true/false對應實例,確切說,只會返回兩個常量實例Boolean.TRUE/FALSE。
  • Short,一樣是緩存了-128到127之間的數值。
  • Byte,數值有限,因此所有都被緩存。
  • Character,緩存範圍’\u0000’ 到 ‘\u007F’。

注意事項:

[1] 基本類型均具備取值範圍,在大數*大數的時候,有可能會出現越界的狀況。

[2] 基本類型轉換時,使用聲明的方式。例: int result= 1234567890 * 24 * 365;結果值必定不會是你所指望的那個值,由於1234567890 * 24已經超過了int的範圍,若是修改成: long result= 1234567890L * 24 * 365;就正常了。

[3] 慎用基本類型處理貨幣存儲。如採用double常會帶來差距,常採用BigDecimal、整型(若是要精確表示分,可將值擴大100倍轉化爲整型)解決該問題。

[4] 優先使用基本類型。原則上,建議避免無心中的裝箱、拆箱行爲,尤爲是在性能敏感的場合,

[5] 若是有線程安全的計算須要,建議考慮使用類型AtomicInteger、 AtomicLong 這樣的線程安全類。部分比較寬的基本數據類型,好比 foat、 double,甚至不能保證更新操做的原子性,
可能出現程序讀取到只更新了一半數據位的數值。

[4].原則上, 建議避免無心中的裝箱、拆箱行爲,尤爲是在性能敏感的場合,建立10萬個Java對象和10萬個整數的開銷可不是一個數量級的,無論是內存使用仍是處理速度,光是對象頭
的空間佔用就已是數量級的差距了。

以咱們常常會使用到的計數器實現爲例,下面是一個常見的線程安全計數器實現。

class Counter {
	private fnal AtomicLong counter = new AtomicLong();
		public void increase() {
		counter.incrementAndGet();
	}
}

若是利用原始數據類型,能夠將其修改成

class CompactCounter {
	private volatile long counter;
	private satic fnal AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class, "counter");
	public void increase() {
		updater.incrementAndGet(this);
	}
}

Java原始數據類型和引用類型侷限性:

前面我談了很是多的技術細節,最後再從Java平臺發展的角度來看看,原始數據類型、對象的侷限性和演進。
對於Java應用開發者,設計複雜而靈活的類型系統彷佛已經習覺得常了。可是坦白說,畢竟這種類型系統的設計是源於不少年前的技術決定,如今已經逐漸暴露出了一些反作用,例
如:

  • 原始數據類型和Java泛型並不能配合使用
    這是由於Java的泛型某種程度上能夠算做僞泛型,它徹底是一種編譯期的技巧, Java編譯期會自動將類型轉換爲對應的特定類型,這就決定了使用泛型,必須保證相應類型能夠轉換
    爲Object。

  • 沒法高效地表達數據,也不便於表達複雜的數據結構,好比vector和tuple咱們知道Java的對象都是引用類型,若是是一個原始數據類型數組,它在內存裏是一段連續的內存,而對象數組則否則,數據存儲的是引用,對象每每是分散地存儲在堆的不一樣位
    置。這種設計雖然帶來了極大靈活性,可是也致使了數據操做的低效,尤爲是沒法充分利用現代CPU緩存機制。

Vector、 ArrayList、 LinkedList有何區別?

典型回答:
Vector是Java早期提供的線程安全的動態數組,若是不須要線程安全,並不建議選擇,畢竟同步是有額外開銷的。 Vector內部是使用對象數組來保存數據,能夠根據須要自動的增長
容量,當數組已滿時,會建立新的數組,並拷貝原有數組數據。

ArrayList是應用更加普遍的動態數組實現,它自己不是線程安全的,因此性能要好不少。與Vector近似, ArrayList也是能夠根據須要調整容量,不過二者的調整邏輯有所區
別, Vector在擴容時會提升1倍,而ArrayList則是增長50%。

LinkedList顧名思義是Java提供的雙向鏈表,因此它不須要像上面兩種那樣調整容量,它也不是線程安全的。

在這裏插入圖片描述

咱們能夠看到Java的集合框架, Collection接口是全部集合的根,而後擴展開提供了三大類集合,分別是:

  • List,也就是咱們前面介紹最多的有序集合,它提供了方便的訪問、插入、刪除等操做。
  • Set, Set是不容許重複元素的,這是和List最明顯的區別,也就是不存在兩個對象equals返回true。咱們在平常開發中有不少須要保證元素惟一性的場合。
  • Queue/Deque,則是Java提供的標準隊列結構的實現,除了集合的基本功能,它還支持相似先入先出( FIFO, First-in-First-Out)或者後入先出( LIFO, Last-In-FirstOut)等特定行爲。這裏不包括BlockingQueue,由於一般是併發編程場合,因此被放置在併發包裏。

今天介紹的這些集合類,都不是線程安全的,對於java.util.concurrent裏面的線程安全容器,我在專欄後面會去介紹。可是,並不表明這些集合徹底不能支持併發編程的場景,
在Collections工具類中,提供了一系列的synchronized方法,好比

static <T> List<T> synchronizedList(List<T> list)

咱們徹底能夠利用相似方法來實現基本的線程安全集合:

List list = Collections.synchronizedList(new ArrayList());

它的實現,基本就是將每一個基本方法,好比get、 set、 add之類,都經過synchronizd添加基本的同步支持,很是簡單粗暴,但也很是實用。注意這些方法建立的線程安全集合,都
符合迭代時fail-fast行爲,當發生意外的併發修改時,儘早拋出ConcurrentModifcationException異常,以免不可預計的行爲。

另一個常常會被考察到的問題,就是理解Java提供的默認排序算法,具體是什麼排序方式以及設計思路等。

這個問題自己就是有點陷阱的意味,由於須要區分是Arrays.sort()仍是Collections.sort() (底層是調用Arrays.sort());什麼數據類型;多大的數據集(過小的數據集,複雜排
序是不必的, Java會直接進行二分插入排序)等。

對於原始數據類型,目前使用的是所謂雙軸快速排序( Dual-Pivot QuickSort),是一種改進的快速排序算法,早期版本是相對傳統的快速排序,你能夠閱讀源碼。

而對於對象數據類型,目前則是使用TimSort,思想上也是一種歸併和二分插入排序( binarySort)結合的優化排序算法。 TimSort並非Java的首創,簡單說它的思路是查找

數據集中已經排好序的分區(這裏叫run),而後合併這些分區來達到排序的目的。

另外, Java 8引入了並行排序算法(直接使用parallelSort方法),這是爲了充分利用現代多核處理器的計算能力,底層實現基於fork-join框架,當處理的數據集比較小的時候,差距不明顯,甚至還表現差一點;可是,當數據集增加到數萬或百萬以上時,提升就很是大了,具體仍是取決於處理器和系統環境。

對比Hashtable、 HashMap、 TreeMap有什麼不一樣?

典型回答
Hashtable、 HashMap、 TreeMap都是最多見的一些Map實現,是以鍵值對的形式存儲和操做數據的容器類型。

Hashtable是早期Java類庫提供的一個哈希表實現,自己是同步的,不支持null鍵和值,因爲同步致使的性能開銷,因此已經不多被推薦使用。

HashMap是應用更加普遍的哈希表實現,行爲上大體上與HashTable一致,主要區別在於HashMap不是同步的,支持null鍵和值等。一般狀況下, HashMap進行put或者get操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選,好比,實現一個用戶ID和用戶信息對應的運行時存儲結構。

TreeMap則是基於紅黑樹的一種提供順序訪問的Map,和HashMap不一樣,它的get、 put、 remove之類操做都是O(log(n))的時間複雜度,具體順序能夠由指定
的Comparator來決定,或者根據鍵的天然順序來判斷。

在這裏插入圖片描述

LinkedHashMap一般提供的是遍歷順序符合插入順序,它的實現是經過爲條目(鍵值對)維護一個雙向鏈表。注意,經過特定構造函數,咱們能夠建立反映訪問順序的實例,所
謂的put、 get、 compute等,都算做「訪問」。

對於TreeMap,它的總體順序是由鍵的順序關係決定的,經過Comparator或Comparable(天然順序)來決定。

HashMap:
而對於負載因子,我建議:

  • 若是沒有特別需求,不要輕易進行更改,由於JDK自身的默認負載因子是很是符合通用場景的需求的。
  • 若是確實須要調整,建議不要設置超過0.75的數值,由於會顯著增長衝突,下降HashMap的性能。
  • 若是使用過小的負載因子,按照上面的公式,預設容量值也進行調整,不然可能會致使更加頻繁的擴容,增長無謂的開銷,自己訪問性能也會受影響。

那麼,爲何HashMap要樹化呢?
本質上這是個安全問題。 由於在元素放置過程當中,若是一個對象哈希衝突,都被放置到同一個桶裏,則會造成一個鏈表,咱們知道鏈表查詢是線性的,會嚴重影響存取的性能。而在現實世界,構造哈希衝突的數據並非很是複雜的事情,惡意代碼就能夠利用這些數據大量與服務器端交互,致使服務器端CPU大量佔用,這就構成了哈希碰撞拒絕服務攻擊,國內一線互聯網公司就發生過相似攻擊事件。

Hashtable、 HashMap、 TreeMap比較:
三者均實現了Map接口,存儲的內容是基於key-value的鍵值對映射,一個映射不能有重複的鍵,一個鍵最多隻能映射一個值。
(1) 元素特性
HashTable中的key、 value都不能爲null; HashMap中的key、 value能夠爲null,很顯然只能有一個key爲null的鍵值對,可是容許有多個值爲null的鍵值對; TreeMap中當未實現Comparator 接口時, key 不能夠爲null;當實現 Comparator 接口時,若未對null狀況進行判斷,則key不能夠爲null,反之亦然。
(2)順序特性
HashTable、 HashMap具備無序特性。 TreeMap是利用紅黑樹來實現的(樹中的每一個節點的值,都會大於或等於它的左子樹種的全部節點的值,而且小於或等於它的右子樹中的全部節點的
值),實現了SortMap接口,可以對保存的記錄根據鍵進行排序。因此通常須要排序的狀況下是選擇TreeMap來進行,默認爲升序排序方式(深度優先搜索),可自定義實現Comparator接口
實現排序方式。
(3)初始化與增加方式
初始化時: HashTable在不指定容量的狀況下的默認容量爲11,且不要求底層數組的容量必定要爲2的整數次冪; HashMap默認容量爲16,且要求容量必定爲2的整數次冪。
擴容時: Hashtable將容量變爲原來的2倍加1; HashMap擴容將容量變爲原來的2倍
(4)線程安全性
HashTable其方法函數都是同步的(採用synchronized修飾),不會出現兩個線程同時對數據進行操做的狀況,所以保證了線程安全性。也正由於如此,在多線程運行環境下效率表現很是低下。由於當一個線程訪問HashTable的同步方法時,其餘線程也訪問同步方法就會進入阻塞狀態。好比當一個線程在添加數據時候,另一個線程即便執行獲取其餘數據的操做也必須被阻塞,大大下降了程序的運行效率,在新版本中已被廢棄,不推薦使用。
HashMap不支持線程的同步,即任一時刻能夠有多個線程同時寫HashMap;可能會致使數據的不一致。若是須要同步(1)能夠用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap類,相較於HashTable鎖住的是對象總體, ConcurrentHashMap基於lock實現鎖分段技術。首先將Map存放的數據分紅一段一段的存儲方式,而後給每一段數據分配一把鎖,當一個線程佔用鎖訪問其中一個段的數據時,其餘段的數據也能被其餘線程訪問。 ConcurrentHashMap不只保證了多線程運行環境下的數據訪問安全性,並且性能上有長足的提高。
(5)一段話HashMap
HashMap基於哈希思想,實現對數據的讀寫。當咱們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,經過鍵對象的equals()方法找到正確的鍵值對,而後返回值對象。 HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每一個鏈表節點中儲存鍵值對對象。當兩個不一樣的鍵對象的hashcode相同時,它們會儲存在同一個bucket位置的鏈表中,可經過鍵對象的equals()方法用來找到鍵值對。若是鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),鏈表就會被改造爲樹形結構。

如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?

典型回答:
Java提供了不一樣層面的線程安全支持。在傳統集合框架內部,除了Hashtable等同步容器,還提供了所謂的同步包裝器(Synchronized Wrapper),咱們能夠調用Collections工具類提供的包裝方法,來獲取一個同步的包裝容器(如Collections.synchronizedMap),可是它們都是利用很是粗粒度的同步方式,在高併發狀況下,性能比較低下。
另外,更加廣泛的選擇是利用併發包提供的線程安全容器類,它提供了:

  • 各類併發容器,好比ConcurrentHashMap、 CopyOnWriteArrayList。
  • 各類線程安全隊列(Queue/Deque),如ArrayBlockingQueue、 SynchronousQueue。
  • 各類有序容器的線程安全版本等。

具體保證線程安全的方式,包括有從簡單的synchronize方式,到基於更加精細化的,好比基於分離鎖實現的ConcurrentHashMap等併發實現等。具體選擇要看開發的場景需求,
整體來講,併發包內提供的容器通用場景,遠優於早期的簡單同步實現。

知識擴展
1.爲何須要ConcurrentHashMap?
Hashtable自己比較低效,由於它的實現基本就是將put、 get、 size等各類方法加上「synchronized」。簡單來講,這就致使了全部併發操做都要競爭同一把鎖,一個線程在進行同步操做時,其餘線程只能等待,大大下降了併發操做的效率。

前面已經提過HashMap不是線程安全的,併發狀況會致使相似CPU佔用100%等一些問題,那麼能不能利用Collections提供的同步包裝器來解決問題呢?

看看下面的代碼片斷,咱們發現同步包裝器只是利用輸入Map構造了另外一個同步版本,全部操做雖然再也不聲明成爲synchronized方法,可是仍是利用了「this」做爲互斥的mutex,沒有真正意義上的改進!

private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
	private final Map<K,V> m; // Backing Map
	final Object mutex; // Object on which to synchronize
	// …
	public int size() {
	synchronized (mutex) {return m.size();}
	}
	// …
}

因此, Hashtable或者同步包裝版本,都只是適合在非高度併發的場景下。

2.ConcurrentHashMap分析
咱們再來看看ConcurrentHashMap是如何設計實現的,爲何它能大大提升併發效率。
首先,我這裏強調, ConcurrentHashMap的設計實現其實一直在演化,好比在Java 8中就發生了很是大的變化(Java 7其實也有很多更新),因此,我這裏將比較分析結構、實現機制等方面,對比不一樣版本的主要區別。

早期ConcurrentHashMap,其實現是基於:

  • 分段鎖,也就是將內部進行分段(Segment),裏面則是HashEntry的數組,和HashMap相似,哈希相同的條目也是以鏈表形式存放。
  • HashEntry內部使用volatile的value字段來保證可見性,也利用了不可變對象的機制以改進利用Unsafe提供的底層能力,好比volatile access,去直接完成部分操做,以最優化性能,畢竟Unsafe中的不少操做都是JVM intrinsic優化過的。

ConcurrentHashMap 1.7中的get操做:get操做須要保證的是可見性,因此並無什麼同步邏輯。
get:

public V get(Object key) {
	Segment<K,V> s; // manually integrate access methods to reduce overhead
	HashEntry<K,V>[] tab;
	int h = hash(key.hashCode());
	//利用位操做替換普通數學運算
	long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
	// 以Segment爲單位,進行定位
	// 利用Unsafe直接進行volatile access
	if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
	(tab = s.table) != null) {
		//省略
	}
	return null;
}

put:而對於put操做,首先是經過二次哈希避免哈希衝突,而後以Unsafe調用方式,直接獲取相應的Segment,而後進行線程安全的put操做:

public V put(K key, V value) {
	Segment<K,V> s;
	if (value == null)
		throw new NullPointerException();
	// 二次哈希,以保證數據的分散性,避免哈希衝突
	int hash = hash(key.hashCode());
	int j = (hash >>> segmentShift) & segmentMask;
	if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
		(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
		s = ensureSegment(j);
	return s.put(key, hash, value, false);
}

因此,從上面的源碼清晰的看出,在進行併發寫操做時:

  • ConcurrentHashMap會獲取再入鎖,以保證數據一致性, Segment自己就是基於ReentrantLock的擴展實現,因此,在併發修改期間,相應Segment是被鎖定的。
  • 在最初階段,進行重複性的掃描,以肯定相應key值是否已經在數組裏面,進而決定是更新仍是放置操做,你能夠在代碼裏看到相應的註釋。重複掃描、檢測衝突
    是ConcurrentHashMap的常見技巧。
  • HashMap可能發生擴容問題,在ConcurrentHashMap中一樣存在。不過有一個明顯區別,就是它進行的不是總體的擴容,而是單獨對Segment進行擴容。

size:
分段計算兩次,兩次結果相同則返回,不然對因此段加鎖從新計算

在Java 8和以後的版本中, ConcurrentHashMap發生了哪些變化呢?

  • 整體結構上,它的內部存儲變得和HashMap結構很是類似,一樣是大的桶(bucket)數組,而後內部也是一個個所謂的鏈表結構(bin),同步的粒度要更細緻一些。
  • 內部仍然有Segment定義,但僅僅是爲了保證序列化時的兼容性而已,再也不有任何結構上的用處。
  • 由於再也不使用Segment,初始化操做大大簡化,修改成lazy-load形式,這樣能夠有效避免初始開銷,解決了老版本不少人抱怨的這一點。
  • 數據存儲利用volatile來保證可見性。
  • 使用CAS等操做,在特定場景進行無鎖併發操做。
  • 使用Unsafe、 LongAdder之類底層手段,進行極端狀況的優化。

1.8 中,數據存儲內部實現,咱們能夠發現Key是final的,由於在生命週期中,一個條目的Key發生變化是不可能的;與此同時val,則聲明爲volatile,以保證可見性

static class Node<K,V> implements Map.Entry<K,V> {
	final int hash;
	final K key;
	volatile V val;
	volatile Node<K,V> next;
	// …
}

put:

final V putVal(K key, V value, boolean onlyIfAbsent) { 
	if (key == null || value == null) throw new NullPointerException();
	int hash = spread(key.hashCode());
	int binCount = 0;
	for (Node<K,V>[] tab = table;;) {
	Node<K,V> f; int n, i, fh; K fk; V fv;
	if (tab == null || (n = tab.length) == 0)
		tab = initTable(); //初始化
	else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
		// 利用CAS去進行無鎖線程安全操做,若是bin是空的
		if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
		break;
	}
	else if ((fh = f.hash) == MOVED)
		tab = helpTransfer(tab, f);
	else if (onlyIfAbsent // 不加鎖,進行檢查
			&& fh == hash
			&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
			&& (fv = f.val) != null)
		return fv;
	else {
		V oldVal = null;
			synchronized (f) {
			// 細粒度的同步修改操做...
			}
		}
		// Bin超過閾值,進行樹化
		if (binCount != 0) {
			if (binCount >= TREEIFY_THRESHOLD)
				treeifyBin(tab, i);
			if (oldVal != null)
				return oldVal;
				break;
			}
		}
	}
	addCount(1L, binCount);
	return null;
}

put CAS 加鎖
1.8中不依賴與segment加鎖, segment數量與桶數量一致;

首先判斷容器是否爲空,爲空則進行初始化利用volatile的sizeCtl做爲互斥手段,若是發現競爭性的初始化,就暫停在那裏,等待條件恢復,不然利用CAS設置排他標誌(U.compareAndSwapInt(this, SIZECTL, sc, -1)) ;不然重試

對key hash計算獲得該key存放的桶位置(再也不是segement),判斷該桶是否爲空,爲空則利用CAS設置新節點

不然使用synchronize加鎖,遍歷桶中數據,替換或新增長點到桶中
最後判斷是否須要轉爲紅黑樹,轉換以前判斷是否須要擴容

size
利用LongAdder累加計算(性能還要高於直接使用AtomicLong)

Java提供了哪些IO方式? NIO如何實現多路複用?

典型回答
Java IO方式有不少種,基於不一樣的IO抽象模型和交互方式,能夠進行簡單區分。

首先,傳統的java.io包,它基於流模型實現,提供了咱們最熟知的一些IO功能,好比File抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動做完成以前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。java.io包的好處是代碼比較簡單、直觀,缺點則是IO效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。

不少時候,人們也把java.net下面提供的部分網絡API,好比Socket、 ServerSocket、 HttpURLConnection也歸類到同步阻塞IO類庫,由於網絡通訊一樣是IO行爲。

第二,在Java 1.4中引入了NIO框架(java.nio包),提供了Channel、 Selector、 Bufer等新的抽象,能夠構建多路複用的、同步非阻塞IO程序,同時提供了更接近操做系統底層的高性能數據操做方式。

第三,在Java 7中, NIO有了進一步的改進,也就是NIO 2,引入了異步非阻塞IO方式,也有不少人叫它AIO(Asynchronous IO)。異步IO操做基於事件和回調機制,能夠簡單理解爲,應用操做直接返回,而不會阻塞在那裏,當後臺處理完成,操做系統會通知相應線程進行後續工做。

知識擴展
首先,須要澄清一些基本概念:

區分同步或異步(synchronous/asynchronous)。簡單來講,同步是一種可靠的有序運行機制,當咱們進行同步操做時,後續的任務是等待當前調用返回,纔會進行下一步;
而異步則相反,其餘任務不須要等待當前調用返回,一般依靠事件、回調等機制來實現任務間次序關係。

區分阻塞與非阻塞(blocking/non-blocking)。在進行阻塞操做時,當前線程會處於阻塞狀態,沒法從事其餘任務,只有當條件就緒才能繼續,好比ServerSocket新鏈接創建完畢,或數據讀取、寫入操做完成;而非阻塞則是無論IO操做是否結束,直接返回,相應操做在後臺繼續處理。

1.Java NIO概覽
首先,熟悉一下NIO的主要組成部分:
Buffer,高效的數據容器,除了布爾類型,全部原始數據類型都有相應的Buffer實現。
Channel,相似在Linux之類操做系統上看到的文件描述符,是NIO中被用來支持批量式IO操做的一種抽象。

File或者Socket,一般被認爲是比較高層次的抽象,而Channel則是更加操做系統底層的一種抽象,這也使得NIO得以充分利用現代操做系統底層機制,得到特定場景的性能優化,例如, DMA(Direct Memory Access)等。不一樣層次的抽象是相互關聯的,咱們能夠經過Socket獲取Channel,反之亦然。

Selector,是NIO實現多路複用的基礎,它提供了一種高效的機制,能夠檢測到註冊在Selector上的多個Channel中,是否有Channel處於就緒狀態,進而實現了單線程對多Channel的高效管理。Selector一樣是基於底層操做系統機制,不一樣模式、不一樣版本都存在區別。

Chartset,提供Unicode字符串定義, NIO也提供了相應的編解碼器等,例如,經過下面的方式進行字符串到ByteBufer的轉換:

Charset.defaultCharset().encode("Hello world!"));

BIO NIO 代碼略。

在Java 7引入的NIO 2中,又增添了一種額外的異步IO模式,利用事件和回調,處理Accept、 Read等操做。 AIO實現看起來是相似這樣子:

AsynchronousServerSocketChannel serverSock =AsynchronousServerSocketChannel.open().bind(sockAddr);
serverSock.accept(serverSock, new CompletionHandler<>() { //爲異步操做指定CompletionHandler回調函數
	@Override
	public void completed(AsynchronousSocketChannel sockChannel,AsynchronousServerSocketChannel serverSock) {
	serverSock.accept(serverSock, this);
	// 另一個 write(sock, CompletionHandler{})
	sayHelloWorld(sockChannel, Charset.defaultCharset().encode("Hello World!"));
	}
	// 省略其餘路徑處理方法...
});

小結:

  • BIO 同步阻塞;
  • NIO 同步非阻塞;
  • AIO 異步非阻塞.

Java有幾種文件拷貝方式?哪種最高效?

典型回答
Java有多種比較典型的文件拷貝實現方式,好比:
利用java.io類庫,直接爲源文件構建一個FileInputStream讀取,而後再爲目標文件構建一個FileOutputStream,完成寫入工做。
在這裏插入圖片描述
或者,利用java.nio類庫提供的transferTo或transferFrom方法實現。
在這裏插入圖片描述
固然, Java標準類庫自己已經提供了幾種Files.copy的實現。
對於Copy的效率,這個其實與操做系統和配置等狀況相關,整體上來講, NIO transferTo/From的方式可能更快,由於它更能利用現代操做系統底層機制,避免沒必要要拷貝和上下
文切換。


談談接口和抽象類有什麼區別?

典型回答
接口和抽象類是Java面向對象設計的兩個基礎機制。

接口是對行爲的抽象,它是抽象方法的集合,利用接口能夠達到API定義和實現分離的目的。接口,不能實例化;不能包含任何很是量成員,任何feld都是隱含着public static
final的意義;同時,沒有非靜態方法實現,也就是說要麼是抽象方法,要麼是靜態方法。 Java標準類庫中,定義了很是多的接口,好比java.util.List。

抽象類是不能實例化的類,用abstract關鍵字修飾class,其目的主要是代碼重用。除了不能實例化,形式上和通常的Java類並無太大區別,能夠有一個或者多個抽象方法,也可
以沒有抽象方法。抽象類大多用於抽取相關Java類的共用方法實現或者是共同成員變量,而後經過繼承的方式達到代碼複用的目的。 Java標準庫中,好比collection框架,不少通用
部分就被抽取成爲抽象類,例如java.util.AbstractList。

設想,爲接口添加任何抽象方法,相應的全部實現了這個接口的類,也必須實現新增方法,不然會出現編譯錯誤。對於抽象類,若是咱們添加非抽象方法,其子類只會享受到能力擴展,而不用擔憂編譯出問題.

接口的職責也不只僅限於抽象方法的集合,其實有各類不一樣的實踐。有一類沒有任何方法的接口,一般叫做Marker Interface,顧名思義,它的目的就是爲了聲明某些東西,好比我
們熟知的Cloneable、 Serializable等。這種用法,也存在於業界其餘的Java產品代碼中。


談談你知道的設計模式?請手動實現單例模式, Spring等框架中使用了哪些模式?

典型回答
大體按照模式的應用目標分類,設計模式能夠分爲建立型模式、結構型模式和行爲型模式。

  • 建立型模式,是對對象建立過程的各類問題和解決方案的總結,包括各類工廠模式(Factory、 Abstract Factory)、單例模式(Singleton)、構建器模式(Builder)、原型模
    式(ProtoType)。
  • 結構型模式,是針對軟件設計結構的總結,關注於類、對象繼承、組合方式的實踐經驗。常見的結構型模式,包括橋接模式(Bridge)、適配器模式(Adapter)、裝飾者模式
    (Decorator)、代理模式(Proxy)、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。
  • 行爲型模式,是從類或對象之間交互、職責劃分等角度總結的模式。比較常見的行爲型模式有策略模式(Strategy)、解釋器模式(Interpreter)、命令模式(Command)、
    觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、訪問者模式(Visitor)。

一塊兒來簡要看看主流開源框架,如Spring等如何在API設計中使用設計模式。你至少要有個大致的印象,如:

  • BeanFactory和ApplicationContext應用了工廠模式。
  • 在Bean的建立中, Spring也爲不一樣scope定義的對象,提供了單例和原型等模式實現。
  • AOP領域則是使用了代理模式、裝飾器模式、適配器模式等。
  • 各類事件監聽器SpringEvent,是觀察者模式的典型應用。
  • 相似JdbcTemplate等則是應用了模板模式。

synchronized和ReentrantLock有什麼區別?有人說synchronized最慢,這話靠譜嗎?

典型回答
synchronized是Java內建的同步機制,因此也有人稱其爲Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其餘試圖獲取的線程只能等待或者阻
塞在那裏。

在Java 5之前, synchronized是僅有的同步手段,在代碼中, synchronized能夠用來修飾方法,也能夠使用在特定的代碼塊兒上,本質上synchronized方法等同於把方法所有語
句用synchronized塊包起來。

ReentrantLock,一般翻譯爲再入鎖,是Java 5提供的鎖實現,它的語義和synchronized基本相同。再入鎖經過代碼直接調用lock()方法獲取,代碼書寫也更加靈活。與此同
時, ReentrantLock提供了不少實用的方法,可以實現不少synchronized沒法作到的細節控制,好比能夠控制fairness,也就是公平性,或者利用定義條件等。可是,編碼中也需
要注意,必需要明確調用unlock()方法釋放,否則就會一直持有該鎖。

synchronized和ReentrantLock的性能不能一律而論,早期版本synchronized在不少場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優
於ReentrantLock。

線程安全須要保證幾個基本特性:

  • 原子性,簡單說就是相關操做不會中途被其餘線程干擾,通常經過同步機制實現。
  • 可見性,是一個線程修改了某個共享變量,其狀態可以當即被其餘線程知曉,一般被解釋爲將線程本地狀態反映到主內存上, volatile就是負責保證可見性的。
  • 有序性,是保證線程內串行語義,避免指令重排等。

ReentrantLock。你可能好奇什麼是再入?它是表示當一個線程試圖獲取一個它已經獲取的鎖時,這個獲取動做就自動成功,這是對鎖獲取粒度的一個概念,也就是鎖的持
有是以線程爲單位而不是基於調用次數。 Java鎖實現強調再入性是爲了和thread的行爲進行區分。

ReentrantLock相比synchronized,由於能夠像普通對象同樣使用,因此能夠利用其提供的各類便利方法,進行精細的同步操做,甚至是實現synchronized難以表達的用例,如:
帶超時的獲取鎖嘗試。
能夠判斷是否有線程,或者某個特定線程,在排隊等待獲取鎖。
能夠響應中斷請求。

這裏我特別想強調條件變量( java.util.concurrent.Condition),若是說ReentrantLock是synchronized的替代選擇, Condition則是將wait、 notify、 notifyAll等操做轉化爲相
應的對象,將複雜而晦澀的同步操做轉變爲直觀可控的對象行爲。
條件變量最爲典型的應用場景就是標準類庫中的ArrayBlockingQueue等。


synchronized底層如何實現?什麼是鎖的升級、降級?

synchronized代碼塊是由一對兒monitorenter/monitorexit指令實現的, Monitor對象是同步的基本實現單元。

在Java 6以前, Monitor的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做。

現代的( Oracle) JDK中, JVM對此進行了大刀闊斧地改進,提供了三種不一樣的Monitor實現,也就是常說的三種不一樣的鎖:偏斜鎖( Biased Locking)、輕量級鎖和重量級鎖,大
大改進了其性能。

所謂鎖的升級、降級,就是JVM優化synchronized運行的機制,當JVM檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。
當沒有競爭出現時,默認會使用偏斜鎖。 JVM會利用CAS操做( compare and swap),在對象頭上的Mark Word部分設置線程ID,以表示這個對象偏向於當前線程,因此並不涉
及真正的互斥鎖。這樣作的假設是基於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。

若是有另外的線程試圖鎖定某個已經被偏斜過的對象, JVM就須要撤銷( revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴CAS操做Mark Word來試圖獲取鎖,若是重試成
功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。

我注意到有的觀點認爲Java不會進行鎖降級。實際上據我所知,鎖降級確實是會發生的,當JVM進入安全點( SafePoint)的時候,會檢查是否有閒置的Monitor,而後試圖進行降
級。

Java核心類庫中還有其餘一些特別的鎖類型,具體請參考下面的圖。
在這裏插入圖片描述

這些鎖居然不都是實現了Lock接口, ReadWriteLock是一個單獨的接口,它一般是表明了一對兒鎖,分別對應只讀和寫操做,標準類庫中提供了再入版本的讀寫
鎖實現( ReentrantReadWriteLock),對應的語義和ReentrantLock比較類似。

StampedLock居然也是個單獨的類型,從類圖結構能夠看出它是不支持再入性的語義的,也就是它不是以持有鎖的線程爲單位。

爲何咱們須要讀寫鎖( ReadWriteLock)等其餘鎖呢?

這是由於,雖然ReentrantLock和synchronized簡單實用,可是行爲上有必定侷限性,通俗點說就是「太霸道」,要麼不佔,要麼獨佔。實際應用場景中,有的時候不須要大量競爭
的寫操做,而是以併發讀取爲主,如何進一步優化併發操做的粒度呢?

Java併發包提供的讀寫鎖等擴展了鎖的能力,它所基於的原理是多個讀操做是不須要互斥的,由於讀操做並不會更改數據,因此不存在互相干擾。而寫操做則會致使併發一致性的問
題,因此寫線程之間、讀寫線程之間,須要精心設計的互斥邏輯。


一個線程兩次調用start()方法會出現什麼狀況?談談線程的生命週期和狀態轉移。

典型回答
Java的線程是不容許啓動兩次的,第二次調用必然會拋出IllegalThreadStateException,這是一種運行時異常,屢次調用start被認爲是編程錯誤。

關於線程生命週期的不一樣狀態,在Java 5之後,線程狀態被明肯定義在其公共內部枚舉類型java.lang.Thread.State中,分別是:

  • 新建( NEW),表示線程被建立出來還沒真正啓動的狀態,能夠認爲它是個Java內部狀態。
  • 就緒( RUNNABLE),表示該線程已經在JVM中執行,固然因爲執行須要計算資源,它多是正在運行,也可能還在等待系統分配給它CPU片斷,在就緒隊列裏面排隊。
  • 在其餘一些分析中,會額外區分一種狀態RUNNING,可是從Java API的角度,並不能表示出來。
  • 阻塞( BLOCKED),這個狀態和咱們前面兩講介紹的同步很是相關,阻塞表示線程在等待Monitor lock。好比,線程試圖經過synchronized去獲取某個鎖,可是其餘線程已經
    獨佔了,那麼當前線程就會處於阻塞狀態。
  • 等待( WAITING),表示正在等待其餘線程採起某些操做。一個常見的場景是相似生產者消費者模式,發現任務條件還沒有知足,就讓當前消費者線程等待( wait),另外的生產
    者線程去準備任務數據,而後經過相似notify等動做,通知消費線程能夠繼續工做了。 Thread.join()也會令線程進入等待狀態。
  • 計時等待( TIMED_WAIT),其進入條件和等待狀態相似,可是調用的是存在超時條件的方法,好比wait或join等方法的指定超時版本,以下面示例:

public fnal native void wait(long timeout) throws InterruptedException;

  • 終止( TERMINATED),無論是意外退出仍是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫做死亡。
    在第二次調用start()方法的時候,線程可能處於終止或者其餘(非NEW)狀態,可是不論如何,都是不能夠再次啓動的。

知識擴展

1.首先,咱們來總體看一下線程是什麼?

從操做系統的角度,能夠簡單認爲,線程是系統調度的最小單元,一個進程能夠包含多個線程,做爲任務的真正運做者,有本身的棧( Stack)、寄存器( Register)、本地存儲
( Thread Local)等,可是會和進程內其餘線程共享文件描述符、虛擬地址空間等。

2.從線程生命週期的狀態開始展開,那麼在Java編程中,有哪些因素可能影響線程的狀態呢?主要有:

  • 線程自身的方法,除了start,還有多個join方法,等待線程結束; yield是告訴調度器,主動讓出CPU;另外,就是一些已經被標記爲過期的resume、 stop、 suspend之類,據
    我所知,在JDK最新版本中, destory/stop方法將被直接移除。
  • 基類Object提供了一些基礎的wait/notify/notifyAll方法。若是咱們持有某個對象的Monitor鎖,調用wait會讓當前線程處於等待狀態,直到其餘線程notify或者notifyAll。所
    以,本質上是提供了Monitor的獲取和釋放的能力,是基本的線程間通訊方式。
  • 併發類庫中的工具,好比CountDownLatch.await()會讓當前線程進入等待狀態,直到latch被基數爲0,這能夠看做是線程間通訊的Signal。

在這裏插入圖片描述


什麼狀況下Java程序會產生死鎖?如何定位、修復?

典型回答
死鎖是一種特定的程序狀態,在實體之間,因爲循環依賴致使彼此一直處於等待之中,沒有任何個體能夠繼續前進。死鎖不只僅是在線程之間會發生,存在資源獨佔的進程之間一樣
也可能出現死鎖。一般來講,咱們大可能是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,因爲互相持有對方須要的鎖,而永久處於阻塞的狀態。

定位死鎖最多見的方式就是利用jstack等工具獲取線程棧,而後定位互相之間的依賴關係,進而找到死鎖。若是是比較明顯的死鎖,每每jstack等就能直接定位,相似JConsole甚至
能夠在圖形界面進行有限的死鎖檢測。

如何在編程中儘可能預防死鎖呢?

首先,咱們來總結一下前面例子中死鎖的產生包含哪些基本元素。基本上死鎖的發生是由於:

  • 互斥條件,相似Java中Monitor都是獨佔的,要麼是我用,要麼是你用。
  • 互斥條件是長期持有的,在使用結束以前,本身不會釋放,也不能被其餘線程搶佔。
  • 循環依賴關係,兩個或者多個個體之間出現了鎖的鏈條環。

第一種方法
若是可能的話,儘可能避免使用多個鎖,而且只有須要時才持有鎖。不然,即便是很是精通併發編程的工程師,也不免會掉進坑裏,嵌套的synchronized或者lock很是容易出問題。

第二種方法
若是必須使用多個鎖,儘可能設計好鎖的獲取順序,這個提及來簡單,作起來可不容易,你能夠參看著名的銀行家算法.

第三種方法
使用帶超時的方法,爲程序帶來更多可控性。
相似Object.wait(…)或者CountDownLatch.await(…),都支持所謂的timed_wait,咱們徹底能夠就不假定該鎖必定會得到,指定超時時間,併爲沒法獲得鎖時準備退出邏輯。


Java併發包提供了哪些併發工具類?

典型回答
咱們一般所說的併發包也就是java.util.concurrent及其子包,集中了Java併發的各類基礎工具類,具體主要包括幾個方面:

  • 提供了比synchronized更加高級的各類同步結構,包括CountDownLatch、 CyclicBarrier、 Semaphore等,能夠實現更加豐富的多線程操做,好比利用Semaphore做爲資源
    控制器,限制同時進行工做的線程數量。
  • 各類線程安全的容器,好比最多見的ConcurrentHashMap、有序的ConcunrrentSkipListMap,或者經過相似快照機制,實現線程安全的動態數
    組CopyOnWriteArrayList等。
  • 各類併發隊列實現,如各類BlockedQueue實現,比較典型的ArrayBlockingQueue、 SynchorousQueue或針對特定場景的PriorityBlockingQueue等。
  • 強大的Executor框架,能夠建立各類不一樣類型的線程池,調度任務運行等,絕大部分狀況下,再也不須要本身從頭實現線程池和任務調度器.

**知識擴展 **

  • CountDownLatch,容許一個或多個線程等待某些操做完成。
  • CyclicBarrier,一種輔助性的同步結構,容許多個線程等待到達某個屏障。
  • Semaphore, Java版本的信號量實現,Semaphore就是個計數器, 其基本邏輯基於acquire/release,並無太複雜的同步邏輯。

Semaphore

1.工做原理

以一個停車場是運做爲例。爲了簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時若是同時來了五輛車,看門人容許其中三輛不受阻礙的進入,而後放下車攔,剩下的車則必須在入口等待,此後來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知後,打開車攔,放入一輛,若是又離開兩輛,則又能夠放入兩輛,如此往復。這個停車系統中,每輛車就比如一個線程,看門人就比如一個信號量,看門人限制了能夠活動的線程。假如裏面依然是三個車位,可是看門人改變了規則,要求每次只能停兩輛車,那麼一開始進入兩輛車,後面得等到有車離開纔能有車進入,可是得保證最多停兩輛車。對於Semaphore類而言,就如同一個看門人,限制了可活動的線程數。

2.主要方法

  • Semaphore(int permits):構造方法,建立具備給定許可數的計數信號量並設置爲非公平信號量。
  • Semaphore(int permits,boolean fair):構造方法,當fair等於true時,建立具備給定許可數的計數信號量並設置爲公平信號量。
  • void acquire():今後信號量獲取一個許可前線程將一直阻塞。至關於一輛車佔了一個車位。
  • void acquire(int n):今後信號量獲取給定數目許可,在提供這些許可前一直將線程阻塞。好比n=2,就至關於一輛車佔了兩個車位。
  • void release():釋放一個許可,將其返回給信號量。就如同車開走返回一個車位。
  • void release(int n):釋放n個許可。
  • int availablePermits():當前可用的許可數。
  1. 更多

https://www.cnblogs.com/klbc/p/9500947.html

下面,來看看CountDownLatch和CyclicBarrier,它們的行爲有必定的類似度,常常會被考察兩者有什麼區別,我來簡單總結一下。

  • CountDownLatch是不能夠重置的,因此沒法重用;而CyclicBarrier則沒有這種限制,能夠重用。
  • CountDownLatch的基本操做組合是countDown/await。調用await的線程阻塞等待countDown足夠的次數,無論你是在一個線程仍是多個線程裏countDown,只要次數足夠
    便可。因此就像Brain Goetz說過的, CountDownLatch操做的是事件。
  • CyclicBarrier的基本操做組合,則就是await,當全部的夥伴( parties)都調用了await,纔會繼續進行任務,並自動進行重置。 注意,正常狀況下, CyclicBarrier的重置都是自
    動發生的,若是咱們調用reset方法,但還有線程在等待,就會致使等待線程被打擾,拋出BrokenBarrierException異常。 CyclicBarrier側重點是線程,而不是調用事件,它的
    典型應用場景是用來等待併發線程結束。

CountDownLatch
模擬五個線程同時啓動:

public static void main(String[] args) {
		
		//全部線程阻塞,而後統一開始
		CountDownLatch begin = new CountDownLatch(1);
		
		//主線程阻塞,直到全部分線程執行完畢
		CountDownLatch end = new CountDownLatch(5);
		
		for(int i = 0; i < 5; i++){
			Thread thread = new Thread(new Runnable() {
				
				@Override
				public void run() {
					try {
						begin.await();
						System.out.println(Thread.currentThread().getName() + " 起跑");
						Thread.sleep(1000);
						System.out.println(Thread.currentThread().getName() + " 到達終點");
						end.countDown();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
				}
			});
			
			thread.start();
		}
		
		try {
			System.out.println("1秒後統一開始");
			Thread.sleep(1000);
			begin.countDown();
 
			end.await();
			System.out.println("中止比賽");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
	}

結果:

1秒後統一開始
Thread-1 起跑
Thread-4 起跑
Thread-3 起跑
Thread-0 起跑
Thread-2 起跑
Thread-3 到達終點
Thread-0 到達終點
Thread-4 到達終點
Thread-1 到達終點
Thread-2 到達終點
中止比賽

併發包裏提供的線程安全Map、 List和Set:

在這裏插入圖片描述

若是咱們的應用側重於Map放入或者獲取的速度,而不在意順序,大多推薦使用ConcurrentHashMap,反之則使
用ConcurrentSkipListMap;若是咱們須要對大量數據進行很是頻繁地修改, ConcurrentSkipListMap也可能表現出優點。

SkipList結構:

在這裏插入圖片描述

關於兩個CopyOnWrite容器,其實CopyOnWriteArraySet是經過包裝了CopyOnWriteArrayList來實現的,因此在學習時,咱們能夠專一於理解一種。

首先, CopyOnWrite究竟是什麼意思呢?它的原理是,任何修改操做,如add、 set、 remove,都會拷貝原數組,修改後替換原來的數組,經過這種防護性的方式,實現另類的線程安全。

public boolean add(E e) {
	synchronized (lock) {
		Object[] elements = getArray();
		int len = elements.length;
		// 拷貝
		Object[] newElements = Arrays.copyOf(elements, len + 1);
		newElements[len] = e;
		// 替換
		setArray(newElements);
		return true;
	}
}
final void setArray(Object[] a) {
	array = a;
}

併發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什麼區別?

典型回答

有時候咱們把併發包下面的全部容器都習慣叫做併發容器,可是嚴格來說,相似ConcurrentLinkedQueue這種「Concurrent*」容器,纔是真正表明併發。

關於問題中它們的區別:

  • Concurrent類型基於lock-free,在常見的多線程訪問場景,通常能夠提供較高吞吐量。
  • 而LinkedBlockingQueue內部則是基於鎖,並提供了BlockingQueue的等待性方法。

不知道你有沒有注意到, java.util.concurrent包提供的容器( Queue、 List、 Set)、 Map,從命名上能夠大概區分爲Concurrent、 CopyOnWrite和Blocking*等三類,一樣是線
程安全容器,能夠簡單認爲:

  • Concurrent類型沒有相似CopyOnWrite之類容器相對較重的修改開銷。
  • 可是,凡事都是有代價的, Concurrent每每提供了較低的遍歷一致性。你能夠這樣理解所謂的弱一致性,例如,當利用迭代器遍歷時,若是容器發生修改,迭代器仍然能夠繼續
    進行遍歷。
  • 與弱一致性對應的,就是我介紹過的同步容器常見的行爲「fast-fail」,也就是檢測到容器在遍歷過程當中發生了修改,則拋出ConcurrentModifcationException,再也不繼續遍歷。
  • 弱一致性的另一個體現是, size等操做準確性是有限的,未必是100%準確。
  • 與此同時,讀取的性能具備必定的不肯定性。

知識擴展

線程安全隊列一覽:

在這裏插入圖片描述

ArrayBlockingQueue是最典型的的有界隊列,其內部以final的數組保存數據,數組的大小就決定了隊列的邊界,因此咱們在建立ArrayBlockingQueue時,都要指定容量,如

public ArrayBlockingQueue(int capacity, boolean fair)

LinkedBlockingQueue,容易被誤解爲無邊界,但其實其行爲和內部代碼都是基於有界的邏輯實現的,只不過若是咱們沒有在建立隊列時就指定容量,那麼其容量限制就自動被
設置爲Integer.MAX_VALUE ,成爲了無界隊列。

SynchronousQueue,這是一個很是奇葩的隊列實現,每一個刪除操做都要等待插入操做,反之每一個插入操做也都要等待刪除動做。那麼這個隊列的容量是多少呢?是1嗎?其實不
是的,其內部容量是0。

PriorityBlockingQueue是無邊界的優先隊列,雖然嚴格意義上來說,其大小總歸是要受系統資源影響。

DelayedQueue和LinkedTransferQueue一樣是無邊界的隊列。對於無邊界的隊列,有一個天然的結果,就是put操做永遠也不會發生其餘BlockingQueue的那種等待狀況。

使用Blocking實現的生產者消費者代碼:

package com.ryze.chapter3;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ConsumerProducer {
    public static final String EXIT_MSG = "Good bye!";

    public static void main(String[] args) {
        // 使用較小的隊列,以更好地在輸出中展現其影響
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }

    static class Producer implements Runnable {
        private BlockingQueue<String> queue;

        public Producer(BlockingQueue<String> q) {
            this.queue = q;
        }

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(5L);
                    String msg = "Message" + i;
                    System.out.println("Produced new item: " + msg);
                    queue.put(msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                System.out.println("Time to say good bye!");
                queue.put(EXIT_MSG);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Consumer implements Runnable {
        private BlockingQueue<String> queue;

        public Consumer(BlockingQueue<String> q) {
            this.queue = q;
        }

        @Override
        public void run() {
            try {
                String msg;
                while (!EXIT_MSG.equalsIgnoreCase((msg = queue.take()))) {
                    System.out.println("Consumed item: " + msg);
                    Thread.sleep(10L);
                }
                System.out.println("Got exit message, bye!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

####前面介紹了各類隊列實現,在平常的應用開發中,如何進行選擇呢?

以LinkedBlockingQueue、 ArrayBlockingQueue和SynchronousQueue爲例,咱們一塊兒來分析一下,根據需求能夠從不少方面考量:

考慮應用場景中對隊列邊界的要求。 ArrayBlockingQueue是有明確的容量限制的,而LinkedBlockingQueue則取決於咱們是否在建立時指定, SynchronousQueue則乾脆不
能緩存任何元素。

從空間利用角度,數組結構的ArrayBlockingQueue要比LinkedBlockingQueue緊湊,由於其不須要建立所謂節點,可是其初始分配階段就須要一段連續的空間,因此初始內存
需求更大。

通用場景中, LinkedBlockingQueue的吞吐量通常優於ArrayBlockingQueue,由於它實現了更加細粒度的鎖操做。

ArrayBlockingQueue實現比較簡單,性能更好預測,屬於表現穩定的「選手」。

若是咱們須要實現的是兩個線程之間接力性( handof)的場景,按照專欄上一講的例子,你可能會選擇CountDownLatch,可是SynchronousQueue也是完美符合這種場景
的,並且線程間協調和數據傳輸統一塊兒來,代碼更加規範。

可能使人意外的是,不少時候SynchronousQueue的性能表現,每每大大超過其餘實現,尤爲是在隊列元素較小的場景。

Java併發類庫提供的線程池有哪幾種? 分別有什麼特色?

典型回答
一般開發者都是利用Executors提供的通用線程池建立方法,去建立不一樣配置的線程池,主要區別在於不一樣的ExecutorService類型或者不一樣的初始參數。
Executors目前提供了5種不一樣的線程池建立配置:

newCachedThreadPool(),它是一種用來處理大量短期工做任務的線程池,具備幾個鮮明特色:它會試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程;如
果線程閒置的時間超過60秒,則被終止並移出緩存;長時間閒置時,這種線程池,不會消耗什麼資源。其內部使用SynchronousQueue做爲工做隊列。

newFixedThreadPool(int nThreads),重用指定數目( nThreads)的線程,其背後使用的是無界的工做隊列,任什麼時候候最多有nThreads個工做線程是活動的。這意味着,如
果任務數量超過了活動隊列數目,將在工做隊列中等待空閒線程出現;若是有工做線程退出,將會有新的工做線程被建立,以補足指定的數目nThreads。

newSingleThreadExecutor(),它的特色在於工做線程數目被限制爲1,操做一個無界的工做隊列,因此它保證了全部任務的都是被順序執行,最多會有一個任務處於活動狀
態,而且不容許使用者改動線程池實例,所以能夠避免其改變線程數目。

newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),建立的是個ScheduledExecutorService,能夠進行定時或週期性的工做調度,
區別在於單一工做線程仍是多個工做線程。

ewWorkStealingPool(int parallelism),這是一個常常被人忽略的線程池, Java 8才加入這個建立方法,其內部會構建ForkJoinPool,利用Work-Stealing算法,並行地處
理任務,不保證處理順序。

Executor框架可不只僅是線程池,我以爲至少下面幾點值得深刻學習:

  • 掌握Executor框架的主要內容,至少要了解組成與職責,掌握基本開發用例中的使用。
  • 對線程池和相關併發工具類型的理解,甚至是源碼層面的掌握。
  • 實踐中有哪些常見問題,基本的診斷思路是怎樣的。
  • 如何根據自身應用特色合理使用線程池。

知識擴展
首先,咱們來看看Executor框架的基本組成,請參考下面的類圖。

在這裏插入圖片描述

Executor是一個基礎的接口,其初衷是將任務提交和任務執行細節解耦,這一點能夠體會其定義的惟一方法。

void execute(Runnable command);

ExecutorService則更加完善,不只提供service的管理功能,好比shutdown等方法,也提供了更加全面的提交任務機制,如返回Future而不是void的submit方法。

<T> Future<T> submit(Callable<T> task);

從源碼角度,分析線程池的設計與實現,我將主要圍繞最基礎的ThreadPoolExecutor源碼:

簡單理解一下:

  • 工做隊列負責存儲用戶提交的各個任務,這個工做隊列,能夠是容量爲0的SynchronousQueue(使用newCachedThreadPool),也能夠是像固定大小線程池
    ( newFixedThreadPool)那樣使用LinkedBlockingQueue。

    private final BlockingQueue<Runnable> workQueue;
  • 內部的「線程池」,這是指保持工做線程的集合,線程池須要在運行過程當中管理線程建立、銷燬。例如,對於帶緩存的線程池,當任務壓力較大時,線程池會建立新的工做線程;當
    業務壓力退去,線程池會在閒置一段時間(默認60秒)後結束線程。

    private final HashSet<Worker> workers = new HashSet<>();

線程池的工做線程被抽象爲靜態內部類Worker,基於AQS實現。

  • ThreadFactory提供上面所須要的建立線程邏輯。
  • 若是任務提交時被拒絕,好比線程池已經處於SHUTDOWN狀態,須要爲其提供處理邏輯, Java標準庫提供了相似ThreadPoolExecutor.AbortPolicy 等默認實現,也能夠按照實
    際需求自定義。

從上面的分析,就能夠看出線程池的幾個基本組成部分,一塊兒都體如今線程池的構造函數中,從字面咱們就能夠大概猜想到其用意:

  • corePoolSize,所謂的核心線程數,能夠大體理解爲長期駐留的線程數目(除非設置了allowCoreThreadTimeOut)。對於不一樣的線程池,這個值可能會有很大區別,比
    如newFixedThreadPool會將其設置爲nThreads,而對於newCachedThreadPool則是爲0。

  • maximumPoolSize,顧名思義,就是線程不夠時可以建立的最大線程數。一樣進行對比,對於newFixedThreadPool,固然就是nThreads,由於其要求是固定大小,
    而newCachedThreadPool則是Integer.MAX_VALUE 。

  • keepAliveTime和TimeUnit,這兩個參數指定了額外的線程可以閒置多久,顯然有些線程池不須要它。

  • workQueue,工做隊列,必須是BlockingQueue。

經過配置不一樣的參數,咱們就能夠建立出行爲截然不同的線程池,這就是線程池高度靈活性的基礎

public ThreadPoolExecutor(int corePoolSize,
						int maximumPoolSize,
						long keepAliveTime,
						TimeUnit unit,
						BlockingQueue<Runnable> workQueue,
						ThreadFactory threadFactory,
						RejectedExecutionHandler handler)

AtomicInteger底層實現原理是什麼?如何在本身的產品代碼中應用CAS操做?

典型回答
AtomicIntger是對int類型的一個封裝,提供原子性的訪問和更新操做,其原子性操做的實現是基於CAS( compare-and-swap).

目前Java提供了兩種公共API,能夠實現這種CAS操做,好比使用java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基於反射機制建立,咱們須要保證類型和字段名稱正確。

AQS內部數據和方法,能夠簡單拆分爲:

  • 一個volatile的整數成員表徵狀態,同時提供了setState和getState方法
private volatile int sate;
  • 一個先入先出( FIFO)的等待線程隊列,以實現多線程間競爭和等待,這是AQS機制的核心之一。
  • 各類基於CAS的基礎操做方法,以及各類指望具體同步結構去實現的acquire/release方法。

利用AQS實現一個同步結構,至少要實現兩個基本類型的方法,分別是acquire操做,獲取資源的獨佔權;還有就是release操做,釋放對某個資源的獨佔

排除掉一些細節,總體地分析acquire方法邏輯,其直接實現是在AQS內部,調用了tryAcquire和acquireQueued,這是兩個須要搞清楚的基本部分。

public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
	selfInterrupt();
}

以非公平的tryAcquire爲例,其內部實現瞭如何配合狀態與CAS獲取鎖,注意,對比公平版本的tryAcquire,它在鎖無人佔有時,並不檢查是否有其餘等待者,這裏體現了非公平的
語義。

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();// 獲取當前AQS內部狀態量
	if (c == 0) { // 0表示無人佔有,則直接用CAS修改狀態位,
		if (compareAndSetState(0, acquires)) {// 不檢查排隊狀況,直接爭搶
			setExclusiveOwnerThread(current); //並設置當前線程獨佔鎖
			return true;
		}
	} else if (current == getExclusiveOwnerThread()) { //即便狀態不是0,也可能當前線程是鎖持有者,由於這是再入鎖
		int nextc = c + acquires;
		if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;
}

再來分析acquireQueued,若是前面的tryAcquire失敗,表明着鎖爭搶失敗,進入排隊競爭階段。這裏就是咱們所說的,利用FIFO隊列,實現線程間對鎖的競爭的部分,
算是是AQS的核心邏輯。

當前線程會被包裝成爲一個排他模式的節點( EXCLUSIVE),經過addWaiter方法添加到隊列中。 acquireQueued的邏輯,簡要來講,就是若是當前節點的前面是頭節點,則試圖
獲取鎖,一切順利則成爲新的頭節點;不然,有必要則等待,具體處理邏輯請參考我添加的註釋。

final boolean acquireQueued(final Node node, int arg) {
	boolean interrupted = false;
	try {
		for (;;) {// 循環
			final Node p = node.predecessor();// 獲取前一個節點
			if (p == head && tryAcquire(arg)) { // 若是前一個節點是頭結點,表示當前節點合適去tryAcquire
				setHead(node); // acquire成功,則設置新的頭節點
				p.next = null; // 將前面節點對當前節點的引用清空
				return interrupted;
			}
			if (shouldParkAfterFailedAcquire(p, node)) // 檢查是否失敗後須要park
			interrupted |= parkAndCheckInterrupt();
		}
	} catch (Throwable t) {
		cancelAcquire(node);// 出現異常,取消
		if (interrupted)
		selfInterrupt();
		throw t;
	}
}

到這裏線程試圖獲取鎖的過程基本展示出來了, tryAcquire是按照特定場景須要開發者去實現的部分,而線程間競爭則是AQS經過Waiter隊列與acquireQueued提供的,
在release方法中,一樣會對隊列進行對應操做.

請介紹類加載過程,什麼是雙親委派模型?

典型回答
通常來講,咱們把Java的類加載過程分爲三個主要步驟:加載、連接、初始化,具體行爲在Java虛擬機規範裏有很是詳細的定義。

首先是加載階段( Loading),它是Java將字節碼數據從不一樣的數據源讀取到JVM中,並映射爲JVM承認的數據結構( Class對象),這裏的數據源多是各類各樣的形態,如jar文
件、 class文件,甚至是網絡數據源等;若是輸入數據不是ClassFile的結構,則會拋出ClassFormatError。
加載階段是用戶參與的階段,咱們能夠自定義類加載器,去實現本身的類加載過程。

第二階段是連接( Linking),這是核心的步驟,簡單說是把原始的類定義信息平滑地轉化入JVM運行的過程當中。這裏可進一步細分爲三個步驟:

驗證( Verifcation),這是虛擬機安全的重要保障, JVM須要覈驗字節信息是符合Java虛擬機規範的,不然就被認爲是VerifyError,這樣就防止了惡意信息或者不合規的信息危
害JVM的運行,驗證階段有可能觸發更多class的加載。

準備( Preparation),建立類或接口中的靜態變量,並初始化靜態變量的初始值。但這裏的「初始化」和下面的顯式初始化階段是有區別的,側重點在於分配所須要的內存空間,
不會去執行更進一步的JVM指令。

解析( Resolution),在這一步會將常量池中的符號引用( symbolic reference)替換爲直接引用。在Java虛擬機規範中,詳細介紹了類、接口、方法和字段等各個方面的解
析。

最後是初始化階段( initialization),這一步真正去執行類初始化的代碼邏輯,包括靜態字段賦值的動做,以及執行類定義中的靜態初始化塊內的邏輯,編譯器在編譯階段就會把這
部分邏輯整理好,父類型的初始化邏輯優先於當前類型的邏輯。

再來談談雙親委派模型,簡單說就是當類加載器( Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應類型,不然儘可能將這個任務代理給當前加載器的父加載器去
作。使用委派模型的目的是避免重複加載Java類型。

一般類加載機制有三個基本特徵:

雙親委派模型。但不是全部類加載都遵照這個模型,有的時候,啓動類加載器所加載的類型,是可能要加載用戶代碼的,好比JDK內部的ServiceProvider/ServiceLoader機制,
用戶能夠在標準API框架上,提供本身的實現, JDK也須要提供些默認的參考實現。 例如, Java 中JNDI、 JDBC、文件系統、 Cipher等不少方面,都是利用的這種機制,這種情
況就不會用雙親委派模型去加載,而是利用所謂的上下文加載器。

可見性,子類加載器能夠訪問父加載器加載的類型,可是反過來是不容許的,否則,由於缺乏必要的隔離,咱們就沒有辦法利用類加載器去實現容器的邏輯。

單一性,因爲父加載器的類型對於子加載器是可見的,因此父加載器中加載過的類型,就不會在子加載器中重複加載。可是注意,類加載器「鄰居」間,同一類型仍然能夠被加載多
次,由於互相併不可見。

談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?

典型回答

一般能夠把JVM內存區域分爲下面幾個方面,其中,有的區域是以線程爲單位,而有的區域則是整個JVM進程惟一的。

首先, 程序計數器( PC, Program Counter Register)。在JVM規範中,每一個線程都有它本身的程序計數器,而且任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方
法。程序計數器會存儲當前線程正在執行的Java方法的JVM指令地址;或者,若是是在執行本地方法,則是未指定值( undefned)。

第二, Java虛擬機棧( Java Virtual Machine Stack),早期也叫Java棧。每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀( Stack Frame),對應着一次次
的Java方法調用。

前面談程序計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,一般叫做當前幀,方法所在的類叫做當前類。若是在該方法中調用了其餘方法,對應
的新的棧幀會被建立出來,成爲新的當前幀,一直到它返回結果或者執行結束。 JVM直接對Java棧的操做只有兩個,就是對棧幀的壓棧和出棧。
棧幀中存儲着局部變量表、操做數( operand)棧、動態連接、方法正常退出或者異常退出的定義等。

第三, 堆( Heap),它是Java內存管理的核心區域,用來放置Java對象實例,幾乎全部建立的Java對象實例都是被直接分配在堆上。堆被全部的線程共享,在虛擬機啓動時,咱們
指定的「Xmx」之類參數就是用來指定最大堆空間等指標。
理所固然,堆也是垃圾收集器重點照顧的區域,因此堆內空間還會被不一樣的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。

第四, 方法區( Method Area)。這也是全部線程共享的一塊內存區域,用於存儲所謂的元( Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。
因爲早期的Hotspot JVM實現,不少人習慣於將方法區稱爲永久代( Permanent Generation)。 Oracle JDK 8中將永久代移除,同時增長了元數據區( Metaspace)。

第五, 運行時常量池( Run-Time Constant Pool),這是方法區的一部分。若是仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各類信息,還有一
項信息就是常量池。 Java的常量池能夠存放各類常量信息,無論是編譯期生成的各類字面量,仍是須要在運行時決定的符號引用,因此它比通常語言的符號表存儲的信息更加寬泛。

第六, 本地方法棧( Native Method Stack)。它和Java虛擬機棧是很是類似的,支持對本地方法的調用,也是每一個線程都會建立一個。在Oracle Hotspot JVM中,本地方法棧
和Java虛擬機棧是在同一起區域,這徹底取決於技術實現的決定,並未在規範中強制。

在這裏插入圖片描述

全部的對象實例都是建立在堆上。

除了程序計數器,其餘區域都有可能會由於可能的空間不足發生OutOfMemoryError,簡單總結以下:

堆內存不足是最多見的OOM緣由之一,拋出的錯誤信息是「java.lang.OutOfMemoryError:Java heap space」,緣由可能千奇百怪,例如,可能存在內存泄漏問題;也頗有可
能就是堆的大小不合理,好比咱們要處理比較可觀的數據量,可是沒有顯式指定JVM堆大小或者指定數值偏小;或者出現JVM處理引用不及時,致使堆積起來,內存沒法釋放等。

而對於Java虛擬機棧和本地方法棧,這裏要稍微複雜一點。若是咱們寫一段程序不斷的進行遞歸調用,並且沒有退出條件,就會致使不斷地進行壓棧。相似這種狀況, JVM實際會
拋出StackOverFlowError;固然,若是JVM試圖去擴展棧空間的的時候失敗,則會拋出OutOfMemoryError。

對於老版本的Oracle JDK,由於永久代的大小是有限的,而且JVM對永久代垃圾回收(如,常量池回收、卸載再也不須要的類型)很是不積極,因此當咱們不斷添加新類型的時
候,永久代出現OutOfMemoryError也很是多見,尤爲是在運行時存在大量動態類型生成的場合;相似Intern字符串緩存佔用太多空間,也會致使OOM問題。對應的異常信息,
會標記出來和永久代相關: 「java.lang.OutOfMemoryError: PermGen space」。

隨着元數據區的引入,方法區內存已經再也不那麼窘迫,因此相應的OOM有所改觀,出現OOM,異常信息則變成了: 「java.lang.OutOfMemoryError: Metaspace」。

直接內存不足,也會致使OOM.

思考
我在試圖分配一個100M bytes大數組的時候發生了OOME,可是GC日誌顯示,明明堆上還有遠不止100M的空間,你以爲可能問題的緣由是什麼?想要弄清楚這個問題,還須要什麼信息呢?

思路1:
若是僅從jvm的角度來看,要看下新生代和老年代的垃圾回收機制是什麼。若是新生代是serial,會默認使用copying算法,利用兩塊eden和survivor來進行處理。可是默認當遇到超大對象
時,會直接將超大對象放置到老年代中,而不用走正常對象的存活次數記錄。由於要放置的是一個byte數組,那麼必然須要申請連續的空間,當空間不足時,會進行gc操做。這裏又須要看老年
代的gc機制是哪種。若是是serial old,那麼會採用mark compat,會進行整理,從而整理出連續空間,若是還不夠,說明是老年代的空間不夠,所謂的堆內存大於100m是新+老共同的結
果。若是採用的是cms(concurrent mark sweep),那麼只會標記清理,並不會壓縮,因此內存會碎片化,同時可能出現浮游垃圾。若是是cms的話,即便老年代的空間大於100m,也會出現
沒有連續的空間供該對象使用。

思路2:
從不一樣的垃圾收集器角度來看:
首先,數組的分配是須要連續的內存空間的(聽說,有個別非主流JVM支持大數組用不連續的內存空間分配��)。因此:
1)對於使用年輕代和老年代來管理內存的垃圾收集器,堆大於 100M,表示的是新生代和老年代加起來總和大於100M,而新生代和老年代各自並無大於 100M 的連續內存空間。
進一步,又因爲大數組通常直接進入老年代(會跳過對對象的年齡的判斷),因此,是否能夠認爲老年代中沒有連續大於 100M 的空間呢。
2)對於 G1 這種按 region 來管理內存的垃圾收集器,可能的狀況是沒有多個連續的 region,它們的內存總和大於 100M。
固然,無論是哪一種垃圾收集器以及收集算法,當內存空間不足時,都會觸發 GC,只不過,可能 GC 以後,仍是沒有連續大於 100M 的內存空間,因而 OOM了。

如何監控和診斷JVM堆內和堆外內存使用?

典型回答
瞭解JVM內存的方法有不少,具體能力範圍也有區別,簡單總結以下:

能夠使用綜合性的圖形化工具,如JConsole、 VisualVM(注意,從Oracle JDK 9開始, VisualVM已經再也不包含在JDK安裝包中)等。這些工具具體使用起來相對比較直觀,直
接鏈接到Java進程,而後就能夠在圖形化界面裏掌握內存使用狀況。

以JConsole爲例,其內存頁面能夠顯示常見的堆內存和各類堆外部分使用狀態。

也能夠使用命令行工具進行運行時查詢,如jstat和jmap等工具都提供了一些選項,能夠查看堆、方法區等使用數據。

或者,也能夠使用jmap等提供的命令,生成堆轉儲( Heap Dump)文件,而後利用jhat或Eclipse MAT等堆轉儲分析工具進行詳細分析。

若是你使用的是Tomcat、 Weblogic等Java EE服務器,這些服務器一樣提供了內存管理相關的功能。

另外,從某種程度上來講, GC日誌等輸出,一樣包含着豐富的信息。

首先,堆內部是什麼結構?
對於堆內存,我在上一講介紹了最多見的新生代和老年代的劃分,其內部結構隨着JVM的發展和新GC方式的引入,能夠有不一樣角度的理解,下圖就是年代視角的堆結構示意圖。

在這裏插入圖片描述

你能夠看到,按照一般的GC年代方式劃分, Java堆內分爲:
1.新生代

新生代是大部分對象建立和銷燬的區域,在一般的Java應用中,絕大部分對象生命週期都是很短暫的。其內部又分爲Eden區域,做爲對象初始分配的區域;兩個Survivor,有時候
也叫from、 to區域,被用來放置從Minor GC中保留下來的對象。

JVM會隨意選取一個Survivor區域做爲「to」,而後會在GC過程當中進行區域間拷貝,也就是將Eden中存活下來的對象和from區域的對象,拷貝到這個「to」區域。這種設計主要是爲
了防止內存的碎片化,並進一步清理無用對象。

從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分, Hotspot JVM還有一個概念叫作Thread Local Allocation Bufer( TLAB),據我所知全部OpenJDK衍生出來
的JVM都提供了TLAB的設計。這是JVM爲每一個線程分配的一個私有緩存區域,不然,多線程同時分配內存時,爲避免操做同一地址,可能須要使用加鎖等機制,進而影響分配速
度,你能夠參考下面的示意圖。從圖中能夠看出, TLAB仍然在堆上,它是分配在Eden區域內的。其內部結構比較直觀易懂, start、 end就是起始地址, top(指針)則表示已經
分配到哪裏了。因此咱們分配新對象, JVM就會移動top,當top和end相遇時,即表示該緩存已滿, JVM會試圖再從Eden裏分配一起。

在這裏插入圖片描述

2.老年代
放置長生命週期的對象,一般都是從Survivor區域拷貝過來的對象。固然,也有特殊狀況,咱們知道普通的對象會被分配在TLAB上;若是對象較大, JVM會試圖直接分配在Eden其
他位置上;若是對象太大,徹底沒法在新生代找到足夠長的連續空閒空間, JVM就會直接分配到老年代。

3.永久代
這部分就是早期Hotspot JVM的方法區實現方式了,儲存Java類元數據、常量池、 Intern字符串緩存,在JDK 8以後就不存在永久代這塊兒了。

那麼,咱們如何利用JVM參數,直接影響堆和內部區域的大小呢?我來簡單總結一下:

  • 最大堆體積
-Xmx value
  • 初始的最小堆體積
-Xms value
  • 老年代和新生代的比例
-XX:NewRatio=value

默認狀況下,這個數值是3,意味着老年代是新生代的3倍大;換句話說,新生代是堆大小的1/4。
固然,也能夠不用比例的方式調整新生代的大小,直接指定下面的參數,設定具體的內存大小數值.

-XX:NewSize=value

Eden和Survivor的大小是按照比例設置的,若是SurvivorRatio是8,那麼Survivor區域就是Eden的1/8大小,也就是新生代的1/10,由於YoungGen=Eden +2*Survivor
JVM參數格式是

-XX:SurvivorRatio=value

思考:
若是用程序的方式而不是工具,對Java內存使用進行監控,有哪些技術能夠作到?

利用JMX MXbean公開出來的api:ManagementFactory;

Java常見的垃圾收集器有哪些?

典型回答:
實際上,垃圾收集器( GC, Garbage Collector)是和具體JVM實現緊密相關的,不一樣廠商( IBM、 Oracle),不一樣版本的JVM,提供的選擇也不一樣。接下來,我來談談最主流
的Oracle JDK。
Serial GC,它是最古老的垃圾收集器, 「Serial」體如今其收集工做是單線程的,而且在進行垃圾收集過程當中,會進入臭名昭著的「Stop-The-World」狀態。固然,其單線程設計也
意味着精簡的GC實現,無需維護複雜的數據結構,初始化也簡單,因此一直是Client模式下JVM的默認選項。
從年代的角度,一般將其老年代實現單獨稱做Serial Old,它採用了標記-整理( Mark-Compact)算法,區別於新生代的複製算法。
Serial GC的對應JVM參數是:

-XX:+UseSerialGC

ParNew GC,很明顯是個新生代GC實現,它實際是Serial GC的多線程版本,最多見的應用場景是配合老年代的CMS GC工做,下面是對應參數

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

CMS( Concurrent Mark Sweep) GC,基於標記-清除( Mark-Sweep)算法,設計目標是儘可能減小停頓時間,這一點對於Web等反應時間敏感的應用很是重要,一直到今
天,仍然有不少系統使用CMS GC。可是, CMS採用的標記-清除算法,存在着內存碎片化問題,因此難以免在長時間運行等狀況下發生full GC,致使惡劣的停頓。另外,既然
強調了併發( Concurrent), CMS會佔用更多CPU資源,並和用戶線程爭搶。

Parrallel GC,在早期JDK 8等版本中,它是server模式JVM的默認GC選擇,也被稱做是吞吐量優先的GC。它的算法和Serial GC比較類似,儘管實現要複雜的多,其特色是新
生代和老年代GC都是並行進行的,在常見的服務器環境中更加高效。

開啓選項是:

-XX:+UseParallelGC

另外, Parallel GC引入了開發者友好的配置項,咱們能夠直接設置暫停時間或吞吐量等目標, JVM會自動進行適應性調整,例以下面參數:

-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC時間和用戶時間比例 = 1 / (N+1)

G1 GC這是一種兼顧吞吐量和停頓時間的GC實現,是Oracle JDK 9之後的默認GC選項。 G1能夠直觀的設定停頓時間的目標,相比於CMS GC, G1未必能作到CMS在最好狀況
下的延時停頓,可是最差狀況要好不少。

G1 GC仍然存在着年代的概念,可是其內存結構並非簡單的條帶式劃分,而是相似棋盤的一個個region。 Region之間是複製算法,但總體上實際可看做是標記-整理( MarkCompact)算法,能夠有效地避免內存碎片,尤爲是當Java堆很是大的時候, G1的優點更加明顯。

G1吞吐量和停頓表現都很是不錯,而且仍然在不斷地完善,與此同時CMS已經在JDK 9中被標記爲廢棄( deprecated),因此G1 GC值得你深刻掌握。

常見的垃圾收集算法,我認爲整體上有個瞭解,理解相應的原理和優缺點,就已經足夠了,其主要分爲三類:

複製( Copying)算法,我前面講到的新生代GC,基本都是基於複製算法,過程就如專欄上一講所介紹的,將活着的對象複製到to區域,拷貝過程當中將對象順序放置,就能夠避
免內存碎片化。
這麼作的代價是,既然要進行復制,既要提早預留內存空間,有必定的浪費;另外,對於G1這種分拆成爲大量region的GC,複製而不是移動,意味着GC須要維護region之間對
象引用關係,這個開銷也不小,無論是內存佔用或者時間開銷。

標記-清除( Mark-Sweep)算法,首先進行標記工做,標識出全部要回收的對象,而後進行清除。這麼作除了標記、清除過程效率有限,另外就是不可避免的出現碎片化問題,
這就致使其不適合特別大的堆;不然,一旦出現Full GC,暫停時間可能根本沒法接受。

標記-整理( Mark-Compact),相似於標記-清除,但爲避免內存碎片化,它會在清理過程當中將對象移動,以確保移動後的對象佔用連續的內存空間。

在垃圾收集的過程,對應到Eden、 Survivor、 Tenured等區域會發生什麼變化呢?

這實際上取決於具體的GC方式,先來熟悉一下一般的垃圾收集流程,我畫了一系列示意圖,但願能有助於你理解清楚這個過程。

第一, Java應用不斷建立對象,一般都是分配在Eden區域,當其空間佔用達到必定閾值時,觸發minor GC。仍然被引用的對象(綠色方塊)存活下來,被複制到JVM選擇
的Survivor區域,而沒有被引用的對象(黃色方塊)則被回收。注意,我給存活對象標記了「數字1」,這是爲了代表對象的存活時間。

第二, 通過一次Minor GC, Eden就會空閒下來,直到再次達到Minor GC觸發條件,這時候,另一個Survivor區域則會成爲to區域, Eden區域的存活對象和From區域對象,都
會被複制到to區域,而且存活的年齡計數會被加1。

第三, 相似第二步的過程會發生不少次,直到有對象年齡計數達到閾值,這時候就會發生所謂的晉升( Promotion)過程,以下圖所示,超過閾值的對象會被晉升到老年代。這個閾
值是能夠經過參數指定:

-XX:MaxTenuringThreshold=<N>

後面就是老年代GC,具體取決於選擇的GC選項,對應不一樣的算法。下面是一個簡單標記-整理算法過程示意圖,老年代中的無用對象被清除後, GC會將對象進行整理,以防止內存
碎片化。

一般咱們把老年代GC叫做Major GC,將對整個堆進行的清理叫做Full GC,可是這個也沒有那麼絕對,由於不一樣的老年代GC算法其實表現差別很大,例如CMS, 「concurrent」就
體如今清理工做是與工做線程一塊兒併發運行的.

總結:

  • JVM提供的收集器較多,特徵不一,適用於不一樣的業務場景:
  • Serial收集器:串行運行;做用於新生代;複製算法;響應速度優先;適用於單CPU環境下的client模式。
  • ParNew收集器:並行運行;做用於新生代;複製算法;響應速度優先;多CPU環境Server模式下與CMS配合使用。
  • Parallel Scavenge收集器:並行運行;做用於新生代;複製算法;吞吐量優先;適用於後臺運算而不須要太多交互的場景。
  • Serial Old收集器:串行運行;做用於老年代;標記-整理算法;響應速度優先;單CPU環境下的Client模式。
  • Parallel Old收集器:並行運行;做用於老年代;標記-整理算法;吞吐量優先;適用於後臺運算而不須要太多交互的場景。
  • CMS收集器:併發運行;做用於老年代;標記-清除算法;響應速度優先;適用於互聯網或B/S業務。(jdk9中已經被標記廢棄)
  • G1收集器:併發運行;可做用於新生代或老年代;標記-整理算法+複製算法;響應速度優先;面向服務端應用。

Java內存模型中的happen-before是什麼?

典型回答
Happen-before關係,是Java內存模型中保證多線程操做可見性的機制,也是對早期語言規範中含糊的可見性概念的一個精肯定義。

它的具體表現形式,包括但遠不止是咱們直覺中的synchronized、 volatile、 lock操做順序等方面,例如:

  • 線程內執行的每一個操做,都保證happen-before後面的操做,這就保證了基本的程序順序規則,這是開發者在書寫程序時的基本約定。
  • 對於volatile變量,對它的寫操做,保證happen-before在隨後對該變量的讀取操做。
  • 對於一個鎖的解鎖操做,保證happen-before加鎖操做。
  • 對象構建完成,保證happen-before於fnalizer的開始動做。
  • 甚至是相似線程內部操做的完成,保證happen-before其餘Thread.join()的線程等。
  • 這些happen-before關係是存在着傳遞性的,若是知足a happen-before b和b happen-before c,那麼a happen-before c也成立。
    前面我一直用happen-before,而不是簡單說先後,是由於它不只僅是對執行時間的保證,也包括對內存讀、寫操做順序的保證。僅僅是時鐘順序上的前後,並不能保證線程交互的
    可見性。

JMM內部的實現一般是依賴於所謂的內存屏障,經過禁止某些重排序的方式,提供內存可見性保證,也就是實現了各類happen-before規則。與此同時,更多複雜度在於,須要儘可能
確保各類編譯器、各類體系結構的處理器,都可以提供一致的行爲。

以volatile爲例,看看如何利用內存屏障實現JMM定義的可見性?

對於一個volatile變量:

  • 對該變量的寫操做以後,編譯器會插入一個寫屏障。
  • 對該變量的讀操做以前,編譯器會插入一個讀屏障。極客時間

內存屏障可以在相似變量讀、寫操做以後,保證其餘線程對volatile變量的修改對當前線程可見,或者本地修改對其餘線程提供可見性。換句話說,線程寫入,寫屏障會經過相似強迫
刷出處理器緩存的方式,讓其餘線程可以拿到最新數值。

Java程序運行在Docker等容器環境有哪些新問題?

典型回答
對於Java來講, Docker畢竟是一個較新的環境,例如,其內存、 CPU等資源限制是經過CGroup( Control Group)實現的,早期的JDK版本( 8u131以前)並不能識別這些限
制,進而會致使一些基礎問題:

若是未配置合適的JVM堆和元數據區、直接內存等參數, Java就有可能試圖使用超過容器限制的內存,最終被容器OOM kill,或者自身發生OOM。

錯誤判斷了可獲取的CPU資源,例如, Docker限制了CPU的核數, JVM就可能設置不合適的GC並行線程數等

知識擴展
首先,咱們先來搞清楚Java在容器環境的侷限性來源, Docker到底有什麼特別?

雖然看起來Docker之類容器和虛擬機很是類似,例如,它也有本身的shell,能獨立安裝軟件包,運行時與其餘容器互不干擾。可是,若是深刻分析你會發現, Docker並非一種完
全的虛擬化技術,而更是一種輕量級的隔離技術。

對於Java平臺來講,這些未隱藏的底層信息帶來了不少意外的困難,主要體如今幾個方面:

第一,容器環境對於計算資源的管理方式是全新的, CGroup做爲相對比較新的技術,歷史版本的Java顯然並不能天然地理解相應的資源限制。

第二, namespace對於容器內的應用細節增長了一些微妙的差別,好比jcmd、 jstack等工具會依賴於「/proc//」下面提供的部分信息,可是Docker的設計改變了這部分信息的原有
結構,咱們須要對原有工具進行修改以適應這種變化。

從JVM運行機制的角度,爲何這些「溝通障礙」會致使OOM等問題呢?

你能夠思考一下,這個問題實際是反映了JVM如何根據系統資源(內存、 CPU等)狀況,在啓動時設置默認參數

這就是所謂的Ergonomics機制,例如:

  • JVM會大概根據檢測到的內存大小,設置最初啓動時的堆大小爲系統內存的1/64;並將堆最大值,設置爲系統內存的1/4。
  • 而JVM檢測到系統的CPU核數,則直接影響到了Parallel GC的並行線程數目和JIT complier線程數目,甚至是咱們應用中ForkJoinPool等機制的並行等級。

這些默認參數,是根據通用場景選擇的初始值。可是因爲容器環境的差別, Java的判斷極可能是基於錯誤信息而作出的。
這就相似,我覺得我住的是整棟別墅,實際上卻只有一個房間是給我住的。

更加嚴重的是, JVM的一些原有診斷或備用機制也會受到影響。爲保證服務的可用性,一種常見的選擇是依賴「-XX:OnOutOfMemoryError」功能,經過調用處理腳本的形式來作一
些補救措施,好比自動重啓服務等。可是,這種機制是基於fork實現的,當Java進程已通過度提交內存時, fork新的進程每每已經不可能正常運行了。

根據前面的總結,彷佛問題很是棘手,那咱們在實踐中, 如何解決這些問題呢?

  • 首先,若是你可以升級到最新的JDK版本,這個問題就迎刃而解了。
  • 針對這種狀況, JDK 9中引入了一些實驗性的參數,以方便Docker和Java「溝通」,例如針對內存限制,能夠使用下面的參數設置:
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap

若是你能夠切換到JDK 10或者更新的版本,問題就更加簡單了。 Java對容器( Docker)的支持已經比較完善,默認就會自適應各類資源限制和實現差別。前面提到的實驗性參
數「UseCGroupMemoryLimitForHeap」已經被標記爲廢棄。

與此同時,新增了參數用以明確指定CPU核心的數目。

-XX:ActiveProcessorCount=N

可是,若是我暫時只能使用老版本的JDK怎麼辦?

這裏有幾個建議:

明確設置堆、元數據區等內存區域大小,保證Java進程的總大小可控。

例如,咱們可能在環境中,這樣限制容器內存:

$ docker run -it --rm --name yourcontainer -p 8080:8080 -m 800M repo/your-java-container:openjdk

那麼,就能夠額外配置下面的環境變量,直接指定JVM堆大小。

-e JAVA_OPTIONS='-Xmx300m'

明確配置GC和JIT並行線程數目,以免兩者佔用過多計算資源。

-XX:ParallelGCThreads
-XX:CICompilerCount

除了我前面介紹的OOM等問題,在不少場景中還發現Java在Docker環境中,彷佛會意外使用Swap。具體緣由待查,但頗有可能也是由於Ergonomics機制失效致使的,我建議配
置下面參數,明確告知JVM系統內存限額。

-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes`

也能夠指定Docker運行參數,例如:

--memory-swappiness=0

你瞭解Java應用開發中的注入攻擊嗎?

下面是幾種主要的注入式攻擊途徑,原則上提供動態執行能力的語言特性,都須要提防發生注入攻擊的可能。

首先,就是最多見的SQL注入攻擊。一個典型的場景就是Web系統的用戶登陸功能,根據用戶輸入的用戶名和密碼,咱們須要去後端數據庫覈實信息.

假設應用邏輯是,後端程序利用界面輸入動態生成相似下面的SQL,而後讓JDBC執行。

Select * from use_info where username = 「input_usr_name」 and password = 「input_pwd」

可是,若是我輸入的input_pwd是相似下面的文本,

「 or 「」=」

那麼,拼接出的SQL字符串就變成了下面的條件, OR的存在致使輸入什麼名字都是複合條件的。

Select * from use_info where username = 「input_usr_name」 and password = 「」 or 「」 = 「」

第二,操做系統命令注入。 Java語言提供了相似Runtime.exec(…)的API,能夠用來執行特定命令,假設咱們構建了一個應用,以輸入文本做爲參數,執行下面的命令:

ls –la input_fle_name

可是若是用戶輸入是 「input_fle_name;rm –rf /*」,這就有可能出現問題了 .

第三, XML注入攻擊。 Java核心類庫提供了全面的XML處理、轉換等各類API,而XML自身是能夠包含動態內容的,例如XPATH,若是使用不當,可能致使訪問惡意內容。
還有相似LDAP等容許動態內容的協議,都是可能利用特定命令,構造注入式攻擊的,包括XSS( Cross-site Scripting)攻擊,雖然並不和Java直接相關,但也可能在JSP等動態頁
面中發生。

知識擴展
首先,一塊兒來看看哪些Java API和工具構成了Java安全基礎。不少方面我在專欄前面的講解中已經有所涉及,能夠簡單歸爲三個主要組成部分:

第一,運行時安全機制。能夠簡單認爲,就是限制Java運行時的行爲,不要作越權或者不靠譜的事情,具體來看:

在類加載過程當中,進行字節碼驗證,以防止不合規的代碼影響JVM運行或者載入其餘惡意代碼。

類加載器自己也能夠對代碼之間進行隔離,例如,應用沒法獲取啓動類加載器( Bootstrap Class-Loader)對象實例,不一樣的類加載器也能夠起到容器的做用,隔離模塊之間不
必要的可見性等。目前, Java Applet、 RMI等特性已經或逐漸退出歷史舞臺,類加載等機制整體上反倒在不斷簡化。

利用SecurityManger機制和相關的組件,限制代碼的運行時行爲能力,其中,你能夠定製policy文件和各類粒度的權限定義,限制代碼的做用域和權限,例如對文件系統的操做
權限,或者監聽某個網絡端口的權限等.

另外,從原則上來講, Java的GC等資源回收管理機制,均可以看做是運行時安全的一部分,若是相應機制失效,就會致使JVM出現OOM等錯誤,可看做是另類的拒絕服務。

第二, Java提供的安全框架API,這是構建安全通訊等應用的基礎。例如:

  • 加密、解密API。
  • 受權、鑑權API。
  • 安全通訊相關的類庫,好比基本HTTPS通訊協議相關標準實現,如TLS 1.3;或者附屬的相似證書撤銷狀態判斷( OSCP)等協議實現。
    注意,這一部分API內部實現是和廠商相關的,不一樣JDK廠商每每會定製本身的加密算法實現。

第三, 就是JDK集成的各類安全工具,例如:

  • keytool,這是個強大的工具,能夠管理安全場景中不可或缺的祕鑰、證書等,而且能夠管理Java程序使用的keystore文件。
  • jarsigner,用於對jar文件進行簽名或者驗證。

在應用實踐中,若是對安全要求很是高,建議打開SecurityManager,

-Djava.security.manager

請注意其開銷,一般只要開啓SecurityManager,就會致使10% ~ 15%的性能降低,在JDK 9之後,這個開銷有所改善.

如何寫出安全的Java代碼?

典型回答
這個問題可能有點寬泛,咱們能夠用特定類型的安全風險爲例,如拒絕服務( DoS)攻擊,分析Java開發者須要重點考慮的點。

DoS是一種常見的網絡攻擊,有人也稱其爲「洪水攻擊」。最多見的表現是,利用大量機器發送請求,將目標網站的帶寬或者其餘資源耗盡,致使其沒法響應正經常使用戶的請求。
我認爲,從Java語言的角度,更加須要重視的是程序級別的攻擊,也就是利用Java、 JVM或應用程序的瑕疵,進行低成本的DoS攻擊,這也是想要寫出安全的Java代碼所必須考慮
的。例如

Java提供了序列化等創新的特性,普遍使用在遠程調用等方面,但也帶來了複雜的安全問題。直到今天,序列化仍然是個安全問題頻發的場景。
針對序列化,一般建議:

  • 敏感信息不要被序列化!在編碼中,建議使用transient關鍵字將其保護起來。
  • 反序列化中,建議在readObject中實現與對象構件過程相同的安全檢查和數據檢查。

後臺服務出現明顯「變慢」,談談你的診斷思路?

典型回答
首先,須要對這個問題進行更加清晰的定義:

  • 服務是忽然變慢仍是長時間運行後觀察到變慢?相似問題是否重複出現?
  • 「慢」的定義是什麼,我可以理解是系統對其餘方面的請求的反應延時變長嗎?

第二,理清問題的症狀,這更便於定位具體的緣由,有如下一些思路:

問題可能來自於Java服務自身,也可能僅僅是受系統裏其餘服務的影響。初始判斷能夠先確認是否出現了意外的程序錯誤,例如檢查應用自己的錯誤日誌。
對於分佈式系統,不少公司都會實現更加系統的日誌、性能等監控系統。一些Java診斷工具也能夠用於這個診斷,例如經過JFR( Java Flight Recorder),監控應用是否大量出
現了某種類型的異常。

若是有,那麼異常可能就是個突破點。

若是沒有,能夠先檢查系統級別的資源等狀況,監控CPU、內存等資源是否被其餘進程大量佔用,而且這種佔用是否不符合系統正常運行情況。
監控Java服務自身,例如GC日誌裏面是否觀察到Full GC等惡劣狀況出現,或者是否Minor GC在變長等;利用jstat等工具,獲取內存使用的統計信息也是個經常使用手段;利
用jstack等工具檢查是否出現死鎖等。

若是還不能肯定具體問題,對應用進行Profling也是個辦法,但由於它會對系統產生侵入性,若是不是很是必要,大多數狀況下並不建議在生產系統進行。
定位了程序錯誤或者JVM配置的問題後,就能夠採起相應的補救措施,而後驗證是否解決,不然還須要重複上面部分過程。

根據系統架構不一樣,分佈式系統和大型單體應用也存在着思路的區別,例如,分佈式系統的性能瓶頸可能更加集中。傳統意義上的性能調優大可能是針對單體應用的調優,專欄的側重
點也是如此, Charlie Hunt曾將其方法論總結爲兩類:

  • 自上而下。從應用的頂層,逐步深刻到具體的不一樣模塊,或者更近一步的技術細節單元,找到可能的問題和解決辦法。這是最多見的性能分析思路,也是大多數工程師的選擇。
  • 自下而上。從相似CPU這種硬件底層,判斷相似Cache-Miss之類的問題和調優機會,出發點是指令級別優化。這每每是專業的性能工程師才能掌握的技能,而且須要專業工具配
    合,大多數是移植到新的平臺上,或須要提供極致性能時纔會進行。

自上而下分析中,各個階段的常見工具和思路。須要注意的是,具體的工具在不一樣的操做系統上可能區別很是大。

系統性能分析中, CPU、內存和IO是主要關注項。

怎麼找到最耗費CPU的Java線程,簡要介紹步驟:

  • 利用top命令獲取相應pid, 「-H」表明thread模式,你能夠配合grep命令更精準定位。
top –H

而後轉換成爲16進制。

printf "%x" your_pid

最後利用jstack獲取的線程棧,對比相應的ID便可。

談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?

典型回答

所謂隔離級別( Isolation Level),就是在數據庫事務中,爲保證併發數據讀寫的正確性而提出的定義,它並非MySQL專有的概念,而是源於ANSI/ISO制定的SQL-92標準。
每種關係型數據庫都提供了各自特點的隔離級別實現,雖然在一般的定義中是以鎖爲實現單元,但實際的實現千差萬別。以最多見的MySQL InnoDB引擎爲例,它是基於
MVCC( Multi-Versioning Concurrency Control)和鎖的複合實現,按照隔離程度從低到高, MySQL事務隔離級別分爲四個不一樣層次:

  • 讀未提交( Read uncommitted),就是一個事務可以看到其餘事務還沒有提交的修改,這是最低的隔離水平,容許髒讀出現。
  • 讀已提交( Read committed),事務可以看到的數據都是其餘事務已經提交的修改,也就是保證不會看到任何中間性狀態,固然髒讀也不會出現。讀已提交仍然是比較低級別的
    隔離,並不保證再次讀取時可以獲取一樣的數據,也就是容許其餘事務併發修改數據,容許不可重複讀和幻象讀( Phantom Read)出現。
  • 可重複讀( Repeatable reads),保證同一個事務中屢次讀取的數據是一致的,這是MySQL InnoDB引擎的默認隔離級別,可是和一些其餘數據庫實現不一樣的是,能夠簡單認
    爲MySQL在可重複讀級別不會出現幻象讀。
  • 串行化( Serializable),併發事務之間是串行化的,一般意味着讀取須要獲取共享讀鎖,更新須要獲取排他寫鎖,若是SQL使用WHERE語句,還會獲取區間鎖
    ( MySQL以GAP鎖形式實現,可重複讀級別中默認也會使用),這是最高的隔離級別。

對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?

典型回答

單獨從性能角度, Netty在基礎的NIO等類庫之上進行了不少改進,例如:

  • 更加優雅的Reactor模式實現、靈活的線程模型、利用EventLoop等創新性的機制,能夠很是高效地管理成百上千的Channel。
  • 充分利用了Java的Zero-Copy機制,而且從多種角度, 「斤斤計較」般的下降內存分配和回收的開銷。例如,使用池化的Direct Bufer等技術,在提升IO性能的同時,減小了對象
    的建立和銷燬;利用反射等技術直接操縱SelectionKey,使用數組而不是Java容器等。
  • 使用更多本地代碼。例如,直接利用JNI調用Open SSL等方式,得到比Java內建SSL引擎更好的性能。
  • 在通訊協議、序列化等其餘角度的優化。

從設計思路和目的上, Netty與Java自身的NIO框架相比有哪些不一樣呢?
從API能力範圍來看, Netty徹底是Java NIO框架的一個大大的超集.

談談經常使用的分佈式ID的設計方案? Snowfake是否受冬令時切換影響?

典型回答

首先,咱們須要明確一般的分佈式ID定義,基本的要求包括:

  • 全局惟一,區別於單點系統的惟一,全局是要求分佈式系統內惟一。
  • 有序性,一般都須要保證生成的ID是有序遞增的。例如,在數據庫存儲等場景中,有序ID便於肯定數據位置,每每更加高效。

目前業界的方案不少,典型方案包括:

  • 基於數據庫自增序列的實現。這種方式優缺點都很是明顯,好處是簡單易用,可是在擴展性和可靠性等方面存在侷限性。
  • 基於Twitter早期開源的Snowfake的實現,以及相關改動方案。這是目前應用相對比較普遍的一種方式,其結構定義你能夠參考下面的示意圖。

在這裏插入圖片描述

  • 總體長度一般是64 ( 1 + 41 + 10+ 12 = 64)位,適合使用Java語言中的long類型來存儲。
  • 頭部是1位的正負標識位。
  • 緊跟着的高位部分包含41位時間戳,一般使用System.currentTimeMillis()。
  • 後面是10位的WorkerID,標準定義是5位數據中心 + 5位機器ID,組成了機器編號,以區分不一樣的集羣節點。
  • 最後的12位就是單位毫秒內可生成的序列號數目的理論極限。

Redis、 Zookeeper、 MangoDB等中間件,也都有各類惟一ID解決方案。其中一些設計也能夠算做是Snowfake方案的變種。例如, MongoDB的ObjectId提供了一個12
byte( 96位)的ID定義,其中32位用於記錄以秒爲單位的時間,機器ID則爲24位, 16位用做進程ID, 24位隨機起始的計數序列。

國內的一些大廠開源了其自身的部分分佈式ID實現, InfoQ就曾經介紹過微信的seqsvr,它採起了相對複雜的兩層架構,並根據社交應用的數據特色進行了針對性設計,具體請參考相關代碼實現。另外, 百度、美團等也都有開源或者分享了不一樣的分佈式ID實現,均可以進行參考。

再補充一些當前分佈式領域的面試熱點,例如:

  • 分佈式事務,包括其產生緣由、業務背景、主流的解決方案等。
  • 理解CAP、 BASE等理論,懂得從最終一致性等角度來思考問題,理解Paxos、 Raft等一致性算法。
  • 理解典型的分佈式鎖實現,例如最多見的Redis分佈式鎖。
  • 負載均衡等分佈式領域的典型算法,至少要了解主要方案的原理。
相關文章
相關標籤/搜索