天天看點

[面面面]搞定計算機面試常見知識點——Java篇2. 語言類

之前的一篇總結已經寫到了十萬字,閱讀起來太不友善了,是以按照類别拆分成多個短篇分享給大家。

文章目錄

  • 2. 語言類
    • 2.1. 程序和線程的差別
    • 2.2. 協程與線程
      • 2.2.1. 協程的優勢
    • 2.3. 線程安全的定義、線程的狀态
    • 2.4. 多線程的實作方式(Runnable和Callable的差別)、start/run方法的差別
    • 2.5. 子線程異常捕捉
    • 2.6. wait()/notify()/sleep()/yield()/join()幾個方法的意義
    • 2.7. notifyAll實作原理及等待池和鎖池的概念
    • 2.8. 線程池的建立方式,7大參數、阻塞隊列、拒絕政策、大小
    • 2.9. 樂觀鎖CAS、悲觀鎖 synchronized和ReentrantLock、實作原理以及差別
    • 2.10. Lock與synchronized的差別
    • 2.11. ABA問題
      • 2.11.1. 使用AtomicStampedReference避免ABA問題
    • 2.12. 鎖優化:偏向鎖、輕量級鎖、自旋鎖、适應性自旋鎖、鎖消除、鎖粗化等
    • 2.13. volatile和synchronized的差別及JAVA記憶體模型
    • 2.14. ThreadLocal線程本地存儲原理
    • 2.15. 指令重排序
    • 2.16. final關鍵字
      • 2.16.1. final實作原理
    • 2.17. 反射
      • 2.17.1. 常用的反射流程
      • 2.17.2. 反射調用為什麼開銷大?
    • 2.18. 記憶體洩漏問題
    • 2.19. AQS同步隊列器原理,CLH隊列
    • 2.20. AQS元件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握
      • 2.20.1. CountDownLatch
      • 2.20.2. CyclicBarrier
      • 2.20.3. Semaphore
    • 2.21. JUC原子類
    • 2.22. JDK
      • 2.22.1. String的實作原理
      • 2.22.2. HashMap實作
      • 2.22.3. LinkedHashMap
    • 2.23. Java集合
      • 2.23.1. HashMap為什麼線程不安全?
      • 2.23.2. equals()和hashcode()
      • 2.23.3. hashmap周遊時用map.remove方法為什麼會報錯?
    • 2.24. 集合架構的多線程實作類
      • 2.24.1. ConcurrentHashMap
      • 2.24.2. ConcurrentLinkedDeque
      • 2.24.3. ConcurrentLinkedQueue
      • 2.24.4. ConcurrentSkipListMap
      • 2.24.5. ConcurrentSkipSet
      • 2.24.6. CopyOnWriteArrayList
      • 2.24.7. CopyOnWriteArraySet
    • 2.25. 談談依賴注入 IOC
    • 2.26. 談談面向切面程式設計 AOP
    • 2.27. Java注解
    • 2.28. Java中面向對象的特性
      • 2.28.1. 封裝
      • 2.28.2. 繼承
      • 2.28.3. 多态
    • 2.29. 動态代理和靜态代理
    • 2.30. Spring的兩種動态代理:Jdk和Cglib 的差別和實作
      • 2.30.1. 為什麼動态代理要實作接口
    • 2.31. JAVA版本特性
      • 2.31.1. Java7
      • 2.31.2. Java8
      • 2.31.3. Java9
    • 2.32. java如何實作泛型?
    • 2.33. JVM
      • 2.33.1. 記憶體模型
        • 2.33.1.1. 程式計數器(線程私有)
        • 2.33.1.2. Java棧(虛拟機棧)
        • 2.33.1.3. 本地方法棧
        • 2.33.1.4. 堆
        • 2.33.1.5. 方法區
      • 2.33.2. 垃圾判斷算法
        • 2.33.2.1. 引用計數
        • 2.33.2.2. 可達性分析
      • 2.33.3. 垃圾回收算法
        • 2.33.3.1. 标記-清除
        • 2.33.3.2. 标記-複制
        • 2.33.3.3. 标記-整理
      • 2.33.4. 垃圾回收器
        • 2.33.4.1. Serial 收集器
        • 2.33.4.2. ParNew收集器其實就是Serial收集器的多線程版本。
        • 2.33.4.3. Parallel Scavenge 收集器
        • 2.33.4.4. Serial Old 收集器
        • 2.33.4.5. Parallel Old 收集器
        • 2.33.4.6. CMS一種以擷取最短回收停頓時間為目标的收集器。
        • 2.33.4.7. G1收集器
      • 2.33.5. JVM參數調優
        • 2.33.5.1. HBase jvm參數調整
    • 2.34. 類加載:雙親委派
      • 2.34.1. 類加載過程
    • 2.35. 設計模式
      • 2.35.1. 單例模式
        • 2.35.1.1. 編寫單例模式
        • 2.35.1.2. 雙重校驗鎖
      • 2.35.2. 工廠模式
      • 2.35.3. 政策模式
      • 2.35.4. 門面模式
      • 2.35.5. 建造模式
      • 2.35.6. 擴充卡模式
      • 2.35.7. 裝飾模式
      • 2.35.8. 代理模式(委托模式)
      • 2.35.9. 外觀模式
      • 2.35.10. 觀察者模式
      • 2.35.11. 指令模式(Command)
      • 2.35.12. 通路者模式(Visitor)

2. 語言類

2.1. 程序和線程的差別

程序是系統進行資源配置設定和排程的一個獨立機關

線程是程序的一個實體,是CPU排程和分派的基本機關,它是比程序更小的能獨立運作的基本機關。線程自己基本上不擁有系統資源,隻擁有一點在運作中必不可少的資源(如程式計數器,一組寄存器和棧),但是它可與同屬一個程序的其他的線程共享程序所擁有的全部資源

2.2. 協程與線程

協程,是一種比線程更加輕量級的存在,協程不是被作業系統核心所管理,而完全是由程式所控制(也就是在使用者态執行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。

子程式,或者稱為函數,在所有語言中都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢傳回,B執行完畢傳回,最後是A執行完畢。是以子程式調用是通過棧實作的,一個線程就是執行一個子程式。子程式調用總是一個入口,一次傳回,調用順序是明确的。而協程的調用和子程式不同。

協程在子程式内部是可中斷的,然後轉而執行别的子程式,在适當的時候再傳回來接着執行。

2.2.1. 協程的優勢

協程的特點在于是一個線程執行,那和多線程比,協程有何優勢?

極高的執行效率:因為子程式切換不是線程切換,而是由程式自身控制,是以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯;

不需要多線程的鎖機制:因為隻有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,隻需要判斷狀态就好了,是以執行效率比多線程高很多。

2.3. 線程安全的定義、線程的狀态

參考了 https://blog.csdn.net/weixin_46416295/article/details/109262143

線程同步

如果有多個線程在同時運作,而這些線程可能會同時運作這段代碼。程式每次運作結果和單線程運作的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的

線程同步

Java中提供了同步機制 (synchronized) 來解決。有三種方式完成同步操作:

  1. 同步代碼塊。
  2. 同步方法。
  3. 鎖機制。

線程狀态

當線程被建立并啟動以後,它既不是一啟動就進入了執行狀态,也不是一直處于執行狀态。線上程的生命周期中, 有幾種狀态呢?在API中 java.lang.Thread.State這個枚舉中給出了六種線程狀态:

線程狀态 導緻狀态發生條件
NEW(建立) 線程剛被建立,但是并未啟動。還沒調用start方法。
Runnable(可運作) 線程可以在java虛拟機中運作的狀态,可能正在運作自己代碼,也可能沒有,這取決于作業系統處理器。
Blocked(鎖阻塞) 當一個線程試圖擷取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 态;當該線程持有鎖時,該線程将變成Runnable狀态。
Waiting(無限等待) 一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀态。進入這個 狀态後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
Timed Waiting(計時等待) 同waiting狀态,有幾個方法有逾時參數,調用他們将進入Timed Waiting狀态。這一狀态 将一直保持到逾時期滿或者接收到喚醒通知。帶有逾時參數的常用方法有Thread.sleep 、 Object.wait。
Teminated(被終止) 因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

2.4. 多線程的實作方式(Runnable和Callable的差別)、start/run方法的差別

java實作多線程有四種方式:繼承Thread類、實作Runnable()接口、實作Callable接口、通過線程池啟動多線程(實作runable)。

Callable通過FutureTask可以有傳回值,而且可以抛出異常。

方法run()稱為線程體。通過調用Thread類的start()方法來啟動一個線程

2.5. 子線程異常捕捉

正常情況下,如果不做特殊的處理,在主線程中是不能夠捕獲到子線程中的異常的。如果想要在主線程中捕獲子線程的異常,我們需要使用

ExecutorService

:

  1. 首先在建立線程工廠的時候

    ExecutorService exec = Executors.newCachedThreadPool(new HandleThreadFactory());

    将我們自定義的工廠類傳入。
  2. 在工廠類中為線程設定

    t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandle());

    異常處理。
  3. 重寫

    Thread.UncaughtExceptionHandler

    中的

    uncaughtException(Thread t, Throwable e)

    方法。

如果不需要每個線程單獨設定異常處理的話,可以使用Thread的靜态方法:

Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());

2.6. wait()/notify()/sleep()/yield()/join()幾個方法的意義

wait方法的作用是将目前運作的線程挂起(即讓其進入阻塞狀态),直到notify或notifyAll方法來喚醒線程

隻要在同一對象上去調用notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。

sleep方法的作用是讓目前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,唯一需要注意的是其與wait方法的差別。最簡單的差別是,wait方法依賴于同步,而sleep方法可以直接調用。而更深層次的差別在于sleep方法隻是暫時讓出CPU的執行權,并不釋放鎖。而wait方法則需要釋放鎖。

yield方法的作用是暫停目前線程,以便其他線程有機會執行,不過不能指定暫停的時間,并且也不能保證目前線程馬上停止。yield方法隻是将Running狀态轉變為Runnable狀态。

join方法的作用是父線程等待子線程執行完成後再執行,換句話說就是将異步執行的線程合并為同步的線程。

2.7. notifyAll實作原理及等待池和鎖池的概念

obj.notifyAll()方法喚醒所有阻塞在 obj 對象上的沉睡線程,然後被喚醒的衆多線程競争 obj 對象的 monitor 占有權,最終得到的那個線程會繼續執行下去,但其他線程繼續阻塞。

每個對象都有一個唯一與之對應的内部鎖(Monitor)。這裡就涉及到鎖池和等待池的概念。當多個線程想要擷取某個對象的鎖,而該對象已經被其他線程擁有,這些線程就會進入該對象的鎖池等待鎖的釋放。當一個線程主動調用wait方法,就會進入等待池不會和其他線程競争鎖的持有。是以這時候就需要調用notify或者notifyAll方法,讓該線程進入鎖池重新參與鎖的争取。notify隻會随機選取一個線程,notifyAll則會将所有等待池中的線程加入到鎖池。

2.8. 線程池的建立方式,7大參數、阻塞隊列、拒絕政策、大小

線程池建立的三種方式。

singleThreadExecutor 建立單個線程

newFixedThreadpool 建立固定線程個數的線程

newCacheThreadpool 可伸縮

7大參數是使用原生線程池建立線程使用的參數。

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            	1, //核心線程數
                3, //最大線程數
                3, //逾時時間
                TimeUnit.SECONDS,//時間機關
                new LinkedBlockingDeque<>(3),//阻塞隊列
                Executors.defaultThreadFactory(),//一個線程工廠,一般使用預設的就OK
                new ThreadPoolExecutor.AbortPolicy()//一種拒絕政策
        );
           

核心線程指的是:線上程中預設建立的線程

最大線程:見名知意指的就是最大的線程數

阻塞隊列:用于核心線程池滿時,讓需要"服務"的線程等待的地方

拒絕政策:指的是最大線程池滿,阻塞隊列也滿的時候,新來的線程如何處置。

4種拒絕政策

new ThreadPoolExecuor.AbortPolicy :不處理這個線程,并抛出異常

new ThreadPoolExecuor.CallerRunsPolicy:這個多的線程不處理,而是交給建立這個線程的去處理

new ThreadPoolExecuor.DiscardPolicy:不處理這個線程,也不抛出異常

new ThreadPoolExecuor.DiscardOldestPolicy:嘗試和最老的線程進行競争,也不會抛出異常

2.9. 樂觀鎖CAS、悲觀鎖 synchronized和ReentrantLock、實作原理以及差別

參考:https://blog.csdn.net/ly199108171231/article/details/88098614這篇文章介紹的非常好,下面是對它内容的一些概述。

悲觀鎖和獨占鎖是一個意思,它假設一定會發生沖突,是以擷取到鎖之後會阻塞其他等待線程。這麼做的好處是簡單安全,但是挂起線程和恢複線程都需要轉入核心态進行,這樣做會帶來很大的性能開銷。悲觀鎖的代表是 synchronized。然而在真實環境中,大部分時候都不會産生沖突。悲觀鎖會造成很大的浪費。而樂觀鎖不一樣,它假設不會産生沖突,先去嘗試執行某項操作,失敗了再進行其他處理(一般都是不斷循環重試)。這種鎖不會阻塞其他的線程,也不涉及上下文切換,性能開銷小。代表實作是 CAS

Java 中的并發鎖大緻分為隐式鎖和顯式鎖兩種。隐式鎖就是我們最常使用的 synchronized 關鍵字,顯式鎖主要包含兩個接口:Lock 和 ReadWriteLock,主要實作類分别為ReentrantLock 和 ReentrantReadWriteLock,這兩個類都是基于AQS(AbstractQueuedSynchronizer) 實作的

CAS是 compare and swap 的簡寫,即比較并交換。它是指一種操作機制。在 Unsafe 類中,調用代碼如下:

複制代碼它需要三個參數,分别是記憶體位置 V,舊的預期值 A 和新的值 B。操作時,先從記憶體位置讀取到值,然後和預期值A比較。如果相等,則将此記憶體位置的值改為新值 B,傳回 true。如果不相等,說明和其他線程沖突了,則不做任何改變,傳回 false。

這種機制在不阻塞其他線程的情況下避免了并發沖突,比獨占鎖的性能高很多。 CAS 在 Java 的原子類和并發包中有大量使用。CAS 底層是靠調用 CPU 指令集的 cmpxchg 完成的,當一個 CPU 核心将記憶體區域的資料讀取到自己的緩存區後,它會鎖定緩存對應的記憶體區域。鎖住期間,其他核心無法操作這塊記憶體區域。CAS 就是通過這種方式(緩存鎖)實作比較和交換操作的原子性的。

ReentrantLock内部有兩個内部類,分别是 FairSync 和 NoFairSync,對應公平鎖和非公平鎖。他們都繼承自 Sync。Sync 又繼承自AQS。

請求鎖時有三種可能:

如果沒有線程持有鎖,則請求成功,目前線程直接擷取到鎖。

如果目前線程已經持有鎖,則使用 CAS 将 state 值加1,表示自己再次申請了鎖,釋放鎖時減1。這就是可重入性的實作。

如果由其他線程持有鎖,那麼将自己添加進等待隊列。

了解 ReentrantLock 和 AQS 之後,再來了解讀寫鎖就很簡單了。讀寫鎖有一個讀鎖和一個寫鎖,分别對應讀操作和鎖操作。鎖的特性如下:

隻有一個線程可以擷取到寫鎖。在擷取寫鎖時,隻有沒有任何線程持有任何鎖才能擷取成功;

如果有線程正持有寫鎖,其他任何線程都擷取不到任何鎖;

沒有線程持有寫鎖時,可以有多個線程擷取到讀鎖。

2.10. Lock與synchronized的差別

  1. Lock是一個接口,屬于JDK層面的實作;而synchronized屬于Java語言的特性,其實作有JVM來控制(代碼執行完畢,出現異常,wait時JVM會主動釋放鎖)。
  2. synchronized在發生異常時,會自動釋放掉鎖,故不會發生死鎖現(此時的死鎖一般是代碼邏輯引起的);而Lock必須在finally中主動unlock鎖,否則就會出現死鎖。
  3. Lock能夠響應中斷,讓等待狀态的線程停止等待;而synchronized不行。
  4. 通過Lock可以知道線程是否成功獲得了鎖,而synchronized不行。
  5. Lock提高了多線程下對讀操作的效率。

2.11. ABA問題

我們假設有兩個線程同時使用CAS操作對同一個值為A的變量進行修改。

線程1:A -> B -> A

線程2:A -> C

由于線程2擷取A之後,線程1對A進行了修改,是以理論上線程2對于C的修改是應該失敗的,但由于線程2在寫入C的時候的預期值是A是以這個操作最後成功了。ABA問題實際上就是說雖然CAS能夠對預期值進行估計,但是卻無法判斷相同的預期值是否發生了修改。是以可以使用版本号來解決這個問題。

2.11.1. 使用AtomicStampedReference避免ABA問題

對于需要自己進行CAS處理的地方,我們可以使用“AtomicStampedReference”來進行資料的處理。它既支援泛型,同時還可以避免傳統CAS中ABA的問題,使資料更加安全。

private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);

stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
           

AtomicStampedReference

主要是使用CAS機制更新新的值reference和時間戳stamp。而最終調用的底層是一個本地的方法對資料進行的修改。

2.12. 鎖優化:偏向鎖、輕量級鎖、自旋鎖、适應性自旋鎖、鎖消除、鎖粗化等

synchronized 是重量級鎖,由于消耗太大,虛拟機對其做了一些優化。

自旋鎖與自适應自旋

在許多應用中,鎖定狀态隻會持續很短的時間,為了這麼一點時間去挂起恢複線程,不值得。我們可以讓等待線程執行一定次數的循環,在循環中去擷取鎖。這項技術稱為自旋鎖,它可以節省系統切換線程的消耗,但仍然要占用處理器。在 JDK1.4.2 中,自選的次數可以通過參數來控制。 JDK 1.6又引入了自适應的自旋鎖,不再通過次數來限制,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。

鎖消除

虛拟機在運作時,如果發現一段被鎖住的代碼中不可能存在共享資料,就會将這個鎖清除。

鎖粗化

當虛拟機檢測到有一串零碎的操作都對同一個對象加鎖時,會把鎖擴充到整個操作序列外部。如 StringBuffer 的 append 操作。

輕量級鎖

對絕大部分的鎖來說,在整個同步周期内都不存在競争。如果沒有競争,輕量級鎖可以使用 CAS 操作避免使用互斥量的開銷。

偏向鎖

偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,當這個線程再次請求鎖時,無需再做任何同步操作,即可擷取鎖。

2.13. volatile和synchronized的差別及JAVA記憶體模型

簡單概括volatile,它能夠使變量在值發生改變時能盡快地讓其他線程知道。

如果要了解volatile就必須先了解java的記憶體模型:

(1)每個線程都有自己的本地記憶體空間(java棧中的幀)。線程執行時,先把變量從記憶體讀到線程自己的本地記憶體空間,然後對變量進行操作。

(2)對該變量操作完成後,在某個時間再把變量重新整理回主記憶體。

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock字首指令”

lock字首指令實際上相當于一個記憶體屏障(也成記憶體栅欄),記憶體屏障會提供3個功能:

1)它確定指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制将對緩存的修改操作立即寫入主存;

3)如果是寫操作,它會導緻其他CPU中對應的緩存行無效。

volatile和synchronized差別

  1. volatile本質是在告訴jvm目前變量在寄存器中的值是不确定的,需要從主存中讀取,synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住.
  2. volatile僅能使用在變量級别,synchronized則可以使用在變量,方法.
  3. volatile僅能實作變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性.

      《Java程式設計思想》上說,定義long或double變量時,如果使用volatile關鍵字,就會獲得(簡單的指派與傳回操作)原子性。

  4. volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞.
  5. 當一個域的值依賴于它之前的值時,volatile就無法工作了,如n=n+1,n++等。如果某個域的值受到其他域的值的限制,那麼volatile也無法工作,如Range類的lower和upper邊界,必須遵循lower<=upper的限制。(其他線程也有可能同步修改,無法保證其他線程可以知道自己棧裡的情況)
  6. 使用volatile而不是synchronized的唯一安全的情況是類中隻有一個可變的域

2.14. ThreadLocal線程本地存儲原理

ThreadLocal是線程本地存儲的一種實作方案。它并不是一個Thread,我們也可以稱之為線程局部變量,在多線程并發通路時,ThreadLocal類為每個使用該變量的線程都建立一個變量值的副本,每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本發生沖突。從線程的角度來看,就感覺像是每個線程都完全擁有該變量一樣。

ThreadLocal的使用場合主要用來解決多線程情況下對資料的讀取因線程并發而産生資料不一緻的問題。ThreadLocal為每個線程中并發通路的資料提供一個本地副本,然後通過對這個本地副本的通路來執行具體的業務邏輯操作,這樣就可以大大減少線程并發控制的複雜度;然而這樣做也需要付出一定的代價,需要耗費一部分記憶體資源,但是相比于線程同步所帶來的性能消耗還是要好上那麼一點點。

連結:https://www.jianshu.com/p/ae653c30b4d9

例如:

//線程本地存儲變量
	private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};
           

這樣我們在任何時候操作

THREAD_LOCAL_NUM.set(n);

都隻操作了目前線程副本裡的值,而不會影響其他線程。

和synchronized差別

  1. 采用synchronized進行同步控制,但是效率略低,使得并發變同步(串行)
  2. 采用ThreadLocal線程本地存儲,為每個使用該變量的線程都存儲一個本地變量副本(線程互不相幹)

2.15. 指令重排序

在虛拟機層面,為了盡可能減少記憶體操作速度遠慢于CPU運作速度所帶來的CPU空置的影響,虛拟機會按照自己的一些規則(這規則後面再叙述)将程式編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以盡可能充分地利用CPU。

在硬體層面,CPU會将接收到的一批指令按照其規則重排序,同樣是基于CPU速度比緩存速度快的原因,和上一點的目的類似,隻是硬體處理的話,每次隻能在接收到的有限指令範圍内重排序,而虛拟機可以在更大層面、更多指令範圍内重排序。硬體的重排序機制參見《從JVM并發看CPU記憶體指令重排序(Memory Reordering)》

Java提供了兩個關鍵字

volatile

synchronized

來保證多線程之間操作的有序性,

volatile

關鍵字本身通過加入記憶體屏障來禁止指令的重排序,而

synchronized

關鍵字通過一個變量在同一時間隻允許有一個線程對其進行加鎖的規則來實作。在單線程程式中,不會發生“指令重排”和“工作記憶體和主記憶體同步延遲”現象,隻在多線程程式中出現。

  1. 編譯器優化的重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。
  2. 指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-LevelParallelism,ILP)來将多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

2.16. final關鍵字

  1. final成員變量表示常量,隻能被指派一次,指派後值不再改變(final要求位址值不能改變;當final修飾一個基本資料類型時,表示該基本資料類型的值一旦在初始化後便不能發生變化
  2. 使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義,不能被重寫;第二個原因是效率,final方法比非final方法要快,因為在編譯的時候已經靜态綁定了,不需要在運作時再動态綁定。(注:類的private方法會隐式地被指定為final方法)
  3. 當用final修飾一個類時,表明這個類不能被繼承。

2.16.1. final實作原理

對于final域,編譯器和處理器要遵守兩個重排序規則:

  1. 在構造函數内對一個final域的寫入,與随後把這個被構造對象的引用指派給一個引用變量,這兩個操作之間不能重排序。

    原因:編譯器會在final域的寫之後,插入一個StoreStore屏障

  2. 初次讀一個包含final域的對象的引用,與随後初次讀這個final域,這兩個操作之間不能重排序。

    編譯器會在讀final域操作的前面插入一個LoadLoad屏障

2.17. 反射

反射的常用類和函數:Java反射機制的實作要借助于4個類:Class,Constructor,Field,Method;其中class代表的是類對象,Constructor-類的構造器對象,Field-類的屬性對象,Method-類的方法對象,通過這四個對象我們可以粗略的看到一個類的各個組成部分。

2.17.1. 常用的反射流程

  1. Class.forName 找到類;
  2. newInstance 開辟記憶體空間,構造執行個體;
  3. getDeclaredMethod 根據方法簽名搜尋方法體;
  4. method.invoke 執行方法調用。

2.17.2. 反射調用為什麼開銷大?

Class.forName 會嘗試先去方法區(1.8以後就是 Metaspace) 中尋找有沒有對應的類,如果沒有則在 classpath 中找到對應的class檔案, 通過類加載器将其加載到記憶體裡。注意這個方法涉及本地方法調用,這裡本地方法調用的切換、類的搜尋以及類的加載都存在開銷;newInstance ,開辟記憶體空間,執行個體化已經找到的 Class;getDeclaredMethod/getMethod 周遊 Class 的方法,以方法簽名比對出所需要的 Method 執行個體,也是比較重的開銷所在;雖然 java.lang.Class 内部維護了一套名為 reflectionData 的資料結構,其本身是 SoftReference 的。Class 會将比對成功的 method 緩存到這裡,以供下次通路命中,這樣一來會減輕 method 周遊的開銷,但是會增加額外的堆空間消耗(以空間置換時間)

  1. Method#invoke 方法會對參數做封裝和解封操作

我們可以看到,invoke 方法的參數是 Object[] 類型,也就是說,如果方法參數是簡單類型的話,需要在此轉化成 Object 類型,例如 long ,在 javac compile 的時候 用了Long.valueOf() 轉型,也就大量了生成了Long 的 Object, 同時 傳入的參數是Object[]數值,那還需要額外封裝object數組。

而在上面 MethodAccessorGenerator#emitInvoke 方法裡我們看到,生成的位元組碼時,會把參數數組拆解開來,把參數恢複到沒有被 Object[] 包裝前的樣子,同時還要對參數做校驗,這裡就涉及到了解封操作。

是以,在反射調用的時候,因為封裝和解封,産生了額外的不必要的記憶體浪費,當調用次數達到一定量的時候,還會導緻 GC。

  1. 需要檢查方法可見性

通過上面的源碼分析,我們會發現,反射時每次調用都必須檢查方法的可見性(在 Method.invoke 裡)

  1. 需要校驗參數

反射時也必須檢查每個實際參數與形式參數的類型比對性(在NativeMethodAccessorImpl.invoke0 裡或者生成的 Java 版 MethodAccessor.invoke 裡);

  1. 反射方法難以内聯

Method#invoke 就像是個獨木橋一樣,各處的反射調用都要擠過去,在調用點上收集到的類型資訊就會很亂,影響内聯程式的判斷,使得 Method.invoke() 自身難以被内聯到調用方。參見 http://www.iteye.com/blog/rednax…

  1. JIT 無法優化

另外,JDK 開發者們也給我們提供了一套優化思路,既然反射調用有性能問題,不好優化,那能不能将反射調用變成普通調用呢?inflation 正是這條思路的解決方案。通過類位元組碼的生成并加載,實作了 inflation 。好處就是這不僅減少了本地方法和普通方法來回切換的開銷,變成普通方法調用後還能享受到 JIT 編譯器的優化福利,其中方法内聯是最重要的優化點。編譯器采集足夠多的運作時資料後,根據統計模型得知某個反射方法成為熱點,此時該反射方法體有幾率會被内聯進調用者的方法體中。

2.18. 記憶體洩漏問題

常見的記憶體洩漏及解決方法

1、單例造成的記憶體洩漏

2、非靜态内部類建立靜态執行個體造成的記憶體洩漏

因為非靜态内部類預設會持有外部類的引用,而該非靜态内部類又建立了一個靜态的執行個體,該執行個體的生命周期和應用的一樣長,這就導緻了該靜态執行個體一直會持有該Activity的引用,進而導緻Activity的記憶體資源不能被正常回收。

解決方法:将該内部類設為靜态内部類或将該内部類抽取出來封裝成一個單例

3、資源未關閉造成的記憶體洩漏

4、集合容器中的記憶體洩露

2.19. AQS同步隊列器原理,CLH隊列

AQS,AbstractQueuedSynchronizer,即隊列同步器。它是建構鎖或者其他同步元件的基礎架構(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并發包的作者(Doug Lea)期望它能夠成為實作大部分同步需求的基礎。它是JUC并發包中的核心基礎元件。

AQS的主要使用方式是繼承,子類通過繼承同步器并實作它的抽象方法來管理同步狀态。 AQS使用一個int類型的成員變量state來表示同步狀态,當state>0時表示已經擷取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀态state進行操作,當然AQS可以確定對state的操作是安全的。 AQS通過内置的FIFO同步隊列來完成資源擷取線程的排隊工作,如果目前線程擷取同步狀态失敗(鎖)時,AQS則會将目前線程以及等待狀态等資訊構造成一個節點(Node)并将其加入同步隊列,同時會阻塞目前線程,當同步狀态釋放時,則會把節點中的線程喚醒,使其再次嘗試擷取同步狀态。

CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀态的管理,目前線程如果擷取同步狀态失敗時,AQS則會将目前線程已經等待狀态等資訊構造成一個節點(Node)并将其加入到CLH同步隊列,同時會阻塞目前線程,當同步狀态釋放時,會把首節點喚醒(公平鎖),使其再次嘗試擷取同步狀态。

2.20. AQS元件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握

參考:https://www.cnblogs.com/jackion5/p/12932343.html

2.20.1. CountDownLatch

CountDownLatch可以了解為是同步計數器,作用是允許一個或多個線程等待其他線程執行完成之後才繼續執行,比如打dota、LoL或者王者榮耀時,建立了一個五人房,隻有當五個玩家都準備了之後,遊戲才能正式開始,否則遊戲主線程會一直等待着直到玩家全部準備。在玩家沒準備之前,遊戲主線程會一直處于等待狀态。如果把CountDownLatch比做此場景都話,相當于開始定義了比對遊戲需要5個線程,隻有當5個線程都準備完成了之後,主線程才會開始進行比對操作。

public static void countDownLatchTest() throws Exception{
        CountDownLatch latch = new CountDownLatch(5);//定義了需要達到條件都線程為5個線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<12; i++){
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            long count = latch.getCount();
                            latch.countDown();/**相當于準備遊戲成功*/
                            if(count > 0) {
                                System.out.println("線程" + Thread.currentThread().getName() + "組隊準備,還需等待" + latch.getCount() + "人準備");
                            }else {
                                System.out.println("線程" + Thread.currentThread().getName() + "組隊準備,房間已滿不可加入");
                            }
                        }
                    }).start();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("遊戲房間等待玩家加入...");
                    /**一直等待到規定數量到線程都完全準備之後才會繼續往下執行*/
                    latch.await();
                    System.out.println("遊戲房間已鎖定...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        System.out.println("等待玩家準備中...");
        /**一直等待到規定數量到線程都完全準備之後才會繼續往下執行*/
        latch.await();
        System.out.println("遊戲比對中...");
    }
           

本案例中有兩個線程都調用了latch.await()方法,則這兩個線程都會被阻塞,直到條件達成。當5個線程調用countDown方法之後,達到了計數器的要求,則後續再執行countDown方法的效果就無效了,因為CountDownLatch僅一次有效。

CountDownLatch的實作完整邏輯如下:

  1. 初始化CountDownLatch實際就是設定了AQS的state為計數的值
  2. 調用CountDownLatch的countDown方法時實際就是調用AQS的釋放同步狀态的方法,每調用一次就自減一次state值
  3. 調用await方法實際就調用AQS的共享式擷取同步狀态的方法acquireSharedInterruptibly(1),這個方法的實作邏輯就調用子類Sync的tryAcquireShared方法,隻有當子類Sync的tryAcquireShared方法傳回大于0的值時才算擷取同步狀态成功,否則就會一直在死循環中不斷重試,直到tryAcquireShared方法傳回大于等于0的值,而Sync的tryAcquireShared方法隻有當AQS中的state值為0時才會傳回1,否則都傳回-1,也就相當于隻有當AQS的state值為0時,await方法才會執行成功,否則就會一直處于死循環中不斷重試。

2.20.2. CyclicBarrier

CyclicBarrier可以了解為一個循環同步屏障,定義一個同步屏障之後,當一組線程都全部達到同步屏障之前都會被阻塞,直到最後一個線程達到了同步屏障之後才會被打開,其他線程才可繼續執行。

還是以dota、LoL和王者榮耀為例,當第一個玩家準備了之後,還需要等待其他4個玩家都準備,遊戲才可繼續,否則準備的玩家會被一直處于等待狀态,隻有當最後一個玩家準備了之後,遊戲才會繼續執行。

public static void CyclicBarrierTest() throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(5);//定義需要達到同步屏障的線程數量
        for (int i=0;i<12;i++){
            Thread.sleep(1000L);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("線程"+Thread.currentThread().getName()+"組隊準備,目前" + (barrier.getNumberWaiting()+1) + "人已準備");
                        barrier.await();/**線程進入等待,直到最後一個線程達到同步屏障*/
                        System.out.println("線程:"+Thread.currentThread().getName()+"開始組隊遊戲");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
           

本案例中定義了達到同步屏障的線程為5個,每當一個線程調用了barrier.await()方法之後表示該線程已達到屏障,此時目前線程會被阻塞,隻有當最後一個線程調用了await方法之後,被阻塞的其他線程才會被喚醒繼續執行。

另外CyclicBarrier是循環同步屏障,同步屏障打開之後立馬會繼續計數,等待下一組線程達到同步屏障。而CountDownLatch僅單次有效。

2.20.3. Semaphore

Semaphore字面意思是信号量,實際可以看作是一個限流器,初始化Semaphore時就定義好了最大通行證數量,每次調用時調用方法來消耗,業務執行完畢則釋放通行證,如果通行證消耗完,再擷取通行證時就需要阻塞線程直到有通行證可以擷取。

public static void semaphoreTest() throws InterruptedException {
        int count = 5;
        Semaphore semaphore = new Semaphore(count);
        System.out.println("初始化" + count + "個銀行櫃台視窗");
        for (int i=0;i<10;i++){
            Thread.sleep(1000L);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("使用者"+Thread.currentThread().getName()+"占用視窗");
                        semaphore.acquire(1);//擷取許可證
                        /**使用者辦理業務需要消耗一定時間*/
                        System.out.println("使用者"+Thread.currentThread().getName()+"開始辦理業務");
                        Thread.sleep(5000L);
                        semaphore.release();//釋放許可證
                        System.out.println("使用者"+Thread.currentThread().getName()+"離開視窗");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
           

2.21. JUC原子類

根據修改的資料類型,可以将JUC包中的原子操作類可以分為4類。

基本類型: AtomicInteger, AtomicLong, AtomicBoolean ;

數組類型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;

引用類型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;

對象的屬性修改類型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater

這些類存在的目的是對相應的資料進行原子操作。所謂原子操作,是指操作過程不會被中斷,保證資料操作是以原子方式進行的。

原理:CAS+volatile + native方法來保證操作的原子性

2.22. JDK

2.22.1. String的實作原理

在Java語言中,所有類似“ABC”的字面值,都是String類的執行個體;String類位于java.lang包下,是Java語言的核心類,提供了字元串的比較、查找、截取、大小寫轉換等操作;Java語言為“+”連接配接符(字元串連接配接符)以及對象轉換為字元串提供了特殊的支援,字元串對象可以使用“+”連接配接其他對象。

  1. String類被final關鍵字修飾,意味着String類不能被繼承,并且它的成員方法都預設為final方法;字元串一旦建立就不能再修改。
  2. String類實作了Serializable、CharSequence、 Comparable接口。
  3. String執行個體的值是通過字元數組實作字元串存儲的。

注意的點:

  1. "+"在字面量兩邊的時候,編譯器會自動優化為一個字元串,除非編譯器無法确定或存在final修飾符。另外一種情況則會隐式建立

    StringBuilder

    對象,轉為

    append()

    操作
  2. 每當建立字元串常量時,JVM會首先檢查字元串常量池,如果該字元串已經存在常量池中,那麼就直接傳回常量池中的執行個體引用。如果字元串不存在常量池中,就會執行個體化該字元串并且将其放到常量池中。常量池是通過一個StringTable類實作的,它是一個Hash表。
  3. 如果不是用雙引号聲明的String對象,可以使用String提供的intern方法。intern 方法是一個native方法,intern方法會從字元串常量池中查詢目前字元串是否存在,如果存在,就直接傳回目前字元串;如果不存在就會将目前字元串放入常量池中。
  4. String是不可變字元序列,StringBuilder和StringBuffer是可變字元序列。StringBuilder是非線程安全的,StringBuffer是線程安全的。

2.22.2. HashMap實作

  1. HashMap 底層資料結構為 Node 類型數組,而 Node 的資料結構為連結清單。
  2. 負載因子預設為 0.75,當 HashMap 目前已使用容量大于目前大小 * 負載因子時,自動擴容一倍空間
  3. 樹型閥值就是當連結清單長度超過這個值時,将 Node 的資料結構修改為紅黑樹,以便優化查找時間,預設值為8
  4. put

    方法:對 key 進行 hash 運算,然後再與目前 map 最後一個下标(長度-1,分布更均勻)進行與運算确定其在數組中的位置,正是因為這個算法,我們可以得知 HashMap 中元素是無序的。
  5. get

    方法:根據 key 的 hash 運算值擷取數組中對應下标的内容。循環連結清單或紅黑樹,然後比對 value 值直至獲得對應的值或傳回 null
  6. 1.8我們在擴充HashMap的時候,不需要像JDK1.7的實作那樣重新計算hash,隻需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”

2.22.3. LinkedHashMap

  1. LinkedHashMap是繼承于HashMap,是基于HashMap和雙向連結清單來實作的。
  2. HashMap無序;LinkedHashMap有序,可分為插入順序和通路順序兩種。如果是通路順序,那put和get操作已存在的Entry時,都會把Entry移動到雙向連結清單的表尾(其實是先删除再插入)。
  3. LinkedHashMap存取資料,還是跟HashMap一樣使用的Entry[]的方式,雙向連結清單隻是為了保證順序。
  4. LinkedHashMap是線程不安全的。

2.23. Java集合

2.23.1. HashMap為什麼線程不安全?

在接近臨界點時,若此時兩個或者多個線程進行put操作,都會進行resize(擴容)和reHash(為key重新計算所在位置),而reHash在并發的情況下可能會形成連結清單環。總結來說就是在多線程環境下,使用HashMap進行put操作會引起死循環,導緻CPU使用率接近100%,是以在并發情況下不能使用HashMap。為什麼在并發執行put操作會引起死循環?是因為多線程會導緻HashMap的Entry連結清單形成環形資料結構,一旦形成環形資料結構,Entry的next節點永遠不為空,就會産生死循環擷取Entry。jdk1.7的情況下,并發擴容時容易形成連結清單環,此情況在1.8時就好太多太多了。

因為在1.8中當連結清單長度達到門檻值(預設長度為8)時,連結清單會被改成樹形(紅黑樹)結構。如果删剩節點變成7個并不會退回連結清單,而是保持不變,删剩6個時就會變回連結清單,7不變是緩沖,防止頻繁變換。

在JDK1.7中,當并發執行擴容操作時會造成環形鍊和資料丢失的情況。

在JDK1.8中,在并發執行put操作時會發生資料覆寫的情況。

2.23.2. equals()和hashcode()

使得equals成立的時候,hashCode相等,也就是a.equals(b)->a.hashCode() == b.hashCode(),或者說此時,equals是hashCode相等的充分條件,hashCode相等是equals的必要條件(從數學課上我們知道它的逆否命題:hashCode不相等也不會equals),但是它的逆命題,hashCode相等一定equals以及否命題不equals時hashCode不等都不成立。

2.23.3. hashmap周遊時用map.remove方法為什麼會報錯?

hashmap裡維護了一個

modCount

變量,疊代器裡維護了一個

expectedModCount

變量,一開始兩者是一樣的。

每次進行hashmap.remove操作的時候就會對modCount+1,此時疊代器裡的expectedModCount還是之前的值。

在下一次對疊代器進行next()調用時,判斷是否

HashMap.this.modCount != this.expectedModCount

,如果是則抛出異常。

解決方法:直接調用疊代器的remove方法。

基本上java集合類(包括list和map)在周遊時沒用疊代器進行删除了都會報ConcurrentModificationException錯誤,這是一種fast-fail的機制,初衷是為了檢測bug。

通俗一點來說,這種機制就是為了防止高并發的情況下,多個線程同時修改map或者list的元素導緻的資料不一緻,這是隻要判斷目前modCount != expectedModCount即可以知道有其他線程修改了集合。

2.24. 集合架構的多線程實作類

CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、ConcurrentLinkedDeque

2.24.1. ConcurrentHashMap

其主要接口方法和HashMap是差不多的。但是,ConcurrentHashMap是使用了ReentranLock(可重入鎖機制)來保證在多線程環境下是線程安全的。在JDK6、7後使用了分段鎖,在JDK8使用Synchronized對Node頭節點加鎖和volatile對指針修飾來實作。

2.24.2. ConcurrentLinkedDeque

線程安全的雙端隊列,當然也可以當棧使用。由于是linked的,是以大小不受限制的。

2.24.3. ConcurrentLinkedQueue

線程安全的隊列。

2.24.4. ConcurrentSkipListMap

基于跳表實作的線程安全的MAP。除了線程安全的特性外,該map還接受比較器進行排序的map,算法複雜度還是log(n)級别的。

2.24.5. ConcurrentSkipSet

基于4實作的set。

2.24.6. CopyOnWriteArrayList

以資源換取并發。通過疊代器快照的方式保證線程并發的通路。

2.24.7. CopyOnWriteArraySet

基于6實作的。

2.25. 談談依賴注入 IOC

IOC(控制反轉)理論提出的觀點大體是這樣的:借助于“第三方”實作具有依賴關系的對象之間的解耦。是以依賴注入 DI和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關系注入的方式,實作對象之間的解耦,除此之外還有另一種方法:服務定位器(Service Locator)。IOC中最基本的技術就是“反射(Reflection)”程式設計,通俗來講就是根據給出的類名(字元串方式)來動态地生成對象。這種程式設計方式可以讓對象在生成時才決定到底是哪一種對象。

2.26. 談談面向切面程式設計 AOP

AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和運作期動态代理實作程式功能的統一維護的一種技術。

在spring中

@Aspect

注解用來描述一個切面類。

@Component

注解将該類交給 Spring 來管理,下面列舉spring中AOP相關注解

注解 用法 例子
@Pointcut 用來定義一個切點,即上文中所關注的某件事情的入口,切入點定義了事件觸發時機。 @Pointcut(“execution(* com.mutest.controller….(…))”) // 定義一個切面,攔截 com.itcodai.course09.controller 包和子包下的所有方法
@Around @Around可以自由選擇增強動作與目标方法的執行順序,還可以選擇可以改變執行目标方法的參數值,也可以改變執行目标方法之後的傳回值。
@Before 注解指定的方法在切面切入目标方法之前執行
@After 指定的方法在切面切入目标方法之後執行,也可以做一些完成某方法之後的 Log 處理。
@AfterReturning 可以用來捕獲切入方法執行完之後的傳回值,對傳回值進行業務邏輯上的增強處理
@AfterThrowing 當被切方法執行過程中抛出異常時,會進入 @AfterThrowing 注解的方法中執行

2.27. Java注解

注解的好處:本來可能需要很多配置檔案,需要很多邏輯才能實作的内容,就可以使用一個或者多個注解來替代,這樣就使得程式設計更加簡潔,代碼更加清晰。

注解這一概念是在java1.5版本提出的,說Java提供了一種原程式中的元素關聯任何資訊和任何中繼資料的途徑的方法。

常見的作用有以下幾種:

  1. 生成文檔。這是最常見的,也是java 最早提供的注解。常用的有@see @param @return 等;
  2. 跟蹤代碼依賴性,實作替代配置檔案功能。比較常見的是spring 2.5 開始的基于注解配置。作用就是減少配置。現在的架構基本都使用了這種配置來減少配置檔案的數量;
  3. 在編譯時進行格式檢查。如@Override放在方法前,如果你這個方法并不是覆寫了超類方法,則編譯時就能檢查出;

2.28. Java中面向對象的特性

繼承、多态和封裝。

2.28.1. 封裝

隐藏類内部的實作機制。

2.28.2. 繼承

重用父類的代碼。

2.28.3. 多态

多态是指一種事物的變現出來的多種特性。Java實作多态有三個必要條件:繼承、重寫、向上轉型(使用父類聲明的指針指向子類執行個體)。

2.29. 動态代理和靜态代理

代理是設計模式的一種,其目的就是為其他對象提供一個代理以控制對某個對象的通路。代理類負責為委托類預處理消息,過濾消息并轉發消息,以及進行消息被委托類執行後的後續處理。Java中的代理分為三種角色:

  1. 代理類(ProxySubject)
  2. 委托類(RealSubject)
  3. 接口(Subject)

靜态:由程式員建立代理類或特定工具自動生成源代碼再對其編譯。在程式運作前代理類的.class檔案就已經存在了。或者使用

Cglib

代理動态生成新的子類,是以不需要實作接口。

動态:在程式運作時運用反射機制動态建立而成。由于建立出來的委托類執行個體繼承自

Proxy

類,由于java不支援多繼承是以委托類必須使用接口。

2.30. Spring的兩種動态代理:Jdk和Cglib 的差別和實作

java動态代理是利用反射機制生成一個實作代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。

而cglib動态代理是利用asm開源包,對代理對象類的class檔案加載進來,通過修改其位元組碼生成子類來處理。

  1. 如果目标對象實作了接口,預設情況下會采用JDK的動态代理實作AOP
  2. 如果目标對象實作了接口,可以強制使用CGLIB實作AOP
  3. 如果目标對象沒有實作了接口,必須采用CGLIB庫,spring會自動在JDK動态代理和CGLIB之間轉換

2.30.1. 為什麼動态代理要實作接口

深入分析JDK動态代理為什麼隻能使用接口

生成的代理類繼承了Proxy,我們知道java是單繼承的,是以JDK動态代理隻能代理接口。

2.31. JAVA版本特性

2.31.1. Java7

  1. 建立泛型對象時應用類型推斷。eg:
  1. switch語句塊中允許以字元串作為分支條件。
  2. try-with-resources(一個語句塊中捕獲多種異常)。

2.31.2. Java8

  1. lambda表達式。允許将功能當作方法參數或代碼當作資料。lambda辨別還可以更簡潔的方式表示隻有一個方法的接口(函數式接口)的執行個體。
  2. 改進的類型推斷。JDK8裡,類型推斷不僅可以用于指派語句,而且可以根據代碼中上下文裡的資訊推斷出更多的資訊
List<String> stringList = new ArrayList<>();
  stringList.add("A");
  //error : addAll(java.util.Collection<? extends java.lang.String>)in List cannot be applied to (java.util.List<java.lang.Object>)
  stringList.addAll(Arrays.asList());
           

上面這段代碼在java7中會報錯,但是在java8中可以編譯通過。

3. 支援多重注解。之前同一個注解需要放在類似數組的結構中,現在可以直接多個@,隻需要對注解标注為

@Repeatable

4. stream接口。

5. 新增Optional接口,用來防止NullPointerException異常的輔助類型。在java8中不推薦傳回null而是傳回Optional。

6. 函數式接口(Functional Interface)。java.util.function 它包含了很多類,用來支援 Java的 函數式程式設計(接受函數過程傳遞),該包中的函數式接口有:

Predicate<T>

接受一個輸入參數,傳回一個布爾值結果。

7. Java 8允許我們給接口添加一個非抽象的方法實作,隻需要使用 default關鍵字即可,這個特征又叫做擴充方法,示例如下:

interface Formula {
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}
           
  1. ::方法與構造函數引用。Java 8 允許你使用

    ::

    關鍵字來傳遞方法或者構造函數引用,我們也可以引用一個對象的方法代碼如下:
converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"
           

接下來看看構造函數是如何使用::關鍵字來引用的:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
           

2.31.3. Java9

  1. 子產品化。子產品化系統的主要目的如下:

更可靠的配置,通過制定明确的類的依賴關系代替以前那種易錯的類路徑(class-path)加載機制。

強大的封裝,允許一個元件聲明它的公有類型(public)中,哪些可以被其他元件通路,哪些不可以。

這些特性将有益于應用的開發者、類庫的開發者和java se平台直接的或者間接地實作者。它可以使系統更加健壯,并且可以提高他們的性能。

一個子產品是一個被命名的,代碼和資料的自描述的集合。它的代碼有一系列包含類型的包組成,例如:java的類和接口。它的資料包括資源檔案(resources)和一些其他的靜态資訊。一個或更多個

requires

項可以被添加到其中,它通過名字聲明了這個子產品依賴的一些其他子產品,在編譯期和運作期都依賴的。最後,

exports

項可以被添加,它可以僅僅使指定包(package)中的公共類型可以被其他的子產品使用。
module com.foo.bar {
    requires org.baz.qux;
    exports com.foo.bar.alpha;
    exports com.foo.bar.beta;
}
           

2.32. java如何實作泛型?

Java 語言中的泛型,它隻在程式源碼中存在,在編譯後的位元組碼檔案中,就已經替換為原來的原生類型(RawType,也稱為裸類 型)了,并且在相應的地方插入了強制轉型代碼,是以,對于運作期的 Java 語言來說,ArrayList<int>與 ArrayList<String>就是同一 個類,是以泛型技術實際上是 Java 語言的一顆文法糖,Java 語言中的泛型實作方法稱為類型擦除,基于這種方法實作的泛型稱為僞泛 型。

将一段 Java 代碼編譯成 Class 檔案,然後再用位元組碼反編譯工具進行反編譯後,将會發現泛型都不見了,程式又變回了 Java 泛型 出現之前的寫法,因為泛型類型都變回了原生類型.

2.33. JVM

2.33.1. 記憶體模型

根據JVM規範,JVM 記憶體共分為虛拟機棧,堆,方法區,程式計數器,本地方法棧五個部分。

2.33.1.1. 程式計數器(線程私有)

程式計數器是一塊很小的記憶體空間,它是線程私有的,可以認作為目前線程的行号訓示器。

注意:如果線程執行的是個java方法,那麼計數器記錄虛拟機位元組碼指令的位址。如果為native【底層方法】,那麼計數器為空。這塊記憶體區域是虛拟機規範中唯一沒有OutOfMemoryError的區域。

2.33.1.2. Java棧(虛拟機棧)

每個方法被執行的時候都會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法傳回位址、附加資訊。每一個方法被調用的過程就對應一個棧幀在虛拟機棧中從入棧到出棧的過程。具體棧幀參考Java虛拟機運作時棧幀結構(周志明書上P237頁)

2.33.1.3. 本地方法棧

本地方法棧是與虛拟機棧發揮的作用十分相似,差別是虛拟機棧執行的是Java方法(也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的native方法服務,可能底層調用的c或者c++,我們打開jdk安裝目錄可以看到也有很多用c編寫的檔案,可能就是native方法所調用的c代碼

2.33.1.4. 堆

對于大多數應用來說,堆是java虛拟機管理記憶體最大的一塊記憶體區域,因為堆存放的對象是線程共享的,是以多線程的時候也需要同步機制。

2.33.1.5. 方法區

方法區同堆一樣,是所有線程共享的記憶體區域,為了區分堆,又被稱為非堆。

用于存儲已被虛拟機加載的類資訊、常量、靜态變量,如static修飾的變量加載類的時候就被加載到方法區中。

2.33.2. 垃圾判斷算法

一般來講有兩種垃圾判斷的方法,分别是引用計數和可達性分析法。JVM主流采用的是可達性分析算法,因為在Java中引用計數法無法處理循環引用的問題。

2.33.2.1. 引用計數

為避免循環引用的問題,c++中會将互相引用的兩個對象中,其中一個引用 weak_ptr,因為 weak_ptr 不會使引用計數加1,是以就不會出現這種互相拖着對方的事情了。

又比如說redis,因為其内部不存在深層次的嵌套,是以也就不存在循環引用的隐患。

2.33.2.2. 可達性分析

JVM采用了另一種方法:定義一個名為"GC Roots"的對象作為起始點,這個"GC Roots"可以有多個,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的,即可以進行垃圾回收。

可以作為GC Roots的對象:

  1. 虛拟機棧中引用的對象(棧幀中的本地變量)
  2. 方法區中類靜态屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中引用的對象

2.33.3. 垃圾回收算法

2.33.3.1. 标記-清除

标記完成後就可以對标記為垃圾的對象進行回收了。但是這種政策的缺點很明顯,回收後記憶體碎片很多,如果之後程式運作時申請大記憶體,可能會又導緻一次GC。

2.33.3.2. 标記-複制

将記憶體分為兩塊,标記完成開始回收時,将一塊記憶體中保留的對象全部複制到另一塊空閑記憶體中。實作起來也很簡單,當大部分對象都被回收時這種政策也很高效。但這種政策也有缺點,可用記憶體變為一半了

怎樣解決呢?辦法總是多過問題的。可以将堆不按1:1的比例分離,而是按8:1:1分成一塊Eden和兩小塊Survivor區,每次将Eden和Survivor中存活的對象複制到另一塊空閑的Survivor中。這三塊區域并不是堆的全部,而是構成了新生代。如果回收時,空閑的那一小塊Survivor不夠用了怎麼辦?這就是老年代的用處。當不夠用時,這些對象将直接通過配置設定擔保機制進入老年代。‘

2.33.3.3. 标記-整理

根據老年代的特點,采用回收掉垃圾對象後對記憶體進行整理的政策再合适不過,将所有存活下來的對象都向一端移動。

2.33.4. 垃圾回收器

參考:https://www.cnblogs.com/chenpt/p/9803298.html

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

2.33.4.1. Serial 收集器

Serial收集器是最基本的、發展曆史最悠久的收集器。

特點:單線程、簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器由于沒有線程互動的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。

應用場景:适用于Client模式下的虛拟機。

2.33.4.2. ParNew收集器其實就是Serial收集器的多線程版本。

除了使用多線程外其餘行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象配置設定規則、回收政策等)。

特點:多線程、ParNew收集器預設開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。和Serial收集器一樣存在Stop The World問題

應用場景:ParNew收集器是許多運作在Server模式下的虛拟機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。

2.33.4.3. Parallel Scavenge 收集器

與吞吐量關系密切,故也稱為吞吐量優先收集器。吞吐量=運作使用者代碼時間/CPU總執行時間。

用于精确吞吐量的兩個參數:1.控制最大垃圾收集停頓時間參數 2.直接設定吞吐量大小的參數。

特點:屬于新生代收集器也是采用複制算法的收集器,又是并行的多線程收集器(與ParNew收集器類似)。

該收集器的目标是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自适應調節政策(與ParNew收集器最重要的一個差別)

2.33.4.4. Serial Old 收集器

Serial Old是Serial收集器的老年代版本。

特點:同樣是單線程收集器,采用标記-整理算法。

應用場景:主要也是使用在Client模式下的虛拟機中。也可在Server模式下使用。

Server模式下主要的兩大用途:

在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。

作為CMS收集器的後備方案,在并發收集Concurent Mode Failure時使用。

2.33.4.5. Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本。

特點:多線程,采用标記-整理算法。

應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。

2.33.4.6. CMS一種以擷取最短回收停頓時間為目标的收集器。

特點: 針對老年代;基于标記-清除算法實作。并發收集、低停頓。

應用場景:适用于注重服務的響應速度,希望系統停頓時間最短,給使用者帶來更好的體驗等場景下。如web程式、b/s服務。

CMS收集器的運作過程分為下列4步:

  1. 初始标記:标記GC Roots能直接到的對象。速度很快但是仍存在Stop The World問題。
  2. 并發标記:進行GC Roots Tracing 的過程,找出存活對象且使用者線程可并發執行。
  3. 重新标記:為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄。仍然存在Stop The World問題。
  4. 并發清除:對标記的對象進行清除回收。

CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。

CMS收集器的缺點:

對CPU資源非常敏感。

無法處理浮動垃圾,可能出現Concurrent Model Failure失敗而導緻另一次Full GC的産生。

因為采用标記-清除算法是以會存在空間碎片的問題,導緻大對象無法配置設定空間,不得不提前觸發一次Full GC。

2.33.4.7. G1收集器

一款面向服務端應用的垃圾收集器。

特點如下:

并行與并發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓時間。部分收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過并發的方式讓Java程式繼續運作。

分代收集:G1能夠獨自管理整個Java堆,并且采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次GC的舊對象以擷取更好的收集效果。

空間整合:使用标記-整理算法。G1運作期間不會産生空間碎片,收集後能提供規整的可用記憶體。

可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明确指定在一個長度為M毫秒的時間段内,消耗在垃圾收集上的時間不得超過N毫秒。

G1為什麼能建立可預測的停頓時間模型?

因為它有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的大小,在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間内可以擷取盡可能高的收集效率。

G1與其他收集器的差別:

其他收集器的工作範圍是整個新生代或者老年代、G1收集器的工作範圍是整個Java堆。在使用G1收集器時,它将整個Java堆劃分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是互相隔離的,他們都是一部分Region(不需要連續)的集合。

G1收集器存在的問題:

Region不可能是孤立的,配置設定在Region中的對象可以與Java堆中的任意對象發生引用關系。在采用可達性分析算法來判斷對象是否存活時,得掃描整個Java堆才能保證準确性。其他收集器也存在這種問題(G1更加突出而已)。會導緻Minor GC效率下降。

G1收集器是如何解決上述問題的?

采用Remembered Set來避免整堆掃描。G1中每個Region都有一個與之對應的Remembered Set,虛拟機發現程式在對Reference類型進行寫操作時,會産生一個Write Barrier暫時中斷寫操作,檢查Reference引用對象是否處于多個Region中(即檢查老年代中是否引用了新生代中的對象),如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆進行掃描也不會有遺漏。

如果不計算維護 Remembered Set 的操作,G1收集器大緻可分為如下步驟:

  1. 初始标記:僅标記GC Roots能直接到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的Region中建立新對象。(需要線程停頓,但耗時很短。)
  2. 并發标記:從GC Roots開始對堆中對象進行可達性分析,找出存活對象。(耗時較長,但可與使用者程式并發執行)
  3. 最終标記:為了修正在并發标記期間因使用者程式執行而導緻标記産生變化的那一部分标記記錄。且對象的變化記錄線上程Remembered Set Logs裡面,把Remembered Set Logs裡面的資料合并到Remembered Set中。(需要線程停頓,但可并行執行。)
  4. 篩選回收:對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃。(可并發執行)

2.33.5. JVM參數調優

指令檢視:

jmap(jmap -heap PID)

2.33.5.1. HBase jvm參數調整

參考:https://www.cnblogs.com/CtripDBA/p/14210220.html

一台region server的記憶體使用主要分成兩部分:

  1. JVM記憶體即我們通常俗稱的堆内記憶體,這塊記憶體區域的大小配置設定在HBase的環境腳本中設定,在堆内記憶體中主要有三塊記憶體區域,20%配置設定給hbase regionserver rpc請求隊列及一些其他操作,80%配置設定給memstore + blockcache
  2. java direct memory即堆外記憶體,

其中一部分記憶體用于HDFS SCR/NIO操作,另一部分用于堆外記憶體bucket cache,其記憶體大小的配置設定同樣在hbase的環境變量腳本中實作

BlockCache用于讀緩存;MemStore用于寫流程,緩存使用者寫入KeyValue資料;還有部分用于RegionServer正常RPC請求運作所必須的記憶體;

Hbase服務是基于JVM的,其中對服務可用性最大的挑戰是jvm執行full gc操作,此時會導緻jvm暫停服務,這個時候,hbase上面所有的讀寫操作将會被用戶端歸入隊列中排隊,一直等到jvm完成gc操作, 服務在遇到full gc操作時會有如下影響

hbase服務長時間暫停會導緻用戶端操作逾時,操作請求處理異常。服務端逾時會導緻region資訊上報異常丢失心跳,會被zk标記為當機,導緻regionserver即便響應恢複之後,也會因為查詢zk上自己的狀态後自殺,此時hmaster 會将該regionserver上的所有region移動到其他regionserver上。

具體參數:

  1. 新生代用并行回收器、老年代用并發回收器,另外配置了CMSInitiatingOccupancyFraction,當老年代記憶體使用率超過70%就開始執行CMS GC,減少GC時間,Master任務比較輕,一般設定4g、8g左右,具體按照群集大小評估
export HBASE_MASTER_OPTS=" -Xms8g -Xmx8g -Xmn1g
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70"
           
  1. RegionServer專有的啟動參數,RegionServer大堆,采用G1回收器,G1會把堆記憶體劃分為多個Region,對各個Region進行單獨的GC,最大限度避免Full GC及其影響

初始堆及最大堆設定為最大實體記憶體的2/3,128G/3*2 ≈80G,在某些度寫緩存比較小的叢集,可以近一步縮小。

InitiatingHeapOccupancyPercent代表了堆占用了多少比例的時候觸發MixGC,預設占用率是整個 Java 堆的45%,改參數的設定取決于IHOP > MemstoreSize%+WriteCache%+10~20%,避免過早的MixedGC中,有大量資料進來導緻Full GC

G1HeapRegionSize 堆中每個region的大小,取值範圍【1M…32M】2^n,目标是根據最小的 Java 堆大小劃分出約 2048 個區域,即heap size / G1HeapRegionSize = 2048 regions

ParallelGCThreads設定垃圾收集器并行階段的線程數量,STW階段工作的GC線程數,8+(logical processors-8)(5/8)

ConcGCThreads并發垃圾收集器使用的線程數量,非STW期間的GC線程數,可以嘗試調大些,能更快的完成GC,避免進入STW階段,但是這也使應用所占的線程數減少,會對吞吐量有一定影響

G1NewSizePercent新生代占堆的最小比例,增加新生代大小會增加GC次數,但是會減少GC的時間,建議設定5/8對應負載normal/heavy叢集

G1HeapWastePercent觸發Mixed GC的堆垃圾占比,預設值5

G1MixedGCCountTarget一個周期内觸發Mixed GC最大次數,預設值8

這兩個參數互為增加到10/16,可以有效的減少1S+ Mixed GC STW times

MaxGCPauseMillis 垃圾回收的最長暫停時間,預設200ms,如果GC時間超長,那麼會逐漸減少GC時回收的區域,以此來靠近此門檻值,一般來說,按照群集的重要性 50/80/200來設定

verbose:gc在日志中輸出GC情況

export HBASE_REGIONSERVER_OPTS="-XX:+UseG1GC
-Xms75g –Xmx75g
-XX:InitiatingHeapOccupancyPercent=83
-XX:G1HeapRegionSize=32M
-XX:ParallelGCThreads=28
-XX:ConcGCThreads=20
-XX:+UnlockExperimentalVMOptions
-XX:G1NewSizePercent=8
-XX:G1HeapWastePercent=10
-XX:MaxGCPauseMillis=80
-XX:G1MixedGCCountTarget=16
-XX:MaxTenuringThreshold=1
-XX:G1OldCSetRegionThresholdPercent=8
-XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:+PerfDisableSharedMem
-XX:-OmitStackTraceInFastThrow
-XX:+PrintFlagsFinal
-verbose:gc
-XX:+PrintGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintGCDetails
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintReferenceGC
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=100M
Xloggc:${HBASE_LOG_DIR}/gc-regionserver$(hostname)-`date +'%Y%m%d%H%M'`.log
-Dcom.sun.management.jmxremote.port=10102
$HBASE_JMX_BASE"
           

2.34. 類加載:雙親委派

參考:【JVM】淺談雙親委派和破壞雙親委派

對于任意一個類,都需要由加載它的類加載器和這個類本身來一同确立其在Java虛拟機中的唯一性。

基于上述的問題:如果不是同一個類加載器加載,即時是相同的class檔案,也會出現判斷不想同的情況,進而引發一些意想不到的情況,為了保證相同的class檔案,在使用的時候,是相同的對象,jvm設計的時候,采用了雙親委派的方式來加載類。

雙親委派:如果一個類加載器收到了加載某個類的請求,則該類加載器并不會去加載該類,而是把這個請求委派給父類加載器,每一個層次的類加載器都是如此,是以所有的類加載請求最終都會傳送到頂端的啟動類加載器;隻有當父類加載器在其搜尋範圍内無法找到所需的類,并将該結果回報給子類加載器,子類加載器會嘗試去自己加載。

2.34.1. 類加載過程

舉個通俗點的例子來說,JVM在執行某段代碼時,遇到了class A, 然而此時記憶體中并沒有class A的相關資訊,于是JVM就會到相應的class檔案中去尋找class A的類資訊,并加載進記憶體中,這就是我們所說的類加載過程。

我們進一步了解類加載器間的關系(并非指繼承關系),主要可以分為以下4點

  1. 啟動類加載器,由C++實作,沒有父類。
  2. 拓展類加載器(ExtClassLoader),由Java語言實作,父類加載器為null
  3. 系統類加載器(AppClassLoader),由Java語言實作,父類加載器為ExtClassLoader
  4. 自定義類加載器,父類加載器肯定為AppClassLoader。

在JVM中表示兩個class對象是否為同一個類對象存在兩個必要條件:類的完整類名必須一緻,包括包名。加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同。

由此可見,JVM不是一開始就把所有的類都加載進記憶體中,而是隻有第一次遇到某個需要運作的類時才會加載,且隻加載一次。類加載的過程總體上可以分為5部分。

  1. 加載

    在加載階段,JVM主要完成下面三件事:

    ①、通過一個類的全限定名來擷取定義此類的二進制位元組流。

    ②、将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。

    ③、在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些資料的通路入口。

  2. 驗證

    作用是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。

  3. 準備

    準備階段正式為類變量配置設定記憶體并設定類變量初始值的階段,這些記憶體是在方法區中配置設定的。上面說的是類變量,也就是被 static 修飾的變量,不包括執行個體變量。執行個體變量會在對象執行個體化時随着對象一起配置設定在堆中。

  4. 解析

    解析階段是虛拟機将常量池中的符号引用替換為直接引用的過程。

符号引用(Symbolic References):符号引用以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義的定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,引用的目标不一定已經加載到記憶體中。

直接引用(Direct References):直接引用可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是與虛拟機實作記憶體布局相關的,同一個符号引用在不同虛拟機執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那麼引用的目标必定已經在記憶體中存在。

  1. 初始化

    初始化階段是執行類構造器

    <clinit>()

    方法的過程。

2.35. 設計模式

總體來說設計模式分為三大類:

  1. 建立型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
  2. 結構型模式,共七種:擴充卡模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
  3. 行為型模式,共十一種:政策模式、模闆方法模式、觀察者模式、疊代子模式、責任鍊模式、指令模式、備忘錄模式、狀态模式、通路者模式、中介者模式、解釋器模式。

2.35.1. 單例模式

該模式主要目的是使記憶體中保持1個對象

2.35.1.1. 編寫單例模式

單例模式有5種,分别是懶漢式、餓漢式、雙重校驗鎖、靜态内部類和枚舉。

2.35.1.2. 雙重校驗鎖

private volatile static CSVService csvService;
    /**
     * 擷取單例 雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
     * @return
     */
    public static CSVService getInstance(){
        if (csvService == null) {
            synchronized (CSVService.class) {
                if (csvService == null) {
                    csvService = new CSVService();
                }
            }
        }
        return csvService;
    }
           

2.35.2. 工廠模式

該模式主要功能是統一提供執行個體對象的引用

public class Factor {
    public ClassDao getDao(){
        ClassDao dao = new ClassDaoImpl();
        return dao;
    }
}
           

2.35.3. 政策模式

這個模式是将行為的抽象,即當有幾個類有相似的方法,将其中通用的部分都提取出來,進而使擴充更容易

public class Addition extends Operation{
    @Override
    public float parameter(float a, float b){
        return a+b;
    }
}
           

2.35.4. 門面模式

用戶端可以調用門面角色的方法。此角色知曉相關的(一個或者多個)子系統的功能和責任。在正常情況下,門面角色會将所有從用戶端發來的請求委派到相應的子系統去。

2.35.5. 建造模式

将一個複雜的對象的建構與它的表示分離,使得同樣的建構過程可以建立不同的表示。在這樣的設計模式中,有以下幾個角色:

Builder:為建立一個産品對象的各個部件指定抽象接口。

ConcreteBuilder:實作Builder的接口以構造和裝配該産品的各個部件,定義并明确它所建立的表示,并提供一個檢索産品的接口。

Director:構造一個使用Builder接口的對象,指導建構過程。

Product:表示被構造的複雜對象。ConcreteBuilder建立該産品的内部表示并定義它的裝配過程,包含定義組成部件的類,包括将這些部件裝配成最終産品的接口。

2.35.6. 擴充卡模式

擴充卡模式(有時候也稱包裝樣式或者包裝)将一個類的接口适配成使用者所期待的。一個适配允許通常因為接口不相容而不能在一起工作的類工作在一起,做法是将類自己的接口包裹在一個已存在的類中。

2.35.7. 裝飾模式

裝飾模式是在不必改變原類檔案和使用繼承的情況下,能對現有對象的内容或功能性稍加修改。它是通過建立一個包裝對象,也就是裝飾來包裹真實的對象。

2.35.8. 代理模式(委托模式)

代理模式 : 為其他對象提供一種代理以控制對這個對象的通路。在某些情況下,一個對象不适合或者不能直接引用另一個對象,而代理對象可以在用戶端和目标對象之間起到中介的作用。目的為了去除核心對象的複雜性

2.35.9. 外觀模式

外觀模式是通過在必要的邏輯和方法的集合前建立簡單的外觀接口,隐藏調用對象的複雜性。

2.35.10. 觀察者模式

觀察者模式也做釋出訂閱模式(Publish/subscribe),在項目中常使用的模式。定義:定義對象間一種一對多的依賴關系,使得每當一個對象改變狀态,則所有依賴它的對象都會得到通知并被自動更新。

2.35.11. 指令模式(Command)

指令模式是一個高内聚的模式,其定義為:将一個請求封裝成一個對象,進而讓你使用不同的請求把用戶端參數化,對請求排隊或者記錄請求日志,可以提供指令的撤銷和恢複功能。

2.35.12. 通路者模式(Visitor)

通路者模式把資料結構和作用于結構上的操作解耦合,使得操作集合可相對自由地演化。通路者模式适用于資料結構相對穩定算法又易變化的系統。因為通路者模式使得算法操作增加變得容易。若系統資料結構對象易于變化,經常有新的資料對象增加進來,則不适合使用通路者模式。通路者模式的優點是增加操作很容易,因為增加操作意味着增加新的通路者。通路者模式将有關行為集中到一個通路者對象中,其改變不影響系統資料結構。其缺點就是增加新的資料結構很困難。