主要內容:Kotlin, Java, RxJava, 多線程/併發, 集合java
LruCache 用來實現基於內存的緩存,LRU 就是最近最少使用的意思,LruCache 基於 LinkedHashMap 實現。LinkedHashMap 是在 HashMap 的基礎之上進行了封裝,除了具備哈希功能,還將數據插入到雙向鏈表中維護。每次讀取的數據會被移動到鏈表的尾部,當達到了緩存的最大的容量的時候就將鏈表的首部移出。使用 LruCache 的時候須要注意的是單位的問題,由於該 API 並不清楚要存儲的數據是如何計算大小的,因此它提供了方法供咱們實現大小的計算方式。(《Android 內存緩存框架 LruCache 的源碼分析》)git
DiskLruCache 與 LruCache 相似,也是用來實現緩存的,而且也是基於 LinkedHashMap 實現的。不一樣的是,它是基於磁盤緩存的,LruCache 是基於內存緩存的。因此,LinkedHashMap 可以存儲的空間更大,可是讀寫的速率也更慢。使用 DiskLruCache 的時候須要到 Github 上面去下載。OkHttp 和 Glide 的磁盤緩存都是基於 DiskLruCache 開發的。DiskLruCahce 內部維護了一個日誌文件,記錄了讀寫的記錄的信息。其餘的基本都是基礎的磁盤 IO 操做。github
若是想線程安全地使用這列表類(能夠參考下面的問題)面試
咱們有幾種方式能夠線程間安全地操做 List. 具體使用哪一種方式,能夠根據具體的業務邏輯進行選擇。一般有如下幾種方式:數據庫
sychronized
進行控制。咱們能夠在咱們本身的業務方法上面進行加鎖來保證線程安全。Collections.synchronizedList()
進行包裝。這個方法內部使用了私有鎖來實現線程安全,就是經過對一個全局變量進行加鎖。調用咱們的 List 的方法以前須要先獲取該私有鎖。私有鎖能夠下降鎖粒度。CopyOnWriteArrayList
代替 ArrayList,使用 ConcurrentLinkedQueue
代替 LinkedList. 併發容器中的 CopyOnWriteArrayList
在讀的時候不加鎖,寫的時候使用 Lock 加鎖。ConcurrentLinkedQueue
則是基於 CAS 的思想,在增刪數據以前會先進行比較。SparseArray 主要用來替換 Java 中的 HashMap,由於 HashMap 將整數類型的鍵默認裝箱成 Integer (效率比較低). 而 SparseArray 經過內部維護兩個數組來進行映射,而且使用二分查找尋找指定的鍵,因此它的鍵對應的數組無需是包裝類型。SparseArray 用於當 HashMap 的鍵是 Integer 的狀況,它會在內部維護一個 int 類型的數組來存儲鍵。同理,還有 LongSparseArray, BooleanSparseArray 等,都是用來經過減小裝箱操做來節省內存空間的。可是,由於它內部使用二分查找尋找鍵,因此其效率不如 HashMap 高,因此當要存儲的鍵值對的數量比較大的時候,考慮使用 HashMap.編程
HashMap (下稱 HM) 是哈希表,ConcurrentHashMap (下稱 CHM) 也是哈希表,它們之間的區別是 HM 不是線程安全的,CHM 線程安全,而且對鎖進行了優化。對應 HM 的還有 HashTable (下稱 HT),它經過對內部的每一個方法加鎖來實現線程安全,效率較低。設計模式
HashMap 的實現原理:HashMap 使用拉鍊法來解決哈希衝突,即當兩個元素的哈希值相等的時候,它們會被方進一個桶當中。當一個桶中的數據量比較多的時候,此時 HashMap 會採起兩個措施,要麼擴容,要麼將桶中元素的數據結構從鏈表轉換成紅黑樹。所以存在幾個常量會決定 HashMap 的表現。在默認的狀況下,當 HashMap 中的已經被佔用的桶的數量達到了 3/4 的時候,會對 HashMap 進行擴容。當一個桶中的元素的數量達到了 8 個的時候,若是桶的數量達到了 64 個,那麼會將該桶中的元素的數據結構從鏈表轉換成紅黑樹。若是桶的數量尚未達到 64 個,那麼此時會對 HashMap 進行擴容,而不是轉換數據結構。數組
從數據結構上,HashMap 中的桶中的元素的數據結構從鏈表轉換成紅黑樹的時候,仍然能夠保留其鏈表關係。由於 HashMap 中的 TreeNode 繼承了 LinkedHashMap 中的 Entry,所以它存在兩種數據結構。緩存
HashMap 在實現的時候對性能進行了不少的優化,好比使用截取後面幾位而不是取餘的方式計算元素在數組中的索引。使用哈希值的高 16 位與低 16 進行異或運算來提高哈希值的隨機性。安全
由於每一個桶的元素的數據結構有兩種可能,所以,當對 HashMap 進行增刪該查的時候都會根據結點的類型分紅兩種狀況來進行處理。當數據結構是鏈表的時候處理起來都很是容易,使用一個循環對鏈表進行遍歷便可。當數據結構是紅黑樹的時候處理起來比較複雜。紅黑樹的查找能夠沿用二叉樹的查找的邏輯。
下面是 HashMap 的插入的邏輯,全部的插入操做最終都會調用到內部的 putVal()
方法來最終完成。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
private V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) { // 原來的數組不存在
n = (tab = resize()).length;
}
i = (n - 1) & hash; // 取哈希碼的後 n-1 位,以獲得桶的索引
p = tab[i]; // 找到桶
if (p == null) {
// 若是指定的桶不存在就建立一個新的,直接new 出一個 Node 來完成
tab[i] = newNode(hash, key, value, null);
} else {
// 指定的桶已經存在
Node<K,V> e; K k;
if (p.hash == hash // 哈希碼相同
&& ((k = p.key) == key || (key != null && key.equals(k))) // 鍵的值相同
) {
// 第一個結點與咱們要插入的鍵值對的鍵相等
e = p;
} else if (p instanceof TreeNode) {
// 桶的數據結構是紅黑樹,調用紅黑樹的方法繼續插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
// 桶的數據結構是鏈表,使用鏈表的處理方式繼續插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 已經遍歷到了鏈表的結尾,尚未找到,須要新建一個結點
p.next = newNode(hash, key, value, null);
// 插入新結點以後,若是某個鏈表的長度 >= 8,則要把鏈表轉成紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
break;
}
if (e.hash == hash // 哈希碼相同
&& ((k = e.key) == key || (key != null && key.equals(k))) // 鍵的值相同
) {
// 說明要插入的鍵值對的鍵是存在的,須要更新以前的結點的數據
break;
}
p = e;
}
}
if (e != null) {
// 說明指定的鍵是存在的,須要更新結點的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
return oldValue;
}
}
++modCount;
// 若是插入了新的結點以後,哈希表的容量大於 threshold 就進行擴容
if (++size > threshold) {
resize(); // 擴容
}
return null;
}
複製代碼
上面是 HashMap 的插入的邏輯,能夠看出,它也是根據頭結點的類型,分紅紅黑樹和鏈表兩種方式來進行處理的。對於鏈表,上面已經給出了具體的插入邏輯。在鏈表的情形中,除了基礎的插入,當鏈表的長度達到了 8 的時候還要將桶的數據結構從鏈表轉型成爲紅黑樹。對於紅黑樹類型的數據結構,它調用 TreeNode 的 putTreeVal()
方法來完成往紅黑樹中插入結點的邏輯。(代碼貼出來,慢慢領會吧:))
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 查找根節點, 索引位置的頭節點並不必定爲紅黑樹的根結點
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) { // 將根節點賦值給 p, 開始遍歷
int dir, ph; K pk;
if ((ph = p.hash) > h)
// 若是傳入的 hash 值小於 p 節點的 hash 值,則將 dir 賦值爲 -1, 表明向 p 的左邊查找樹
dir = -1;
else if (ph < h)
// 若是傳入的 hash 值大於 p 節點的 hash 值,則將 dir 賦值爲 1, 表明向 p 的右邊查找樹
dir = 1;
// 若是傳入的 hash 值和 key 值等於 p 節點的 hash 值和 key 值, 則 p 節點即爲目標節點, 返回 p 節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 若是 k 所屬的類沒有實現 Comparable 接口 或者 k 和 p 節點的 key 相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
// 第一次符合條件, 該方法只有第一次才執行
TreeNode<K,V> q, ch;
searched = true;
// 從 p 節點的左節點和右節點分別調用 find 方法進行查找, 若是查找到目標節點則返回
if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null)
|| ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null))
return q;
}
// 使用定義的一套規則來比較 k 和 p 節點的 key 的大小, 用來決定向左仍是向右查找
dir = tieBreakOrder(k, pk); // dir<0 則表明 k<pk,則向 p 左邊查找;反之亦然
}
TreeNode<K,V> xp = p; // xp 賦值爲 x 的父節點,中間變量,用於下面給x的父節點賦值
// dir<=0 則向 p 左邊查找,不然向 p 右邊查找,若是爲 null,則表明該位置即爲 x 的目標位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 走進來表明已經找到 x 的位置,只需將 x 放到該位置便可
Node<K,V> xpn = xp.next; // xp 的 next 節點
// 建立新的節點, 其中 x 的 next 節點爲 xpn, 即將 x 節點插入 xp 與 xpn 之間
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0) // 若是時 dir <= 0, 則表明 x 節點爲 xp 的左節點
xp.left = x;
else // 若是時 dir> 0, 則表明 x 節點爲 xp 的右節點
xp.right = x;
xp.next = x; // 將 xp 的n ext 節點設置爲 x
x.parent = x.prev = xp; // 將 x 的 parent 和 prev 節點設置爲xp
// 若是 xpn 不爲空,則將 xpn 的 prev 節點設置爲 x 節點,與上文的 x 節點的 next 節點對應
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x)); // 進行紅黑樹的插入平衡調整
return null;
}
}
}
複製代碼
HashSet 內部經過 HashMap 實現,HashMap 解決哈希衝突使用的是拉鍊法,碰撞的元素會放進鏈表中,鏈表長度超過 8,而且已經使用的桶的數量大於 64 的時候,會將桶的數據結構從鏈表轉換成紅黑樹。HashMap 在求得每一個結點在數組中的索引的時候,會使用對象的哈希碼的高八位和低八位求異或,來增長哈希碼的隨機性。
HashSet 內部使用 HashMap 實現,當咱們經過 put()
方法將一個鍵值對添加到哈希表當中的時候,會根據哈希值和鍵是否相等兩個條件進行判斷,只有當二者徹底相等的時候才認爲元素髮生了重複。
HashSet 不容許列表中存在重複的元素,HashSet 內部使用的是 HashMap 實現的。在咱們向 HashSet 中添加一個元素的時候,會將該元素做爲鍵,一個默認的對象做爲值,構成一個鍵值對插入到內部的 HashMap 中。(HashMap 的實現見上文。)
TreeMap 是基於紅黑樹實現的(後續完善)
Java 註解在 Android 中比較常見的使用方式有 3 種:
bind()
的時候直接反射調用生成的方法。Room 也是在編譯期間爲使用註解的方法生成數據庫方法的。在開發這種第三方庫的時候還可能使用到 Javapoet 來幫助咱們生成 Java 文件。@IntDef({/*各類枚舉值*/})
來指定整型的取值範圍。而後使用註解修飾咱們要方法的參數便可。這樣 IDE 會給出一個提示信息,提示咱們只能使用指定範圍的值。(《Java 註解及其在 Android 中的應用》)關聯:ButterKnife, ARouter
這兩個方法都具備決定一個對象身份功能,因此二者的行爲必須一致,覆寫這兩個方法須要遵循必定的原則。能夠從業務的角度考慮使用對象的惟一特徵,好比 ID 等,或者使用它的所有字段來進行計算獲得一個整數的哈希值。通常,我不會直接覆寫該方法,除非業務特徵很是明顯。由於一旦修改以後,它的做用範圍將是全局的。咱們還能夠經過 IDEA 的 generate 直接生成該方法。
wait() & notify()
, 用來對線程進行控制,以讓當前線程等待,直到其餘線程調用了 notify()/notifyAll()
方法。wait()
發生等待的前提是當前線程獲取了對象的鎖(監視器)。調用該方法以後當前線程會釋放獲取到的鎖,而後讓出 CPU,進入等待狀態。notify/notifyAll()
的執行只是喚醒沉睡的線程,而不會當即釋放鎖,鎖的釋放要看代碼塊的具體執行狀況。clone()
與對象克隆相關的方法(深拷貝&淺拷貝?)finilize()
toString()
equal() & hashCode()
,見上前者是線程安全的,每一個方法上面都使用 synchronized 關鍵字進行了加鎖,後者是非線程安全的。通常狀況下使用 StringBuilder 便可,由於非多線程環境進行加鎖是一種沒有必要的開銷。
intern()
方法將該字符串對象存儲在字符串池,若是字符串池已經有了一樣值的字符串,則返回引用。使用雙引號直接建立字符串的時候,JVM 先去字符串池找有沒有值相等字符串,若是有,則返回找到的字符串引用;不然建立一個新的字符串對象並存儲在字符串池。UTF-8 編碼把一個 Unicode 字符根據不一樣的數字大小編碼成 1-6 個字節,經常使用的英文字母被編碼成 1 個字節,漢字一般是 3 個字節,只有很生僻的字符纔會被編碼成 4-6 個字節。
參考文章,瞭解字符串編碼的淵源:字符編碼 ASCII UNICODE UTF-8
如何開啓線程,線程池參數;注意的問題:線程數量,內存泄漏
// 方式 1:Thread 覆寫 run() 方法;
private class MyThread extends Thread {
@Override
public void run() {
// 業務邏輯
}
}
// 方式 2:Thread + Runnable
new Thread(new Runnable() {
public void run() {
// 業務邏輯
}
}).start();
// 方式 3:ExectorService + Callable
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<Integer>> results = new ArrayList<Future<Integer>>();
for (int i=0; i<5; i++) {
results.add(executor.submit(new CallableTask(i, i)));
}
複製代碼
線程數量的問題:
Android 中並無明確規定能夠建立的線程的數量,可是每一個進程的資源是有限的,線程自己會佔有必定的資源,因此受內存大小的限制,會有數量的上限。一般,咱們在使用線程或者線程池的時候,不會建立太多的線程。線程池的大小經驗值應該這樣設置:(其中 N 爲 CPU 的核數)
N + 1
;(大部分時間在計算)2N + 1
;(大部分時間在讀寫,Android)下面是 Android 中的 AysncTask 中建立線程池的代碼(建立線程池的核心參數的說明已經家在了註釋中),
// CPU 的數量
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 核心線程的數量:只有提交任務的時候纔會建立線程,噹噹前線程數量小於核心線程數量,新添加任務的時候,會建立新線程來執行任務
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
// 線程池容許建立的最大線程數量:當任務隊列滿了,而且當前線程數量小於最大線程數量,則會建立新線程來執行任務
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
// 非核心線程的閒置的超市時間:超過這個時間,線程將被回收,若是任務多且執行時間短,應設置一個較大的值
private static final int KEEP_ALIVE_SECONDS = 30;
// 線程工廠:自定義建立線程的策略,好比定義一個名字
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
// 任務隊列:若是當前線程的數量大於核心線程數量,就將任務添加到這個隊列中
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
/*corePoolSize=*/ CORE_POOL_SIZE,
/*maximumPoolSize=*/ MAXIMUM_POOL_SIZE,
/*keepAliveTime=*/ KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
/*workQueue=*/ sPoolWorkQueue,
/*threadFactory=*/ sThreadFactory
/*handler*/ defaultHandler); // 飽和策略:AysncTask 沒有這個參數
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
複製代碼
飽和策略:任務隊列和線程池都滿了的時候執行的邏輯,Java 提供了 4 種實現;
其餘:
prestartAllcoreThread()
方法的時候,線程池會提早啓動並建立全部核心線程來等待任務;allowCoreThreadTimeOut()
方法的時候,超時時間到了以後,閒置的核心線程也會被移除。run()
和 start()
方法區別:start()
會調用 native 的 start()
方法,而後 run()
方法會被回調,此時 run()
異步執行;若是直接調用 run()
,它會使用默認的實現(除非覆寫了),而且會在當前線程中執行,此時 Thread 如同一個普通的類。
private Runnable target;
public void run() {
if (target != null) target.run();
}
複製代碼
線程關閉,有兩種方式能夠選擇,一種是使用中斷標誌位進行判斷。當須要中止線程的時候,調用線程的 interupt()
方法便可。這種狀況下須要注意的地方是,當線程處於阻塞狀態的時候調用了中斷方法,此時會拋出一個異常,並將中斷標誌位復位。此時,咱們是沒法退出線程的。因此,咱們須要同時考慮通常狀況和線程處於阻塞時中斷兩種狀況。
另外一個方案是使用一個 volatile 類型的布爾變量,使用該變量來判斷是否應該結束線程。
// 方式 1:使用中斷標誌位
@Override
public void run() {
try {
while (!isInterrupted()) {
// do something
}
} catch (InterruptedException ie) {
// 線程由於阻塞時被中斷而結束了循環
}
}
private static class MyRunnable2 implements Runnable {
// 注意使用 volatile 修飾
private volatile boolean canceled = false;
@Override
public void run() {
while (!canceled) {
// do something
}
}
public void cancel() {
canceled = true;
}
}
複製代碼
防止線程內存泄漏:
onDestroy()
方法中終止線程。wait()/notify():
wait()、notify() 和 notifyAll()
方法是 Object 的本地 final 方法,沒法被重寫。wait()
使當前線程阻塞,直到接到通知或被中斷爲止。前提是必須先得到鎖,通常配合 synchronized 關鍵字使用,在 synchronized 同步代碼塊裏使用 wait()、notify() 和 notifyAll()
方法。若是調用 wait()
或者 notify()
方法時,線程並未獲取到鎖的話,則會拋出 IllegalMonitorStateException 異常。再次獲取到鎖,當前線程才能從 wait()
方法處成功返回。wait()、notify() 和 notifyAll()
在 synchronized 代碼塊執行,說明當前線程必定是獲取了鎖的。當線程執行 wait()
方法時候,會釋放當前的鎖,而後讓出 CPU,進入等待狀態。只有當 notify()/notifyAll()
被執行時候,纔會喚醒一個或多個正處於等待狀態的線程,而後繼續往下執行,直到執行完 synchronized 代碼塊或是中途遇到 wait()
,再次釋放鎖。notify()/notifyAll()
的執行只是喚醒沉睡的線程,而不會當即釋放鎖,鎖的釋放要看代碼塊的具體執行狀況。因此在編程中,儘可能在使用了 notify()/notifyAll()
後當即退出臨界區,以喚醒其餘線程。wait()
須要被 try catch
包圍,中斷也可使 wait
等待的線程喚醒。notify()
和 wait()
的順序不能錯,若是 A 線程先執行 notify()
方法,B 線程再執行 wait()
方法,那麼 B 線程是沒法被喚醒的。notify()
和 notifyAll()
的區別:notify()
方法只喚醒一個等待(對象的)線程並使該線程開始執行。因此若是有多個線程等待一個對象,這個方法只會喚醒其中一個線程,選擇哪一個線程取決於操做系統對多線程管理的實現。notifyAll()
會喚醒全部等待 (對象的) 線程,儘管哪個線程將會第一個處理取決於操做系統的實現。若是當前狀況下有多個線程須要被喚醒,推薦使用 notifyAll()
方法。好比在生產者-消費者裏面的使用,每次都須要喚醒全部的消費者或是生產者,以判斷程序是否能夠繼續往下執行。對於 sleep()
和 wait()
方法之間的區別,總結以下,
sleep()
方法是 Thread 的靜態方法,而 wait()
是 Object 實例方法。wait()
方法必需要在同步方法或者同步塊中調用,也就是必須已經得到對象鎖。而 sleep()
方法沒有這個限制能夠在任何地方種使用。wait()
方法會釋放佔有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而 sleep()
方法只是會讓出 CPU 並不會釋放掉對象鎖;sleep()
方法在休眠時間達到後若是再次得到 CPU 時間片就會繼續執行,而 wait()
方法必須等待 Object.notift()/Object.notifyAll()
通知後,纔會離開等待池,而且再次得到 CPU 時間片纔會繼續執行。start()
方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取 CPU 的使用權 。o.wait()
方法,JVM 會把該線程放入等待隊列 (waitting queue) 中。Thread.sleep(long)
或 t.join()
方法,或者發出了 I/O 請求時,JVM 會把該線程置爲阻塞狀態。當 sleep()
狀態超時、join()
等待線程終止或者超時、或者 I/O 處理完畢時,線程從新轉入 RUNNABLE 狀態。run()
、main()
方法執行結束,或者因異常退出了 run()
方法,則該線程結束生命週期。死亡的線程不可再次復生。當兩個線程彼此佔有對方須要的資源,同時彼此又沒法釋放本身佔有的資源的時候就發生了死鎖。發生死鎖須要知足下面四個條件,
產生死鎖須要四個條件,那麼,只要這四個條件中至少有一個條件得不到知足,就不可能發生死鎖了。因爲互斥條件是非共享資源所必須的,不只不能改變,還應加以保證,因此,主要是破壞產生死鎖的其餘三個條件。
破壞佔有且等待的問題:容許進程只得到運行初期須要的資源,便開始運行,在運行過程當中逐步釋放掉分配到的已經使用完畢的資源,而後再去請求新的資源。
破壞不可搶佔條件:當一個已經持有了一些資源的進程在提出新的資源請求沒有獲得知足時,它必須釋放已經保持的全部資源,待之後須要使用的時候再從新申請。釋放已經保持的資源頗有可能會致使進程以前的工做實效等,反覆的申請和釋放資源會致使進程的執行被無限的推遲,這不只會延長進程的週轉週期,還會影響系統的吞吐量。
破壞循環等待條件:能夠經過定義資源類型的線性順序來預防,可將每一個資源編號,當一個進程佔有編號爲i的資源時,那麼它下一次申請資源只能申請編號大於 i 的資源。
sychronized 原理(表面的):
Java 虛擬機中的同步 (Synchronization) 基於進入和退出管程 (Monitor) 對象實現,不管是顯式同步 (有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊),仍是隱式同步都是如此。進入 monitorenter 時 monitor 中的計數器 count 加 1,釋放當前持有的 monitor,count 自減 1. 反編譯代碼以後常常看到兩個 monitorexit 指令對應一個 monitorenter,這是用來防止程序執行過程當中出現異常的。虛擬機須要保證即便程序容許中途出了異常,鎖也同樣能夠被釋放(執行第二個 monitorexit)。
對同步方法,JVM 能夠從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當調用方法時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有 monitor,而後再執行方法,最後再方法完成 (不管是正常完成仍是非正常完成) 時釋放monitor. 在方法執行期間,其餘任何線程都沒法再得到同一個 monitor. 若是一個同步方法執行期間拋出了異常,而且在方法內部沒法處理此異常,那這個同步方法所持有的 monitor 將在異常拋到同步方法以外時自動釋放。
sychronized 原理(底層的):
在 Java 對象的對象頭中,有一塊區域叫作 MarkWord,其中存儲了重量級鎖 sychronized 的標誌位,其指針指向的是 monitor 對象。每一個對象都存在着一個 monitor 與之關聯。在 monitor 的數據結構中定義了兩個隊列,_WaitSet 和 _EntryList. 當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把 monitor 中的 owner 變量設置爲當前線程同時 monitor 中的計數器 count 加 1,若線程調用 wait() 方法,將釋放當前持有的 monitor,owner 變量恢復爲 null,count 自減 1,同時該線程進入 _WaitSet 集合中等待被喚醒。若當前線程執行完畢也將釋放 monitor (鎖)並復位變量的值,以便其餘線程進入獲取 monitor (鎖)。
由此看來,monitor 對象存在於每一個 Java 對象的對象頭中(存儲的指針的指向),synchronized 鎖即是經過這種方式獲取鎖的,也是爲何 Java 中任意對象能夠做爲鎖的緣由,同時也是 notify()/notifyAll()/wait()
等方法存在於頂級對象 Object 中的緣由。
固然,從 MarkWord 的結構中也能夠看出 Java 對 sychronized 的優化:Java 6 以後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖,鎖效率也獲得了優化。
(關於 sychronized 的底層實現原理能夠參考筆者的文章:併發編程專題 3:synchronized)
sychronized 與 lock 的區別體如今下面四個方面:
ReentrantLock 的實現原理:
ReentrantLock 的實現是基於 AQS(同步器),同步器設計的思想是 CAS. 同步器中維護了一個鏈表,藉助 CAS 的思想向鏈表中增刪數據。其底層使用的是 sun.misc.Unsafe
類中的方法來完成 CAS 操做的。在 ReentrantLock 中實現兩個 AQS 的子類,分別是 NonfairSync
和 FairSync
. 也就是用來實現公平鎖和非公平鎖的關鍵。當咱們使用構造方法獲取 ReentrantLock 實例的時候,能夠經過一個布爾類型的參數指定使用公平鎖仍是非公平鎖。在實現上, NonfairSync
和 FairSync
的區別僅僅是,在當前線程獲取到鎖以前,是否會從上述隊列中判斷是否存在比本身更早申請鎖的線程。對於公平鎖,當存在這麼一個線程的話,那麼當前線程獲取鎖失敗。噹噹前線程獲取到鎖的時候,也會使用一個 CAS 操做將鎖獲取次數 +1. 當線程再次獲取鎖的時候,會根據線程來進行判斷,若是當前持有鎖的線程是申請鎖的線程,那麼容許它再次獲取鎖,以此來實現鎖的可重入。
所謂 CAS 就是 Compare-And-Swape,相似於樂觀加鎖。但與咱們熟知的樂觀鎖不一樣的是,它在判斷的時候會涉及到 3 個值:「新值」、「舊值」 和 「內存中的值」,在實現的時候會使用一個無限循環,每次拿 「舊值」 與 「內存中的值」 進行比較,若是兩個值同樣就說明 「內存中的值」 沒有被其餘線程修改過;不然就被修改過,須要從新讀取內存中的值爲 「舊值」,再拿 「舊值」 與 「內存中的值」 進行判斷。直到 「舊值」 與 「內存中的值」 同樣,就把 「新值」 更新到內存當中。
這裏要注意上面的 CAS 操做是分 3 個步驟的,可是這 3 個步驟必須一次性完成,由於否則的話,當判斷 「內存中的值」 與 「舊值」 相等以後,向內存寫入 「新值」 之間被其餘線程修改就可能會獲得錯誤的結果。JDK 中的 sun.misc.Unsafe
中的 compareAndSwapInt 等一系列方法 Native 就是用來完成這種操做的。另外還要注意,上面的 CAS 操做存在一些問題:
AtomicReference
。voliate 關鍵字的兩個做用
volatile 是經過內存屏障(Memory Barrier)
來實現其在 JMM 中的語義的。內存屏障,又稱內存柵欄,是一個 CPU 指令,它的做用有兩個,一是保證特定操做的執行順序,二是保證某些變量的內存可見性。若是在指令間插入一條內存屏障則會告訴編譯器和 CPU,無論什麼指令都不能和這條 Memory Barrier 指令重排序。Memory Barrier 的另一個做用是強制刷出各類 CPU 的緩存數據,所以任何 CPU 上的線程都能讀取到這些數據的最新版本。
參考 《併發編程專題-5:生產者和消費者模式》 中的三種寫法。
ThreadLocal 經過將每一個線程本身的局部變量存在本身的內部來實現線程安全。使用它的時候會定義它的靜態變量,每一個線程看似是從 TL 中獲取數據,而實際上 TL 只起到了鍵值對的鍵的做用,實際的數據會以哈希表的形式存儲在 Thread 實例的 Map 類型局部變量中。當調用 TL 的 get()
方法的時候會使用 Thread.currentThread()
獲取當前 Thread 實例,而後從該實例的 Map 局部變量中,使用 TL 做爲鍵來獲取存儲的值。Thread 內部的 Map 使用線性數組解決哈希衝突。(《ThreadLocal的使用及其源碼實現》)
poll()
和 add()
等方法藉助 CAS 思想實現。鎖比較輕量。NIO
多線程斷點續傳原理
斷點續傳和斷點下載都是用的用的都是 RandomAccessFile,它能夠從指定的位置開始讀取數據。斷點續傳是由服務器給客戶端一個已經上傳的位置標記position,而後客戶端再將文件指針移動到相應的 position,經過輸入流將文件剩餘部分讀出來傳輸給服務器。
若是要使用多線程來實現斷點續傳,那麼能夠給每一個線程分配固定的字節的文件,分別去讀,而後分別上傳到服務器。
協程實際上就是極大程度的複用線程,經過讓線程滿載運行,達到最大程度的利用 CPU,進而提高應用性能。相比於線程,協程不須要進行線程切換,和多線程比,線程數量越多,協程的性能優點就越明顯。第二大優點就是不須要多線程的鎖機制,由於只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只須要判斷狀態就行了,因此執行效率比多線程高不少。
協程和線程,都能用來實現異步調用,可是這二者之間是有本質區別的:
編譯器
級別的,線程是系統級別的。協程的切換是由程序來控制
的,線程的切換是由操做系統來控制的。協做式
的,線程是搶佔式
的。協程是由程序來控制何時進行切換的,而線程是有操做系統來決定線程之間的切換的。一個線程
中執行。4. 協程適合 IO 密集型
的程序,多線程適合 計算密集型
的程序(適用於多核 CPU 的狀況)。當你的程序大部分是文件讀寫操做或者網絡請求操做的時候,這時你應該首選協程而不是多線程,首先這些操做大部分不是利用 CPU 進行計算而是等待數據的讀寫,其次由於協程執行效率較高,子程序切換不是線程切換,是由程序自身控制,所以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優點就越明顯。順序調用
異步代碼,避免回調地獄
。參考:是繼續Rxjava,仍是應該試試Kotlin的協程 - Android架構的文章 - 知乎
Kotlin 是一門基於 JVM 的語言,它提供了很是多便利的語法特性。若是說 Kotlin 爲何那麼優秀的話,那隻能說是由於它站在了 Java 的肩膀上。學習了一段時間以後,你會發現它的許多語法的設計很是符合咱們實際開發中的使用習慣。
好比,對於一個類,一般咱們不會去覆寫它。尤爲是 Java Web 方向,不少的類用來做爲 Java Bean,它們沒有特別多的繼承關係。而 Kotlin 中的類默認就是不容許繼承的,想容許本身的類被繼承,你還必須顯式地使用 open 關鍵字指定。
對於 Java Bean,做爲一個業務對象,它會有許多的字段。按照 Java 中的處理方式,咱們要爲它們聲明一系列的 setter 和 getter 方法。而後,獲取屬性的時候必須使用 setter 和 getter 方法。致使咱們的代碼中出現很是多的括號。而使用 Kotlin 則能夠直接對屬性進行賦值,顯得優雅地多。
再好比 Java 中使用 switch 的時候,咱們一般會在每一個 case 後面加上 break,而 kotlin 默認幫助咱們 break,這樣就節省了不少的代碼量。
另外 Kotlin 很是優秀的地方在於對 NPE 的控制。在 Android 開發中,咱們可使用 @NoneNull 和 @Nullable 註解來標明某個字段是否可能爲空。在 Java 中默認字段是空的,而且沒有任何提示。你一個不留神可能就致使了 NPE,但 Kotlin 中就默認變量是非空的,你想讓它爲空必須單獨聲明。這樣,對於可能爲空的變量就給了咱們提示的做用,咱們知道它可能爲空,就會去特地對其進行處理。對於可能爲空的類,Kotlin 定義了以下的規則,使得咱們處理起來 NPE 也變得很是簡單:
?
在類型的後面則說明這個變量是可空的;?.
,以 a?.method()
爲例,當 a 不爲 null 則整個表達式的結果是 a.method()
不然是 null;?:
,以 a ?: "A"
爲例,當 a 不爲 null 則整個表達式的結果是 a,不然是 「A」;as?
,以 foo as? Typ
e 爲例,當 foo 是 Type 類型則將 foo 轉換成 Type 類型的實例,不然返回 null;!!
,用在某個變量後面表示斷言其非空,如 a!!
;val b = "AA".let { it + "A" }
返回 「AAA」;諸如此類,不少時候,我以爲 Java 設計的一些規則對人們產生了誤導,實際開發中並不符合咱們的使用習慣。而 Kotlin 則是根據多年來人們使用 Java 的經驗,簡化了許多的調用,更加符合咱們使用習慣。因此說,Kotlin 之因此強大是由於站在 Java 的肩膀上。
觀察者設計模式相似於咱們常用的接口回調,下面的代碼中在觀察者的構造方法中訂閱了主題,其實這個倒不怎麼重要,何時訂閱均可以。核心的地方就是主題中維護的這個隊列,須要通知的時候調一下通知的方法便可。另外,若是在多線程環境中還要考慮如何進行線程安全控制,好比使用線程安全的集合等等。下面只是一個很是基礎的示例程序,瞭解設計思想,用的時候能夠靈活一些,沒必要循規蹈矩。
public class ConcreteSubject implements Subject {
private List<Observer> observers = new LinkedList<>(); // 維護觀察者列表
@Override
public void registerObserver(Observer o) { // 註冊一個觀察者
observers.add(o);
}
@Override
public void removeObserver(Observer o) { // 移除一個觀察者
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(o);
}
}
@Override
public void notifyObservers() { // 通知全部觀察者主題的更新
for (Observer o : observers) {
o.method();
}
}
}
public class ConcreteObserver implements Observer {
private Subject subject; // 該觀察者訂閱的主題
public ConcreteObserver(Subject subject) {
this.subject = subject;
subject.registerObserver(this); // 將當前觀察者添加到主題訂閱列表中
}
// 當主題發生變化的時候,主題會遍歷觀察者列表並經過調用該方法來通知觀察者
@Override
public void method() {
// ...
}
}
複製代碼
(瞭解更多關於觀察者設計模式的內容,請參考文章:設計模式解析:觀察者模式)
// 飽漢:就是在調用單例方法的時候,實例已經初始化過了
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}
// 懶漢:在調用方法的時候才進行初始化
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
sychronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
複製代碼
另外,單例須要注意的問題是:1.若是用戶使用反射進行初始化怎麼辦?能夠在建立第二個實例的時候拋出異常;2.若是用戶使用 Java 的序列化機制反覆建立單例呢?將全部的實例域設置成 transient 的,而後覆寫 readResolve()
方法並返回單例。
另外,單實例太多的時候能夠想辦法使用一個 Map 將它們存儲起來,而後經過一種規則從哈希表中取出,這樣就不必聲明一大堆的單例變量了。
(瞭解更多關於單例設計模式的內容,請參考文章:設計模式-4:單例模式)
四個設計模式相同的地方是,它們都須要你傳入一個類,而後內部使用你傳入的這個類來完成業務邏輯。
咱們以字母 A,B,C 來表示 3 種不一樣的類(某種東西)。
外觀模式要隱藏內部的差別,提供一個一致的對外的接口 X,那麼讓定義 3 個類 AX, BX, CX 而且都實現 X 接口,其中分別引用 A, B, C 按照各自的方式實現 X 接口的方法便可。以相機開發爲例,Camera1 和 Camera2 各有本身的實現方式,定義一個統一的接口和兩個實現類。
假如如今有一個類 X,其中引用到了接口 A 的實現 AX. AX 的邏輯存在點問題,咱們想把它完善一下。咱們提供了 3 種方案,分別是 A1, A2 和 A3. 那麼此時,咱們讓 A1, A2 和 A3 都實現 A 接口,而後其中引用 AX 完成業務,在實現的 A 接口的方法中分別使用各自的方案進行優化便可。這種方式,咱們對 AX 進行了修飾,使其 A1, A2 和 A3 能夠直接應用到 X 中。
對於適配器模式,假如如今有一個類 X,其中引用到了接口 A. 如今咱們不得不使用 B 來完成 A 的邏輯。由於 A 和 B 屬於兩個不一樣的類,因此此時咱們須要一個適配器模式,讓 A 的實現 AX 引用 B 的實現 BX 完成 A 接口的各個方法。
外觀模式的目的是隱藏各種間的差別性,提供一致的對外接口。裝飾者模式對外的接口是一致的,可是內部引用的實例是同一個,其目的是對該實例進行拓展,使其具備多種功能。因此,前者是多對一,後者是一對多的關係。而適配器模式適用的是兩個不一樣的類,它使用一種類來實現另外一個類的功能,是一對一的。相比之下,代理模式也是用一類來完成某種功能,而且一對一,但它是在同類之間,目的是爲了加強類的功能,而適配器是在不一樣的類之間。裝飾者和代理都用來加強類的功能,可是裝飾者裝飾以後仍然是同類,能夠無縫替換以前的類的功能。而代理類被修飾以後已是代理類了,是另外一個類,沒法替換原始類的位置。
Android 高級面試系列文章,關注做者及時獲取更多面試資料,
本系列以及其餘系列的文章均維護在 Github 上面:Github / Android-notes,歡迎 Star & Fork. 若是你喜歡這篇文章,願意支持做者的工做,請爲這篇文章點個贊👍!