典型回答:
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
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語言提供的一種基礎功能,賦予程序在運行時自省( introspect,官方用語)的能力。經過反射咱們能夠直接操做類或者對象,好比獲取某個對象的類定義,獲取類
聲明的屬性和方法,調用方法或者構造對象,甚至能夠運行時修改類定義。
動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,不少場景都是利用相似機制作到的,好比用來包裝RPC調用、面向切面的編程( AOP)。
實現動態代理的方式不少,好比JDK自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其餘的實現方式,好比利用傳說中更高性能的字節碼操做機制,類
似ASM、 cglib(基於ASM)、 Javassist等。
咱們知道Spring AOP支持兩種模式的動態代理, JDK Proxy或者cglib,若是咱們選擇cglib方式,你會發現對接口的依賴被克服了。
cglib動態代理採起的是建立目標類的子類的方式,由於是子類化,咱們能夠達到近似使用被調用者自己的效果。
典型回答:
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纔有,一樣存在於其餘的一些包裝類,好比:
注意事項:
[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是Java早期提供的線程安全的動態數組,若是不須要線程安全,並不建議選擇,畢竟同步是有額外開銷的。 Vector內部是使用對象數組來保存數據,能夠根據須要自動的增長
容量,當數組已滿時,會建立新的數組,並拷貝原有數組數據。
ArrayList是應用更加普遍的動態數組實現,它自己不是線程安全的,因此性能要好不少。與Vector近似, ArrayList也是能夠根據須要調整容量,不過二者的調整邏輯有所區
別, Vector在擴容時會提升1倍,而ArrayList則是增長50%。
LinkedList顧名思義是Java提供的雙向鏈表,因此它不須要像上面兩種那樣調整容量,它也不是線程安全的。
咱們能夠看到Java的集合框架, Collection接口是全部集合的根,而後擴展開提供了三大類集合,分別是:
今天介紹的這些集合類,都不是線程安全的,對於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都是最多見的一些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:
而對於負載因子,我建議:
那麼,爲何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),鏈表就會被改造爲樹形結構。
典型回答:
Java提供了不一樣層面的線程安全支持。在傳統集合框架內部,除了Hashtable等同步容器,還提供了所謂的同步包裝器(Synchronized Wrapper),咱們能夠調用Collections工具類提供的包裝方法,來獲取一個同步的包裝容器(如Collections.synchronizedMap),可是它們都是利用很是粗粒度的同步方式,在高併發狀況下,性能比較低下。
另外,更加廣泛的選擇是利用併發包提供的線程安全容器類,它提供了:
具體保證線程安全的方式,包括有從簡單的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,其實現是基於:
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); }
因此,從上面的源碼清晰的看出,在進行併發寫操做時:
size:
分段計算兩次,兩次結果相同則返回,不然對因此段加鎖從新計算
在Java 8和以後的版本中, ConcurrentHashMap發生了哪些變化呢?
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方式有不少種,基於不一樣的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!")); } // 省略其餘路徑處理方法... });
小結:
典型回答
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等如何在API設計中使用設計模式。你至少要有個大致的印象,如:
典型回答
synchronized是Java內建的同步機制,因此也有人稱其爲Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其餘試圖獲取的線程只能等待或者阻
塞在那裏。
在Java 5之前, synchronized是僅有的同步手段,在代碼中, synchronized能夠用來修飾方法,也能夠使用在特定的代碼塊兒上,本質上synchronized方法等同於把方法所有語
句用synchronized塊包起來。
ReentrantLock,一般翻譯爲再入鎖,是Java 5提供的鎖實現,它的語義和synchronized基本相同。再入鎖經過代碼直接調用lock()方法獲取,代碼書寫也更加靈活。與此同
時, ReentrantLock提供了不少實用的方法,可以實現不少synchronized沒法作到的細節控制,好比能夠控制fairness,也就是公平性,或者利用定義條件等。可是,編碼中也需
要注意,必需要明確調用unlock()方法釋放,否則就會一直持有該鎖。
synchronized和ReentrantLock的性能不能一律而論,早期版本synchronized在不少場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優
於ReentrantLock。
線程安全須要保證幾個基本特性:
ReentrantLock。你可能好奇什麼是再入?它是表示當一個線程試圖獲取一個它已經獲取的鎖時,這個獲取動做就自動成功,這是對鎖獲取粒度的一個概念,也就是鎖的持
有是以線程爲單位而不是基於調用次數。 Java鎖實現強調再入性是爲了和thread的行爲進行區分。
ReentrantLock相比synchronized,由於能夠像普通對象同樣使用,因此能夠利用其提供的各類便利方法,進行精細的同步操做,甚至是實現synchronized難以表達的用例,如:
帶超時的獲取鎖嘗試。
能夠判斷是否有線程,或者某個特定線程,在排隊等待獲取鎖。
能夠響應中斷請求。
…
這裏我特別想強調條件變量( java.util.concurrent.Condition),若是說ReentrantLock是synchronized的替代選擇, Condition則是將wait、 notify、 notifyAll等操做轉化爲相
應的對象,將複雜而晦澀的同步操做轉變爲直觀可控的對象行爲。
條件變量最爲典型的應用場景就是標準類庫中的ArrayBlockingQueue等。
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併發包提供的讀寫鎖等擴展了鎖的能力,它所基於的原理是多個讀操做是不須要互斥的,由於讀操做並不會更改數據,因此不存在互相干擾。而寫操做則會致使併發一致性的問
題,因此寫線程之間、讀寫線程之間,須要精心設計的互斥邏輯。
典型回答
Java的線程是不容許啓動兩次的,第二次調用必然會拋出IllegalThreadStateException,這是一種運行時異常,屢次調用start被認爲是編程錯誤。
關於線程生命週期的不一樣狀態,在Java 5之後,線程狀態被明肯定義在其公共內部枚舉類型java.lang.Thread.State中,分別是:
public fnal native void wait(long timeout) throws InterruptedException;
知識擴展
1.首先,咱們來總體看一下線程是什麼?
從操做系統的角度,能夠簡單認爲,線程是系統調度的最小單元,一個進程能夠包含多個線程,做爲任務的真正運做者,有本身的棧( Stack)、寄存器( Register)、本地存儲
( Thread Local)等,可是會和進程內其餘線程共享文件描述符、虛擬地址空間等。
2.從線程生命週期的狀態開始展開,那麼在Java編程中,有哪些因素可能影響線程的狀態呢?主要有:
典型回答
死鎖是一種特定的程序狀態,在實體之間,因爲循環依賴致使彼此一直處於等待之中,沒有任何個體能夠繼續前進。死鎖不只僅是在線程之間會發生,存在資源獨佔的進程之間一樣
也可能出現死鎖。一般來講,咱們大可能是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,因爲互相持有對方須要的鎖,而永久處於阻塞的狀態。
定位死鎖最多見的方式就是利用jstack等工具獲取線程棧,而後定位互相之間的依賴關係,進而找到死鎖。若是是比較明顯的死鎖,每每jstack等就能直接定位,相似JConsole甚至
能夠在圖形界面進行有限的死鎖檢測。
如何在編程中儘可能預防死鎖呢?
首先,咱們來總結一下前面例子中死鎖的產生包含哪些基本元素。基本上死鎖的發生是由於:
第一種方法
若是可能的話,儘可能避免使用多個鎖,而且只有須要時才持有鎖。不然,即便是很是精通併發編程的工程師,也不免會掉進坑裏,嵌套的synchronized或者lock很是容易出問題。
第二種方法
若是必須使用多個鎖,儘可能設計好鎖的獲取順序,這個提及來簡單,作起來可不容易,你能夠參看著名的銀行家算法.
第三種方法
使用帶超時的方法,爲程序帶來更多可控性。
相似Object.wait(…)或者CountDownLatch.await(…),都支持所謂的timed_wait,咱們徹底能夠就不假定該鎖必定會得到,指定超時時間,併爲沒法獲得鎖時準備退出邏輯。
典型回答
咱們一般所說的併發包也就是java.util.concurrent及其子包,集中了Java併發的各類基礎工具類,具體主要包括幾個方面:
**知識擴展 **
Semaphore
1.工做原理
以一個停車場是運做爲例。爲了簡單起見,假設停車場只有三個車位,一開始三個車位都是空的。這時若是同時來了五輛車,看門人容許其中三輛不受阻礙的進入,而後放下車攔,剩下的車則必須在入口等待,此後來的車也都不得不在入口處等待。這時,有一輛車離開停車場,看門人得知後,打開車攔,放入一輛,若是又離開兩輛,則又能夠放入兩輛,如此往復。這個停車系統中,每輛車就比如一個線程,看門人就比如一個信號量,看門人限制了能夠活動的線程。假如裏面依然是三個車位,可是看門人改變了規則,要求每次只能停兩輛車,那麼一開始進入兩輛車,後面得等到有車離開纔能有車進入,可是得保證最多停兩輛車。對於Semaphore類而言,就如同一個看門人,限制了可活動的線程數。
2.主要方法
https://www.cnblogs.com/klbc/p/9500947.html
下面,來看看CountDownLatch和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這種「Concurrent*」容器,纔是真正表明併發。
關於問題中它們的區別:
不知道你有沒有注意到, java.util.concurrent包提供的容器( Queue、 List、 Set)、 Map,從命名上能夠大概區分爲Concurrent、 CopyOnWrite和Blocking*等三類,一樣是線
程安全容器,能夠簡單認爲:
知識擴展
線程安全隊列一覽:
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的性能表現,每每大大超過其餘實現,尤爲是在隊列元素較小的場景。
典型回答
一般開發者都是利用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是一個基礎的接口,其初衷是將任務提交和任務執行細節解耦,這一點能夠體會其定義的惟一方法。
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實現。
從上面的分析,就能夠看出線程池的幾個基本組成部分,一塊兒都體如今線程池的構造函數中,從字面咱們就能夠大概猜想到其用意:
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)
典型回答
AtomicIntger是對int類型的一個封裝,提供原子性的訪問和更新操做,其原子性操做的實現是基於CAS( compare-and-swap).
目前Java提供了兩種公共API,能夠實現這種CAS操做,好比使用java.util.concurrent.atomic.AtomicLongFieldUpdater,它是基於反射機制建立,咱們須要保證類型和字段名稱正確。
AQS內部數據和方法,能夠簡單拆分爲:
private volatile int sate;
利用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內存區域分爲下面幾個方面,其中,有的區域是以線程爲單位,而有的區域則是整個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內存的方法有不少,具體能力範圍也有區別,簡單總結以下:
能夠使用綜合性的圖形化工具,如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;
典型回答:
實際上,垃圾收集器( 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」就
體如今清理工做是與工做線程一塊兒併發運行的.
總結:
典型回答
Happen-before關係,是Java內存模型中保證多線程操做可見性的機制,也是對早期語言規範中含糊的可見性概念的一個精肯定義。
它的具體表現形式,包括但遠不止是咱們直覺中的synchronized、 volatile、 lock操做順序等方面,例如:
JMM內部的實現一般是依賴於所謂的內存屏障,經過禁止某些重排序的方式,提供內存可見性保證,也就是實現了各類happen-before規則。與此同時,更多複雜度在於,須要儘可能
確保各類編譯器、各類體系結構的處理器,都可以提供一致的行爲。
以volatile爲例,看看如何利用內存屏障實現JMM定義的可見性?
對於一個volatile變量:
內存屏障可以在相似變量讀、寫操做以後,保證其餘線程對volatile變量的修改對當前線程可見,或者本地修改對其餘線程提供可見性。換句話說,線程寫入,寫屏障會經過相似強迫
刷出處理器緩存的方式,讓其餘線程可以拿到最新數值。
典型回答
對於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機制,例如:
這些默認參數,是根據通用場景選擇的初始值。可是因爲容器環境的差別, Java的判斷極可能是基於錯誤信息而作出的。
這就相似,我覺得我住的是整棟別墅,實際上卻只有一個房間是給我住的。
更加嚴重的是, JVM的一些原有診斷或備用機制也會受到影響。爲保證服務的可用性,一種常見的選擇是依賴「-XX:OnOutOfMemoryError」功能,經過調用處理腳本的形式來作一
些補救措施,好比自動重啓服務等。可是,這種機制是基於fork實現的,當Java進程已通過度提交內存時, fork新的進程每每已經不可能正常運行了。
根據前面的總結,彷佛問題很是棘手,那咱們在實踐中, 如何解決這些問題呢?
-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
下面是幾種主要的注入式攻擊途徑,原則上提供動態執行能力的語言特性,都須要提防發生注入攻擊的可能。
首先,就是最多見的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,這是構建安全通訊等應用的基礎。例如:
第三, 就是JDK集成的各類安全工具,例如:
在應用實踐中,若是對安全要求很是高,建議打開SecurityManager,
-Djava.security.manager
請注意其開銷,一般只要開啓SecurityManager,就會致使10% ~ 15%的性能降低,在JDK 9之後,這個開銷有所改善.
典型回答
這個問題可能有點寬泛,咱們能夠用特定類型的安全風險爲例,如拒絕服務( DoS)攻擊,分析Java開發者須要重點考慮的點。
DoS是一種常見的網絡攻擊,有人也稱其爲「洪水攻擊」。最多見的表現是,利用大量機器發送請求,將目標網站的帶寬或者其餘資源耗盡,致使其沒法響應正經常使用戶的請求。
我認爲,從Java語言的角度,更加須要重視的是程序級別的攻擊,也就是利用Java、 JVM或應用程序的瑕疵,進行低成本的DoS攻擊,這也是想要寫出安全的Java代碼所必須考慮
的。例如
Java提供了序列化等創新的特性,普遍使用在遠程調用等方面,但也帶來了複雜的安全問題。直到今天,序列化仍然是個安全問題頻發的場景。
針對序列化,一般建議:
典型回答
首先,須要對這個問題進行更加清晰的定義:
第二,理清問題的症狀,這更便於定位具體的緣由,有如下一些思路:
問題可能來自於Java服務自身,也可能僅僅是受系統裏其餘服務的影響。初始判斷能夠先確認是否出現了意外的程序錯誤,例如檢查應用自己的錯誤日誌。
對於分佈式系統,不少公司都會實現更加系統的日誌、性能等監控系統。一些Java診斷工具也能夠用於這個診斷,例如經過JFR( Java Flight Recorder),監控應用是否大量出
現了某種類型的異常。
若是有,那麼異常可能就是個突破點。
若是沒有,能夠先檢查系統級別的資源等狀況,監控CPU、內存等資源是否被其餘進程大量佔用,而且這種佔用是否不符合系統正常運行情況。
監控Java服務自身,例如GC日誌裏面是否觀察到Full GC等惡劣狀況出現,或者是否Minor GC在變長等;利用jstat等工具,獲取內存使用的統計信息也是個經常使用手段;利
用jstack等工具檢查是否出現死鎖等。
若是還不能肯定具體問題,對應用進行Profling也是個辦法,但由於它會對系統產生侵入性,若是不是很是必要,大多數狀況下並不建議在生產系統進行。
定位了程序錯誤或者JVM配置的問題後,就能夠採起相應的補救措施,而後驗證是否解決,不然還須要重複上面部分過程。
根據系統架構不一樣,分佈式系統和大型單體應用也存在着思路的區別,例如,分佈式系統的性能瓶頸可能更加集中。傳統意義上的性能調優大可能是針對單體應用的調優,專欄的側重
點也是如此, Charlie Hunt曾將其方法論總結爲兩類:
自上而下分析中,各個階段的常見工具和思路。須要注意的是,具體的工具在不一樣的操做系統上可能區別很是大。
系統性能分析中, CPU、內存和IO是主要關注項。
怎麼找到最耗費CPU的Java線程,簡要介紹步驟:
top –H
而後轉換成爲16進制。
printf "%x" your_pid
最後利用jstack獲取的線程棧,對比相應的ID便可。
典型回答
所謂隔離級別( Isolation Level),就是在數據庫事務中,爲保證併發數據讀寫的正確性而提出的定義,它並非MySQL專有的概念,而是源於ANSI/ISO制定的SQL-92標準。
每種關係型數據庫都提供了各自特點的隔離級別實現,雖然在一般的定義中是以鎖爲實現單元,但實際的實現千差萬別。以最多見的MySQL InnoDB引擎爲例,它是基於
MVCC( Multi-Versioning Concurrency Control)和鎖的複合實現,按照隔離程度從低到高, MySQL事務隔離級別分爲四個不一樣層次:
典型回答
單獨從性能角度, Netty在基礎的NIO等類庫之上進行了不少改進,例如:
從設計思路和目的上, Netty與Java自身的NIO框架相比有哪些不一樣呢?
從API能力範圍來看, Netty徹底是Java NIO框架的一個大大的超集.
典型回答
首先,咱們須要明確一般的分佈式ID定義,基本的要求包括:
目前業界的方案不少,典型方案包括:
Redis、 Zookeeper、 MangoDB等中間件,也都有各類惟一ID解決方案。其中一些設計也能夠算做是Snowfake方案的變種。例如, MongoDB的ObjectId提供了一個12
byte( 96位)的ID定義,其中32位用於記錄以秒爲單位的時間,機器ID則爲24位, 16位用做進程ID, 24位隨機起始的計數序列。
國內的一些大廠開源了其自身的部分分佈式ID實現, InfoQ就曾經介紹過微信的seqsvr,它採起了相對複雜的兩層架構,並根據社交應用的數據特色進行了針對性設計,具體請參考相關代碼實現。另外, 百度、美團等也都有開源或者分享了不一樣的分佈式ID實現,均可以進行參考。
再補充一些當前分佈式領域的面試熱點,例如: