Android技術要點概括(一)

一、volatile關鍵字的做用和應用場景

① volatile保證可見性 ,即它會保證修改的值當即被更新到主存中,當有其餘線程須要讀取時,它會去內存中讀取新值算法

(Java 內存模型規定了 全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了被該線程所使用的到的變量,線程對變量的操做都必須在工做內存中進行,不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞須要經過主內存來完成)數組

② 禁止進行指令重排序緩存

使用volatile 必須具有如下2個條件:安全

  1. 對變量的寫操做不依賴當前值bash

  2. 該變量沒有包含在具備其餘變量的不變式中數據結構

二、談談對線程池的理解

線程池的優點:多線程

① 下降系統資源消耗,經過重用已存在的線程,下降線程建立和銷燬形成的消耗;併發

② 提升系統響應速度,當有任務到達時,無需等待新線程的建立便能當即執行;app

③ 方便線程併發數的管控,線程如果無限制的建立,不只會額外消耗大量系統資源,更是佔用過多資源而阻塞資源或oom等情況,從而下降系統的穩定性,線程池有效管控線程,統一分配、調優,提供資源使用率;ide

線程池關閉方式:

① shutdown() : 將線程池設置成shutdown狀態,而後中斷全部沒有正在執行任務的線程。

② sutdownNow(): 將線程池設置成stop狀態, 而後中斷全部任務(包括正在執行的任務)的線程,並返回等待執行任務的列表。

Java中有四種不一樣功能的線程池:

一、newFixedThreadPool : 該線程池鎖容納的最大線程數就是所設置的核心線程數,若是線程池中的線程處於空閒狀態,它們不會被回收,除非是這個線程池被關閉了,若是全部的線程都處於活動狀態,新任務就會處於等待狀態,直到有線程空閒出來。優點:可以更快速響應外界請求。

二、newCachedThreadPool : 該線程池中核心線程數爲0,最大線程數爲Integer.Max_Value,當線程池中的線程都處於活動狀態的時候,線程池就會建立一個新的線程來處理任務,該線程池中的線程超時時長爲60秒,,因此當線程處於閒置狀態超過60秒的時候就會被回收

三、newScheduledThreadPool : 核心線程數是固定的,當非核心線程處於限制狀態時會當即被回收,經常使用於執行定時任務。

四、newSingleThreadExecutor : 該線程池只有一個核心線程,對於任務隊列沒有大小限制,也就意味着這一任務處於活動狀態時,其餘任務都會在任務隊列中排隊等候依次執行。

三、Java中的鎖有哪些? 以及區別?

  1. 公平鎖/非公平鎖
    • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖的
    • 非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能會形成優先級反轉後者飢餓現象。
  2. 可重入鎖
    • 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。 eg:
    synchronized void setA() throws Exception() {
       Thread.sleep(1000);
       setB();
    }
     synchronized void setB() throws Exception() {
       Thread.sleep(1000);
    }
    複製代碼
  3. 獨享鎖/共享鎖
    • 獨享鎖是指該鎖一次只能被一個線程所持有
    • 共享鎖是指該鎖可被多個線程持有
  4. 互斥鎖/讀寫鎖
    • 互斥鎖:一次只能一個線程擁有互斥鎖,其餘線程只有等待
    • 讀寫鎖:多個讀者能夠同時進行,寫者優於讀者,寫者必須互斥。
  5. 樂觀鎖/悲觀鎖
    • 悲觀鎖:當一個線程被掛起的時候,加入到阻塞隊列,在必定的時間或條件下,再經過notify(), notifyAll() 喚醒,在某個資源不可用時,就將cpu讓出,把當前等待線程切換爲阻塞狀態,等到資源可用了,就將線程喚醒,讓他進入runnable狀態等待cpu調度。
    • 樂觀鎖:每次不加鎖而是假設修改數據以前其餘線程必定不會修改,若是由於修改過產生衝突而失敗就重試,直到成功爲止。
  6. 分段鎖
    • 分段鎖是一種鎖的設計,並非具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。ConcurrentHashMap 中的分段鎖成爲Segment,它相似於HashMap 的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;當須要put元素時,並非對整個hashmap進行加鎖,而是先經過hashCode來指導它要放在哪一個分段中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行插入。可是在統計size 的時候,可就是獲取hashMap 全局信息的時候,就須要獲取全部的分段鎖才能統計。

四、Java內存區域與內存模型

1、Java 內存區域

  • 方法區(公有):用戶存儲已被虛擬機加載的類信息、常量、靜態常量,即時編譯器編譯後的代碼等數據
  • 堆(公有):是JVM所管理的內存中最大的一塊,惟一的目的就是存放實力對象,Java堆是垃圾收集器管理的主要區域,所以不少時候也被稱爲GC堆。
  • 虛擬機棧(線程私有):Java方法執行的內存模型,每一個方法在執行時都會建立一個棧幀,用戶局部變量表,操做數棧,動態鏈接,方法出口等信息,每一個方法從調用直至完成的過程,都對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
  • 本地方法棧(線程私有):本地方法棧爲虛擬機使用到的Native方法服務。
  • 程序計數器(線程私有):一塊較小的內存,當前線程所執行的字節碼的行號指示器,字節碼解釋器工做時,就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令。

2、Java 內存模型

Java 內存模型規定了全部的變量都存儲到主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量,線程對變量的全部操做都必須在工做內存執行,不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞須要經過主內存來完成。

五、Java類加載機制及類加載器

類加載機制定義:把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型。

雙親委派模型:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器(Bootstrap ClassLoader)中。只有當父加載器在它的搜索範圍內沒有找到所需的類時,子加載器纔會嘗試本身去加載該類。

這樣的好處是不一樣層次的類加載器具備不一樣優先級,好比全部Java對象的超級父類Object,位於rt.jar,不管哪一個類加載器加載該類,最終都是由啓動加載器進行加載,保證安全,若是開發者本身編寫一個Object類放入程序中,雖能正常編譯,但不會被加載運行,保證不會出現混亂。

六、JVM中垃圾收集算法

  1. 標記-清除算法 ① 首先標記出全部須要回收的對象 ② 在標記完成後統一回收全部被標記的對象

    缺點:標記和清除兩個過程效率不高,標記清除以後產生大量不連續的內存碎片,空間碎片太多可能會致使運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做

  2. 複製算法

    將可用內存按照容量大小劃分爲大小相等的兩塊,每次只使用其中的一塊,當一塊內存使用完了,就將還存活着的對象複製到另外一塊上面,而後把使用過的內存空間一次清理掉,這樣使得每次都是對整個半區進行內存回收,內存分配時也不用考慮內存碎片等複雜狀況。

    缺點:將內存縮小爲原來的一半。

  3. 標記-整理算法 複製收集算法在對象存活率比較高時,就要進行比較多的複製操做,效率就會變低; 標記過程仍然與 「標記-清除」算法同樣,可是後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。

  4. 分代收集算法 通常把Java堆分爲新生代和老生代, 這樣就能夠根據各個年代的特色採用最適合的收集算法。

    在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法。

    在老生代中,由於對象存活率高,沒有額外空間對它進行分配擔保,就必須採用「標記-清除」或「標記-整理」算法來進行回收。

七、JVM怎麼判斷對象是否已死?

1、引用計數法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1,當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能被再使用的。

主流的JVM裏面沒有選用引用計數法來管理內存,其中最主要的緣由就是它很難解決對象間的互循環引用的問題。

2、可達性分析算法

經過一些稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用連,當一個對象到GC Roots沒有任何引用連相連時,證實此對象是不可用的,因此它們會被斷定爲可回收對象。如圖B的對象是不可達的

在Java中,能夠做爲GC Roots的對象包括下面幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象
 方法區中靜態屬性引用的對象
 方法區中常量引用的對象
 本地方法棧JNI(通常說的Native方法)引用的對象
複製代碼

在可達性分析算法中,要真正宣告一個對象死亡, 至少要經歷兩次標記過程:

  1. 若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲沒有必要執行
  2. 若是這個對象唄斷定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue隊列之中,並在稍後由一個虛擬機自動創建的、低優先級的Finalizer線程去執行它。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue 中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身,只要從新與引用鏈上的任何一個對象創建關聯便可,那在第二次標記時它將會被移除出即將回收的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。

八、LruCache 原理解析

LruCache的核心思想就是要維護一個緩存對象列表,其中對象列表的排列方式是按照訪問順序實現的,即一直沒訪問的對象,放在隊尾,即將被淘汰,而最近訪問的對象放在對頭,最後被淘汰。 如圖所示:

這個隊列是由LinkedHashMap來維護的,而LinkedHashMap是由數組+雙向鏈表 的數據結構來實現的,其中雙向鏈表的結構能夠實現訪問順序和插入順序,使得LinkedHashMap中的key,value對按照必定順序排列起來。

經過下面構造函數來指定LinkedHashMap中雙向鏈表的結構是訪問順序仍是插入順序。

public LinkedHashMap(int initialCapacity,
    float loadFactor,
    boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
複製代碼

其中accessOrder設置爲true 則爲訪問順序,爲false,則爲插入順序。 以具體的例子解釋,當設置爲true 時

public static final void main(String[] args) {
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
    map.put(0, 0);
    map.put(1, 1);
    map.put(2, 2);
    map.put(3, 3);
    map.put(4, 4);
    map.put(5, 5);
    map.put(6, 6);
    map.get(1);
    map.get(2);

    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}
複製代碼

輸出結果:

0:0
3:3
4:4
5:5
6:6
1:1
2:2
複製代碼

即最近訪問的最後輸出,那麼就正好知足的LRU緩存算法的思想,可見LruCache巧妙實現,就是利用了LinkedHashMap的這種數據結構。 下面咱們在LruCache源碼中具體查看,怎麼應用LinkedHashMap來實現緩存的添加,得到和刪除的。

/**
     * @param maxSize for caches that do not override {@link #sizeOf}, this is
     *     the maximum number of entries in the cache. For all other caches,
     *     this is the maximum sum of the sizes of the entries in this cache.
     */
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
複製代碼

從LruCache的構造函數中能夠看到正是用了LinkedHashMap的訪問順序。

put()方法

/**
     * Caches {@code value} for {@code key}. The value is moved to the head of
     * the queue.
     *
     * @return the previous value mapped by {@code key}.
     */
    public final V put(K key, V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, value);
        }

        trimToSize(maxSize);
        return previous;
    }
複製代碼

能夠看到put()方法並無什麼難點,重要的就是在添加過緩存對象後,調用 trimToSize()方法,來判斷緩存是否已滿,若是滿了就要刪除近期最少使用的算法。

trimToSize()方法

/**
     * Remove the eldest entries until the total of remaining entries is at or
     * below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1
     *            to evict even 0-sized elements.
     */
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
複製代碼

trimToSize()方法不斷地刪除LinkedHashMap中隊尾的元素,即近期最少訪問的,直到緩存大小小於最大值。

當調用LruCache的get()方法獲取集合中的緩存對象時,就表明訪問了一次該元素,將會更新隊列,保持整個隊列是按照訪問順序排序。這個更新過程就是在LinkedHashMap中的get()方法中完成的。

先看LruCache的get()方法

public final V get(K key) {
        //key爲空拋出異常
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
            //獲取對應的緩存對象
            //get()方法會實現將訪問的元素更新到隊列頭部的功能
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
複製代碼

其中LinkedHashMap的get()方法以下:

public V get(Object key) {
    LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
    if (e == null)
        return null;
    //實現排序的關鍵方法
    e.recordAccess(this);
    return e.value;
}
複製代碼

調用recordAccess()方法以下:

void recordAccess(HashMap<K,V> m) {
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
    //判斷是不是訪問排序
    if (lm.accessOrder) {
        lm.modCount++;
        //刪除此元素
        remove();
        //將此元素移動到隊列的頭部
        addBefore(lm.header);
    }
}
複製代碼

因而可知LruCache中維護了一個集合LinkedHashMap,該LinkedHashMap是以訪問順序排序的。當調用put()方法時,就會在結合中添加元素,並調用trimToSize()判斷緩存是否已滿,若是滿了就用LinkedHashMap的迭代器刪除隊尾元素,即近期最少訪問的元素。當調用get()方法訪問緩存對象時,就會調用LinkedHashMap的get()方法得到對應集合元素,同時會更新該元素到隊頭。


整理了最近一段時間的學習要點,作一個概括總結,後續還會再作更新。。。。

相關文章
相關標籤/搜索