JAVA核心面試知識整理
- 目錄
- 目錄…1
- JVM…19
2.1. 執行緒 …20
2.2. JVM 記憶體區域 …21
2.2.1. 程式計數器(執行緒私有)…22
2.2.2. 虛擬機器棧(執行緒私有)…22
2.2.3. 本地方法區(執行緒私有)…23
2.2.4. 堆(Heap-執行緒共享)-執行時資料區 …23
2.2.5. 方法區/永久代(執行緒共享) …23
2.3. JVM 執行時記憶體 …24
2.3.1. 新生代 …24
2.3.1.1. Eden 區…24
2.3.1.2. ServivorFrom…24
2.3.1.3. ServivorTo …24
2.3.1.4. MinorGC 的過程(複製->清空->互換) …24
1:eden、servicorFrom 複製到 ServicorTo,年齡+1…25
2:清空 eden、servicorFrom…25 3:ServicorTo 和 ServicorFrom 互換…25
2.3.2. 老年代 …25
2.3.3. 永久代 …25
2.3.3.1. JAVA8 與後設資料…25
2.4. 垃圾回收與演算法 …26
2.4.1. 如何確定垃圾 …26
2.4.1.1. 引用計數法…26
2.4.1.2. 可達性分析…26
2.4.2. 標記清除演算法(Mark-Sweep) …27
2.4.3. 複製演算法(copying)…27
2.4.4. 標記整理演算法(Mark-Compact)…28
2.4.5. 分代收集演算法 …29
2.4.5.1. 新生代與複製演算法 …29
2.4.5.2. 老年代與標記複製演算法 …29
2.5. JAVA 四中引用型別 …30
2.5.1. 強引用 …30
2.5.2. 軟引用 …30
2.5.3. 弱引用 …30
2.5.4. 虛引用 …30
2.6. GC 分代收集演算法 VS 分割槽收集演算法…30
2.6.1. 分代收集演算法 …30
2.6.1.1. 在新生代-複製演算法…30
2.6.1.2. 在老年代-標記整理演算法…30
2.6.2. 分割槽收集演算法 …31
2.7. GC 垃圾收集器 …31
2.7.1. Serial 垃圾收集器(單執行緒、複製演算法)…31
2.7.2. ParNew 垃圾收集器(Serial+多執行緒)…31
2.7.3. Parallel Scavenge 收集器(多執行緒複製演算法、高效)…32
2.7.4. Serial Old 收集器(單執行緒標記整理演算法 ) …32
2.7.5. Parallel Old 收集器(多執行緒標記整理演算法) …33
2.7.6. CMS 收集器(多執行緒標記清除演算法)…33
2.7.6.1. 初始標記…3313/04/2018 Page 2 of 283
2.7.6.2. 併發標記…34
2.7.6.3. 重新標記…34
2.7.6.4. 併發清除…34
2.7.7. G1 收集器…34
2.8. JAVA IO/NIO…34
2.8.1. 阻塞 IO 模型 …34
2.8.2. 非阻塞 IO 模型 …35
2.8.3. 多路複用 IO 模型 …35
2.8.4. 訊號驅動 IO 模型 …36
2.8.5. 非同步 IO 模型 …36
2.8.1. JAVA IO 包…36
2.8.2. JAVA NIO…37
2.8.2.1. NIO 的緩衝區 …38
2.8.2.2. NIO 的非阻塞 …38
2.8.3. Channel …40
2.8.4. Buffer…40
2.8.5. Selector…40
2.9. JVM 類載入機制 …41
2.9.1.1. 載入 …41
2.9.1.2. 驗證 …41
2.9.1.3. 準備 …41
2.9.1.4. 解析 …41
2.9.1.5. 符號引用…42
2.9.1.6. 直接引用…42
2.9.1.7. 初始化 …42
2.9.1.8. 類構造器…42
2.9.2. 類載入器 …42
2.9.2.1. 啟動類載入器(Bootstrap ClassLoader) …43
2.9.2.2. 擴充套件類載入器(Extension ClassLoader)…43
2.9.2.3. 應用程式類載入器(Application ClassLoader): …43
2.9.3. 雙親委派 …43
2.9.4. OSGI(動態模型系統) …44
2.9.4.1. 動態改變構造…44
2.9.4.2. 模組化程式設計與熱插拔 …44 - JAVA 集合…45
3.1. 介面繼承關係和實現 …45
3.2. LIST…47
3.2.1. ArrayList(陣列)…47
3.2.2. Vector(陣列實現、執行緒同步)…47
3.2.3. LinkList(連結串列)…47
3.3. SET …48
3.3.1.1. HashSet(Hash 表)…48
3.3.1.2. TreeSet(二叉樹) …49
3.3.1.3. LinkHashSet(HashSet+LinkedHashMap)…49
3.4. MAP…50
3.4.1. HashMap(陣列+連結串列+紅黑樹)…50
3.4.1.1. JAVA7 實現 …50
3.4.1.2. JAVA8 實現 …51
3.4.2. ConcurrentHashMap…51
3.4.2.1. Segment 段…51
3.4.2.2. 執行緒安全(Segment 繼承 ReentrantLock 加鎖)…51
3.4.2.3. 並行度(預設 16) …52
3.4.2.4. Java8 實現 (引入了紅黑樹) …5213/04/2018 Page 3 of 283
3.4.3. HashTable(執行緒安全) …53
3.4.4. TreeMap(可排序) …53
3.4.5. LinkHashMap(記錄插入順序)…53 - JAVA 多執行緒併發…54
4.1.1. JAVA 併發知識庫 …54
4.1.2. JAVA 執行緒實現/建立方式 …54
4.1.2.1. 繼承 Thread 類…54
4.1.2.2. 實現 Runnable 介面。…54
4.1.2.3. ExecutorService、Callable、Future 有返回值執行緒…55
4.1.2.4. 基於執行緒池的方式…56
4.1.3. 4 種執行緒池 …56
4.1.3.1. newCachedThreadPool…57
4.1.3.2. newFixedThreadPool…57
4.1.3.3. newScheduledThreadPool…58
4.1.3.4. newSingleThreadExecutor …58
4.1.4. 執行緒生命週期(狀態)…58
4.1.4.1. 新建狀態(NEW) …58
4.1.4.2. 就緒狀態(RUNNABLE):…59
4.1.4.3. 執行狀態(RUNNING): …59
4.1.4.4. 阻塞狀態(BLOCKED):…59
等待阻塞(o.wait->等待對列):…59
同步阻塞(lock->鎖池) …59
其他阻塞(sleep/join) …59
4.1.4.5. 執行緒死亡(DEAD)…59
正常結束…59
異常結束…59
呼叫 stop…59
4.1.5. 終止執行緒 4 種方式 …60
4.1.5.1. 正常執行結束…60
4.1.5.2. 使用退出標誌退出執行緒…60
4.1.5.3. Interrupt 方法結束執行緒…60
4.1.5.4. stop 方法終止執行緒(執行緒不安全)…61
4.1.6. sleep 與 wait 區別…61
4.1.7. start 與 run 區別…62
4.1.8. JAVA 後臺執行緒 …62
4.1.9. JAVA 鎖 …63
4.1.9.1. 樂觀鎖 …63
4.1.9.2. 悲觀鎖 …63
4.1.9.3. 自旋鎖 …63
自旋鎖的優缺點…63
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)…63
自旋鎖的開啟…64
4.1.9.4. Synchronized 同步鎖…64
Synchronized 作用範圍…64
Synchronized 核心元件…64
Synchronized 實現…64
4.1.9.5. ReentrantLock…66
Lock 介面的主要方法…66
非公平鎖…66
公平鎖…67
ReentrantLock 與 synchronized …67
ReentrantLock 實現…67
Condition 類和 Object 類鎖方法區別區別…68
tryLock 和 lock 和 lockInterruptibly 的區別…68
4.1.9.6. Semaphore 訊號量…68
實現互斥鎖(計數器為 1)…68
程式碼實現…68
Semaphore 與 ReentrantLock …69
4.1.9.7. AtomicInteger…6913/04/2018 Page 4 of 283
4.1.9.8. 可重入鎖(遞迴鎖)…69
4.1.9.9. 公平鎖與非公平鎖…70
公平鎖(Fair)…70
非公平鎖(Nonfair)…70
4.1.9.10. ReadWriteLock 讀寫鎖…70
讀鎖…70
寫鎖…70
4.1.9.11. 共享鎖和獨佔鎖 …70
獨佔鎖…70
共享鎖…70
4.1.9.12. 重量級鎖(Mutex Lock) …71
4.1.9.13. 輕量級鎖…71
鎖升級…71
4.1.9.14. 偏向鎖…71
4.1.9.15. 分段鎖…71
4.1.9.16. 鎖優化…71
減少鎖持有時間…72
減小鎖粒度…72
鎖分離…72
鎖粗化…72
鎖消除…72
4.1.10. 執行緒基本方法 …72
4.1.10.1. 執行緒等待(wait)…73
4.1.10.2. 執行緒睡眠(sleep)…73
4.1.10.3. 執行緒讓步(yield)…73
4.1.10.4. 執行緒中斷(interrupt)…73
4.1.10.5. Join 等待其他執行緒終止…74
4.1.10.6. 為什麼要用 join()方法? …74
4.1.10.7. 執行緒喚醒(notify)…74
4.1.10.8. 其他方法:…74
4.1.11. 執行緒上下文切換 …75
4.1.11.1. 程式…75
4.1.11.2. 上下文…75
4.1.11.3. 暫存器…75
4.1.11.4. 程式計數器…75
4.1.11.5. PCB-“切換楨”…75
4.1.11.6. 上下文切換的活動: …76
4.1.11.7. 引起執行緒上下文切換的原因 …76
4.1.12. 同步鎖與死鎖 …76
4.1.12.1. 同步鎖…76
4.1.12.2. 死鎖…76
4.1.13. 執行緒池原理 …76
4.1.13.1. 執行緒複用…76
4.1.13.2. 執行緒池的組成…76
4.1.13.3. 拒絕策略…78
4.1.13.4. Java 執行緒池工作過程 …78
4.1.14. JAVA 阻塞佇列原理…79
4.1.14.1. 阻塞佇列的主要方法 …80
插入操作:…80
獲取資料操作:…81
4.1.14.2. Java 中的阻塞佇列 …81
4.1.14.3. ArrayBlockingQueue(公平、非公平) …82
4.1.14.4. LinkedBlockingQueue(兩個獨立鎖提高併發) …82
4.1.14.5. PriorityBlockingQueue(compareTo 排序實現優先)…82
4.1.14.6. DelayQueue(快取失效、定時任務 )…82
4.1.14.7. SynchronousQueue(不儲存資料、可用於傳遞資料)…83
4.1.14.8. LinkedTransferQueue…8313/04/2018 Page 5 of 283
4.1.14.9. LinkedBlockingDeque…83
4.1.15. CyclicBarrier、CountDownLatch、Semaphore 的用法…84
4.1.15.1. CountDownLatch(執行緒計數器 ) …84
4.1.15.2. CyclicBarrier(迴環柵欄-等待至 barrier 狀態再全部同時執行) …84
4.1.15.3. Semaphore(訊號量-控制同時訪問的執行緒個數) …85
4.1.16. volatile 關鍵字的作用(變數可見性、禁止重排序) …87
變數可見性…87
禁止重排序…87
比 sychronized 更輕量級的同步鎖…87
適用場景…87
4.1.17. 如何在兩個執行緒之間共享資料…88
將資料抽象成一個類,並將資料的操作作為這個類的方法…88
Runnable 物件作為一個類的內部類 …89
4.1.18. ThreadLocal 作用(執行緒本地儲存)…90
ThreadLocalMap(執行緒的一個屬性) …90
使用場景…91
4.1.19. synchronized 和 ReentrantLock 的區別 …91
4.1.19.1. 兩者的共同點: …91
4.1.19.2. 兩者的不同點: …92
4.1.20. ConcurrentHashMap 併發…92
4.1.20.1. 減小鎖粒度…92
4.1.20.2. ConcurrentHashMap 分段鎖…92
ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成…93
4.1.21. Java 中用到的執行緒排程 …93
4.1.21.1. 搶佔式排程:…93
4.1.21.2. 協同式排程:…93
4.1.21.3. JVM 的執行緒排程實現(搶佔式排程) …94
4.1.21.4. 執行緒讓出 cpu 的情況: …94
4.1.22. 程式排程演算法 …94
4.1.22.1. 優先排程演算法…94
4.1.22.2. 高優先權優先排程演算法 …95
4.1.22.3. 基於時間片的輪轉排程演算法 …96
4.1.23. 什麼是 CAS(比較並交換-樂觀鎖機制-鎖自旋) …96
4.1.23.1. 概念及特性…96
4.1.23.2. 原子包 java.util.concurrent.atomic(鎖自旋) …97
4.1.23.3. ABA 問題…98
4.1.24. 什麼是 AQS(抽象的佇列同步器)…98
Exclusive 獨佔資源-ReentrantLock …99
Share 共享資源-Semaphore/CountDownLatch …99
同步器的實現是 ABS 核心(state 資源狀態計數)…100
ReentrantReadWriteLock 實現獨佔和共享兩種方式…100 - JAVA 基礎…101
5.1.1. JAVA 異常分類及處理…101
5.1.1.1. 概念…101
5.1.1.2. 異常分類…101
Error…101
Exception(RuntimeException、CheckedException) …101
5.1.1.3. 異常的處理方式 …102
遇到問題不進行具體處理,而是繼續拋給呼叫者 (throw,throws)…102
try catch 捕獲異常針對性處理方式…102
5.1.1.4. Throw 和 throws 的區別: …10213/04/2018 Page 6 of 283
位置不同…102
功能不同:…102
5.1.2. JAVA 反射 …103
5.1.2.1. 動態語言…103
5.1.2.2. 反射機制概念 (執行狀態中知道類所有的屬性和方法)…103
5.1.2.3. 反射的應用場合 …103
編譯時型別和執行時型別…103
的編譯時型別無法獲取具體方法…104
5.1.2.4. Java 反射 API…104
反射 API 用來生成 JVM 中的類、介面或則物件的資訊。 …104
5.1.2.5. 反射使用步驟(獲取 Class 物件、呼叫物件方法) …104
5.1.2.6. 獲取 Class 物件的 3 種方法 …104
呼叫某個物件的 getClass()方法…104
呼叫某個類的 class 屬性來獲取該類對應的 Class 物件…104
使用 Class 類中的 forName()靜態方法(最安全/效能最好)…104
5.1.2.7. 建立物件的兩種方法 …105
Class 物件的 newInstance()…105
呼叫 Constructor 物件的 newInstance()…105
5.1.3. JAVA 註解 …106
5.1.3.1. 概念 …106
5.1.3.2. 4 種標準元註解…106
@Target 修飾的物件範圍 …106
@Retention 定義 被保留的時間長短…106
@Documented ᧿述-javadoc…106
@Inherited 闡述了某個被標註的型別是被繼承的 …106
5.1.3.3. 註解處理器…107
5.1.4. JAVA 內部類 …109
5.1.4.1. 靜態內部類…109
5.1.4.2. 成員內部類…110
5.1.4.3. 區域性內部類(定義在方法中的類)…110
5.1.4.4. 匿名內部類(要繼承一個父類或者實現一個介面、直接使用 new 來生成一個物件的引用)…111
5.1.5. JAVA 泛型 …112
5.1.5.1. 泛型方法()…112
5.1.5.2. 泛型類 …112
5.1.5.3. 型別萬用字元? …113
5.1.5.4. 型別擦除 …113
5.1.6. JAVA 序列化(建立可複用的 Java 物件)…113
儲存(持久化)物件及其狀態到記憶體或者磁碟…113
序列化物件以位元組陣列保持-靜態成員不儲存…113
序列化使用者遠端物件傳輸…113
Serializable 實現序列化 …113
ObjectOutputStream 和 ObjectInputStream 對物件進行序列化及反序列化…113
writeObject 和 readObject 自定義序列化策略…113
序列化 ID…113
序列化並不儲存靜態變數…114
序列化子父類說明…114
Transient 關鍵字阻止該變數被序列化到檔案中…114
5.1.7. JAVA 複製 …114
5.1.7.1. 直接賦值複製…114
5.1.7.2. 淺複製(複製引用但不復制引用的物件)…114
5.1.7.3. 深複製(複製物件和其應用物件)…115
5.1.7.4. 序列化(深 clone 一中實現)…115 - SPRING 原理 …116
6.1.1. Spring 特點…116
6.1.1.1. 輕量級…11613/04/2018 Page 7 of 283
6.1.1.2. 控制反轉…116
6.1.1.3. 面向切面…116
6.1.1.4. 容器…116
6.1.1.5. 框架集合…116
6.1.2. Spring 核心元件…117
6.1.3. Spring 常用模組…117
6.1.4. Spring 主要包…118
6.1.5. Spring 常用註解…118
6.1.6. Spring 第三方結合…119
6.1.7. Spring IOC 原理…120
6.1.7.1. 概念 …120
6.1.7.2. Spring 容器高層檢視 …120
6.1.7.3. IOC 容器實現…120
BeanFactory-框架基礎設施…120
1.1…1.1.1 BeanDefinitionRegistry 登錄檔…121
1.1…1.1.2 BeanFactory 頂層介面…121
1.1…1.1.3 ListableBeanFactory …121
1.1…1.1.4 HierarchicalBeanFactory 父子級聯…121
1.1…1.1.5 ConfigurableBeanFactory…121
1.1…1.1.6 AutowireCapableBeanFactory 自動裝配 …122
1.1…1.1.7 SingletonBeanRegistry 執行期間註冊單例 Bean…122
1.1…1.1.8 依賴日誌框框…122
ApplicationContext 面向開發應用…122
WebApplication 體系架構 …123
6.1.7.4. Spring Bean 作用域…123
singleton:單例模式(多執行緒下不安全) …123
prototype:原型模式每次使用時建立 …124
Request:一次 request 一個例項…124
session…124
global Session…124
6.1.7.5. Spring Bean 生命週期…124
例項化…124
IOC 依賴注入…124
setBeanName 實現…124
BeanFactoryAware 實現…124
ApplicationContextAware 實現…125
postProcessBeforeInitialization 介面實現-初始化預處理…125
init-method…125
postProcessAfterInitialization…125
Destroy 過期自動清理階段 …125
destroy-method 自配置清理 …125
6.1.7.6. Spring 依賴注入四種方式…126
構造器注入…126
setter 方法注入…127
靜態工廠注入…127
例項工廠…127
6.1.7.7. 5 種不同方式的自動裝配…128
6.1.8. Spring APO 原理 …129
6.1.8.1. 概念 …129
6.1.8.2. AOP 核心概念 …129
6.1.8.1. AOP 兩種代理方式 …130
JDK 動態介面代理 …130
CGLib 動態代理…131
6.1.8.2. 實現原理 …131
6.1.9. Spring MVC 原理…132
6.1.9.1. MVC 流程…132
Http 請求到 DispatcherServlet …133
HandlerMapping 尋找處理器…133
呼叫處理器 Controller…13313/04/2018 Page 8 of 283
Controller 呼叫業務邏輯處理後,返回 ModelAndView…133
DispatcherServlet 查詢 ModelAndView …133
ModelAndView 反饋瀏覽器 HTTP …133
6.1.9.1. MVC 常用註解…133
6.1.10. Spring Boot 原理…134 - 建立獨立的 Spring 應用程式…134
- 嵌入的 Tomcat,無需部署 WAR 檔案…134
- 簡化 Maven 配置…134
- 自動配置 Spring …134
- ᨀ供生產就緒型功能,如指標,健康檢查和外部配置…134
- 絕對沒有程式碼生成和對 XML 沒有要求配置 [1]…134
6.1.11. JPA 原理…134
6.1.11.1. 事務…134
6.1.11.2. 本地事務…134
6.1.11.1. 分散式事務…135
6.1.11.1. 兩階段ᨀ交…136
1 準備階段…136
2 ᨀ交階段:…136
6.1.12. Mybatis 快取…137
6.1.12.1. Mybatis 的一級快取原理(sqlsession 級別)…138
6.1.12.2. 二級快取原理(mapper 基本)…138
具體使用需要配置:…139
6.1.13. Tomcat 架構…139 - 微服務 …140
7.1.1. 服務註冊發現 …140
7.1.1.1. 客戶端註冊(zookeeper)…140
7.1.1.2. 第三方註冊(獨立的服務 Registrar)…140
7.1.1.3. 客戶端發現…141
7.1.1.4. 服務端發現…142
7.1.1.5. Consul…142
7.1.1.6. Eureka…142
7.1.1.7. SmartStack…142
7.1.1.8. Etcd …142
7.1.2. API 閘道器…142
7.1.2.1. 請求轉發 …143
7.1.2.2. 響應合併 …143
7.1.2.3. 協議轉換 …143
7.1.2.4. 資料轉換 …143
7.1.2.5. 安全認證 …144
7.1.3. 配置中心 …144
7.1.3.1. zookeeper 配置中心…144
7.1.3.2. 配置中心資料分類…144
7.1.4. 事件排程(kafka)…144
7.1.5. 服務跟蹤(starter-sleuth)…144
7.1.6. 服務熔斷(Hystrix) …145
7.1.6.1. Hystrix 斷路器機制…146
7.1.7. API 管理…146 - NETTY 與 RPC …147
8.1.1. Netty 原理…147
8.1.2. Netty 高效能…147
8.1.2.1. 多路複用通訊方式 …147
8.1.2.1. 非同步通訊 NIO…148
8.1.2.2. 零拷貝(DIRECT BUFFERS 使用堆外直接記憶體)…149
8.1.2.3. 記憶體池(基於記憶體池的緩衝區重用機制) …149
8.1.2.4. 高效的 Reactor 執行緒模型…149
Reactor 單執行緒模型…149
Reactor 多執行緒模型…15013/04/2018 Page 9 of 283
主從 Reactor 多執行緒模型…150
8.1.2.5. 無鎖設計、執行緒繫結…151
8.1.2.6. 高效能的序列化框架…151
小包封大包,防止網路阻塞…152
軟中斷 Hash 值和 CPU 繫結…152
8.1.3. Netty RPC 實現…152
8.1.3.1. 概念 …152
8.1.3.2. 關鍵技術 …152
8.1.3.3. 核心流程 …152
8.1.3.1. 訊息編解碼…153
息資料結構(介面名稱+方法名+引數型別和引數值+超時時間+ requestID) …153
序列化…154
8.1.3.1. 通訊過程 …154
核心問題(執行緒暫停、訊息亂序) …154
通訊流程…154
requestID 生成-AtomicLong …154
存放回撥物件 callback 到全域性 ConcurrentHashMap …154
synchronized 獲取回撥物件 callback 的鎖並自旋 wait…154
監聽訊息的執行緒收到訊息,找到 callback 上的鎖並喚醒 …155
8.1.4. RMI 實現方式…155
8.1.4.1. 實現步驟 …155
8.1.5. Protoclol Buffer…156
8.1.5.1. 特點 …157
8.1.6. Thrift …157 - 網路…159
9.1.1. 網路 7 層架構 …159
9.1.2. TCP/IP 原理…160
9.1.2.1. 網路訪問層(Network Access Layer)…160
9.1.2.2. 網路層(Internet Layer) …160
9.1.2.3. 傳輸層(Tramsport Layer-TCP/UDP) …160
9.1.2.4. 應用層(Application Layer)…160
9.1.3. TCP 三次握手/四次揮手 …161
9.1.3.1. 資料包說明…161
9.1.3.2. 三次握手 …162
9.1.3.3. 四次揮手 …163
9.1.4. HTTP 原理…164
9.1.4.1. 傳輸流程 …164
1:地址解析 …164
2:封裝 HTTP 請求資料包 …165
3:封裝成 TCP 包並建立連線…165
4:客戶機傳送請求命…165
5:伺服器響應…165
6:伺服器關閉 TCP 連線…165
9.1.4.2. HTTP 狀態 …165
9.1.4.3. HTTPS …166
建立連線獲取證照…167
證照驗證…167
資料加密和傳輸…167
9.1.5. CDN 原理…167
9.1.5.1. 分發服務系統…167
9.1.5.2. 負載均衡系統:…168
9.1.5.3. 管理系統:…168 - 日誌 …169
10.1.1. Slf4j …169
10.1.2. Log4j …169
10.1.3. LogBack…169
10.1.3.1. Logback 優點…169
10.1.4. ELK…17013/04/2018 Page 10 of 283 - ZOOKEEPER …171
11.1.1. Zookeeper 概念 …171
11.1.1. Zookeeper 角色 …171
11.1.1.1. Leader…171
11.1.1.2. Follower…171
11.1.1.3. Observer…171
11.1.1.1. ZAB 協議 …172
事務編號 Zxid(事務請求計數器+ epoch) …172
epoch…172
Zab 協議有兩種模式-恢復模式(選主)、廣播模式(同步)…172
ZAB 協議 4 階段…172
Leader election(選舉階段-選出準 Leader) …172
Discovery(發現階段-接受ᨀ議、生成 epoch、接受 epoch)…173
Synchronization(同步階段-同步 follower 副本)…173
Broadcast(廣播階段-leader 訊息廣播)…173
ZAB 協議 JAVA 實現(FLE-發現階段和同步合併為 Recovery Phase(恢復階段))…173
11.1.1.2. 投票機制…173
11.1.2. Zookeeper 工作原理(原子廣播)…174
11.1.3. Znode 有四種形式的目錄節點 …174 - KAFKA…175
12.1.1. Kafka 概念 …175
12.1.2. Kafka 資料儲存設計 …175
12.1.2.1. partition 的資料檔案(offset,MessageSize,data)…175
12.1.2.2. 資料檔案分段 segment(順序讀寫、分段命令、二分查詢)…176
12.1.2.3. 資料檔案索引(分段索引、稀疏儲存)…176
12.1.3. 生產者設計 …176
12.1.3.1. 負載均衡(partition 會均衡分佈到不同 broker 上)…176
12.1.3.2. 批量傳送…177
12.1.3.3. 壓縮(GZIP 或 Snappy)…177
12.1.1. 消費者設計 …177
12.1.1.1. Consumer Group …178 - RABBITMQ …179
13.1.1. 概念 …179
13.1.2. RabbitMQ 架構 …179
13.1.2.1. Message …180
13.1.2.2. Publisher …180
13.1.2.3. Exchange(將訊息路由給佇列 ) …180
13.1.2.4. Binding(訊息佇列和交換器之間的關聯)…180
13.1.2.5. Queue …180
13.1.2.6. Connection…180
13.1.2.7. Channel …180
13.1.2.8. Consumer…180
13.1.2.9. Virtual Host …180
13.1.2.10. Broker…181
13.1.3. Exchange 型別 …181
13.1.3.1. Direct 鍵(routing key)分佈: …181
13.1.3.2. Fanout(廣播分發)…181
13.1.3.3. topic 交換器(模式匹配) …18213/04/2018 Page 11 of 283 - HBASE…183
14.1.1. 概念 …183
14.1.2. 列式儲存 …183
14.1.3. Hbase 核心概念…184
14.1.3.1. Column Family 列族…184
14.1.3.2. Rowkey(Rowkey 查詢,Rowkey 範圍掃描,全表掃描)…184
14.1.3.3. Region 分割槽…184
14.1.3.4. TimeStamp 多版本…184
14.1.4. Hbase 核心架構…184
14.1.4.1. Client:…185
14.1.4.2. Zookeeper:…185
14.1.4.3. Hmaster…185
14.1.4.4. HregionServer…185
14.1.4.5. Region 定址方式(通過 zookeeper .META)…186
14.1.4.6. HDFS …186
14.1.5. Hbase 的寫邏輯…187
14.1.5.1. Hbase 的寫入流程 …187
獲取 RegionServer …187
請求寫 Hlog …187
請求寫 MemStore …187
14.1.5.2. MemStore 刷盤…187
全域性記憶體控制…188
MemStore 達到上限…188
RegionServer 的 Hlog 數量達到上限…188
手工觸發…188
關閉 RegionServer 觸發…188
Region 使用 HLOG 恢復完資料後觸發…188
14.1.6. HBase vs Cassandra…188 - MONGODB…190
15.1.1. 概念 …190
15.1.2. 特點 …190 - CASSANDRA…192
16.1.1. 概念 …192
16.1.2. 資料模型 …192
Key Space(對應 SQL 資料庫中的 database)…192
Key(對應 SQL 資料庫中的主鍵)…192
column(對應 SQL 資料庫中的列)…192
super column(SQL 資料庫不支援)…192
Standard Column Family(相對應 SQL 資料庫中的 table)…192
Super Column Family(SQL 資料庫不支援) …192
16.1.3. Cassandra 一致 Hash 和虛擬節點 …192
一致性 Hash(多米諾 down 機)…192
虛擬節點(down 機多節點託管)…193
16.1.4. Gossip 協議…193
Gossip 節點的通訊方式及收斂性 …194
Gossip 兩個節點(A、B)之間存在三種通訊方式(push、pull、push&pull)…194
gossip 的協議和 seed list(防止叢集分列) …194
16.1.5. 資料複製 …194
Partitioners(計算 primary key token 的 hash 函式)…194
兩種可用的複製策略: …194
SimpleStrategy:僅用於單資料中心,…194
將第一個 replica 放在由 partitioner 確定的節點中,其餘的 replicas 放在上述節點順時針方向的後續節
點中。…19413/04/2018 Page 12 of 283
NetworkTopologyStrategy:可用於較複雜的多資料中心。…194
可以指定在每個資料中心分別儲存多少份 replicas。…194
16.1.6. 資料寫請求和協調者 …195
協調者(coordinator)…195
16.1.7. 資料讀請求和後臺修復 …195
16.1.8. 資料儲存(CommitLog、MemTable、SSTable)…196
SSTable 檔案構成(BloomFilter、index、data、static)…196
16.1.9. 二級索引(對要索引的 value 摘要,生成 RowKey)…196
16.1.10. 資料讀寫 …197
資料寫入和更新(資料追加) …197
資料的寫和刪除效率極高…197
錯誤恢復簡單…197
讀的複雜度高…197
資料刪除(column 的墓碑) …197
墓碑…198
垃圾回收 compaction …198
資料讀取(memtable+SStables)…198
行快取和鍵快取請求流程圖 …199
Row Cache(SSTables 中頻繁被訪問的資料)…199
Bloom Filter(查詢資料可能對應的 SSTable)…200
Partition Key Cache(查詢資料可能對應的 Partition key) …200
Partition Summary(記憶體中儲存一些 partition index 的樣本)…200
Partition Index(磁碟中) …200
Compression offset map(磁碟中)…200 - 設計模式…201
17.1.1. 設計原則 …201
17.1.2. 工廠方法模式 …201
17.1.3. 抽象工廠模式 …201
17.1.4. 單例模式 …201
17.1.5. 建造者模式 …201
17.1.6. 原型模式 …201
17.1.7. 介面卡模式 …201
17.1.8. 裝飾器模式 …201
17.1.9. 代理模式 …201
17.1.10. 外觀模式 …201
17.1.11. 橋接模式 …201
17.1.12. 組合模式 …201
17.1.13. 享元模式 …201
17.1.14. 策略模式 …201
17.1.15. 模板方法模式 …201
17.1.16. 觀察者模式 …201
17.1.17. 迭代子模式 …201
17.1.18. 責任鏈模式 …201
17.1.19. 命令模式 …201
17.1.20. 備忘錄模式 …201
17.1.21. 狀態模式 …202
17.1.22. 訪問者模式 …202
17.1.23. 中介者模式 …202
17.1.24. 直譯器模式 …202 - 負載均衡…203
18.1.1. 四層負載均衡 vs 七層負載均衡 …203
18.1.1.1. 四層負載均衡(目標地址和埠交換)…203
F5:硬體負載均衡器,功能很好,但是成本很高。…203
lvs:重量級的四層負載軟體。 …203
nginx:輕量級的四層負載軟體,帶快取功能,正規表示式較靈活。 …20313/04/2018 Page 13 of 283
haproxy:模擬四層轉發,較靈活。…203
18.1.1.2. 七層負載均衡(內容交換) …203
haproxy:天生負載均衡技能,全面支援七層代理,會話保持,標記,路徑轉移;…204
nginx:只在 http 協議和 mail 協議上功能比較好,效能與 haproxy 差不多;…204
apache:功能較差…204
Mysql proxy:功能尚可。…204
18.1.2. 負載均衡演算法/策略 …204
18.1.2.1. 輪循均衡(Round Robin) …204
18.1.2.2. 權重輪循均衡(Weighted Round Robin)…204
18.1.2.3. 隨機均衡(Random) …204
18.1.2.4. 權重隨機均衡(Weighted Random)…204
18.1.2.5. 響應速度均衡(Response Time 探測時間)…204
18.1.2.6. 最少連線數均衡(Least Connection)…205
18.1.2.7. 處理能力均衡(CPU、記憶體) …205
18.1.2.8. DNS 響應均衡(Flash DNS) …205
18.1.2.9. 雜湊演算法…205
18.1.2.10. IP 地址雜湊(保證客戶端伺服器對應關係穩定)…205
18.1.2.11. URL 雜湊…205
18.1.3. LVS…206
18.1.3.1. LVS 原理…206
IPVS …206
18.1.3.1. LVS NAT 模式 …207
18.1.3.2. LVS DR 模式(區域網改寫 mac 地址)…208
18.1.3.3. LVS TUN 模式(IP 封裝、跨網段)…209
18.1.3.4. LVS FULLNAT 模式…210
18.1.4. Keepalive…211
18.1.5. Nginx 反向代理負載均衡 …211
18.1.5.1. upstream_module 和健康檢測…212
18.1.5.1. proxy_pass 請求轉發…212
18.1.6. HAProxy …213 - 資料庫 …214
19.1.1. 儲存引擎 …214
19.1.1.1. 概念…214
19.1.1.2. InnoDB(B+樹) …214
19.1.1.3. TokuDB(Fractal Tree-節點帶資料) …215
19.1.1.4. MyIASM…215
19.1.1.5. Memory…215
19.1.2. 索引 …215
19.1.2.1. 常見索引原則有 …216
1.選擇唯一性索引 …216
2.為經常需要排序、分組和聯合操作的欄位建立索引: …216
3.為常作為查詢條件的欄位建立索引。…216
4.限制索引的數目:…216
儘量使用資料量少的索引…216
儘量使用字首來索引…216
7.刪除不再使用或者很少使用的索引…216
8 . 最左字首匹配原則,非常重要的原則。…216
10 . 儘量選擇區分度高的列作為索引 …216
11 .索引列不能參與計算,保持列“乾淨”:帶函式的查詢不參與索引。 …216
12 .儘量的擴充套件索引,不要新建索引。…216
19.1.3. 資料庫三正規化 …216
19.1.3.1. 第一正規化(1st NF -列都是不可再分)…216
19.1.3.2. 第二正規化(2nd NF-每個表只描述一件事情)…216
19.1.3.3. 第三正規化(3rd NF- 不存在對非主鍵列的傳遞依賴)…217
19.1.4. 資料庫是事務 …21713/04/2018 Page 14 of 283
原子性(Atomicity)…217
一致性(Consistency)…217
隔離性(Isolation)…218
永久性(Durability) …218
19.1.5. 儲存過程(特定功能的 SQL 語句集)…218
儲存過程優化思路: …218
19.1.6. 觸發器(一段能自動執行的程式)…218
19.1.7. 資料庫併發策略 …218
19.1.7.1. 樂觀鎖…218
19.1.7.2. 悲觀鎖…219
19.1.7.3. 時間戳…219
19.1.8. 資料庫鎖 …219
19.1.8.1. 行級鎖…219
19.1.8.2. 表級鎖…219
19.1.8.1. 頁級鎖…219
19.1.9. 基於 Redis 分散式鎖 …219
19.1.10. 分割槽分表 …220
垂直切分(按照功能模組) …220
水平切分(按照規則劃分儲存) …220
19.1.11. 兩階段ᨀ交協議 …220
19.1.11.1. 準備階段…221
19.1.11.2. ᨀ交階段…221
19.1.11.3. 缺點…221
同步阻塞問題…221
單點故障…221
資料不一致(腦裂問題)…221
二階段無法解決的問題(資料狀態不確定) …221
19.1.12. 三階段ᨀ交協議 …222
19.1.12.1. CanCommit 階段 …222
19.1.12.2. PreCommit 階段 …222
19.1.12.3. doCommit 階段…222
19.1.13. 柔性事務 …222
19.1.13.1. 柔性事務…222
兩階段型…222
補償型…222
非同步確保型…223
最大努力通知型(多次嘗試)…223
19.1.14. CAP…224
一致性(C): …224
可用性(A): …224
分割槽容忍性(P):…224 - 一致性演算法…225
20.1.1. Paxos …225
Paxos 三種角色:Proposer,Acceptor,Learners …225
Proposer: …225
Acceptor:…225
Learner:…225
Paxos 演算法分為兩個階段。具體如下:…225
階段一(準 leader 確定 ):…225
階段二(leader 確認):…225
20.1.2. Zab …225
1.崩潰恢復:主要就是 Leader 選舉過程…226
2.資料同步:Leader 伺服器與其他伺服器進行資料同步…226
3.訊息廣播:Leader 伺服器將資料傳送給其他伺服器…226
20.1.3. Raft…226
20.1.3.1. 角色…226
Leader(領導者-日誌管理) …226
Follower(追隨者-日誌同步)…226
Candidate(候選者-負責選票)…22613/04/2018 Page 15 of 283
20.1.3.2. Term(任期)…226
20.1.3.3. 選舉(Election)…227
選舉定時器…227
20.1.3.4. 安全性(Safety)…227
20.1.3.5. raft 協議和 zab 協議區別 …227
20.1.4. NWR…228
N:在分散式儲存系統中,有多少份備份資料…228
W:代表一次成功的更新操作要求至少有 w 份資料寫入成功 …228
R: 代表一次成功的讀資料操作要求至少有 R 份資料成功讀取…228
20.1.5. Gossip…228
20.1.6. 一致性 Hash…229
20.1.6.1. 一致性 Hash 特性…229
20.1.6.2. 一致性 Hash 原理…229
1.建構環形 hash 空間:…229
2.把需要快取的內容(物件)對映到 hash 空間…229
3.把伺服器(節點)對映到 hash 空間 …229
4.把物件對映到服務節點…229
考察 cache 的變動…230
虛擬節點…230 - JAVA 演算法 …232
21.1.1. 二分查詢 …232
21.1.2. 氣泡排序演算法 …232
21.1.3. 插入排序演算法 …233
21.1.4. 快速排序演算法 …234
21.1.1. 希爾排序演算法 …236
21.1.2. 歸併排序演算法 …237
21.1.3. 桶排序演算法 …240
21.1.4. 基數排序演算法 …241
21.1.5. 剪枝演算法 …243
21.1.6. 回溯演算法 …243
21.1.7. 最短路徑演算法 …243
21.1.8. 最大子陣列演算法 …243
21.1.9. 最長公共子序演算法 …243
21.1.10. 最小生成樹演算法 …243 - 資料結構…245
22.1.1. 棧(stack)…245
22.1.2. 佇列(queue) …245
22.1.3. 連結串列(Link)…245
22.1.4. 雜湊表(Hash Table)…246
22.1.5. 排序二叉樹 …246
22.1.5.1. 插入操作…246
22.1.5.2. 刪除操作…247
22.1.5.3. 查詢操作…248
22.1.6. 紅黑樹 …248
22.1.6.1. 紅黑樹的特性…248
22.1.6.1. 左旋…248
22.1.6.1. 右旋…249
22.1.6.1. 新增…250
22.1.6.2. 刪除…251
22.1.7. B-TREE…252
22.1.8. 點陣圖 …254 - 加密演算法…255
23.1.1. AES …255
23.1.2. RSA…255
23.1.3. CRC…256
23.1.4. MD5…25613/04/2018 Page 16 of 283 - 分散式快取…257
24.1.1. 快取雪崩 …257
24.1.2. 快取穿透 …257
24.1.3. 快取預熱 …257
24.1.4. 快取更新 …257
24.1.5. 快取降級 …257 - HADOOP …259
25.1.1. 概念 …259
25.1.2. HDFS …259
25.1.2.1. Client…259
25.1.2.2. NameNode…259
25.1.2.3. Secondary NameNode …259
25.1.2.4. DataNode…259
25.1.3. MapReduce…260
25.1.3.1. Client…260
25.1.3.2. JobTracker …260
25.1.3.3. TaskTracker…261
25.1.3.4. Task …261
25.1.3.5. Reduce Task 執行過程 …261
25.1.4. Hadoop MapReduce 作業的生命週期…262
1.作業ᨀ交與初始化…262
2.任務排程與監控。…262
3.任務執行環境準備…262
4.任務執行 …262
5.作業完成。 …262 - SPARK…263
26.1.1. 概念 …263
26.1.2. 核心架構 …263
Spark Core …263
Spark SQL …263
Spark Streaming…263
Mllib …263
GraphX…263
26.1.3. 核心元件 …264
Cluster Manager-制整個叢集,監控 worker …264
Worker 節點-負責控制計算節點…264
Driver: 執行 Application 的 main()函式…264
Executor:執行器,是為某個 Application 執行在 worker node 上的一個程式…264
26.1.4. SPARK 程式設計模型…264
26.1.5. SPARK 計算模型…265
26.1.6. SPARK 執行流程…266 - 構建 Spark Application 的執行環境,啟動 SparkContext…267
- SparkContext 向資源管理器(可以是 Standalone,Mesos,Yarn)申請執行 Executor 資源,並啟
動 StandaloneExecutorbackend,…267 - Executor 向 SparkContext 申請 Task …267
- SparkContext 將應用程式分發給 Executor…267
- SparkContext 構建成 DAG 圖,將 DAG 圖分解成 Stage、將 Taskset 傳送給 Task Scheduler,最
後由 Task Scheduler 將 Task 傳送給 Executor 執行…267 - Task 在 Executor 上執行,執行完釋放所有資源…267
26.1.7. SPARK RDD 流程 …267
26.1.8. SPARK RDD…267
(1)RDD 的建立方式…267
(2)RDD 的兩種操作運算元(轉換(Transformation)與行動(Action))…268 - STORM …26913/04/2018 Page 17 of 283
27.1.1. 概念 …269
27.1.1. 叢集架構 …269
27.1.1.1. Nimbus(master-程式碼分發給 Supervisor) …269
27.1.1.2. Supervisor(slave-管理 Worker 程式的啟動和終止)…269
27.1.1.3. Worker(具體處理元件邏輯的程式)…269
27.1.1.4. Task …270
27.1.1.5. ZooKeeper …270
27.1.2. 程式設計模型(spout->tuple->bolt)…270
27.1.2.1. Topology…270
27.1.2.2. Spout…270
27.1.2.3. Bolt…270
27.1.2.4. Tuple…270
27.1.2.5. Stream …271
27.1.3. Topology 執行…271
(1). Worker(程式) (2). Executor(執行緒) (3). Task…271
27.1.3.1. Worker(1 個 worker 程式執行的是 1 個 topology 的子集) …271
27.1.3.2. Executor(executor 是 1 個被 worker 程式啟動的單獨執行緒)…271
27.1.3.3. Task(最終執行 spout 或 bolt 中程式碼的單元)…272
27.1.4. Storm Streaming Grouping…272
27.1.4.1. huffle Grouping…273
27.1.4.2. Fields Grouping…273
27.1.4.3. All grouping :廣播…273
27.1.4.4. Global grouping…274
27.1.4.5. None grouping :不分組…274
27.1.4.6. Direct grouping :直接分組 指定分組 …274 - YARN …275
28.1.1. 概念 …275
28.1.2. ResourceManager …275
28.1.3. NodeManager…275
28.1.4. ApplicationMaster …276
28.1.5. YARN 執行流程 …277 - 機器學習…278
29.1.1. 決策樹 …278
29.1.2. 隨機森林演算法 …278
29.1.3. 邏輯迴歸 …278
29.1.4. SVM…278
29.1.5. 樸素貝葉斯 …278
29.1.6. K 最近鄰演算法…278
29.1.7. K 均值演算法…278
29.1.8. Adaboost 演算法 …278
29.1.9. 神經網路 …278
29.1.10. 馬爾可夫 …278 - 雲端計算 …279
30.1.1. SaaS …279
30.1.2. PaaS …279
30.1.3. IaaS…279
30.1.4. Docker…279
30.1.4.1. 概念…279
30.1.4.2. Namespaces…280
30.1.4.3. 程式(CLONE_NEWPID 實現的程式隔離)…281
30.1.4.4. Libnetwork 與網路隔離…281
30.1.4.5. 資源隔離與 CGroups …282
30.1.4.6. 映象與 UnionFS…282
30.1.4.7. 儲存驅動…28213/04/2018 Page 18 of 283
30.1.5. Openstack …28313/04/2018 Page 19 of 283 - JVM
(1) 基本概念:
JVM 是可執行 Java 程式碼的假想計算機 ,包括一套位元組碼指令集、一組暫存器、一個棧、
一個垃圾回收,堆 和 一個儲存方法域。JVM 是執行在作業系統之上的,它與硬體沒有直接
的互動。
(2) 執行過程:13/04/2018 Page 20 of 283
我們都知道 Java 原始檔,通過編譯器,能夠生產相應的.Class 檔案,也就是位元組碼檔案,
而位元組碼檔案又通過 Java 虛擬機器中的直譯器,編譯成特定機器上的機器碼 。
也就是如下:
① Java 原始檔—->編譯器—->位元組碼檔案
② 位元組碼檔案—->JVM—->機器碼
每一種平臺的直譯器是不同的,但是實現的虛擬機器是相同的,這也就是 Java 為什麼能夠
跨平臺的原因了 ,當一個程式從開始執行,這時虛擬機器就開始例項化了,多個程式啟動就會
存在多個虛擬機器例項。程式退出或者關閉,則虛擬機器例項消亡,多個虛擬機器例項之間資料不
能共享。
2.1.執行緒
這裡所說的執行緒指程式執行過程中的一個執行緒實體。JVM 允許一個應用併發執行多個執行緒。
Hotspot JVM 中的 Java 執行緒與原生作業系統執行緒有直接的對映關係。當執行緒本地儲存、緩
衝區分配、同步物件、棧、程式計數器等準備好以後,就會建立一個作業系統原生執行緒。
Java 執行緒結束,原生執行緒隨之被回收。作業系統負責排程所有執行緒,並把它們分配到任何可
用的 CPU 上。當原生執行緒初始化完畢,就會呼叫 Java 執行緒的 run() 方法。當執行緒結束時,13/04/2018 Page 21 of 283
會釋放原生執行緒和 Java 執行緒的所有資源。
Hotspot JVM 後臺執行的系統執行緒主要有下面幾個:
虛擬機器執行緒
(VM thread)
這個執行緒等待 JVM 到達安全點操作出現。這些操作必須要在獨立的執行緒裡執行,因為當
堆修改無法進行時,執行緒都需要 JVM 位於安全點。這些操作的型別有:stop-theworld 垃圾回收、執行緒棧 dump、執行緒暫停、執行緒偏向鎖(biased locking)解除。
週期性任務執行緒 這執行緒負責定時器事件(也就是中斷),用來排程週期性操作的執行。
GC 執行緒 這些執行緒支援 JVM 中不同的垃圾回收活動。
編譯器執行緒 這些執行緒在執行時將位元組碼動態編譯成本地平臺相關的機器碼。
訊號分發執行緒 這個執行緒接收傳送到 JVM 的訊號並呼叫適當的 JVM 方法處理。
2.2.JVM 記憶體區域
JVM 記憶體區域主要分為執行緒私有區域【程式計數器、虛擬機器棧、本地方法區】、執行緒共享區
域【JAVA 堆、方法區】、直接記憶體。
執行緒私有資料區域生命週期與執行緒相同, 依賴使用者執行緒的啟動/結束 而 建立/銷燬(在 Hotspot
VM 內, 每個執行緒都與作業系統的本地執行緒直接對映, 因此這部分記憶體區域的存/否跟隨本地執行緒的
生/死對應)。13/04/2018 Page 22 of 283
執行緒共享區域隨虛擬機器的啟動/關閉而建立/銷燬。
直接記憶體並不是 JVM 執行時資料區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提
供了基於 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函式庫直接分配堆外記憶體, 然後使用
DirectByteBuffer 物件作為這塊記憶體的引用進行操作(詳見: Java I/O 擴充套件), 這樣就避免了在 Java
堆和 Native 堆中來回複製資料, 因此在一些場景中可以顯著提高效能。
2.2.1. 程式計數器(執行緒私有)
一塊較小的記憶體空間, 是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的
程式計數器,這類記憶體也稱為“執行緒私有”的記憶體。
正在執行 java 方法的話,計數器記錄的是虛擬機器位元組碼指令的地址(當前指令的地址)。如
果還是 Native 方法,則為空。
這個記憶體區域是唯一一個在虛擬機器中沒有規定任何 OutOfMemoryError 情況的區域。
2.2.2. 虛擬機器棧(執行緒私有)
是描述java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame)
用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成
的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
棧幀( Frame)是用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結
(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法呼叫而創13/04/2018 Page 23 of 283
建,隨著方法結束而銷燬——無論方法是正常完成還是異常完成(丟擲了在方法內未被捕獲的異
常)都算作方法結束。
2.2.3. 本地方法區(執行緒私有)
本地方法區和 Java Stack 作用類似, 區別是虛擬機器棧為執行 Java 方法服務, 而本地方法棧則為
Native 方法服務, 如果一個 VM 實現使用 C-linkage 模型來支援 Native 呼叫, 那麼該棧將會是一個
C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機器棧合二為一。
2.2.4. 堆(Heap-執行緒共享)-執行時資料區
是被執行緒共享的一塊記憶體區域,建立的物件和陣列都儲存在 Java 堆記憶體中,也是垃圾收集器進行
垃圾收集的最重要的記憶體區域。由於現代 VM 採用分代收集演算法, 因此 Java 堆從 GC 的角度還可以
細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。
2.2.5. 方法區/永久代(執行緒共享)
即我們常說的永久代(Permanent Generation), 用於儲存被 JVM 載入的類資訊、常量、靜
態變數、即時編譯器編譯後的程式碼等資料. HotSpot VM把GC分代收集擴充套件至方法區, 即使用Java
堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分記憶體,
而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目標是針對常量池的回收和型別
的解除安裝, 因此收益一般很小)。
執行時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有類的版
本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池13/04/2018 Page 24 of 283
(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加
載後存放到方法區的執行時常量池中。 Java 虛擬機器對 Class 檔案的每一部分(自然也包括常量
池)的格式都有嚴格的規定,每一個位元組用於儲存哪種資料都必須符合規範上的要求,這樣才會
被虛擬機器認可、裝載和執行。
2.3.JVM 執行時記憶體
Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年
代。
2.3.1. 新生代
是用來存放新生的物件。一般佔據堆的 1/3 空間。由於頻繁建立物件,所以新生代會頻繁觸發
MinorGC 進行垃圾回收。新生代又分為 Eden 區、ServivorFrom、ServivorTo 三個區。
2.3.1.1. Eden 區
Java 新物件的出生地(如果新建立的物件佔用記憶體很大,則直接分配到老
年代)。當 Eden 區記憶體不夠的時候就會觸發 MinorGC,對新生代區進行
一次垃圾回收。
2.3.1.2. ServivorFrom
上一次 GC 的倖存者,作為這一次 GC 的被掃描者。
2.3.1.3. ServivorTo
保留了一次 MinorGC 過程中的倖存者。
2.3.1.4. MinorGC 的過程(複製->清空->互換)
MinorGC 採用複製演算法。13/04/2018 Page 25 of 283
1:eden、servicorFrom 複製到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的物件複製到 ServicorTo 區域(如果有物件的年
齡以及達到了老年的標準,則賦值到老年代區),同時把這些物件的年齡+1(如果 ServicorTo 不
夠位置了就放到老年區);
2:清空 eden、servicorFrom
然後,清空 Eden 和 ServicorFrom 中的物件;
3:ServicorTo 和 ServicorFrom 互換
最後,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom
區。
2.3.2. 老年代
主要存放應用程式中生命週期長的記憶體物件。
老年代的物件比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行
了一次 MinorGC,使得有新生代的物件晉身入老年代,導致空間不夠用時才觸發。當無法找到足
夠大的連續空間分配給新建立的較大物件時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的物件,然後回收沒
有標記的物件。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減
少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的
時候,就會丟擲 OOM(Out of Memory)異常。
2.3.3. 永久代
指記憶體的永久儲存區域,主要存放 Class 和 Meta(後設資料)的資訊,Class 在被載入的時候被
放入永久區域,它和和存放例項的區域不同,GC 不會在主程式執行期對永久區域進行清理。所以這
也導致了永久代的區域會隨著載入的 Class 的增多而脹滿,最終丟擲 OOM 異常。
2.3.3.1. JAVA8 與後設資料
在 Java8 中,永久代已經被移除,被一個稱為“後設資料區”(元空間)的區域所取代。元空間
的本質和永久代類似,元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用
本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。類的後設資料放入 native
memory, 字串池和類的靜態變數放入 java 堆中,這樣可以載入多少類的後設資料就不再由
MaxPermSize 控制, 而由系統的實際可用空間來控制。13/04/2018 Page 26 of 283
2.4.垃圾回收與演算法
2.4.1. 如何確定垃圾
2.4.1.1. 引用計數法
在 Java 中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。因此,很顯然一個簡單
的辦法是通過引用計數來判斷一個物件是否可以回收。簡單說,即一個物件如果沒有任何與之關
聯的引用,即他們的引用計數都不為 0,則說明物件不太可能再被用到,那麼這個物件就是可回收
物件。
2.4.1.2. 可達性分析
為了解決引用計數法的迴圈引用問題,Java 使用了可達性分析的方法。通過一系列的“GC roots”
物件作為起點搜尋。如果在“GC roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的。13/04/2018 Page 27 of 283
要注意的是,不可達物件不等價於可回收物件,不可達物件變為可回收物件至少要經過兩次標記
過程。兩次標記後仍然是可回收物件,則將面臨回收。
2.4.2. 標記清除演算法(Mark-Sweep)
最基礎的垃圾回收演算法,分為兩個階段,標註和清除。標記階段標記出所有需要回收的物件,清
除階段回收被標記的物件所佔用的空間。如圖
從圖中我們就可以發現,該演算法最大的問題是記憶體碎片化嚴重,後續可能發生大物件不能找到可
利用空間的問題。
2.4.3. 複製演算法(copying)
為了解決 Mark-Sweep 演算法記憶體碎片化的缺陷而被提出的演算法。按記憶體容量將記憶體劃分為等大小
的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用
的記憶體清掉,如圖:13/04/2018 Page 28 of 283
這種演算法雖然實現簡單,記憶體效率高,不易產生碎片,但是最大的問題是可用記憶體被壓縮到了原
本的一半。且存活物件增多的話,Copying 演算法的效率會大大降低。
2.4.4. 標記整理演算法(Mark-Compact)
結合了以上兩個演算法,為了避免缺陷而提出。標記階段和 Mark-Sweep 演算法相同,標記後不是清
理物件,而是將存活物件移向記憶體的一端。然後清除端邊界外的物件。如圖:13/04/2018 Page 29 of 283
2.4.5. 分代收集演算法
分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據物件存活的不同生命週期將記憶體
劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(Young
Generation)。老生代的特點是每次垃圾回收時只有少量物件需要被回收,新生代的特點是每次垃
圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的演算法。
2.4.5.1. 新生代與複製演算法
目前大部分 JVM 的 GC 對於新生代都採取 Copying 演算法,因為新生代中每次垃圾回收都要
回收大部分物件,即要複製的操作比較少,但通常並不是按照 1:1 來劃分新生代。一般將新生代
劃分為一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用
Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的物件複製到另
一塊 Survivor 空間中。
2.4.5.2. 老年代與標記複製演算法
而老年代因為每次只回收少量物件,因而採用 Mark-Compact 演算法。 - JAVA 虛擬機器提到過的處於方法區的永生代(Permanet Generation),它用來儲存 class 類,
常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。 - 物件的記憶體分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目
前存放物件的那一塊),少數情況會直接分配到老生代。 - 當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden
Space 和 From Space 區的存活物件會被挪到 To Space,然後將 Eden Space 和 From
Space 進行清理。 - 如果 To Space 無法足夠儲存某個物件,則將這個物件儲存到老生代。
- 在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆迴圈。
- 當物件在 Survivor 區躲過一次 GC 後,其年齡就會+1。預設情況下年齡到達 15 的物件會被
移到老生代中。13/04/2018 Page 30 of 283
2.5.JAVA 四中引用型別
2.5.1. 強引用
在 Java 中最常見的就是強引用,把一個物件賦給一個引用變數,這個引用變數就是一個強引
用。當一個物件被強引用變數引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即
使該物件以後永遠都不會被用到 JVM 也不會回收。因此強引用是造成 Java 記憶體洩漏的主要原因之
一。
2.5.2. 軟引用
軟引用需要用 SoftReference 類來實現,對於只有軟引用的物件來說,當系統記憶體足夠時它
不會被回收,當系統記憶體空間不足時它會被回收。軟引用通常用在對記憶體敏感的程式中。
2.5.3. 弱引用
弱引用需要用 WeakReference 類來實現,它比軟引用的生存期更短,對於只有弱引用的物件
來說,只要垃圾回收機制一執行,不管 JVM 的記憶體空間是否足夠,總會回收該物件佔用的記憶體。
2.5.4. 虛引用
虛引用需要 PhantomReference 類來實現,它不能單獨使用,必須和引用佇列聯合使用。虛
引用的主要作用是跟蹤物件被垃圾回收的狀態。
2.6.GC 分代收集演算法 VS 分割槽收集演算法
2.6.1. 分代收集演算法
當前主流 VM 垃圾收集都採用”分代收集”(Generational Collection)演算法, 這種演算法會根據
物件存活週期的不同將記憶體劃分為幾塊, 如 JVM 中的 新生代、老年代、永久代,這樣就可以根據
各年代特點分別採用最適當的 GC 演算法
2.6.1.1. 在新生代-複製演算法
每次垃圾收集都能發現大批物件已死, 只有少量存活. 因此選用複製演算法, 只需要付出少量
存活物件的複製成本就可以完成收集.
2.6.1.2. 在老年代-標記整理演算法
因為物件存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標
記—整理”演算法來進行回收, 不必進行記憶體複製, 且直接騰出空閒記憶體.13/04/2018 Page 31 of 283
2.6.2. 分割槽收集演算法
分割槽演算法則將整個堆空間劃分為連續的不同小區間, 每個小區間獨立使用, 獨立回收. 這樣做的
好處是可以控制一次回收多少個小區間 , 根據目標停頓時間, 每次合理地回收若干個小區間(而不是
整個堆), 從而減少一次 GC 所產生的停頓。
2.7.GC 垃圾收集器
Java 堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法;
年老代主要使用標記-整理垃圾回收演算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不
同的垃圾收集器,JDK1.6 中 Sun HotSpot 虛擬機器的垃圾收集器如下:
2.7.1. Serial 垃圾收集器(單執行緒、複製演算法)
Serial(英文連續)是最基本垃圾收集器,使用複製演算法,曾經是JDK1.3.1 之前新生代唯一的垃圾
收集器。Serial 是一個單執行緒的收集器,它不但只會使用一個 CPU 或一條執行緒去完成垃圾收集工
作,並且在進行垃圾收集的同時,必須暫停其他所有的工作執行緒,直到垃圾收集結束。
Serial 垃圾收集器雖然在收集垃圾過程中需要暫停所有其他的工作執行緒,但是它簡單高效,對於限
定單個 CPU 環境來說,沒有執行緒互動的開銷,可以獲得最高的單執行緒垃圾收集效率,因此 Serial
垃圾收集器依然是 java 虛擬機器執行在 Client 模式下預設的新生代垃圾收集器。
2.7.2. ParNew 垃圾收集器(Serial+多執行緒)
ParNew 垃圾收集器其實是 Serial 收集器的多執行緒版本,也使用複製演算法,除了使用多執行緒進行垃
圾收集之外,其餘的行為和 Serial 收集器完全一樣,ParNew 垃圾收集器在垃圾收集過程中同樣也
要暫停所有其他的工作執行緒。13/04/2018 Page 32 of 283
ParNew 收集器預設開啟和 CPU 數目相同的執行緒數,可以通過-XX:ParallelGCThreads 引數來限
制垃圾收集器的執行緒數。【Parallel:平行的】
ParNew雖然是除了多執行緒外和Serial 收集器幾乎完全一樣,但是ParNew垃圾收集器是很多 java
虛擬機器執行在 Server 模式下新生代的預設垃圾收集器。
2.7.3. Parallel Scavenge 收集器(多執行緒複製演算法、高效)
Parallel Scavenge 收集器也是一個新生代垃圾收集器,同樣使用複製演算法,也是一個多執行緒的垃
圾收集器,它重點關注的是程式達到一個可控制的吞吐量(Thoughput,CPU 用於執行使用者程式碼
的時間/CPU 總消耗時間,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)),
高吞吐量可以最高效率地利用 CPU 時間,儘快地完成程式的運算任務,主要適用於在後臺運算而
不需要太多互動的任務。自適應調節策略也是 ParallelScavenge 收集器與 ParNew 收集器的一個
重要區別。
2.7.4. Serial Old 收集器(單執行緒標記整理演算法 )
Serial Old 是 Serial 垃圾收集器年老代版本,它同樣是個單執行緒的收集器,使用標記-整理演算法,
這個收集器也主要是執行在 Client 預設的 java 虛擬機器預設的年老代垃圾收集器。 在 Server 模式下,主要有兩個用途: - 在 JDK1.5 之前版本中與新生代的 Parallel Scavenge 收集器搭配使用。
- 作為年老代中使用 CMS 收集器的後備垃圾收集方案。
新生代 Serial 與年老代 Serial Old 搭配垃圾收集過程圖:
新生代 Parallel Scavenge 收集器與 ParNew 收集器工作原理類似,都是多執行緒的收集器,都使
用的是複製演算法,在垃圾收集過程中都需要暫停所有的工作執行緒。新生代 Parallel
Scavenge/ParNew 與年老代 Serial Old 搭配垃圾收集過程圖:13/04/2018 Page 33 of 283
2.7.5. Parallel Old 收集器(多執行緒標記整理演算法)
Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多執行緒的標記-整理演算法,在 JDK1.6
才開始提供。
在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只
能保證新生代的吞吐量優先,無法保證整體的吞吐量,Parallel Old 正是為了在年老代同樣提供吞
吐量優先的垃圾收集器,如果系統對吞吐量要求比較高,可以優先考慮新生代 Parallel Scavenge
和年老代 Parallel Old 收集器的搭配策略。
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配執行過程圖:
2.7.6. CMS 收集器(多執行緒標記清除演算法)
Concurrent mark sweep(CMS)收集器是一種年老代垃圾收集器,其最主要目標是獲取最短垃圾
回收停頓時間,和其他年老代使用標記-整理演算法不同,它使用多執行緒的標記-清除演算法。
最短的垃圾收集停頓時間可以為互動比較高的程式提高使用者體驗。
CMS 工作機制相比其他的垃圾收集器來說更復雜,整個過程分為以下 4 個階段:
2.7.6.1. 初始標記
只是標記一下 GC Roots 能直接關聯的物件,速度很快,仍然需要暫停所有的工作執行緒。13/04/2018 Page 34 of 283
2.7.6.2. 併發標記
進行 GC Roots 跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒。
2.7.6.3. 重新標記
為了修正在併發標記期間,因使用者程式繼續執行而導致標記產生變動的那一部分物件的標記
記錄,仍然需要暫停所有的工作執行緒。
2.7.6.4. 併發清除
清除 GC Roots 不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒。由於耗時最長的並
發標記和併發清除過程中,垃圾收集執行緒可以和使用者現在一起併發工作,所以總體上來看
CMS 收集器的記憶體回收和使用者執行緒是一起併發地執行。
CMS 收集器工作過程:
2.7.7. G1 收集器
Garbage first 垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比與 CMS 收集器,G1 收
集器兩個最突出的改進是: - 基於標記-整理演算法,不產生記憶體碎片。
- 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。
G1 收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,並且跟蹤這些區域
的垃圾收集進度,同時在後臺維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾
最多的區域。區域劃分和優先順序區域回收機制,確保 G1 收集器可以在有限時間獲得最高的垃圾收
集效率。
2.8. JAVA IO/NIO
2.8.1. 阻塞 IO 模型
最傳統的一種 IO 模型,即在讀寫資料過程中會發生阻塞現象。當使用者執行緒發出 IO 請求之後,內
核會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者執行緒就會處於阻塞狀態,用
戶執行緒交出 CPU。當資料就緒之後,核心會將資料拷貝到使用者執行緒,並返回結果給使用者執行緒,用13/04/2018 Page 35 of 283
戶執行緒才解除 block 狀態。典型的阻塞 IO 模型的例子為:data = socket.read();如果資料沒有就
緒,就會一直阻塞在 read 方法。
2.8.2. 非阻塞 IO 模型
當使用者執行緒發起一個 read 操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個
error 時,它就知道資料還沒有準備好,於是它可以再次傳送 read 操作。一旦核心中的資料準備
好了,並且又再次收到了使用者執行緒的請求,那麼它馬上就將資料拷貝到了使用者執行緒,然後返回。
所以事實上,在非阻塞 IO 模型中,使用者執行緒需要不斷地詢問核心資料是否就緒,也就說非阻塞 IO
不會交出 CPU,而會一直佔用 CPU。典型的非阻塞 IO 模型一般如下:
while(true){
data = socket.read();
if(data!= error){
處理資料
break;
}}
但是對於非阻塞 IO 就有一個非常嚴重的問題,在 while 迴圈中需要不斷地去詢問核心資料是否就
緒,這樣會導致 CPU 佔用率非常高,因此一般情況下很少使用 while 迴圈這種方式來讀取資料。
2.8.3. 多路複用 IO 模型
多路複用 IO 模型是目前使用得比較多的模型。Java NIO 實際上就是多路複用 IO。在多路複用 IO
模型中,會有一個執行緒不斷去輪詢多個 socket 的狀態,只有當 socket 真正有讀寫事件時,才真
正呼叫實際的 IO 讀寫操作。因為在多路複用 IO 模型中,只需要使用一個執行緒就可以管理多個
socket,系統不需要建立新的程式或者執行緒,也不必維護這些執行緒和程式,並且只有在真正有
socket 讀寫事件進行時,才會使用 IO 資源,所以它大大減少了資源佔用。在 Java NIO 中,是通
過 selector.select()去查詢每個通道是否有到達事件,如果沒有事件,則一直阻塞在那裡,因此這
種方式會導致使用者執行緒的阻塞。多路複用 IO 模式,通過一個執行緒就可以管理多個 socket,只有當
socket 真正有讀寫事件發生才會佔用資源來進行實際的讀寫操作。因此,多路複用 IO 比較適合連
接數比較多的情況。
另外多路複用 IO 為何比非阻塞 IO 模型的效率高是因為在非阻塞 IO 中,不斷地詢問 socket 狀態
時通過使用者執行緒去進行的,而在多路複用 IO 中,輪詢每個 socket 狀態是核心在進行的,這個效
率要比使用者執行緒要高的多。
不過要注意的是,多路複用 IO 模型是通過輪詢的方式來檢測是否有事件到達,並且對到達的事件
逐一進行響應。因此對於多路複用 IO 模型來說,一旦事件響應體很大,那麼就會導致後續的事件
遲遲得不到處理,並且會影響新的事件輪詢。13/04/2018 Page 36 of 283
2.8.4. 訊號驅動 IO 模型
在訊號驅動 IO 模型中,當使用者執行緒發起一個 IO 請求操作,會給對應的 socket 註冊一個訊號函
數,然後使用者執行緒會繼續執行,當核心資料就緒時會傳送一個訊號給使用者執行緒,使用者執行緒接收到
訊號之後,便在訊號函式中呼叫 IO 讀寫操作來進行實際的 IO 請求操作。
2.8.5. 非同步 IO 模型
非同步 IO 模型才是最理想的 IO 模型,在非同步 IO 模型中,當使用者執行緒發起 read 操作之後,立刻就
可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個 asynchronous read 之後,
它會立刻返回,說明 read 請求已經成功發起了,因此不會對使用者執行緒產生任何 block。然後,內
核會等待資料準備完成,然後將資料拷貝到使用者執行緒,當這一切都完成之後,核心會給使用者執行緒
傳送一個訊號,告訴它 read 操作完成了。也就說使用者執行緒完全不需要實際的整個 IO 操作是如何
進行的,只需要先發起一個請求,當接收核心返回的成功訊號時表示 IO 操作已經完成,可以直接
去使用資料了。
也就說在非同步 IO 模型中,IO 操作的兩個階段都不會阻塞使用者執行緒,這兩個階段都是由核心自動完
成,然後傳送一個訊號告知使用者執行緒操作已完成。使用者執行緒中不需要再次呼叫 IO 函式進行具體的
讀寫。這點是和訊號驅動模型有所不同的,在訊號驅動模型中,當使用者執行緒接收到訊號表示資料
已經就緒,然後需要使用者執行緒呼叫 IO 函式進行實際的讀寫操作;而在非同步 IO 模型中,收到訊號
表示 IO 操作已經完成,不需要再在使用者執行緒中呼叫 IO 函式進行實際的讀寫操作。
注意,非同步 IO 是需要作業系統的底層支援,在 Java 7 中,提供了 Asynchronous IO。
更多參考: http://www.importnew.com/19816.html
2.8.1. JAVA IO 包13/04/2018 Page 37 of 283
2.8.2. JAVA NIO
NIO 主要有三大核心部分:Channel(通道),Buffer(緩衝區), Selector。傳統 IO 基於位元組流和字
符流進行操作,而 NIO 基於 Channel 和 Buffer(緩衝區)進行操作,資料總是從通道讀取到緩衝區
中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連線開啟,
資料到達)。因此,單個執行緒可以監聽多個資料通道。13/04/2018 Page 38 of 283
NIO 和傳統 IO 之間第一個最大的區別是,IO 是面向流的,NIO 是面向緩衝區的。
2.8.2.1. NIO 的緩衝區
Java IO 面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何
地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它緩
存到一個緩衝區。NIO 的緩衝導向方法不同。資料讀取到一個它稍後處理的緩衝區,需要時可在
緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所
有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的
資料。
2.8.2.2. NIO 的非阻塞
IO 的各種流是阻塞的。這意味著,當一個執行緒呼叫 read() 或 write()時,該執行緒被阻塞,直到有
一些資料被讀取,或資料完全寫入。該執行緒在此期間不能再幹任何事情了。 NIO 的非阻塞模式,
使一個執行緒從某通道傳送請求讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可
用時,就什麼都不會獲取。而不是保持執行緒阻塞,所以直至資料變的可以讀取之前,該執行緒可以
繼續做其他的事情。 非阻塞寫也是如此。一個執行緒請求寫入一些資料到某通道,但不需要等待它
完全寫入,這個執行緒同時可以去做別的事情。 執行緒通常將非阻塞 IO 的空閒時間用於在其它通道上
執行 IO 操作,所以一個單獨的執行緒現在可以管理多個輸入和輸出通道(channel)。13/04/2018 Page 39 of 28313/04/2018 Page 40 of 283
2.8.3. Channel
首先說一下 Channel,國內大多翻譯成“通道”。Channel 和 IO 中的 Stream(流)是差不多一個
等級的。只不過 Stream 是單向的,譬如:InputStream, OutputStream,而 Channel 是雙向
的,既可以用來進行讀操作,又可以用來進行寫操作。
NIO 中的 Channel 的主要實現有: - FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
這裡看名字就可以猜出個所以然來:分別可以對應檔案 IO、UDP 和 TCP(Server 和 Client)。
下面演示的案例基本上就是圍繞這 4 個型別的 Channel 進行陳述的。
2.8.4. Buffer
Buffer,故名思意,緩衝區,實際上是一個容器,是一個連續陣列。Channel 提供從檔案、
網路讀取資料的渠道,但是讀取或寫入的資料都必須經由 Buffer。
上面的圖描述了從一個客戶端向服務端傳送資料,然後服務端接收資料的過程。客戶端傳送
資料時,必須先將資料存入 Buffer 中,然後將 Buffer 中的內容寫入通道。服務端這邊接收資料必
須通過 Channel 將資料讀入到 Buffer 中,然後再從 Buffer 中取出資料來處理。 在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,常用的 Buffer 的子類有:
ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、
ShortBuffer
2.8.5. Selector
Selector 類是 NIO 的核心類,Selector 能夠檢測多個註冊的通道上是否有事件發生,如果有事
件發生,便獲取事件然後針對每個事件進行相應的響應處理。這樣一來,只是用一個單執行緒就可
以管理多個通道,也就是管理多個連線。這樣使得只有在連線真正有讀寫事件發生時,才會呼叫
函式來進行讀寫,就大大地減少了系統開銷,並且不必為每個連線都建立一個執行緒,不用去維護
多個執行緒,並且避免了多執行緒之間的上下文切換導致的開銷。13/04/2018 Page 41 of 283
2.9.JVM 類載入機制
JVM 類載入機制分為五個部分:載入,驗證,準備,解析,初始化,下面我們就分別來看一下這
五個過程。
2.9.1.1. 載入
載入是類載入過程中的一個階段,這個階段會在記憶體中生成一個代表這個類的 java.lang.Class 對 象,作為方法區這個類的各種資料的入口。注意這裡不一定非得要從一個 Class 檔案獲取,這裡既
可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在執行時計算生成(動態代理),
也可以由其它檔案生成(比如將 JSP 檔案轉換成對應的 Class 類)。
2.9.1.2. 驗證
這一階段的主要目的是為了確保 Class 檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並
且不會危害虛擬機器自身的安全。
2.9.1.3. 準備
準備階段是正式為類變數分配記憶體並設定類變數的初始值階段,即在方法區中分配這些變數所使
用的記憶體空間。注意這裡所說的初始值概念,比如一個類變數定義為:
public static int v = 8080;
實際上變數 v 在準備階段過後的初始值為 0 而不是 8080,將 v 賦值為 8080 的 put static 指令是
程式被編譯後,存放於類構造器方法之中。
但是注意如果宣告為:
public static final int v = 8080;
在編譯階段會為 v 生成 ConstantValue 屬性,在準備階段虛擬機器會根據 ConstantValue 屬性將 v
賦值為 8080。
2.9.1.4. 解析
解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。符號引用就是 class 檔案中
的:13/04/2018 Page 42 of 283 - CONSTANT_Class_info
- CONSTANT_Field_info
- CONSTANT_Method_info
等型別的常量。
2.9.1.5. 符號引用
„ 符號引用與虛擬機器實現的佈局無關,引用的目標並不一定要已經載入到記憶體中。各種虛擬
機實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引
用的字面量形式明確定義在 Java 虛擬機器規範的 Class 檔案格式中。
2.9.1.6. 直接引用
„ 直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制程式碼。如果有
了直接引用,那引用的目標必定已經在記憶體中存在。
2.9.1.7. 初始化
初始化階段是類載入最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入
器以外,其它操作都由 JVM 主導。到了初始階段,才開始真正執行類中定義的 Java 程式程式碼。
2.9.1.8. 類構造器
初始化階段是執行類構造器方法的過程。方法是由編譯器自動收集類中的類變
量的賦值操作和靜態語句塊中的語句合併而成的。虛擬機器會保證子方法執行之前,父類
的方法已經執行完畢,如果一個類中沒有對靜態變數賦值也沒有靜態語句塊,那麼編譯
器可以不為這個類生成()方法。
注意以下幾種情況不會執行類初始化: - 通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。
- 定義物件陣列,不會觸發該類的初始化。
- 常量在編譯期間會存入呼叫類的常量池中,本質上並沒有直接引用定義常量的類,不會觸
發定義常量所在的類。 - 通過類名獲取 Class 物件,不會觸發類的初始化。
- 通過 Class.forName 載入指定類時,如果指定引數 initialize 為 false 時,也不會觸發類初
始化,其實這個引數是告訴虛擬機器,是否要對類進行初始化。 - 通過 ClassLoader 預設的 loadClass 方法,也不會觸發初始化動作。
2.9.2. 類載入器
虛擬機器設計團隊把載入動作放到 JVM 外部實現,以便讓應用程式決定如何獲取所需的類,JVM 提
供了 3 種類載入器:13/04/2018 Page 43 of 283
2.9.2.1. 啟動類載入器(Bootstrap ClassLoader) - 負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath 引數指定路徑中的,且被
虛擬機器認可(按檔名識別,如 rt.jar)的類。
2.9.2.2. 擴充套件類載入器(Extension ClassLoader) - 負責載入 JAVA_HOME\lib\ext 目錄中的,或通過 java.ext.dirs 系統變數指定路徑中的類
庫。
2.9.2.3. 應用程式類載入器(Application ClassLoader): - 負責載入使用者路徑(classpath)上的類庫。
JVM 通過雙親委派模型進行類的載入,當然我們也可以通過繼承 java.lang.ClassLoader
實現自定義的類載入器。
2.9.3. 雙親委派
當一個類收到了類載入請求,他首先不會嘗試自己去載入這個類,而是把這個請求委派給父
類去完成,每一個層次類載入器都是如此,因此所有的載入請求都應該傳送到啟動類載入其中,
只有當父類載入器反饋自己無法完成這個請求的時候(在它的載入路徑下沒有找到所需載入的
Class),子類載入器才會嘗試自己去載入。
採用雙親委派的一個好處是比如載入位於 rt.jar 包中的類 java.lang.Object,不管是哪個載入
器載入這個類,最終都是委託給頂層的啟動類載入器進行載入,這樣就保證了使用不同的類載入
器最終得到的都是同樣一個 Object 物件。13/04/2018 Page 44 of 283
2.9.4. OSGI(動態模型系統)
OSGi(Open Service Gateway Initiative),是面向 Java 的動態模型系統,是 Java 動態化模組化系
統的一系列規範。
2.9.4.1. 動態改變構造
OSGi 服務平臺提供在多種網路裝置上無需重啟的動態改變構造的功能。為了最小化耦合度和促使
這些耦合度可管理,OSGi 技術提供一種面向服務的架構,它能使這些元件動態地發現對方。
2.9.4.2. 模組化程式設計與熱插拔
OSGi 旨在為實現 Java 程式的模組化程式設計提供基礎條件,基於 OSGi 的程式很可能可以實現模組級
的熱插拔功能,當程式升級更新時,可以只停用、重新安裝然後啟動程式的其中一部分,這對企
業級程式開發來說是非常具有誘惑力的特性。
OSGi 描繪了一個很美好的模組化開發目標,而且定義了實現這個目標的所需要服務與架構,同時
也有成熟的框架進行實現支援。但並非所有的應用都適合採用 OSGi 作為基礎架構,它在提供強大
功能同時,也引入了額外的複雜度,因為它不遵守了類載入的雙親委託模型。13/04/2018 Page 45 of 283 - JAVA 集合
3.1.介面繼承關係和實現
集合類存放於 Java.util 包中,主要有 3 種:set(集)、list(列表包含 Queue)和 map(對映)。 - Collection:Collection 是集合 List、Set、Queue 的最基本的介面。
- Iterator:迭代器,可以通過迭代器遍歷集合中的資料
- Map:是對映表的基礎介面13/04/2018 Page 46 of 28313/04/2018 Page 47 of 283
3.2.List
Java 的 List 是非常常用的資料型別。List 是有序的 Collection。Java List 一共三個實現類:
分別是 ArrayList、Vector 和 LinkedList。
3.2.1. ArrayList(陣列)
ArrayList 是最常用的 List 實現類,內部是通過陣列實現的,它允許對元素進行快速隨機訪問。數
組的缺點是每個元素之間不能有間隔,當陣列大小不滿足時需要增加儲存能力,就要將已經有數
組的資料複製到新的儲存空間中。當從 ArrayList 的中間位置插入或者刪除元素時,需要對陣列進
行復制、移動、代價比較高。因此,它適合隨機查詢和遍歷,不適合插入和刪除。
3.2.2. Vector(陣列實現、執行緒同步)
Vector 與 ArrayList 一樣,也是通過陣列實現的,不同的是它支援執行緒的同步,即某一時刻只有一
個執行緒能夠寫 Vector,避免多執行緒同時寫而引起的不一致性,但實現同步需要很高的花費,因此,
訪問它比訪問 ArrayList 慢。
3.2.3. LinkList(連結串列)
LinkedList 是用連結串列結構儲存資料的,很適合資料的動態插入和刪除,隨機訪問和遍歷速度比較
慢。另外,他還提供了 List 介面中沒有定義的方法,專門用於操作表頭和表尾元素,可以當作堆
棧、佇列和雙向佇列使用。13/04/2018 Page 48 of 283
3.3.Set
Set 注重獨一無二的性質,該體系集合用於儲存無序(存入和取出的順序不一定相同)元素,值不能重
復。物件的相等性本質是物件 hashCode 值(java 是依據物件的記憶體地址計算出的此序號)判斷
的,如果想要讓兩個不同的物件視為相等的,就必須覆蓋 Object 的 hashCode 方法和 equals 方
法。
3.3.1.1. HashSet(Hash 表)
雜湊表邊存放的是雜湊值。HashSet 儲存元素的順序並不是按照存入時的順序(和 List 顯然不
同) 而是按照雜湊值來存的所以取資料也是按照雜湊值取得。元素的雜湊值是通過元素的
hashcode 方法來獲取的, HashSet 首先判斷兩個元素的雜湊值,如果雜湊值一樣,接著會比較
equals 方法 如果 equls 結果為 true ,HashSet 就視為同一個元素。如果 equals 為 false 就不是
同一個元素。
雜湊值相同 equals 為 false 的元素是怎麼儲存呢,就是在同樣的雜湊值下順延(可以認為雜湊值相
同的元素放在一個雜湊桶中)。也就是雜湊一樣的存一列。如圖 1 表示 hashCode 值不相同的情
況;圖 2 表示 hashCode 值相同,但 equals 不相同的情況。13/04/2018 Page 49 of 283
HashSet 通過 hashCode 值來確定元素在記憶體中的位置。一個 hashCode 位置上可以存放多個元
素。
3.3.1.2. TreeSet(二叉樹) - TreeSet()是使用二叉樹的原理對新 add()的物件按照指定的順序排序(升序、降序),每增
加一個物件都會進行排序,將物件插入的二叉樹指定的位置。 - Integer 和 String 物件都可以進行預設的 TreeSet 排序,而自定義類的物件是不可以的,自
己定義的類必須實現 Comparable 介面,並且覆寫相應的 compareTo()函式,才可以正常使
用。 - 在覆寫 compare()函式時,要返回相應的值才能使 TreeSet 按照一定的規則來排序
- 比較此物件與指定物件的順序。如果該物件小於、等於或大於指定物件,則分別返回負整
數、零或正整數。
3.3.1.3. LinkHashSet(HashSet+LinkedHashMap)
對於 LinkedHashSet 而言,它繼承與 HashSet、又基於 LinkedHashMap 來實現的。
LinkedHashSet 底層使用 LinkedHashMap 來儲存所有元素,它繼承與 HashSet,其所有的方法
操作上又與 HashSet 相同,因此 LinkedHashSet 的實現上非常簡單,只提供了四個構造方法,並
通過傳遞一個標識引數,呼叫父類的構造器,底層構造一個 LinkedHashMap 來實現,在相關操
作上與父類 HashSet 的操作相同,直接呼叫父類 HashSet 的方法即可。13/04/2018 Page 50 of 283
3.4.Map
3.4.1. HashMap(陣列+連結串列+紅黑樹)
HashMap 根據鍵的 hashCode 值儲存資料,大多數情況下可以直接定位到它的值,因而具有很快
的訪問速度,但遍歷順序卻是不確定的。 HashMap 最多隻允許一條記錄的鍵為 null,允許多條記
錄的值為 null。HashMap 非執行緒安全,即任一時刻可以有多個執行緒同時寫 HashMap,可能會導
致資料的不一致。如果需要滿足執行緒安全,可以用 Collections 的 synchronizedMap 方法使
HashMap 具有執行緒安全的能力,或者使用 ConcurrentHashMap。我們用下面這張圖來介紹
HashMap 的結構。
3.4.1.1. JAVA7 實現
大方向上,HashMap 裡面是一個陣列,然後陣列中每個元素是一個單向連結串列。上圖中,每個綠色
的實體是巢狀類 Entry 的例項,Entry 包含四個屬性:key, value, hash 值和用於單向連結串列的 next。 - capacity:當前陣列容量,始終保持 2^n,可以擴容,擴容後陣列大小為當前的 2 倍。
- loadFactor:負載因子,預設為 0.75。13/04/2018 Page 51 of 283
- threshold:擴容的閾值,等於 capacity * loadFactor
3.4.1.2. JAVA8 實現
Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑
樹 組成。
根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的
具體下標,但是之後的話,需要順著連結串列一個個比較下去才能找到我們需要的,時間複雜度取決
於連結串列的長度,為 O(n)。為了降低這部分的開銷,在 Java8 中,當連結串列中的元素超過了 8 個以後,
會將連結串列轉換為紅黑樹,在這些位置進行查詢的時候可以降低時間複雜度為 O(logN)。
3.4.2. ConcurrentHashMap
3.4.2.1. Segment 段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因為它支援併發操作,所以要複雜一
些。整個 ConcurrentHashMap 由一個個 Segment 組成,Segment 代表”部分“或”一段“的
意思,所以很多地方都會將其描述為分段鎖。注意,行文中,我很多地方用了“槽”來代表一個
segment。
3.4.2.2. 執行緒安全(Segment 繼承 ReentrantLock 加鎖)
簡單理解就是,ConcurrentHashMap 是一個 Segment 陣列,Segment 通過繼承
ReentrantLock 來進行加鎖,所以每次需要加鎖的操作鎖住的是一個 segment,這樣只要保證每
個 Segment 是執行緒安全的,也就實現了全域性的執行緒安全。13/04/2018 Page 52 of 283
3.4.2.3. 並行度(預設 16)
concurrencyLevel:並行級別、併發數、Segment 數,怎麼翻譯不重要,理解它。預設是 16,
也就是說 ConcurrentHashMap 有 16 個 Segments,所以理論上,這個時候,最多可以同時支
持 16 個執行緒併發寫,只要它們的操作分別分佈在不同的 Segment 上。這個值可以在初始化的時
候設定為其他值,但是一旦初始化以後,它是不可以擴容的。再具體到每個 Segment 內部,其實
每個 Segment 很像之前介紹的 HashMap,不過它要保證執行緒安全,所以處理起來要麻煩些。
3.4.2.4. Java8 實現 (引入了紅黑樹)
Java8 對 ConcurrentHashMap 進行了比較大的改動,Java8 也引入了紅黑樹。13/04/2018 Page 53 of 283
3.4.3. HashTable(執行緒安全)
Hashtable 是遺留類,很多對映的常用功能與 HashMap 類似,不同的是它承自 Dictionary 類,
並且是執行緒安全的,任一時間只有一個執行緒能寫 Hashtable,併發性不如 ConcurrentHashMap,
因為 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新程式碼中使用,不需要執行緒安全
的場合可以用 HashMap 替換,需要執行緒安全的場合可以用 ConcurrentHashMap 替換。
3.4.4. TreeMap(可排序)
TreeMap 實現 SortedMap 介面,能夠把它儲存的記錄根據鍵排序,預設是按鍵值的升序排序,
也可以指定排序的比較器,當用 Iterator 遍歷 TreeMap 時,得到的記錄是排過序的。
如果使用排序的對映,建議使用 TreeMap。
在使用 TreeMap 時,key 必須實現 Comparable 介面或者在構造 TreeMap 傳入自定義的
Comparator,否則會在執行時丟擲 java.lang.ClassCastException 型別的異常。
參考:https://www.ibm.com/developerworks/cn/java/j-lo-tree/index.html
3.4.5. LinkHashMap(記錄插入順序)
LinkedHashMap 是 HashMap 的一個子類,儲存了記錄的插入順序,在用 Iterator 遍歷
LinkedHashMap 時,先得到的記錄肯定是先插入的,也可以在構造時帶引數,按照訪問次序排序。
參考 1:http://www.importnew.com/28263.html
參考 2:http://www.importnew.com/20386.html#comment-64812313/04/2018 Page 54 of 283 - JAVA 多執行緒併發
4.1.1. JAVA 併發知識庫
4.1.2. JAVA 執行緒實現/建立方式
4.1.2.1. 繼承 Thread 類
Thread 類本質上是實現了 Runnable 介面的一個例項,代表一個執行緒的例項。啟動執行緒的唯一方
法就是通過 Thread 類的 start()例項方法。start()方法是一個 native 方法,它將啟動一個新線
程,並執行 run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println(“MyThread.run()”);
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
4.1.2.2. 實現 Runnable 介面。
如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個
Runnable 介面。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println(“MyThread.run()”);
}
} 13/04/2018 Page 55 of 283
//啟動 MyThread,需要首先例項化一個 Thread,並傳入自己的 MyThread 例項:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事實上,當傳入一個 Runnable target 引數給 Thread 後,Thread 的 run()方法就會呼叫
target.run()
public void run() {
if (target != null) {
target.run();
}
}
4.1.2.3. ExecutorService、Callable、Future 有返回值執行緒
有返回值的任務必須實現 Callable 介面,類似的,無返回值的任務必須 Runnable 介面。執行
Callable 任務後,可以獲取一個 Future 的物件,在該物件上呼叫 get 就可以獲取到 Callable 任務
返回的 Object 了,再結合執行緒池介面 ExecutorService 就可以實現傳說中有返回結果的多執行緒
了。
//建立一個執行緒池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 建立多個有返回值的任務
List list = new ArrayList();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " “);
// 執行任務並獲取 Future 物件
Future f = pool.submit©;
list.add(f);
}
// 關閉執行緒池
pool.shutdown();
// 獲取所有併發任務的執行結果
for (Future f : list) {
// 從 Future 物件上獲取任務的返回值,並輸出到控制檯
System.out.println(“res:” + f.get().toString());
} 13/04/2018 Page 56 of 283
4.1.2.4. 基於執行緒池的方式
執行緒和資料庫連線這些資源都是非常寶貴的資源。那麼每次需要的時候建立,不需要的時候銷
毀,是非常浪費資源的。那麼我們就可以使用快取的策略,也就是使用執行緒池。
// 建立執行緒池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多個執行緒任務,並執行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running …”);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
} }
4.1.3. 4 種執行緒池
Java 裡面執行緒池的頂級介面是 Executor,但是嚴格意義上講 Executor 並不是一個執行緒池,而
只是一個執行執行緒的工具。真正的執行緒池介面是 ExecutorService。13/04/2018 Page 57 of 283
4.1.3.1. newCachedThreadPool
建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。對於執行
很多短期非同步任務的程式而言,這些執行緒池通常可提高程式效能。呼叫 execute 將重用以前構造
的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並
從快取中移除那些已有 60 秒鐘未被使用的執行緒。因此,長時間保持空閒的執行緒池不會使用任何資
源。
4.1.3.2. newFixedThreadPool
建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列方式來執行這些執行緒。在任意點,在大
多數 nThreads 執行緒會處於處理任務的活動狀態。如果在所有執行緒處於活動狀態時提交附加任務,
則在有可用執行緒之前,附加任務將在佇列中等待。如果在關閉前的執行期間由於失敗而導致任何
執行緒終止,那麼一個新執行緒將代替它執行後續的任務(如果需要)。在某個執行緒被顯式地關閉之
前,池中的執行緒將一直存在。13/04/2018 Page 58 of 283
4.1.3.3. newScheduledThreadPool
建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println(“延遲三秒”);
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println(“延遲 1 秒後每三秒執行一次”);
}
},1,3,TimeUnit.SECONDS);
4.1.3.4. newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一個執行緒池(這個執行緒池只有一個執行緒),這個執行緒
池可以線上程死後(或發生異常時)重新啟動一個執行緒來替代原來的執行緒繼續執行下去!
4.1.4. 執行緒生命週期(狀態)
當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。
線上程的生命週期中,它要經過新建(New)、就緒(Runnable)、執行(Running)、阻塞
(Blocked)和死亡(Dead)5 種狀態。尤其是當執行緒啟動以後,它不可能一直"霸佔"著 CPU 獨自
執行,所以 CPU 需要在多條執行緒之間切換,於是執行緒狀態也會多次在執行、阻塞之間切換
4.1.4.1. 新建狀態(NEW)
當程式使用 new 關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時僅由 JVM 為其分配
記憶體,並初始化其成員變數的值13/04/2018 Page 59 of 283
4.1.4.2. 就緒狀態(RUNNABLE):
當執行緒物件呼叫了 start()方法之後,該執行緒處於就緒狀態。Java 虛擬機器會為其建立方法呼叫棧和
程式計數器,等待排程執行。
4.1.4.3. 執行狀態(RUNNING):
如果處於就緒狀態的執行緒獲得了 CPU,開始執行 run()方法的執行緒執行體,則該執行緒處於執行狀
態。
4.1.4.4. 阻塞狀態(BLOCKED):
阻塞狀態是指執行緒因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice,暫時停止執行。
直到執行緒進入可執行(runnable)狀態,才有機會再次獲得 cpu timeslice 轉到執行(running)狀
態。阻塞的情況分三種:
等待阻塞(o.wait->等待對列):
執行(running)的執行緒執行 o.wait()方法,JVM 會把該執行緒放入等待佇列(waitting queue)
中。
同步阻塞(lock->鎖池)
執行(running)的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則 JVM 會把該線
程放入鎖池(lock pool)中。
其他阻塞(sleep/join)
執行(running)的執行緒執行 Thread.sleep(long ms)或 t.join()方法,或者發出了 I/O 請求時,
JVM 會把該執行緒置為阻塞狀態。當 sleep()狀態超時、join()等待執行緒終止或者超時、或者 I/O
處理完畢時,執行緒重新轉入可執行(runnable)狀態。
4.1.4.5. 執行緒死亡(DEAD)
執行緒會以下面三種方式結束,結束後就是死亡狀態。
正常結束 - run()或 call()方法執行完成,執行緒正常結束。
異常結束 - 執行緒丟擲一個未捕獲的 Exception 或 Error。
呼叫 stop - 直接呼叫該執行緒的 stop()方法來結束該執行緒—該方法通常容易導致死鎖,不推薦使用。13/04/2018 Page 60 of 283
4.1.5. 終止執行緒 4 種方式
4.1.5.1. 正常執行結束
程式執行結束,執行緒自動結束。
4.1.5.2. 使用退出標誌退出執行緒
一般 run()方法執行完,執行緒就會正常結束,然而,常常有些執行緒是伺服執行緒。它們需要長時間的
執行,只有在外部某些條件滿足的情況下,才能關閉這些執行緒。使用一個變數來控制迴圈,例如:
最直接的方法就是設一個 boolean 型別的標誌,並通過設定這個標誌為 true 或 false 來控制 while
迴圈是否退出,程式碼示例:
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定義了一個退出標誌 exit,當 exit 為 true 時,while 迴圈退出,exit 的預設值為 false.在定義 exit
時,使用了一個 Java 關鍵字 volatile,這個關鍵字的目的是使 exit 同步,也就是說在同一時刻只
能由一個執行緒來修改 exit 的值。
4.1.5.3. Interrupt 方法結束執行緒
使用 interrupt()方法來中斷執行緒有兩種情況:13/04/2018 Page 61 of 283 - 執行緒處於阻塞狀態:如使用了 sleep,同步鎖的 wait,socket 中的 receiver,accept 等方法時,
會使執行緒處於阻塞狀態。當呼叫執行緒的 interrupt()方法時,會丟擲 InterruptException 異常。
阻塞中的那個方法丟擲這個異常,通過程式碼捕獲該異常,然後 break 跳出迴圈狀態,從而讓
我們有機會結束這個執行緒的執行。通常很多人認為只要呼叫 interrupt 方法執行緒就會結束,實
際上是錯的, 一定要先捕獲 InterruptedException 異常之後通過 break 來跳出迴圈,才能正
常結束 run 方法。 - 執行緒未處於阻塞狀態:使用 isInterrupted()判斷執行緒的中斷標誌來退出迴圈。當使用
interrupt()方法時,中斷標誌就會置 true,和使用自定義的標誌來控制迴圈是一樣的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞過程中通過判斷中斷標誌來退出
try{
Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕獲到異常之後,執行 break 跳出迴圈
}
}
}
}
4.1.5.4. stop 方法終止執行緒(執行緒不安全)
程式中可以直接使用 thread.stop()來強行終止執行緒,但是 stop 方法是很危險的,就象突然關
閉計算機電源,而不是按正常程式關機一樣,可能會產生不可預料的結果,不安全主要是:
thread.stop()呼叫之後,建立子執行緒的執行緒就會丟擲 ThreadDeatherror 的錯誤,並且會釋放子
執行緒所持有的所有鎖。一般任何進行加鎖的程式碼塊,都是為了保護資料的一致性,如果在呼叫
thread.stop()後導致了該執行緒所持有的所有鎖的突然釋放(不可控制),那麼被保護資料就有可能呈
現不一致性,其他執行緒在使用這些被破壞的資料時,有可能導致一些很奇怪的應用程式錯誤。因
此,並不推薦使用 stop 方法來終止執行緒。
4.1.6. sleep 與 wait 區別 - 對於 sleep()方法,我們首先要知道該方法是屬於 Thread 類中的。而 wait()方法,則是屬於
Object 類中的。13/04/2018 Page 62 of 283 - sleep()方法導致了程式暫停執行指定的時間,讓出 cpu 該其他執行緒,但是他的監控狀態依然
保持者,當指定的時間到了又會自動恢復執行狀態。 - 在呼叫 sleep()方法的過程中,執行緒不會釋放物件鎖。
- 而當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此
物件呼叫 notify()方法後本執行緒才進入物件鎖定池準備獲取物件鎖進入執行狀態。
4.1.7. start 與 run 區別 - start()方法來啟動執行緒,真正實現了多執行緒執行。這時無需等待 run 方法體程式碼執行完畢,
可以直接繼續執行下面的程式碼。 - 通過呼叫 Thread 類的 start()方法來啟動一個執行緒, 這時此執行緒是處於就緒狀態, 並沒有運
行。 - 方法 run()稱為執行緒體,它包含了要執行的這個執行緒的內容,執行緒就進入了執行狀態,開始運
行 run 函式當中的程式碼。 Run 方法執行結束, 此執行緒終止。然後 CPU 再排程其它執行緒。
4.1.8. JAVA 後臺執行緒 - 定義:守護執行緒–也稱“服務執行緒”,他是後臺執行緒,它有一個特性,即為使用者執行緒 提供 公
共服務,在沒有使用者執行緒可服務時會自動離開。 - 優先順序:守護執行緒的優先順序比較低,用於為系統中的其它物件和執行緒提供服務。
- 設定:通過 setDaemon(true)來設定執行緒為“守護執行緒”;將一個使用者執行緒設定為守護執行緒
的方式是在 執行緒物件建立 之前 用執行緒物件的 setDaemon 方法。 - 在 Daemon 執行緒中產生的新執行緒也是 Daemon 的。
- 執行緒則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個執行緒,這個執行緒的
生命週期並不會和 Web 應用程式保持同步。也就是說,即使你停止了 Web 應用,這個執行緒
依舊是活躍的。 - example: 垃圾回收執行緒就是一個經典的守護執行緒,當我們的程式中不再有任何執行的Thread,
程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收執行緒是 JVM 上僅剩的線
程時,垃圾回收執行緒會自動離開。它始終在低階別的狀態中執行,用於實時監控和管理系統
中的可回收資源。 - 生命週期:守護程式(Daemon)是執行在後臺的一種特殊程式。它獨立於控制終端並且周
期性地執行某種任務或等待處理某些發生的事件。也就是說守護執行緒不依賴於終端,但是依
賴於系統,與系統“同生共死”。當 JVM 中所有的執行緒都是守護執行緒的時候,JVM 就可以退
出了;如果還有一個或以上的非守護執行緒則 JVM 不會退出。13/04/2018 Page 63 of 283
4.1.9. JAVA 鎖
4.1.9.1. 樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到併發寫的可能性低,每次去拿資料的時候都認為
別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數
據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),
如果失敗則要重複讀-比較-寫的操作。
java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入
值是否一樣,一樣則更新,否則失敗。
4.1.9.2. 悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到併發寫的可能性高,每次去拿資料的時候都認為別人
會修改,所以每次在讀寫資料的時候都會上鎖,這樣別人想讀寫這個資料就會 block 直到拿到鎖。
java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,
才會轉換為悲觀鎖,如 RetreenLock。
4.1.9.3. 自旋鎖
自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖
的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),
等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。
執行緒自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那執行緒
也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖
的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態。
自旋鎖的優缺點
自旋鎖儘可能的減少執行緒的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間非常短的程式碼塊來
說效能能大幅度的提升,因為自旋的消耗會小於執行緒阻塞掛起再喚醒的操作的消耗,這些操作會
導致執行緒發生兩次上下文切換!
但是如果鎖的競爭激烈,或者持有鎖的執行緒需要長時間佔用鎖執行同步塊,這時候就不適合
使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是佔用 cpu 做無用功,佔著 XX 不 XX,同時有大量
執行緒在競爭一個鎖,會導致獲取鎖的時間很長,執行緒自旋的消耗大於執行緒阻塞掛起操作的消耗,
其它需要 cup 的執行緒又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)
自旋鎖的目的是為了佔著 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇
自旋的執行時間呢?如果自旋執行時間太長,會有大量的執行緒處於自旋狀態佔用 CPU 資源,進而
會影響整體系統的效能。因此自旋的週期選的額外重要!13/04/2018 Page 64 of 283
JVM 對於自旋週期的選擇,jdk1.5 這個限度是一定的寫死的,在 1.6 引入了適應性自旋鎖,適應
性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁
有者的狀態來決定,基本認為一個執行緒上下文切換的時間是最佳的一個時間,同時 JVM 還針對當
前 CPU 的負荷情況做了較多的優化,如果平均負載小於 CPUs 則一直自旋,如果有超過(CPUs/2)
個執行緒正在自旋,則後來執行緒直接阻塞,如果正在自旋的執行緒發現 Owner 發生了變化則延遲自旋
時間(自旋計數)或進入阻塞,如果 CPU 處於節電模式則停止自旋,自旋時間的最壞情況是 CPU
的儲存延遲(CPU A 儲存了一個資料,到 CPU B 得知這個資料直接的時間差),自旋時會適當放
棄執行緒優先順序之間的差異。
自旋鎖的開啟
JDK1.6 中-XX:+UseSpinning 開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7 後,去掉此引數,由 jvm 控制;
4.1.9.4. Synchronized 同步鎖
synchronized 它可以把任意一個非 NULL 的物件當作鎖。他屬於獨佔式的悲觀鎖,同時屬於可重
入鎖。
Synchronized 作用範圍 - 作用於方法時,鎖住的是物件的例項(this);
- 當作用於靜態方法時,鎖住的是Class例項,又因為Class的相關資料儲存在永久帶PermGen
(jdk1.8 則是 metaspace),永久帶是全域性共享的,因此靜態方法鎖相當於類的一個全域性鎖,
會鎖所有呼叫該方法的執行緒; - synchronized 作用於一個物件例項時,鎖住的是所有以該物件為鎖的程式碼塊。它有多個佇列,
當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些執行緒儲存在不同的容器中。
Synchronized 核心元件
- Wait Set:哪些呼叫 wait 方法被阻塞的執行緒被放置在這裡;
- Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中;
- Entry List:Contention List 中那些有資格成為候選資源的執行緒被移動到 Entry List 中;
- OnDeck:任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為 OnDeck;
- Owner:當前已經獲取到所資源的執行緒被稱為 Owner;
- !Owner:當前釋放鎖的執行緒。
Synchronized 實現13/04/2018 Page 65 of 283
- JVM 每次從佇列的尾部取出一個資料用於鎖競爭候選者(OnDeck),但是併發情況下,
ContentionList 會被大量的併發執行緒進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將
一部分執行緒移動到 EntryList 中作為候選競爭執行緒。 - Owner 執行緒會在 unlock 時,將 ContentionList 中的部分執行緒遷移到 EntryList 中,並指定
EntryList 中的某個執行緒為 OnDeck 執行緒(一般是最先進去的那個執行緒)。 - Owner 執行緒並不直接把鎖傳遞給 OnDeck 執行緒,而是把鎖競爭的權利交給 OnDeck,
OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在
JVM 中,也把這種選擇行為稱之為“競爭切換”。 - OnDeck 執行緒獲取到鎖資源後會變為 Owner 執行緒,而沒有得到鎖資源的仍然停留在 EntryList
中。如果 Owner 執行緒被 wait 方法阻塞,則轉移到 WaitSet 佇列中,直到某個時刻通過 notify
或者 notifyAll 喚醒,會重新進去 EntryList 中。 - 處於 ContentionList、EntryList、WaitSet 中的執行緒都處於阻塞狀態,該阻塞是由作業系統
來完成的(Linux 核心下采用 pthread_mutex_lock 核心函式實現的)。 - Synchronized 是非公平鎖。 Synchronized 線上程進入 ContentionList 時,等待的執行緒會先
嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對於已經進入佇列的執行緒是
不公平的,還有一個不公平的事情就是自旋獲取鎖的執行緒還可能直接搶佔 OnDeck 執行緒的鎖
資源。
參考:https://blog.csdn.net/zqz_zqz/article/details/70233767 - 每個物件都有個 monitor 物件,加鎖就是在競爭 monitor 物件,程式碼塊加鎖是在前後分別加
上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的 - synchronized 是一個重量級操作,需要呼叫作業系統相關介面,效能是低效的,有可能給線
程加鎖消耗的時間比有用操作消耗的時間更多。 - Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向
鎖等,效率有了本質上的提高。在之後推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做
了優化。引入了偏向鎖和輕量級鎖。都是在物件頭中有標記位,不需要經過作業系統加鎖。 - 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;
- JDK 1.6 中預設是開啟偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖。13/04/2018 Page 66 of 283
4.1.9.5. ReentrantLock
ReentantLock 繼承介面 Lock 並實現了介面中定義的方法,他是一種可重入鎖,除了能完
成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等
避免多執行緒死鎖的方法。
Lock 介面的主要方法 - void lock(): 執行此方法時, 如果鎖處於空閒狀態, 當前執行緒將獲取到鎖. 相反, 如果鎖已經
被其他執行緒持有, 將禁用當前執行緒, 直到當前執行緒獲取到鎖. - boolean tryLock():如果鎖可用, 則獲取鎖, 並立即返回 true, 否則返回 false. 該方法和
lock()的區別在於, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前執行緒被禁用,
當前執行緒仍然繼續往下執行程式碼. 而 lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一
直等待, 在未獲得鎖之前,當前執行緒並不繼續向下執行. - void unlock():執行此方法時, 當前執行緒將釋放持有的鎖. 鎖只能由持有者釋放, 如果執行緒
並不持有鎖, 卻執行該方法, 可能導致異常的發生. - Condition newCondition():條件物件,獲取等待通知元件。該元件和當前的鎖繫結,
當前執行緒只有獲取了鎖,才能呼叫該元件的 await()方法,而呼叫後,當前執行緒將縮放鎖。 - getHoldCount() :查詢當前執行緒保持此鎖的次數,也就是執行此執行緒執行 lock 方法的次
數。 - getQueueLength():返回正等待獲取此鎖的執行緒估計數,比如啟動 10 個執行緒,1 個
執行緒獲得鎖,此時返回的是 9 - getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線
程估計數。比如 10 個執行緒,用同一個 condition 物件,並且此時這 10 個執行緒都執行了
condition 物件的 await 方法,那麼此時執行此方法返回 10 - hasWaiters(Condition condition):查詢是否有執行緒等待與此鎖有關的給定條件
(condition),對於指定 contidion 物件,有多少執行緒執行了 condition.await 方法 - hasQueuedThread(Thread thread):查詢給定執行緒是否等待獲取此鎖
- hasQueuedThreads():是否有執行緒等待此鎖
- isFair():該鎖是否公平鎖
- isHeldByCurrentThread(): 當前執行緒是否保持鎖鎖定,執行緒的執行 lock 方法的前後分
別是 false 和 true - isLock():此鎖是否有任意執行緒佔用
- lockInterruptibly():如果當前執行緒未被中斷,獲取鎖
- tryLock():嘗試獲得鎖,僅在呼叫時鎖未被執行緒佔用,獲得鎖
- tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個執行緒保持,
則獲取該鎖。
非公平鎖
JVM 按隨機、就近原則分配鎖的機制則稱為不公平鎖,ReentrantLock 在建構函式中提供了
是否公平鎖的初始化方式,預設為非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非
程式有特殊需要,否則最常用非公平鎖的分配機制。13/04/2018 Page 67 of 283
公平鎖
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的執行緒會先被分配到鎖,
ReentrantLock 在建構函式中提供了是否公平鎖的初始化方式來定義公平鎖。
ReentrantLock 與 synchronized - ReentrantLock 通過方法 lock()與 unlock()來進行加鎖與解鎖操作,與 synchronized 會 被 JVM 自動解鎖機制不同,ReentrantLock 加鎖後需要手動進行解鎖。為了避免程式出
現異常而無法正常解鎖的情況,使用 ReentrantLock 必須在 finally 控制塊中進行解鎖操
作。 - ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。這種情況下需要
使用 ReentrantLock。
ReentrantLock 實現
public class MyService {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平鎖
//Lock lock=new ReentrantLock(false);//非公平鎖
private Condition condition=lock.newCondition();//建立 Condition
public void testMethod() {
try {
lock.lock();//lock 加鎖
//1:wait 方法等待:
//System.out.println(“開始 wait”);
condition.await();
//通過建立 Condition 物件來使執行緒 wait,必須先執行 lock.lock 方法獲得鎖
//:2:signal 方法喚醒
condition.signal();//condition 物件的 signal 方法可以喚醒 wait 執行緒
for (int i = 0; i < 5; i++) {
System.out.println(“ThreadName=” + Thread.currentThread().getName()+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
finally13/04/2018 Page 68 of 283
{
lock.unlock();
}
} }
Condition 類和 Object 類鎖方法區別區別 - Condition 類的 awiat 方法和 Object 類的 wait 方法等效
- Condition 類的 signal 方法和 Object 類的 notify 方法等效
- Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
- ReentrantLock 類可以喚醒指定條件的執行緒,而 object 的喚醒是隨機的
tryLock 和 lock 和 lockInterruptibly 的區別 - tryLock 能獲得鎖就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit
unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false - lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖
- lock 和 lockInterruptibly,如果兩個執行緒分別執行這兩個方法,但此時中斷這兩個執行緒,
lock 不會丟擲異常,而 lockInterruptibly 會丟擲異常。
4.1.9.6. Semaphore 訊號量
Semaphore 是一種基於計數的訊號量。它可以設定一個閾值,基於此,多個執行緒競爭獲取許可信
號,做完自己的申請後歸還,超過閾值後,執行緒申請許可訊號將會被阻塞。Semaphore 可以用來
構建一些物件池,資源池之類的,比如資料庫連線池
實現互斥鎖(計數器為 1)
我們也可以建立計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元訊號量,
表示兩種互斥狀態。
程式碼實現
它的用法如下:
// 建立一個計數閾值為 5 的訊號量物件
// 只能 5 個執行緒同時訪問
Semaphore semp = new Semaphore(5);
try { // 申請許可
semp.acquire();
try {
// 業務邏輯13/04/2018 Page 69 of 283
} catch (Exception e) {
} finally {
// 釋放許可
semp.release();
}
} catch (InterruptedException e) {
}
Semaphore 與 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也與之類似,通過 acquire()與
release()方法來獲得和釋放臨界資源。經實測,Semaphone.acquire()方法預設為可響應中斷鎖,
與 ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被
Thread.interrupt()方法中斷。
此外,Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock
不同,其使用方法與 ReentrantLock 幾乎一致。Semaphore 也提供了公平與非公平鎖的機制,也
可在建構函式中進行設定。
Semaphore 的鎖釋放操作也由手動進行,因此與 ReentrantLock 一樣,為避免執行緒因丟擲異常而
無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 程式碼塊中完成。
4.1.9.7. AtomicInteger
首先說明,此處 AtomicInteger ,一個提供原子操作的 Integer 的類,常見的還有
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他們的實現原理相同,
區別在與運算物件型別的不同。令人興奮地,還可以通過 AtomicReference將一個物件的所
有操作轉化成原子操作。
我們知道,在多執行緒程式中,諸如++i 或 i++等運算不具有原子性,是不安全的執行緒操作之一。
通常我們會使用 synchronized 將該操作變成一個原子操作,但 JVM 為此類操作特意提供了一些
同步類,使得使用更方便,且使程式執行效率變得更高。通過相關資料顯示,通常AtomicInteger
的效能是 ReentantLock 的好幾倍。
4.1.9.8. 可重入鎖(遞迴鎖)
本文裡面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。可重入鎖,也叫
做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受
影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。13/04/2018 Page 70 of 283
4.1.9.9. 公平鎖與非公平鎖
公平鎖(Fair)
加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待 - 非公平鎖效能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個佇列
- Java 中的 synchronized 是非公平鎖,ReentrantLock 預設的 lock()方法採用的是非公平鎖。
4.1.9.10. ReadWriteLock 讀寫鎖
為了提高效能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如
果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程式的執行效率。讀寫鎖分為讀鎖和寫
鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可。
讀鎖
如果你的程式碼只讀資料,可以很多人同時讀,但不能同時寫,那就上讀鎖
寫鎖
如果你的程式碼修改資料,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上
讀鎖,寫的時候上寫鎖!
Java 中讀寫鎖有個介面 java.util.concurrent.locks.ReadWriteLock ,也有具體的實現
ReentrantReadWriteLock。
4.1.9.11. 共享鎖和獨佔鎖
java 併發包提供的加鎖模式分為獨佔鎖和共享鎖。
獨佔鎖
獨佔鎖模式下,每次只能有一個執行緒能持有鎖,ReentrantLock 就是以獨佔方式實現的互斥鎖。
獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,如果某個只讀執行緒獲取鎖,則其他讀線
程都只能等待,這種情況下就限制了不必要的併發性,因為讀操作並不會影響資料的一致性。
共享鎖
共享鎖則允許多個執行緒同時獲取鎖,併發訪問 共享資源,如:ReadWriteLock。共享鎖則是一種
樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的執行緒同時訪問共享資源。 - AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 佇列中等
待執行緒的鎖獲取模式。 - java 的併發包中提供了 ReadWriteLock,讀-寫鎖。它允許一個資源可以被多個讀操作訪問,
或者被一個 寫操作訪問,但兩者不能同時進行。13/04/2018 Page 71 of 283
4.1.9.12. 重量級鎖(Mutex Lock)
Synchronized 是通過物件內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又
是依賴於底層的作業系統的 Mutex Lock 來實現的。而作業系統實現執行緒之間的切換這就需要從用
戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼
Synchronized 效率低的原因。因此,這種依賴於作業系統 Mutex Lock 所實現的鎖我們稱之為
“重量級鎖”。JDK 中對 Synchronized 做的種種優化,其核心都是為了減少這種重量級鎖的使用。
JDK1.6 以後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,提高效能,引入了“輕量級鎖”和
“偏向鎖”。
4.1.9.13. 輕量級鎖
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
鎖升級
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,
也就是說只能從低到高升級,不會出現鎖的降級)。
“輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,
輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量
級鎖使用產生的效能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場
景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹
為重量級鎖。
4.1.9.14. 偏向鎖
Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一線
程多次獲得。偏向鎖的目的是在某個執行緒獲得鎖之後,消除這個執行緒鎖重入(CAS)的開銷,看起
來讓這個執行緒得到了偏護。引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級
鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換
ThreadID 的時候依賴一次 CAS 原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所
以偏向鎖的撤銷操作的效能損耗必須小於節省下來的 CAS 原子指令的效能消耗)。上面說過,輕
量級鎖是為了線上程交替執行同步塊時提高效能,而偏向鎖則是在只有一個執行緒執行同步塊時進
一步提高效能。
4.1.9.15. 分段鎖
分段鎖也並非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐
4.1.9.16. 鎖優化13/04/2018 Page 72 of 283
減少鎖持有時間
只用在有執行緒安全要求的程式上加鎖
減小鎖粒度
將大物件(這個物件可能會被很多執行緒訪問),拆成小物件,大大增加並行度,降低鎖競爭。
降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。最最典型的減小鎖粒度的案例就是
ConcurrentHashMap。
鎖分離
最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互
斥,讀寫互斥,寫寫互斥,即保證了執行緒安全,又提高了效能,具體也請檢視[高併發 Java 五]
JDK 併發包 1。讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。比如
LinkedBlockingQueue 從頭部取出,從尾部放資料
鎖粗化
通常情況下,為了保證多執行緒間的有效併發,會要求每個執行緒持有鎖的時間儘量短,即在使用完
公共資源後,應該立即釋放鎖。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步
和釋放,其本身也會消耗系統寶貴的資源,反而不利於效能的優化 。
鎖消除
鎖消除是在編譯器級別的事情。在即時編譯器時,如果發現不可能被共享的物件,則可以消除這
些物件的鎖操作,多數是因為程式設計師編碼不規範引起。
參考:https://www.jianshu.com/p/39628e1180a9
4.1.10. 執行緒基本方法
執行緒相關的基本方法有 wait,notify,notifyAll,sleep,join,yield 等。13/04/2018 Page 73 of 283
4.1.10.1. 執行緒等待(wait)
呼叫該方法的執行緒進入 WAITING 狀態,只有等待另外執行緒的通知或被中斷才會返回,需要注意的
是呼叫 wait()方法後,會釋放物件的鎖。因此,wait 方法一般用在同步方法或同步程式碼塊中。
4.1.10.2. 執行緒睡眠(sleep)
sleep 導致當前執行緒休眠,與 wait 方法不同的是 sleep 不會釋放當前佔有的鎖,sleep(long)會導致
執行緒進入 TIMED-WATING 狀態,而 wait()方法會導致當前執行緒進入 WATING 狀態
4.1.10.3. 執行緒讓步(yield)
yield 會使當前執行緒讓出 CPU 執行時間片,與其他執行緒一起重新競爭 CPU 時間片。一般情況下,
優先順序高的執行緒有更大的可能性成功競爭得到 CPU 時間片,但這又不是絕對的,有的作業系統對
執行緒優先順序並不敏感。
4.1.10.4. 執行緒中斷(interrupt)
中斷一個執行緒,其本意是給這個執行緒一個通知訊號,會影響這個執行緒內部的一箇中斷標識位。這
個執行緒本身並不會因此而改變狀態(如阻塞,終止等)。 - 呼叫 interrupt()方法並不會中斷一個正在執行的執行緒。也就是說處於 Running 狀態的線
程並不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。 - 若呼叫 sleep()而使執行緒處於 TIMED-WATING 狀態,這時呼叫 interrupt()方法,會丟擲
InterruptedException,從而使執行緒提前結束 TIMED-WATING 狀態。13/04/2018 Page 74 of 283 - 許多宣告丟擲 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),丟擲異
常前,都會清除中斷標識位,所以丟擲異常後,呼叫 isInterrupted()方法將會返回 false。 - 中斷狀態是執行緒固有的一個標識位,可以通過此標識位安全的終止執行緒。比如,你想終止
一個執行緒 thread 的時候,可以呼叫 thread.interrupt()方法,線上程的 run 方法內部可以
根據 thread.isInterrupted()的值來優雅的終止執行緒。
4.1.10.5. Join 等待其他執行緒終止
join() 方法,等待其他執行緒終止,在當前執行緒中呼叫一個執行緒的 join() 方法,則當前執行緒轉為阻塞
狀態,回到另一個執行緒結束,當前執行緒再由阻塞狀態變為就緒狀態,等待 cpu 的寵幸。
4.1.10.6. 為什麼要用 join()方法?
很多情況下,主執行緒生成並啟動了子執行緒,需要用到子執行緒返回的結果,也就是需要主執行緒需要
在子執行緒結束後再結束,這時候就要用到 join() 方法。
System.out.println(Thread.currentThread().getName() + “執行緒執行開始!”);
Thread6 thread1 = new Thread6();
thread1.setName(“執行緒 B”);
thread1.join();
System.out.println(“這時 thread1 執行完畢之後才能執行主執行緒”);
4.1.10.7. 執行緒喚醒(notify)
Object 類中的 notify() 方法,喚醒在此物件監視器上等待的單個執行緒,如果所有執行緒都在此物件
上等待,則會選擇喚醒其中一個執行緒,選擇是任意的,並在對實現做出決定時發生,執行緒通過調
用其中一個 wait() 方法,在物件的監視器上等待,直到當前的執行緒放棄此物件上的鎖定,才能繼
續執行被喚醒的執行緒,被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競
爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有執行緒。
4.1.10.8. 其他方法: - sleep():強迫一個執行緒睡眠N毫秒。
- isAlive(): 判斷一個執行緒是否存活。
- join(): 等待執行緒終止。
- activeCount(): 程式中活躍的執行緒數。
- enumerate(): 列舉程式中的執行緒。
- currentThread(): 得到當前執行緒。
- isDaemon(): 一個執行緒是否為守護執行緒。
- setDaemon(): 設定一個執行緒為守護執行緒。(使用者執行緒和守護執行緒的區別在於,是否等待主線
程依賴於主執行緒結束而結束) - setName(): 為執行緒設定一個名稱。
- wait(): 強迫一個執行緒等待。13/04/2018 Page 75 of 283
- notify(): 通知一個執行緒繼續執行。
- setPriority(): 設定一個執行緒的優先順序。
- getPriority()::獲得一個執行緒的優先順序。
4.1.11. 執行緒上下文切換
巧妙地利用了時間片輪轉的方式, CPU 給每個任務都服務一定的時間,然後把當前任務的狀態儲存
下來,在載入下一任務的狀態後,繼續服務下一任務,任務的狀態儲存及再載入, 這段過程就叫做
上下文切換。時間片輪轉的方式使多個任務在同一顆 CPU 上執行變成了可能。
4.1.11.1. 程式
(有時候也稱做任務)是指一個程式執行的例項。在 Linux 系統中,執行緒就是能並行執行並且
與他們的父程式(建立他們的程式)共享同一地址空間(一段記憶體區域)和其他資源的輕量
級的程式。
4.1.11.2. 上下文
是指某一時間點 CPU 暫存器和程式計數器的內容。
4.1.11.3. 暫存器
是 CPU 內部的數量較少但是速度很快的記憶體(與之對應的是 CPU 外部相對較慢的 RAM 主內
存)。暫存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程式執行的速
度。
4.1.11.4. 程式計數器
是一個專用的暫存器,用於表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令
的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。
4.1.11.5. PCB-“切換楨”
上下文切換可以認為是核心(作業系統的核心)在 CPU 上對於程式(包括執行緒)進行切換,上下
文切換過程中的資訊是儲存在程式控制塊(PCB, process control block)中的。PCB 還經常被稱
作“切換楨”(switchframe)。資訊會一直儲存到 CPU 的記憶體中,直到他們被再次使用。13/04/2018 Page 76 of 283
4.1.11.6. 上下文切換的活動: - 掛起一個程式,將這個程式在 CPU 中的狀態(上下文)儲存於記憶體中的某處。
- 在記憶體中檢索下一個程式的上下文並將其在 CPU 的暫存器中恢復。
- 跳轉到程式計數器所指向的位置(即跳轉到程式被中斷時的程式碼行),以恢復該程式在程式
中。
4.1.11.7. 引起執行緒上下文切換的原因 - 當前執行任務的時間片用完之後,系統 CPU 正常排程下一個任務;
- 當前執行任務碰到 IO 阻塞,排程器將此任務掛起,繼續下一任務;
- 多個任務搶佔鎖資源,當前任務沒有搶到鎖資源,被排程器掛起,繼續下一任務;
- 使用者程式碼掛起當前任務,讓出 CPU 時間;
- 硬體中斷;
4.1.12. 同步鎖與死鎖
4.1.12.1. 同步鎖
當多個執行緒同時訪問同一個資料時,很容易出現問題。為了避免這種情況出現,我們要保證執行緒
同步互斥,就是指併發執行的多個執行緒,在同一時間內只允許一個執行緒訪問共享資料。 Java 中可
以使用 synchronized 關鍵字來取得一個物件的同步鎖。
4.1.12.2. 死鎖
何為死鎖,就是多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。
4.1.13. 執行緒池原理
執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務放入佇列,然後線上程建立後
啟動這些任務,如果執行緒數量超過了最大數量超出數量的執行緒排隊等候,等其它執行緒執行完畢,
再從佇列中取出任務來執行。他的主要特點為:執行緒複用;控制最大併發數;管理執行緒。
4.1.13.1. 執行緒複用
每一個 Thread 的類都有一個 start 方法。 當呼叫 start 啟動執行緒時 Java 虛擬機器會呼叫該類的 run
方法。 那麼該類的 run() 方法中就是呼叫了 Runnable 物件的 run() 方法。 我們可以繼承重寫
Thread 類,在其 start 方法中新增不斷迴圈呼叫傳遞過來的 Runnable 物件。 這就是執行緒池的實
現原理。迴圈方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以
是阻塞的。
4.1.13.2. 執行緒池的組成
一般的執行緒池主要分為以下 4 個組成部分:13/04/2018 Page 77 of 283 - 執行緒池管理器:用於建立並管理執行緒池
- 工作執行緒:執行緒池中的執行緒
- 任務介面:每個任務必須實現的介面,用於工作執行緒排程其執行
- 任務佇列:用於存放待處理的任務,提供一種緩衝機制
Java 中的執行緒池是通過 Executor 框架實現的,該框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 這幾個類。
ThreadPoolExecutor 的構造方法如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
} - corePoolSize:指定了執行緒池中的執行緒數量。
- maximumPoolSize:指定了執行緒池中的最大執行緒數量。
- keepAliveTime:當前執行緒池數量超過 corePoolSize 時,多餘的空閒執行緒的存活時間,即多
次時間內會被銷燬。 - unit:keepAliveTime 的單位。
- workQueue:任務佇列,被提交但尚未被執行的任務。
- threadFactory:執行緒工廠,用於建立執行緒,一般用預設的即可。
- handler:拒絕策略,當任務太多來不及處理,如何拒絕任務。13/04/2018 Page 78 of 283
4.1.13.3. 拒絕策略
執行緒池中的執行緒已經用完了,無法繼續為新任務服務,同時,等待佇列也已經排滿了,再也
塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。
JDK 內建的拒絕策略如下: - AbortPolicy : 直接丟擲異常,阻止系統正常執行。
- CallerRunsPolicy : 只要執行緒池未關閉,該策略直接在呼叫者執行緒中,執行當前被丟棄的
任務。顯然這樣做不會真的丟棄任務,但是,任務提交執行緒的效能極有可能會急劇下降。 - DiscardOldestPolicy : 丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再
次提交當前任務。 - DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟
失,這是最好的一種方案。
以上內建拒絕策略均實現了 RejectedExecutionHandler 介面,若以上策略仍無法滿足實際
需要,完全可以自己擴充套件 RejectedExecutionHandler 介面。
4.1.13.4. Java 執行緒池工作過程 - 執行緒池剛建立時,裡面沒有一個執行緒。任務佇列是作為引數傳進來的。不過,就算佇列裡面
有任務,執行緒池也不會馬上執行它們。 - 當呼叫 execute() 方法新增一個任務時,執行緒池會做如下判斷:
a) 如果正在執行的執行緒數量小於 corePoolSize,那麼馬上建立執行緒執行這個任務;
b) 如果正在執行的執行緒數量大於或等於 corePoolSize,那麼將這個任務放入佇列;
c) 如果這時候佇列滿了,而且正在執行的執行緒數量小於 maximumPoolSize,那麼還是要
建立非核心執行緒立刻執行這個任務;
d) 如果佇列滿了,而且正在執行的執行緒數量大於或等於 maximumPoolSize,那麼執行緒池
會丟擲異常 RejectExecutionException。 - 當一個執行緒完成任務時,它會從佇列中取下一個任務來執行。
- 當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,如果當前運
行的執行緒數大於 corePoolSize,那麼這個執行緒就被停掉。所以執行緒池的所有任務完成後,它
最終會收縮到 corePoolSize 的大小。13/04/2018 Page 79 of 283
4.1.14. JAVA 阻塞佇列原理
阻塞佇列,關鍵字是阻塞,先理解阻塞的含義,在阻塞佇列中,執行緒阻塞有這樣的兩種情況: - 當佇列中沒有資料的情況下,消費者端的所有執行緒都會被自動阻塞(掛起),直到有資料放
入佇列。 - 當佇列中填滿資料的情況下,生產者端的所有執行緒都會被自動阻塞(掛起),直到佇列中有
空的位置,執行緒被自動喚醒。13/04/2018 Page 80 of 283
4.1.14.1. 阻塞佇列的主要方法
„ 丟擲異常:丟擲一個異常;
„ 特殊值:返回一個特殊值(null 或 false,視情況而定)
„ 則塞:在成功操作之前,一直阻塞執行緒
„ 超時:放棄前只在最大的時間內阻塞
插入操作:
1:public abstract boolean add(E paramE):將指定元素插入此佇列中(如果立即可行
且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則拋
出 IllegalStateException。如果該元素是 NULL,則會丟擲 NullPointerException 異常。
2:public abstract boolean offer(E paramE):將指定元素插入此佇列中(如果立即可行
且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則返回 false。 3:public abstract void put(E paramE) throws InterruptedException: 將指定元素插
入此佇列中,將等待可用的空間(如果有必要)
public void put(E paramE) throws InterruptedException {
checkNotNull(paramE);
ReentrantLock localReentrantLock = this.lock;
localReentrantLock.lockInterruptibly();
try {
while (this.count == this.items.length)
this.notFull.await();//如果佇列滿了,則執行緒阻塞等待
enqueue(paramE);13/04/2018 Page 81 of 283
localReentrantLock.unlock();
} finally {
localReentrantLock.unlock();
}
} 4:offer(E o, long timeout, TimeUnit unit):可以設定等待的時間,如果在指定的時間
內,還不能往佇列中加入 BlockingQueue,則返回失敗。
獲取資料操作: 1:poll(time):取走 BlockingQueue 裡排在首位的物件,若不能立即取出,則可以等 time 引數
規定的時間,取不到時返回 null;
2:poll(long timeout, TimeUnit unit):從 BlockingQueue 取出一個隊首的物件,如果在
指定時間內,佇列一旦有資料可取,則立即返回佇列中的資料。否則直到時間超時還沒有數
據可取,返回失敗。
3:take():取走 BlockingQueue 裡排在首位的物件,若 BlockingQueue 為空,阻斷進入等待狀
態直到 BlockingQueue 有新的資料被加入。
4.drainTo():一次性從 BlockingQueue 獲取所有可用的資料物件(還可以指定獲取資料的個
數),通過該方法,可以提升獲取資料效率;不需要多次分批加鎖或釋放鎖。
4.1.14.2. Java 中的阻塞佇列 - ArrayBlockingQueue :由陣列結構組成的有界阻塞佇列。
- LinkedBlockingQueue :由連結串列結構組成的有界阻塞佇列。
- PriorityBlockingQueue :支援優先順序排序的無界阻塞佇列。
- DelayQueue:使用優先順序佇列實現的無界阻塞佇列。
- SynchronousQueue:不儲存元素的阻塞佇列。
- LinkedTransferQueue:由連結串列結構組成的無界阻塞佇列。
- LinkedBlockingDeque:由連結串列結構組成的雙向阻塞佇列13/04/2018 Page 82 of 283
4.1.14.3. ArrayBlockingQueue(公平、非公平)
用陣列實現的有界阻塞佇列。此佇列按照先進先出(FIFO)的原則對元素進行排序。預設情況下
不保證訪問者公平的訪問佇列,所謂公平訪問佇列是指阻塞的所有生產者執行緒或消費者執行緒,當
佇列可用時,可以按照阻塞的先後順序訪問佇列,即先阻塞的生產者執行緒,可以先往佇列裡插入
元素,先阻塞的消費者執行緒,可以先從佇列裡獲取元素。通常情況下為了保證公平性會降低吞吐
量。我們可以使用以下程式碼建立一個公平的阻塞佇列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
4.1.14.4. LinkedBlockingQueue(兩個獨立鎖提高併發)
基於連結串列的阻塞佇列,同 ArrayListBlockingQueue 類似,此佇列按照先進先出(FIFO)的原則對
元素進行排序。而 LinkedBlockingQueue 之所以能夠高效的處理併發資料,還因為其對於生產者
端和消費者端分別採用了獨立的鎖來控制資料同步,這也意味著在高併發的情況下生產者和消費
者可以並行地操作佇列中的資料,以此來提高整個佇列的併發效能。
LinkedBlockingQueue 會預設一個類似無限大小的容量(Integer.MAX_VALUE)。
4.1.14.5. PriorityBlockingQueue(compareTo 排序實現優先)
是一個支援優先順序的無界佇列。預設情況下元素採取自然順序升序排列。可以自定義實現
compareTo()方法來指定元素進行排序規則,或者初始化 PriorityBlockingQueue 時,指定構造
引數 Comparator 來對元素進行排序。需要注意的是不能保證同優先順序元素的順序。
4.1.14.6. DelayQueue(快取失效、定時任務 )
是一個支援延時獲取元素的無界阻塞佇列。佇列使用 PriorityQueue 來實現。佇列中的元素必須實
現 Delayed 介面,在建立元素時可以指定多久才能從佇列中獲取當前元素。只有在延遲期滿時才
能從佇列中提取元素。我們可以將 DelayQueue 運用在以下應用場景: - 快取系統的設計:可以用 DelayQueue 儲存快取元素的有效期,使用一個執行緒迴圈查詢
DelayQueue,一旦能從 DelayQueue 中獲取元素時,表示快取有效期到了。13/04/2018 Page 83 of 283 - 定時任務排程:使用 DelayQueue 儲存當天將會執行的任務和執行時間,一旦從
DelayQueue 中獲取到任務就開始執行,從比如 TimerQueue 就是使用 DelayQueue 實現的。
4.1.14.7. SynchronousQueue(不儲存資料、可用於傳遞資料)
是一個不儲存元素的阻塞佇列。每一個 put 操作必須等待一個 take 操作,否則不能繼續新增元素。
SynchronousQueue 可以看成是一個傳球手,負責把生產者執行緒處理的資料直接傳遞給消費者線
程。佇列本身並不儲存任何元素,非常適合於傳遞性場景,比如在一個執行緒中使用的資料,傳遞給
另外一個執行緒使用, SynchronousQueue 的吞吐量高於 LinkedBlockingQueue 和
ArrayBlockingQueue。
4.1.14.8. LinkedTransferQueue
是一個由連結串列結構組成的無界阻塞 TransferQueue 佇列。相對於其他阻塞佇列,
LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。 - transfer 方法:如果當前有消費者正在等待接收元素(消費者使用 take()方法或帶時間限制的
poll()方法時),transfer 方法可以把生產者傳入的元素立刻 transfer(傳輸)給消費者。如
果沒有消費者在等待接收元素,transfer 方法會將元素存放在佇列的 tail 節點,並等到該元素
被消費者消費了才返回。 - tryTransfer 方法。則是用來試探下生產者傳入的元素是否能直接傳給消費者。如果沒有消費
者等待接收元素,則返回 false。和 transfer 方法的區別是 tryTransfer 方法無論消費者是否
接收,方法立即返回。而 transfer 方法是必須等到消費者消費了才返回。
對於帶有時間限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,則是試圖把生產者傳
入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時
還沒消費元素,則返回 false,如果在超時時間內消費了元素,則返回 true。
4.1.14.9. LinkedBlockingDeque
是一個由連結串列結構組成的雙向阻塞佇列。所謂雙向佇列指的你可以從佇列的兩端插入和移出元素。
雙端佇列因為多了一個操作佇列的入口,在多執行緒同時入隊時,也就減少了一半的競爭。相比其
他的阻塞佇列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,
peekFirst,peekLast 等方法,以 First 單詞結尾的方法,表示插入,獲取(peek)或移除雙端隊
列的第一個元素。以 Last 單詞結尾的方法,表示插入,獲取或移除雙端佇列的最後一個元素。另
外插入方法 add 等同於 addLast,移除方法 remove 等效於 removeFirst。但是 take 方法卻等同
於 takeFirst,不知道是不是 Jdk 的 bug,使用時還是用帶有 First 和 Last 字尾的方法更清楚。
在初始化 LinkedBlockingDeque 時可以設定容量防止其過渡膨脹。另外雙向阻塞佇列可以運用在
“工作竊取”模式中。13/04/2018 Page 84 of 283
4.1.15. CyclicBarrier、CountDownLatch、Semaphore 的用法
4.1.15.1. CountDownLatch(執行緒計數器 )
CountDownLatch 類位於 java.util.concurrent 包下,利用它可以實現類似計數器的功能。比如有
一個任務 A,它要等待其他 4 個任務執行完畢之後才能執行,此時就可以利用 CountDownLatch
來實現這種功能了。
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){public void run() {
System.out.println(“子執行緒”+Thread.currentThread().getName()+“正在執行”);
Thread.sleep(3000);
System.out.println(“子執行緒”+Thread.currentThread().getName()+“執行完畢”);
latch.countDown();
};}.start();
new Thread(){ public void run() {
System.out.println(“子執行緒”+Thread.currentThread().getName()+“正在執行”);
Thread.sleep(3000);
System.out.println(“子執行緒”+Thread.currentThread().getName()+“執行完畢”);
latch.countDown();
};}.start();
System.out.println(“等待 2 個子執行緒執行完畢…”);
latch.await();
System.out.println(“2 個子執行緒已經執行完畢”);
System.out.println(“繼續執行主執行緒”);
}
4.1.15.2. CyclicBarrier(迴環柵欄-等待至 barrier 狀態再全部同時執行)
字面意思迴環柵欄,通過它可以實現讓一組執行緒等待至某個狀態之後再全部同時執行。叫做迴環
是因為當所有等待執行緒都被釋放以後,CyclicBarrier 可以被重用。我們暫且把這個狀態就叫做
barrier,當呼叫 await()方法之後,執行緒就處於 barrier 了。
CyclicBarrier 中最重要的方法就是 await 方法,它有 2 個過載版本: - public int await():用來掛起當前執行緒,直至所有執行緒都到達 barrier 狀態再同時執行後續任
務; - public int await(long timeout, TimeUnit unit):讓這些執行緒等待至一定的時間,如果還有
執行緒沒有到達 barrier 狀態就直接讓到達 barrier 的執行緒執行後續任務。13/04/2018 Page 85 of 283
具體使用如下,另外 CyclicBarrier 是可以重用的。
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
Thread.sleep(5000); //以睡眠來模擬執行緒需要預定寫入資料操作
System.out.println(“執行緒”+Thread.currentThread().getName()+“寫入資料完
畢,等待其他執行緒寫入完畢”);
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println(“所有執行緒寫入完畢,繼續處理其他任務,比如資料操作”);
}
}
4.1.15.3. Semaphore(訊號量-控制同時訪問的執行緒個數) Semaphore 翻譯成字面意思為 訊號量,Semaphore 可以控制同時訪問的執行緒個數,通過
acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。
Semaphore 類中比較重要的幾個方法: - public void acquire(): 用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許
可。 - public void acquire(int permits):獲取 permits 個許可
- public void release() { } :釋放許可。注意,在釋放許可之前,必須先獲獲得許可。
- public void release(int permits) { }:釋放 permits 個許可
上面 4 個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法13/04/2018 Page 86 of 283 - public boolean tryAcquire():嘗試獲取一個許可,若獲取成功,則立即返回 true,若獲取失
敗,則立即返回 false - public boolean tryAcquire(long timeout, TimeUnit unit):嘗試獲取一個許可,若在指定的
時間內獲取成功,則立即返回 true,否則則立即返回 false - public boolean tryAcquire(int permits):嘗試獲取 permits 個許可,若獲取成功,則立即返
回 true,若獲取失敗,則立即返回 false - public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 嘗試獲取 permits
個許可,若在指定的時間內獲取成功,則立即返回 true,否則則立即返回 false - 還可以通過 availablePermits()方法得到可用的許可數目。
例子:若一個工廠有 5 臺機器,但是有 8 個工人,一臺機器同時只能被一個工人使用,只有使用完
了,其他工人才能繼續使用。那麼我們就可以通過 Semaphore 來實現:
int N = 8; //工人數
Semaphore semaphore = new Semaphore(5); //機器數目
for(int i=0;i<N;i++)
new Worker(i,semaphore).start();
}
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(“工人”+this.num+“佔用一個機器在生產…”);
Thread.sleep(2000);
System.out.println(“工人”+this.num+“釋放出機器”);
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
} } „ CountDownLatch 和 CyclicBarrier 都能夠實現執行緒之間的等待,只不過它們側重點不
同;CountDownLatch 一般用於某個執行緒 A 等待若干個其他執行緒執行完任務之後,它才13/04/2018 Page 87 of 283
執行;而 CyclicBarrier 一般用於一組執行緒互相等待至某個狀態,然後這一組執行緒再同時
執行;另外,CountDownLatch 是不能夠重用的,而 CyclicBarrier 是可以重用的。
„ Semaphore 其實和鎖有點類似,它一般用於控制對某組資源的訪問許可權。
4.1.16. volatile 關鍵字的作用(變數可見性、禁止重排序)
Java 語言提供了一種稍弱的同步機制,即 volatile 變數,用來確保將變數的更新操作通知到其他
執行緒。volatile 變數具備兩種特性,volatile 變數不會被快取在暫存器或者對其他處理器不可見的
地方,因此在讀取 volatile 型別的變數時總會返回最新寫入的值。
變數可見性
其一是保證該變數對所有執行緒可見,這裡的可見性指的是當一個執行緒修改了變數的值,那麼新的
值對於其他執行緒是可以立即獲取的。
禁止重排序
volatile 禁止了指令重排。 比 sychronized 更輕量級的同步鎖
在訪問 volatile 變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此 volatile 變數是一
種比 sychronized 關鍵字更輕量級的同步機制。volatile 適合這種場景:一個變數被多個執行緒共
享,執行緒直接給這個變數賦值。
當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體拷貝變數到 CPU 快取中。如果計算機有
多個 CPU,每個執行緒可能在不同的 CPU 上被處理,這意味著每個執行緒可以拷貝到不同的 CPU
cache 中。而宣告變數是 volatile 的,JVM 保證了每次讀變數都從記憶體中讀,跳過 CPU cache
這一步。
適用場景
值得說明的是對 volatile 變數的單次讀/寫操作可以保證原子性的,如 long 和 double 型別變數,
但是並不能保證 i++這種操作的原子性,因為本質上 i++是讀、寫兩次操作。在某些場景下可以
代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的場13/04/2018 Page 88 of 283
景下,才能適用 volatile。總的來說,必須同時滿足下面兩個條件才能保證在併發環境的執行緒安
全:
(1)對變數的寫操作不依賴於當前值(比如 i++),或者說是單純的變數賦值(boolean
flag = true)。 (2)該變數沒有包含在具有其他變數的不變式中,也就是說,不同的 volatile 變數之間,不
能互相依賴。只有在狀態真正獨立於程式內其他內容時才能使用 volatile。
4.1.17. 如何在兩個執行緒之間共享資料
Java 裡面進行多執行緒通訊的主要方式就是共享記憶體的方式,共享記憶體主要的關注點有兩個:可見
性和有序性原子性。Java 記憶體模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的
問題,理想情況下我們希望做到“同步”和“互斥”。有以下常規實現方法:
將資料抽象成一個類,並將資料的操作作為這個類的方法 - 將資料抽象成一個類,並將對這個資料的操作作為這個類的方法,這麼設計可以和容易做到
同步,只要在方法上加”synchronized“
public class MyData {
private int j=0;
public synchronized void add(){
j++;
System.out.println(“執行緒”+Thread.currentThread().getName()+“j 為:”+j);
}
public synchronized void dec(){
j–;
System.out.println(“執行緒”+Thread.currentThread().getName()+“j 為:”+j);
}
public int getData(){
return j;
} }
public class AddRunnable implements Runnable{
MyData data;
public AddRunnable(MyData data){
this.data= data;
} 13/04/2018 Page 89 of 283
public void run() {
data.add();
} }
public class DecRunnable implements Runnable {
MyData data;
public DecRunnable(MyData data){
this.data = data;
}
public void run() {
data.dec();
} }
public static void main(String[] args) {
MyData data = new MyData();
Runnable add = new AddRunnable(data);
Runnable dec = new DecRunnable(data);
for(int i=0;i<2;i++){
new Thread(add).start();
new Thread(dec).start();
}
Runnable 物件作為一個類的內部類 - 將 Runnable 物件作為一個類的內部類,共享資料作為這個類的成員變數,每個執行緒對共享數
據的操作方法也封裝在外部類,以便實現對資料的各個操作的同步和互斥,作為內部類的各
個 Runnable 物件呼叫外部類的這些方法。
public class MyData {
private int j=0;
public synchronized void add(){
j++;
System.out.println(“執行緒”+Thread.currentThread().getName()+“j 為:”+j);
}
public synchronized void dec(){
j–;
System.out.println(“執行緒”+Thread.currentThread().getName()+“j 為:”+j);
}
public int getData(){
return j;13/04/2018 Page 90 of 283
} }
public class TestThread {
public static void main(String[] args) {
final MyData data = new MyData();
for(int i=0;i<2;i++){
new Thread(new Runnable(){
public void run() {
data.add();
}
}).start();
new Thread(new Runnable(){
public void run() {
data.dec();
}
}).start();
}
} }
4.1.18. ThreadLocal 作用(執行緒本地儲存)
ThreadLocal,很多地方叫做執行緒本地變數,也有些地方叫做執行緒本地儲存,ThreadLocal 的作用
是提供執行緒內的區域性變數,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或
者元件之間一些公共變數的傳遞的複雜度。
ThreadLocalMap(執行緒的一個屬性) - 每個執行緒中都有一個自己的 ThreadLocalMap 類物件,可以將執行緒自己的物件保持到其中,
各管各的,執行緒可以正確的訪問到自己的物件。 - 將一個共用的 ThreadLocal 靜態例項作為 key,將不同物件的引用儲存到不同執行緒的
ThreadLocalMap 中,然後線上程執行的各處通過這個靜態 ThreadLocal 例項的 get()方法取
得自己執行緒儲存的那個物件,避免了將這個物件作為引數傳遞的麻煩。 - ThreadLocalMap 其實就是執行緒裡面的一個屬性,它在 Thread 類中定義
ThreadLocal.ThreadLocalMap threadLocals = null;13/04/2018 Page 91 of 283
使用場景
最常見的 ThreadLocal 使用場景為 用來解決 資料庫連線、Session 管理等。
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
4.1.19. synchronized 和 ReentrantLock 的區別
4.1.19.1. 兩者的共同點: - 都是用來協調多執行緒對共享物件、變數的訪問
- 都是可重入鎖,同一執行緒可以多次獲得同一個鎖
- 都保證了可見性和互斥性13/04/2018 Page 92 of 283
4.1.19.2. 兩者的不同點: - ReentrantLock 顯示的獲得、釋放鎖,synchronized 隱式獲得釋放鎖
- ReentrantLock 可響應中斷、可輪迴,synchronized 是不可以響應中斷的,為處理鎖的
不可用性提供了更高的靈活性 - ReentrantLock 是 API 級別的,synchronized 是 JVM 級別的
- ReentrantLock 可以實現公平鎖
- ReentrantLock 通過 Condition 可以繫結多個條件
- 底層實現不一樣, synchronized 是同步阻塞,使用的是悲觀併發策略,lock 是同步非阻
塞,採用的是樂觀併發策略 - Lock 是一個介面,而 synchronized 是 Java 中的關鍵字,synchronized 是內建的語言
實現。 - synchronized 在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;
而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放鎖,則很可能造成死鎖現象,
因此使用 Lock 時需要在 finally 塊中釋放鎖。 - Lock 可以讓等待鎖的執行緒響應中斷,而 synchronized 卻不行,使用 synchronized 時,
等待的執行緒會一直等待下去,不能夠響應中斷。 - 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。
- Lock 可以提高多個執行緒進行讀操作的效率,既就是實現讀寫鎖等。
4.1.20. ConcurrentHashMap 併發
4.1.20.1. 減小鎖粒度
減小鎖粒度是指縮小鎖定物件的範圍,從而減小鎖衝突的可能性,從而提高系統的併發能力。減
小鎖粒度是一種削弱多執行緒鎖競爭的有效手段,這種技術典型的應用是 ConcurrentHashMap(高
效能的 HashMap)類的實現。對於 HashMap 而言,最重要的兩個方法是 get 與 set 方法,如果我
們對整個 HashMap 加鎖,可以得到執行緒安全的物件,但是加鎖粒度太大。Segment 的大小也被
稱為 ConcurrentHashMap 的併發度。
4.1.20.2. ConcurrentHashMap 分段鎖
ConcurrentHashMap,它內部細分了若干個小的 HashMap,稱之為段(Segment)。預設情況下
一個 ConcurrentHashMap 被進一步細分為 16 個段,既就是鎖的併發度。
如果需要在 ConcurrentHashMap 中新增一個新的表項,並不是將整個 HashMap 加鎖,而是首
先根據 hashcode 得到該表項應該存放在哪個段中,然後對該段加鎖,並完成 put 操作。在多執行緒
環境中,如果多個執行緒同時進行 put操作,只要被加入的表項不存放在同一個段中,則執行緒間可以
做到真正的並行。13/04/2018 Page 93 of 283
ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成
ConcurrentHashMap 是由 Segment 陣列結構和 HashEntry 陣列結構組成。Segment 是一種可
重入鎖 ReentrantLock,在 ConcurrentHashMap 裡扮演鎖的角色,HashEntry 則用於儲存鍵值
對資料。一個 ConcurrentHashMap 裡包含一個 Segment 陣列,Segment 的結構和 HashMap
類似,是一種陣列和連結串列結構, 一個 Segment 裡包含一個 HashEntry 陣列,每個 HashEntry 是
一個連結串列結構的元素, 每個 Segment 守護一個 HashEntry 陣列裡的元素,當對 HashEntry 陣列的
資料進行修改時,必須首先獲得它對應的 Segment 鎖。
4.1.21. Java 中用到的執行緒排程
4.1.21.1. 搶佔式排程:
搶佔式排程指的是每條執行緒執行的時間、執行緒的切換都由系統控制,系統控制指的是在系統某種
執行機制下,可能每條執行緒都分同樣的執行時間片,也可能是某些執行緒執行的時間片較長,甚至
某些執行緒得不到執行的時間片。在這種機制下,一個執行緒的堵塞不會導致整個程式堵塞。
4.1.21.2. 協同式排程:
協同式排程指某一執行緒執行完後主動通知系統切換到另一執行緒上執行,這種模式就像接力賽一樣,
一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。執行緒的執行時間由執行緒
本身控制,執行緒切換可以預知,不存在多執行緒同步問題,但它有一個致命弱點:如果一個執行緒編
寫有問題,執行到一半就一直堵塞,那麼可能導致整個系統崩潰。13/04/2018 Page 94 of 283
4.1.21.3. JVM 的執行緒排程實現(搶佔式排程)
java 使用的執行緒調使用搶佔式排程,Java 中執行緒會按優先順序分配 CPU 時間片執行,且優先順序越高
越優先執行,但優先順序高並不代表能獨自佔用執行時間片,可能是優先順序高得到越多的執行時間
片,反之,優先順序低的分到的執行時間少但不會分配不到執行時間。
4.1.21.4. 執行緒讓出 cpu 的情況: - 當前執行執行緒主動放棄 CPU,JVM 暫時放棄 CPU 操作(基於時間片輪轉排程的 JVM 操作系
統不會讓執行緒永久放棄 CPU,或者說放棄本次時間片的執行權),例如呼叫 yield()方法。 - 當前執行執行緒因為某些原因進入阻塞狀態,例如阻塞在 I/O 上。
- 當前執行執行緒結束,即執行完 run()方法裡面的任務。
4.1.22. 程式排程演算法
4.1.22.1. 優先排程演算法 - 先來先服務排程演算法(FCFS)
當在作業排程中採用該演算法時,每次排程都是從後備作業佇列中選擇一個或多個最先進入該隊
列的作業,將它們調入記憶體,為它們分配資源、建立程式,然後放入就緒佇列。在程式排程中採
用 FCFS 演算法時,則每次排程是從就緒佇列中選擇一個最先進入該佇列的程式,為之分配處理機,13/04/2018 Page 95 of 283
使之投入執行。該程式一直執行到完成或發生某事件而阻塞後才放棄處理機,特點是:演算法比較
簡單,可以實現基本上的公平。 - 短作業(程式)優先排程演算法
短作業優先(SJF)的排程演算法是從後備佇列中選擇一個或若干個估計執行時間最短的作業,將它們
調入記憶體執行。而短程式優先(SPF)排程演算法則是從就緒佇列中選出一個估計執行時間最短的程式,
將處理機分配給它,使它立即執行並一直執行到完成,或發生某事件而被阻塞放棄處理機時再重
新排程。該演算法未照顧緊迫型作業。
4.1.22.2. 高優先權優先排程演算法
為了照顧緊迫型作業,使之在進入系統後便獲得優先處理,引入了最高優先權優先(FPF)排程
演算法。當把該演算法用於作業排程時,系統將從後備佇列中選擇若干個優先權最高的作業裝入記憶體。
當用於程式排程時,該演算法是把處理機分配給就緒佇列中優先權最高的程式。 - 非搶佔式優先權演算法
在這種方式下,系統一旦把處理機分配給就緒佇列中優先權最高的程式後,該程式便一直執行下
去,直至完成;或因發生某事件使該程式放棄處理機時。這種排程演算法主要用於批處理系統中;
也可用於某些對實時性要求不嚴的實時系統中。 - 搶佔式優先權排程演算法
在這種方式下,系統同樣是把處理機分配給優先權最高的程式,使之執行。但在其執行期間,只
要又出現了另一個其優先權更高的程式,程式排程程式就立即停止當前程式(原優先權最高的程式)
的執行,重新將處理機分配給新到的優先權最高的程式。顯然,這種搶佔式的優先權排程演算法能
更好地滿足緊迫作業的要求,故而常用於要求比較嚴格的實時系統中,以及對效能要求較高的批
處理和分時系統中。
2.高響應比優先排程演算法
在批處理系統中,短作業優先演算法是一種比較好的演算法,其主要的不足之處是長作業的執行
得不到保證。如果我們能為每個作業引入前面所述的動態優先權,並使作業的優先順序隨著等待時
間的增加而以速率 a 提高,則長作業在等待一定的時間後,必然有機會分配到處理機。該優先權的
變化規律可描述為:
(1) 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該演算法有利於
短作業。
(2) 當要求服務的時間相同時,作業的優先權決定於其等待時間,等待時間愈長,其優先權
愈高,因而它實現的是先來先服務。13/04/2018 Page 96 of 283
(3) 對於長作業,作業的優先順序可以隨等待時間的增加而提高,當其等待時間足夠長時,其
優先順序便可升到很高,從而也可獲得處理機。簡言之,該演算法既照顧了短作業,又考慮了作業到
達的先後次序,不會使長作業長期得不到服務。因此,該演算法實現了一種較好的折衷。當然,在
利用該演算法時,每要進行排程之前,都須先做響應比的計算,這會增加系統開銷。
4.1.22.3. 基於時間片的輪轉排程演算法 - 時間片輪轉法
在早期的時間片輪轉法中,系統將所有的就緒程式按先來先服務的原則排成一個佇列,每次排程
時,把 CPU 分配給隊首程式,並令其執行一個時間片。時間片的大小從幾 ms 到幾百 ms。當執行
的時間片用完時,由一個計時器發出時鐘中斷請求,排程程式便據此訊號來停止該程式的執行,
並將它送往就緒佇列的末尾;然後,再把處理機分配給就緒佇列中新的隊首程式,同時也讓它執
行一個時間片。這樣就可以保證就緒佇列中的所有程式在一給定的時間內均能獲得一時間片的處
理機執行時間。 - 多級反饋佇列排程演算法
(1) 應設定多個就緒佇列,併為各個佇列賦予不同的優先順序。第一個佇列的優先順序最高,第二
個佇列次之,其餘各佇列的優先權逐個降低。該演算法賦予各個佇列中程式執行時間片的大小也各
不相同,在優先權愈高的佇列中,為每個程式所規定的執行時間片就愈小。例如,第二個佇列的
時間片要比第一個佇列的時間片長一倍,……,第 i+1 個佇列的時間片要比第 i 個佇列的時間片長
一倍。
(2) 當一個新程式進入記憶體後,首先將它放入第一佇列的末尾,按 FCFS 原則排隊等待排程。當
輪到該程式執行時,如它能在該時間片內完成,便可準備撤離系統;如果它在一個時間片結束時
尚未完成,排程程式便將該程式轉入第二佇列的末尾,再同樣地按 FCFS 原則等待排程執行;如果
它在第二佇列中執行一個時間片後仍未完成,再依次將它放入第三佇列,……,如此下去,當一個
長作業(程式)從第一佇列依次降到第 n 佇列後,在第 n 佇列便採取按時間片輪轉的方式執行。
(3) 僅當第一佇列空閒時,排程程式才排程第二佇列中的程式執行;僅當第 1~(i-1)佇列均空時,
才會排程第 i 佇列中的程式執行。如果處理機正在第 i 佇列中為某程式服務時,又有新程式進入優
先權較高的佇列(第 1~(i-1)中的任何一個佇列),則此時新程式將搶佔正在執行程式的處理機,即
由排程程式把正在執行的程式放回到第 i 佇列的末尾,把處理機分配給新到的高優先權程式。
在多級反饋佇列排程演算法中,如果規定第一個佇列的時間片略大於多數人機互動所需之處理時間
時,便能夠較好的滿足各種型別使用者的需要。
4.1.23. 什麼是 CAS(比較並交換-樂觀鎖機制-鎖自旋)
4.1.23.1. 概念及特性
CAS(Compare And Swap/Set)比較並交換,CAS 演算法的過程是這樣:它包含 3 個引數
CAS(V,E,N)。V 表示要更新的變數(記憶體值),E 表示預期值(舊的),N 表示新值。當且僅當 V 值等13/04/2018 Page 97 of 283
於 E 值時,才會將 V 的值設為 N,如果 V 值和 E 值不同,則說明已經有其他執行緒做了更新,則當
前執行緒什麼都不做。最後,CAS 返回當前 V 的真實值。
CAS 操作是抱著樂觀的態度進行的(樂觀鎖),它總是認為自己可以成功完成操作。當多個執行緒同時
使用 CAS 操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛
起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,
CAS 操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。
4.1.23.2. 原子包 java.util.concurrent.atomic(鎖自旋)
JDK1.5 的原子包:java.util.concurrent.atomic 這個包裡面提供了一組原子類。其基本的特性就
是在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性,即當某個
執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等
到該方法執行完成,才由 JVM 從等待佇列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解。
相對於對於 synchronized 這種阻塞演算法,CAS 是非阻塞演算法的一種常見實現。由於一般 CPU 切
換時間比 CPU 指令集操作更加長, 所以 J.U.C 在效能上有了很大的提升。如下程式碼:
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;? { //CAS 自旋,一直嘗試,直達成功
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}13/04/2018 Page 98 of 283
getAndIncrement 採用了 CAS 操作,每次從記憶體中讀取資料然後將此資料和+1 後的結果進行
CAS 操作,如果成功就返回結果,否則重試直到成功為止。而 compareAndSet 利用 JNI 來完成
CPU 指令的操作。
4.1.23.3. ABA 問題
CAS 會導致“ABA 問題”。CAS 演算法實現一個重要前提需要取出記憶體中某時刻的資料,而在下時
刻比較並替換,那麼在這個時間差類會導致資料的變化。
比如說一個執行緒 one 從記憶體位置 V 中取出 A,這時候另一個執行緒 two 也從記憶體中取出 A,並且
two 進行了一些操作變成了 B,然後 two 又將 V 位置的資料變成 A,這時候執行緒 one 進行 CAS 操
作發現記憶體中仍然是 A,然後 one 操作成功。儘管執行緒 one 的 CAS 操作成功,但是不代表這個過
程就是沒有問題的。
部分樂觀鎖的實現是通過版本號(version)的方式來解決 ABA 問題,樂觀鎖每次在執行資料的修
改操作時,都會帶上一個版本號,一旦版本號和資料的版本號一致就可以執行修改操作並對版本
號執行+1 操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現 ABA 問
題,因為版本號只會增加不會減少。
4.1.24. 什麼是 AQS(抽象的佇列同步器)
AbstractQueuedSynchronizer 類如其名,抽象的佇列式的同步器,AQS 定義了一套多執行緒訪問
共享資源的同步器框架,許多同步類實現都依賴於它,如常用的
ReentrantLock/Semaphore/CountDownLatch。13/04/2018 Page 99 of 283
它維護了一個 volatile int state(代表共享資源)和一個 FIFO 執行緒等待佇列(多執行緒爭用資源被
阻塞時會進入此佇列)。這裡 volatile 是核心關鍵詞,具體 volatile 的語義,在此不述。state 的
訪問方式有三種:
getState()
setState()
compareAndSetState()
AQS 定義兩種資源共享方式
Exclusive 獨佔資源-ReentrantLock
Exclusive(獨佔,只有一個執行緒能執行,如 ReentrantLock)
Share 共享資源-Semaphore/CountDownLatch
Share(共享,多個執行緒可同時執行,如 Semaphore/CountDownLatch)。
AQS 只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實現,AQS 這裡只定義了一個
介面,具體資源的獲取交由自定義同步器去實現了(通過 state 的 get/set/CAS)之所以沒有定義成
abstract ,是因為獨佔模式下只用實現 tryAcquire-tryRelease ,而共享模式下只用實現
tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模
式下的介面。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實
現共享資源 state 的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/
喚醒出隊等),AQS 已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
1.isHeldExclusively():該執行緒是否正在獨佔資源。只有用到 condition 才需要去實現它。
2.tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回 true,失敗則返回 false。 3.tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回 true,失敗則返回 false。 4.tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0 表示成功,但沒有剩餘
可用資源;正數表示成功,且有剩餘資源。
5.tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回
true,否則返回 false。13/04/2018 Page 100 of 283
同步器的實現是 ABS 核心(state 資源狀態計數)
同步器的實現是 ABS 核心,以 ReentrantLock 為例,state 初始化為 0,表示未鎖定狀態。A 執行緒
lock()時,會呼叫 tryAcquire()獨佔該鎖並將 state+1。此後,其他執行緒再 tryAcquire()時就會失
敗,直到 A 執行緒 unlock()到 state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖。當然,釋放
鎖之前,A 執行緒自己是可以重複獲取此鎖的(state 會累加),這就是可重入的概念。但要注意,
獲取多少次就要釋放多麼次,這樣才能保證 state 是能回到零態的。
以 CountDownLatch 以例,任務分為 N 個子執行緒去執行,state 也初始化為 N(注意 N 要與
執行緒個數一致)。這 N 個子執行緒是並行執行的,每個子執行緒執行完後 countDown()一次,state
會 CAS 減 1。等到所有子執行緒都執行完後(即 state=0),會 unpark()主呼叫執行緒,然後主呼叫執行緒
就會從 await()函式返回,繼續後餘動作。
ReentrantReadWriteLock 實現獨佔和共享兩種方式
一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一種即可。但 AQS 也支援自定義同步器
同時實現獨佔和共享兩種方式,如 ReentrantReadWriteLock。13/04/2018 Page 101 of 283 - JAVA 基礎
5.1.1. JAVA 異常分類及處理
5.1.1.1. 概念
如果某個方法不能按照正常的途徑完成任務,就可以通過另一種路徑退出方法。在這種情況下
會丟擲一個封裝了錯誤資訊的物件。此時,這個方法會立刻退出同時不返回任何值。另外,呼叫
這個方法的其他程式碼也無法繼續執行,異常處理機制會將程式碼執行交給異常處理器。
5.1.1.2. 異常分類
Throwable 是 Java 語言中所有錯誤或異常的超類。下一層分為 Error 和 Exception
Error - Error 類是指 java 執行時系統的內部錯誤和資源耗盡錯誤。應用程式不會丟擲該類物件。如果
出現了這樣的錯誤,除了告知使用者,剩下的就是盡力使程式安全的終止。
Exception(RuntimeException、CheckedException) - Exception 又有兩個分支,一個是執行時異常 RuntimeException ,一個是
CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一個是檢查異常
CheckedException,如 I/O 錯誤導致的 IOException、SQLException。 RuntimeException 是
那些可能在 Java 虛擬機器正常執行期間丟擲的異常的超類。 如果出現 RuntimeException,那麼一
定是程式設計師的錯誤.13/04/2018 Page 102 of 283
檢查異常 CheckedException:一般是外部錯誤,這種異常都發生在編譯階段,Java 編譯器會強
製程式去捕獲此類異常,即會出現要求你把這段可能出現異常的程式進行 try catch,該類異常一
般包括幾個方面: - 試圖在檔案尾部讀取資料
- 試圖開啟一個錯誤格式的 URL
- 試圖根據給定的字串查詢 class 物件,而這個字串表示的類並不存在
5.1.1.3. 異常的處理方式
遇到問題不進行具體處理,而是繼續拋給呼叫者 (throw,throws)
丟擲異常有三種形式,一是 throw,一個 throws,還有一種系統自動拋異常。
public static void main(String[] args) {
String s = “abc”;
if(s.equals(“abc”)) {
throw new NumberFormatException();
} else {
System.out.println(s);
}
}
int div(int a,int b) throws Exception{
return a/b;}
try catch 捕獲異常針對性處理方式
5.1.1.4. Throw 和 throws 的區別:
位置不同 - throws 用在函式上,後面跟的是異常類,可以跟多個;而 throw 用在函式內,後面跟的
是異常物件。
功能不同: - throws 用來宣告異常,讓呼叫者只知道該功能可能出現的問題,可以給出預先的處理方
式;throw 丟擲具體的問題物件,執行到 throw,功能就已經結束了,跳轉到呼叫者,並
將具體的問題物件拋給呼叫者。也就是說 throw 語句獨立存在時,下面不要定義其他語
句,因為執行不到。 - throws 表示出現異常的一種可能性,並不一定會發生這些異常;throw 則是丟擲了異常,
執行 throw 則一定丟擲了某種異常物件。13/04/2018 Page 103 of 283 - 兩者都是消極處理異常的方式,只是丟擲或者可能丟擲異常,但是不會由函式去處理異
常,真正的處理異常由函式的上層呼叫處理。
5.1.2. JAVA 反射
5.1.2.1. 動態語言
動態語言,是指程式在執行時可以改變其結構:新的函式可以引進,已有的函式可以被刪除等結
構上的變化。比如常見的 JavaScript 就是動態語言,除此之外 Ruby,Python 等也屬於動態語言,
而 C、C++則不屬於動態語言。從反射角度說 JAVA 屬於半動態語言。
5.1.2.2. 反射機制概念 (執行狀態中知道類所有的屬性和方法) 在 Java 中的反射機制是指在執行狀態中,對於任意一個類都能夠知道這個類所有的屬性和方法;
並且對於任意一個物件,都能夠呼叫它的任意一個方法;這種動態獲取資訊以及動態呼叫物件方
法的功能成為 Java 語言的反射機制。
5.1.2.3. 反射的應用場合
編譯時型別和執行時型別
在 Java 程式中許多物件在執行是都會出現兩種型別:編譯時型別和執行時型別。 編譯時的型別由
宣告物件時實用的型別來決定,執行時的型別由實際賦值給物件的型別決定 。如:
Person p=new Student();
其中編譯時型別為 Person,執行時型別為 Student。13/04/2018 Page 104 of 283
的編譯時型別無法獲取具體方法
程式在執行時還可能接收到外部傳入的物件,該物件的編譯時型別為 Object,但是程式有需要呼叫
該物件的執行時型別的方法。為了解決這些問題,程式需要在執行時發現物件和類的真實資訊。
然而,如果編譯時根本無法預知該物件和類屬於哪些類,程式只能依靠執行時資訊來發現該物件
和類的真實資訊,此時就必須使用到反射了。
5.1.2.4. Java 反射 API
反射 API 用來生成 JVM 中的類、介面或則物件的資訊。 - Class 類:反射的核心類,可以獲取類的屬性,方法等資訊。
- Field 類:Java.lang.reflec 包中的類,表示類的成員變數,可以用來獲取和設定類之中的屬性
值。 - Method 類: Java.lang.reflec 包中的類,表示類的方法,它可以用來獲取類中的方法資訊或
者執行方法。 - Constructor 類: Java.lang.reflec 包中的類,表示類的構造方法。
5.1.2.5. 反射使用步驟(獲取 Class 物件、呼叫物件方法) - 獲取想要操作的類的 Class 物件,他是反射的核心,通過 Class 物件我們可以任意呼叫類的方
法。 - 呼叫 Class 類中的方法,既就是反射的使用階段。
- 使用反射 API 來操作這些資訊。
5.1.2.6. 獲取 Class 物件的 3 種方法
呼叫某個物件的 getClass()方法
Person p=new Person();
Class clazz=p.getClass();
呼叫某個類的 class 屬性來獲取該類對應的 Class 物件
Class clazz=Person.class;
使用 Class 類中的 forName()靜態方法(最安全/效能最好)
Class clazz=Class.forName(“類的全路徑”); (最常用)
當我們獲得了想要操作的類的 Class 物件後,可以通過 Class 類中的方法獲取並檢視該類中的方法
和屬性。
//獲取 Person 類的 Class 物件
Class clazz=Class.forName(“reflection.Person”);13/04/2018 Page 105 of 283
//獲取 Person 類的所有方法資訊
Method[] method=clazz.getDeclaredMethods();
for(Method m:method){
System.out.println(m.toString());
}
//獲取 Person 類的所有成員屬性資訊
Field[] field=clazz.getDeclaredFields();
for(Field f:field){
System.out.println(f.toString());
}
//獲取 Person 類的所有構造方法資訊
Constructor[] constructor=clazz.getDeclaredConstructors();
for(Constructor c:constructor){
System.out.println(c.toString());
}
5.1.2.7. 建立物件的兩種方法
Class 物件的 newInstance() - 使用 Class 物件的 newInstance()方法來建立該 Class 物件對應類的例項,但是這種方法要求
該 Class 物件對應的類有預設的空構造器。
呼叫 Constructor 物件的 newInstance() - 先使用 Class 物件獲取指定的 Constructor 物件,再呼叫 Constructor 物件的 newInstance()
方法來建立 Class 物件對應類的例項,通過這種方法可以選定構造方法建立例項。
//獲取 Person 類的 Class 物件
Class clazz=Class.forName(“reflection.Person”);
//使用.newInstane 方法建立物件
Person p=(Person) clazz.newInstance();
//獲取構造方法並建立物件
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
//建立物件並設定屬性13/04/2018 Page 106 of 283
Person p1=(Person) c.newInstance(“李四”,“男”,20);
5.1.3. JAVA 註解
5.1.3.1. 概念
Annotation(註解)是 Java 提供的一種對元程式中元素關聯資訊和後設資料(metadata)的途徑
和方法。Annatation(註解)是一個介面,程式可以通過反射來獲取指定程式中元素的 Annotation
物件,然後通過該 Annotation 物件來獲取註解中的後設資料資訊。
5.1.3.2. 4 種標準元註解
元註解的作用是負責註解其他註解。 Java5.0 定義了 4 個標準的 meta-annotation 型別,它們被
用來提供對其它 annotation 型別作說明。
@Target 修飾的物件範圍
@Target說明了Annotation所修飾的物件範圍: Annotation可被用於 packages、types(類、
介面、列舉、Annotation 型別)、型別成員(方法、構造方法、成員變數、列舉值)、方法引數
和本地變數(如迴圈變數、catch 引數)。在 Annotation 型別的宣告中使用了 target 可更加明晰
其修飾的目標
@Retention 定義 被保留的時間長短
Retention 定義了該 Annotation 被保留的時間長短:表示需要在什麼級別儲存註解資訊,用於描
述註解的生命週期(即:被描述的註解在什麼範圍內有效),取值(RetentionPoicy)由:
„ SOURCE:在原始檔中有效(即原始檔保留)
„ CLASS:在 class 檔案中有效(即 class 保留)
„ RUNTIME:在執行時有效(即執行時保留)
@Documented ᧿述-javadoc
@ Documented 用於描述其它型別的 annotation 應該被作為被標註的程式成員的公共 API,因
此可以被例如 javadoc 此類的工具文件化。
@Inherited 闡述了某個被標註的型別是被繼承的
@Inherited 元註解是一個標記註解,@Inherited 闡述了某個被標註的型別是被繼承的。如果一
個使用了@Inherited 修飾的 annotation 型別被用於一個 class,則這個 annotation 將被用於該
class 的子類。13/04/2018 Page 107 of 283
5.1.3.3. 註解處理器
如果沒有用來讀取註解的方法和工作,那麼註解也就不會比註釋更有用處了。使用註解的過程中,
很重要的一部分就是建立於使用註解處理器。Java SE5 擴充套件了反射機制的 API,以幫助程式設計師快速
的構造自定義註解處理器。下面實現一個註解處理器。
/1:*** 定義註解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/供應商編號/
public int id() default -1;
/** 供應商名稱*/
public String name() default “”;13/04/2018 Page 108 of 283
/** * 供應商地址*/
public String address() default “”;
}
//2:註解使用
public class Apple {
@FruitProvider(id = 1, name = “陝西紅富士集團”, address = “陝西省西安市延安路”)
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
} }/3:*********** 註解處理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = “供應商資訊:”;
Field[] fields = clazz.getDeclaredFields();//通過反射獲取處理註解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//註解資訊的處理地方
strFruitProvicer = " 供應商編號:" + fruitProvider.id() + " 供應商名稱:"
- fruitProvider.name() + " 供應商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
} }13/04/2018 Page 109 of 283
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/輸出結果****/
// 供應商編號:1 供應商名稱:陝西紅富士集團 供應商地址:陝西省西安市延
} }
5.1.4. JAVA 內部類
Java 類中不僅可以定義變數和方法,還可以定義類,這樣定義在類內部的類就被稱為內部類。根
據定義的方式不同,內部類分為靜態內部類,成員內部類,區域性內部類,匿名內部類四種。
5.1.4.1. 靜態內部類
定義在類內部的靜態類,就是靜態內部類。
public class Out {
private static int a;
private int b;
public static class Inner {
public void print() {
System.out.println(a);
}
} }
- 靜態內部類可以訪問外部類所有的靜態變數和方法,即使是 private 的也一樣。
- 靜態內部類和一般類一致,可以定義靜態變數、方法,構造方法等。
- 其它類使用靜態內部類需要使用“外部類.靜態內部類”方式,如下所示:Out.Inner inner =
new Out.Inner();inner.print(); - Java集合類HashMap內部就有一個靜態內部類Entry。Entry是HashMap存放元素的抽象,
HashMap 內部維護 Entry 陣列用了存放元素,但是 Entry 對使用者是透明的。像這種和外部
類關係密切的,且不依賴外部類例項的,都可以使用靜態內部類。13/04/2018 Page 110 of 283
5.1.4.2. 成員內部類
定義在類內部的非靜態類,就是成員內部類。成員內部類不能定義靜態方法和變數(final 修飾的
除外)。這是因為成員內部類是非靜態的,類初始化的時候先初始化靜態成員,如果允許成員內
部類定義靜態變數,那麼成員內部類的靜態變數初始化順序是有歧義的。
public class Out {
private static int a;
private int b;
public class Inner {
public void print() {
System.out.println(a);
System.out.println(b);
}
} }
5.1.4.3. 區域性內部類(定義在方法中的類)
定義在方法中的類,就是區域性類。如果一個類只在某個方法中使用,則可以考慮使用區域性類。
public class Out {
private static int a;
private int b;
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println©;
}
}
} }13/04/2018 Page 111 of 283
5.1.4.4. 匿名內部類(要繼承一個父類或者實現一個介面、直接使用
new 來生成一個物件的引用)
匿名內部類我們必須要繼承一個父類或者實現一個介面,當然也僅能只繼承一個父類或者實現一
個介面。同時它也是沒有 class 關鍵字,這是因為匿名內部類是直接使用 new 來生成一個物件的引
用。
public abstract class Bird {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract int fly();
}
public class Test {
public void test(Bird bird){
System.out.println(bird.getName() + "能夠飛 " + bird.fly() + “米”);
}
public static void main(String[] args) {
Test test = new Test();
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return “大雁”;
}
});
} }13/04/2018 Page 112 of 283
5.1.5. JAVA 泛型
泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。泛型的本
質是引數化型別,也就是說所操作的資料型別被指定為一個引數。比如我們要寫一個排序方法,
能夠對整型陣列、字串陣列甚至其他任何型別的陣列進行排序,我們就可以使用 Java 泛型。
5.1.5.1. 泛型方法()
你可以寫一個泛型方法,該方法在呼叫時可以接收不同型別的引數。根據傳遞給泛型方法的引數
型別,編譯器適當地處理每一個方法呼叫。
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
} - <? extends T>表示該萬用字元所代表的型別是 T 型別的子類。
- <? super T>表示該萬用字元所代表的型別是 T 型別的父類。
5.1.5.2. 泛型類
泛型類的宣告和非泛型類的宣告類似,除了在類名後面新增了型別引數宣告部分。和泛型方法一
樣,泛型類的型別引數宣告部分也包含一個或多個型別引數,引數間用逗號隔開。一個泛型引數,
也被稱為一個型別變數,是用於指定一個泛型型別名稱的識別符號。因為他們接受一個或多個引數,
這些類被稱為引數化的類或引數化的型別。
public class Box {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}13/04/2018 Page 113 of 283
5.1.5.3. 型別萬用字元?
型別萬用字元一般是使用 ? 代替具體的型別引數。例如 List<?> 在邏輯上是
List,List 等所有 List<具體型別實參>的父類。
5.1.5.4. 型別擦除
Java 中的泛型基本上都是在編譯器這個層次來實現的。在生成的 Java 位元組程式碼中是不包含泛
型中的型別資訊的。使用泛型的時候加上的型別引數,會被編譯器在編譯的時候去掉。這個
過程就稱為型別擦除。如在程式碼中定義的 List和 List等型別,在編譯之後
都會變成 List。JVM 看到的只是 List,而由泛型附加的型別資訊對 JVM 來說是不可見的。
型別擦除的基本過程也比較簡單,首先是找到用來替換型別引數的具體類。這個具體類一般
是 Object。如果指定了型別引數的上界的話,則使用這個上界。把程式碼中的型別引數都替換
成具體的類。
5.1.6. JAVA 序列化(建立可複用的 Java 物件)
儲存(持久化)物件及其狀態到記憶體或者磁碟
Java 平臺允許我們在記憶體中建立可複用的 Java 物件,但一般情況下,只有當 JVM 處於執行時,
這些物件才可能存在,即,這些物件的生命週期不會比 JVM 的生命週期更長。但在現實應用中,
就可能要求在JVM停止執行之後能夠儲存(持久化)指定的物件,並在將來重新讀取被儲存的物件。
Java 物件序列化就能夠幫助我們實現該功能。
序列化物件以位元組陣列保持-靜態成員不儲存
使用 Java 物件序列化,在儲存物件時,會把其狀態儲存為一組位元組,在未來,再將這些位元組組裝
成物件。必須注意地是,物件序列化儲存的是物件的”狀態”,即它的成員變數。由此可知,對
象序列化不會關注類中的靜態變數。
序列化使用者遠端物件傳輸
除了在持久化物件時會用到物件序列化之外,當使用 RMI(遠端方法呼叫),或在網路中傳遞物件時,
都會用到物件序列化。Java序列化API為處理物件序列化提供了一個標準機制,該API簡單易用。
Serializable 實現序列化
在 Java 中,只要一個類實現了 java.io.Serializable 介面,那麼它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 對物件進行序列化及反序列化
通過 ObjectOutputStream 和 ObjectInputStream 對物件進行序列化及反序列化。
writeObject 和 readObject 自定義序列化策略
在類中增加 writeObject 和 readObject 方法可以實現自定義序列化策略。
序列化 ID
虛擬機器是否允許反序列化,不僅取決於類路徑和功能程式碼是否一致,一個非常重要的一點是兩個
類的序列化 ID 是否一致(就是 private static final long serialVersionUID)13/04/2018 Page 114 of 283
序列化並不儲存靜態變數
序列化子父類說明
要想將父類物件也序列化,就需要讓父類也實現 Serializable 介面。
Transient 關鍵字阻止該變數被序列化到檔案中
- 在變數宣告前加上 Transient 關鍵字,可以阻止該變數被序列化到檔案中,在被反序列
化後,transient 變數的值被設為初始值,如 int 型的是 0,物件型的是 null。 - 伺服器端給客戶端傳送序列化物件資料,物件中有一些資料是敏感的,比如密碼字串
等,希望對該密碼欄位在序列化時,進行加密,而客戶端如果擁有解密的金鑰,只有在
客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化物件的
資料安全。
5.1.7. JAVA 複製
將一個物件的引用複製給另外一個物件,一共有三種方式。第一種方式是直接賦值,第二種方式
是淺拷貝,第三種是深拷貝。所以大家知道了哈,這三種概念實際上都是為了拷貝物件。
5.1.7.1. 直接賦值複製
直接賦值。在 Java 中,A a1 = a2,我們需要理解的是這實際上覆制的是引用,也就是
說 a1 和 a2 指向的是同一個物件。因此,當 a1 變化的時候,a2 裡面的成員變數也會跟
著變化。
5.1.7.2. 淺複製(複製引用但不復制引用的物件)
建立一個新物件,然後將當前物件的非靜態欄位複製到該新物件,如果欄位是值型別的,
那麼對該欄位執行復制;如果該欄位是引用型別的話,則複製引用但不復制引用的物件。
因此,原始物件及其副本引用同一個物件。
class Resume implements Cloneable{
public Object clone() {
try {
return (Resume)super.clone();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
} 13/04/2018 Page 115 of 283
5.1.7.3. 深複製(複製物件和其應用物件)
深拷貝不僅複製物件本身,而且複製物件包含的引用指向的所有物件。
class Student implements Cloneable {
String name;
int age;
Professor p;
Student(String name, int age, Professor p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student o = null;
try {
o = (Student) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
o.p = (Professor) p.clone();
return o;
} }
5.1.7.4. 序列化(深 clone 一中實現)
在 Java 語言裡深複製一個物件,常常可以先使物件實現 Serializable 介面,然後把對
象(實際上只是物件的一個拷貝)寫到一個流裡,再從流裡讀出來,便可以重建物件。13/04/2018 Page 116 of 283 - Spring 原理
它是一個全面的、企業應用開發一站式的解決方案,貫穿表現層、業務層、持久層。但是 Spring
仍然可以和其他的框架無縫整合。
6.1.1. Spring 特點
6.1.1.1. 輕量級
6.1.1.2. 控制反轉
6.1.1.3. 面向切面
6.1.1.4. 容器
6.1.1.5. 框架集合13/04/2018 Page 117 of 283
6.1.2. Spring 核心元件
6.1.3. Spring 常用模組13/04/2018 Page 118 of 283
6.1.4. Spring 主要包
6.1.5. Spring 常用註解
bean 注入與裝配的的方式有很多種,可以通過 xml,get set 方式,建構函式或者註解等。簡單易
用的方式就是使用 Spring 的註解了,Spring 提供了大量的註解方式。13/04/2018 Page 119 of 283
6.1.6. Spring 第三方結合13/04/2018 Page 120 of 283
6.1.7. Spring IOC 原理
6.1.7.1. 概念
Spring 通過一個配置檔案描述 Bean 及 Bean 之間的依賴關係,利用 Java 語言的反射功能例項化
Bean 並建立 Bean 之間的依賴關係。 Spring 的 IoC 容器在完成這些底層工作的基礎上,還提供
了 Bean 例項快取、生命週期管理、 Bean 例項代理、事件釋出、資源裝載等高階服務。
6.1.7.2. Spring 容器高層檢視
Spring 啟動時讀取應用程式提供的 Bean 配置資訊,並在 Spring 容器中生成一份相應的 Bean 配
置登錄檔,然後根據這張登錄檔例項化 Bean,裝配好 Bean 之間的依賴關係,為上層應用提供準
備就緒的執行環境。其中 Bean 快取池為 HashMap 實現
6.1.7.3. IOC 容器實現
BeanFactory-框架基礎設施
BeanFactory 是 Spring 框架的基礎設施,面向 Spring 本身;ApplicationContext 面向使用
Spring 框架的開發者,幾乎所有的應用場合我們都直接使用 ApplicationContext 而非底層
的 BeanFactory。13/04/2018 Page 121 of 283
1.1…1.1.1 BeanDefinitionRegistry 登錄檔 - Spring 配置檔案中每一個節點元素在 Spring 容器裡都通過一個 BeanDefinition 物件表示,
它描述了 Bean 的配置資訊。而 BeanDefinitionRegistry 介面提供了向容器手工註冊
BeanDefinition 物件的方法。
1.1…1.1.2 BeanFactory 頂層介面 - 位於類結構樹的頂端 ,它最主要的方法就是 getBean(String beanName),該方法從容器中
返回特定名稱的 Bean,BeanFactory 的功能通過其他的介面得到不斷擴充套件:
1.1…1.1.3 ListableBeanFactory - 該介面定義了訪問容器中 Bean 基本資訊的若干方法,如檢視 Bean 的個數、獲取某一型別
Bean 的配置名、檢視容器中是否包括某一 Bean 等方法;
1.1…1.1.4 HierarchicalBeanFactory 父子級聯 - 父子級聯 IoC 容器的介面,子容器可以通過介面方法訪問父容器; 通過
HierarchicalBeanFactory 介面, Spring 的 IoC 容器可以建立父子層級關聯的容器體系,子
容器可以訪問父容器中的 Bean,但父容器不能訪問子容器的 Bean。Spring 使用父子容器實
現了很多功能,比如在 Spring MVC 中,展現層 Bean 位於一個子容器中,而業務層和持久
層的 Bean 位於父容器中。這樣,展現層 Bean 就可以引用業務層和持久層的 Bean,而業務
層和持久層的 Bean 則看不到展現層的 Bean。
1.1…1.1.5 ConfigurableBeanFactory - 是一個重要的介面,增強了 IoC 容器的可定製性,它定義了設定類裝載器、屬性編輯器、容
器初始化後置處理器等方法;13/04/2018 Page 122 of 283
1.1…1.1.6 AutowireCapableBeanFactory 自動裝配 - 定義了將容器中的 Bean 按某種規則(如按名字匹配、按型別匹配等)進行自動裝配的方法;
1.1…1.1.7 SingletonBeanRegistry 執行期間註冊單例 Bean - 定義了允許在執行期間向容器註冊單例項 Bean 的方法;對於單例項( singleton)的 Bean
來說,BeanFactory 會快取 Bean 例項,所以第二次使用 getBean() 獲取 Bean 時將直接從
IoC 容器的快取中獲取 Bean 例項。Spring 在 DefaultSingletonBeanRegistry 類中提供了一
個用於快取單例項 Bean 的快取器,它是一個用 HashMap 實現的快取器,單例項的 Bean 以
beanName 為鍵儲存在這個 HashMap 中。
1.1…1.1.8 依賴日誌框框 - 在初始化 BeanFactory 時,必須為其提供一種日誌框架,比如使用 Log4J, 即在類路徑下提
供 Log4J 配置檔案,這樣啟動 Spring 容器才不會報錯。
ApplicationContext 面向開發應用
ApplicationContext 由 BeanFactory 派生而來,提供了更多面向實際應用的功能。
ApplicationContext 繼承了 HierarchicalBeanFactory 和 ListableBeanFactory 介面,在此基礎
上,還通過多個其他的介面擴充套件了 BeanFactory 的功能: - ClassPathXmlApplicationContext:預設從類路徑載入配置檔案13/04/2018 Page 123 of 283
- FileSystemXmlApplicationContext:預設從檔案系統中裝載配置檔案
- ApplicationEventPublisher:讓容器擁有釋出應用上下文事件的功能,包括容器啟動事
件、關閉事件等。 - MessageSource:為應用提供 i18n 國際化訊息訪問的功能;
- ResourcePatternResolver : 所 有 ApplicationContext 實現類都實現了類似於
PathMatchingResourcePatternResolver 的功能,可以通過帶字首的 Ant 風格的資源文
件路徑裝載 Spring 的配置檔案。 - LifeCycle:該介面是 Spring 2.0 加入的,該介面提供了 start()和 stop()兩個方法,主要
用於控制非同步處理過程。在具體使用時,該介面同時被 ApplicationContext 實現及具體
Bean 實現, ApplicationContext 會將 start/stop 的資訊傳遞給容器中所有實現了該接
口的 Bean,以達到管理和控制 JMX、任務排程等目的。 - ConfigurableApplicationContext 擴充套件於 ApplicationContext,它新增加了兩個主要
的方法: refresh()和 close(),讓 ApplicationContext 具有啟動、重新整理和關閉應用上下
文的能力。在應用上下文關閉的情況下呼叫 refresh()即可啟動應用上下文,在已經啟動
的狀態下,呼叫 refresh()則清除快取並重新裝載配置資訊,而呼叫 close()則可關閉應用
上下文。
WebApplication 體系架構
WebApplicationContext 是專門為 Web 應用準備的,它允許從相對於 Web 根目錄的
路徑中裝載配置檔案完成初始化工作。從 WebApplicationContext 中可以獲得
ServletContext 的引用,整個 Web 應用上下文物件將作為屬性放置到 ServletContext
中,以便 Web 應用環境可以訪問 Spring 應用上下文。
6.1.7.4. Spring Bean 作用域
Spring 3 中為 Bean 定義了 5 中作用域,分別為 singleton(單例)、prototype(原型)、
request、session 和 global session,5 種作用域說明如下:
singleton:單例模式(多執行緒下不安全) - singleton:單例模式,Spring IoC 容器中只會存在一個共享的 Bean 例項,無論有多少個
Bean 引用它,始終指向同一物件。該模式在多執行緒下是不安全的。Singleton 作用域是
Spring 中的預設作用域,也可以顯示的將 Bean 定義為 singleton 模式,配置為:
13/04/2018 Page 124 of 283
prototype:原型模式每次使用時建立 - prototype:原型模式,每次通過 Spring 容器獲取 prototype 定義的 bean 時,容器都將建立
一個新的 Bean 例項,每個 Bean 例項都有自己的屬性和狀態,而 singleton 全域性只有一個對
象。根據經驗,對有狀態的bean使用prototype作用域,而對無狀態的bean使用singleton
作用域。 Request:一次 request 一個例項 - request:在一次 Http 請求中,容器會返回該 Bean 的同一例項。而對不同的 Http 請求則會
產生新的 Bean,而且該 bean 僅在當前 Http Request 內有效,當前 Http 請求結束,該 bean
例項也將會被銷燬。
session - session:在一次 Http Session 中,容器會返回該 Bean 的同一例項。而對不同的 Session 請
求則會建立新的例項,該 bean 例項僅在當前 Session 內有效。同 Http 請求相同,每一次
session 請求建立新的例項,而不同的例項之間不共享屬性,且例項僅在自己的 session 請求
內有效,請求結束,則例項將被銷燬。
global Session - global Session:在一個全域性的 Http Session 中,容器會返回該 Bean 的同一個例項,僅在
使用 portlet context 時有效。
6.1.7.5. Spring Bean 生命週期
例項化 - 例項化一個 Bean,也就是我們常說的 new。
IOC 依賴注入 - 按照 Spring 上下文對例項化的 Bean 進行配置,也就是 IOC 注入。
setBeanName 實現 - 如果這個 Bean 已經實現了 BeanNameAware 介面,會呼叫它實現的 setBeanName(String)
方法,此處傳遞的就是 Spring 配置檔案中 Bean 的 id 值
BeanFactoryAware 實現 - 如果這個 Bean 已經實現了 BeanFactoryAware 介面,會呼叫它實現的 setBeanFactory,
setBeanFactory(BeanFactory)傳遞的是 Spring 工廠自身(可以用這個方式來獲取其它 Bean,
只需在 Spring 配置檔案中配置一個普通的 Bean 就可以)。13/04/2018 Page 125 of 283
ApplicationContextAware 實現 - 如果這個 Bean 已經實現了 ApplicationContextAware 介面,會呼叫
setApplicationContext(ApplicationContext)方法,傳入 Spring 上下文(同樣這個方式也
可以實現步驟 4 的內容,但比 4 更好,因為 ApplicationContext 是 BeanFactory 的子接
口,有更多的實現方法)
postProcessBeforeInitialization 介面實現-初始化預處理 - 如果這個 Bean 關聯了 BeanPostProcessor 介面,將會呼叫
postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor 經常被用
作是 Bean 內容的更改,並且由於這個是在 Bean 初始化結束時呼叫那個的方法,也可以被應
用於記憶體或快取技術。
init-method - 如果 Bean 在 Spring 配置檔案中配置了 init-method 屬性會自動呼叫其配置的初始化方法。
postProcessAfterInitialization - 如果這個 Bean 關聯了 BeanPostProcessor 介面,將會呼叫
postProcessAfterInitialization(Object obj, String s)方法。
注:以上工作完成以後就可以應用這個 Bean 了,那這個 Bean 是一個 Singleton 的,所以一
般情況下我們呼叫同一個 id 的 Bean 會是在內容地址相同的例項,當然在 Spring 配置檔案中
也可以配置非 Singleton。 Destroy 過期自動清理階段 - 當 Bean 不再需要時,會經過清理階段,如果 Bean 實現了 DisposableBean 這個介面,會調
用那個其實現的 destroy()方法;
destroy-method 自配置清理 - 最後,如果這個 Bean 的 Spring 配置中配置了 destroy-method 屬性,會自動呼叫其配置的
銷燬方法。13/04/2018 Page 126 of 283 - bean 標籤有兩個重要的屬性(init-method 和 destroy-method)。用它們你可以自己定製
初始化和登出方法。它們也有相應的註解(@PostConstruct 和@PreDestroy)。
6.1.7.6. Spring 依賴注入四種方式
構造器注入
/帶引數,方便利用構造器進行注入/
public CatDaoImpl(String message){
this. message = message;
}
13/04/2018 Page 127 of 283
setter 方法注入
public class Id {
private int id;
public int getId() { return id; }
public void setId(int id) { this.id = id; }
}
靜態工廠注入
靜態工廠顧名思義,就是通過呼叫靜態工廠的方法來獲取自己需要的物件,為了讓 spring 管理所
有物件,我們不能直接通過"工程類.靜態方法()"來獲取物件,而是依然通過 spring 注入的形式獲
取:
public class DaoFactory { //靜態工廠
public static final FactoryDao getStaticFactoryDaoImpl(){
return new StaticFacotryDaoImpl();
}
}
public class SpringAction {
private FactoryDao staticFactoryDao; //注入物件
//注入物件的 set 方法
public void setStaticFactoryDao(FactoryDao staticFactoryDao) {
this.staticFactoryDao = staticFactoryDao;
}
}
//factory-method="getStaticFactoryDaoImpl"指定呼叫哪個工廠方法
例項工廠
例項工廠的意思是獲取物件例項的方法不是靜態的,所以你需要首先 new 工廠類,再呼叫普通的
例項方法:
public class DaoFactory { //例項工廠
public FactoryDao getFactoryDaoImpl(){
return new FactoryDaoImpl(); 13/04/2018 Page 128 of 283
}
}
public class SpringAction {
private FactoryDao factoryDao; //注入物件
public void setFactoryDao(FactoryDao factoryDao) {
this.factoryDao = factoryDao;
}
}
6.1.7.7. 5 種不同方式的自動裝配
Spring 裝配包括手動裝配和自動裝配,手動裝配是有基於 xml 裝配、構造方法、setter 方法等
自動裝配有五種自動裝配的方式,可以用來指導 Spring 容器用自動裝配方式來進行依賴注入。
- no:預設的方式是不進行自動裝配,通過顯式設定 ref 屬性來進行裝配。
- byName:通過引數名 自動裝配,Spring 容器在配置檔案中發現 bean 的 autowire 屬性被設
置成 byname,之後容器試圖匹配、裝配和該 bean 的屬性具有相同名字的 bean。 - byType:通過引數型別自動裝配,Spring 容器在配置檔案中發現 bean 的 autowire 屬性被
設定成 byType,之後容器試圖匹配、裝配和該 bean 的屬性具有相同型別的 bean。如果有多
個 bean 符合條件,則丟擲錯誤。 - constructor:這個方式類似於 byType, 但是要提供給構造器引數,如果沒有確定的帶引數
的構造器引數型別,將會丟擲異常。 - autodetect:首先嚐試使用 constructor 來自動裝配,如果無法工作,則使用 byType 方式。13/04/2018 Page 129 of 283
6.1.8. Spring APO 原理
6.1.8.1. 概念
“橫切"的技術,剖解開封裝的物件內部,並將那些影響了多個類的公共行為封裝到一個可重用模組,
並將其命名為"Aspect”,即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模組所共
同呼叫的邏輯或責任封裝起來,便於減少系統的重複程式碼,降低模組之間的耦合度,並有利於未
來的可操作性和可維護性。
使用"橫切"技術,AOP 把軟體系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流
程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生
在核心關注點的多處,而各處基本相似,比如許可權認證、日誌、事物。AOP 的作用在於分離系統
中的各種關注點,將核心關注點和橫切關注點分離開來。
AOP 主要應用場景有: - Authentication 許可權
- Caching 快取
- Context passing 內容傳遞
- Error handling 錯誤處理
- Lazy loading 懶載入
- Debugging 除錯
- logging, tracing, profiling and monitoring 記錄跟蹤 優化 校準
- Performance optimization 效能優化
- Persistence 持久化
- Resource pooling 資源池
- Synchronization 同步
- Transactions 事務
6.1.8.2. AOP 核心概念
1、切面(aspect):類是對物體特徵的抽象,切面就是對橫切關注點的抽象
2、橫切關注點:對哪些方法進行攔截,攔截後怎麼處理,這些關注點稱之為橫切關注點。 3、連線點(joinpoint):被攔截到的點,因為 Spring 只支援方法型別的連線點,所以在 Spring
中連線點指的就是被攔截到的方法,實際上連線點還可以是欄位或者構造器。 4、切入點(pointcut):對連線點進行攔截的定義
5、通知(advice):所謂通知指的就是指攔截到連線點之後要執行的程式碼,通知分為前置、後置、
異常、最終、環繞通知五類。 6、目標物件:代理的目標物件
7、織入(weave):將切面應用到目標物件並導致代理物件建立的過程13/04/2018 Page 130 of 283
8、引入(introduction):在不修改程式碼的前提下,引入可以在執行期為類動態地新增一些方法
或欄位。
參考:https://segmentfault.com/a/1190000007469968
6.1.8.1. AOP 兩種代理方式
Spring 提供了兩種方式來生成代理物件: JDKProxy 和 Cglib,具體使用哪種方式生成由
AopProxyFactory 根據 AdvisedSupport 物件的配置來決定。預設的策略是如果目標類是介面,
則使用 JDK 動態代理技術,否則使用 Cglib 來生成代理。
JDK 動態介面代理 - JDK 動態代理主要涉及到 java.lang.reflect 包中的兩個類:Proxy 和 InvocationHandler。
InvocationHandler是一個介面,通過實現該介面定義橫切邏輯,並通過反射機制呼叫目標類
的程式碼,動態將橫切邏輯和業務邏輯編制在一起。Proxy 利用 InvocationHandler 動態建立
一個符合某一介面的例項,生成目標類的代理物件。13/04/2018 Page 131 of 283
CGLib 動態代理 - :CGLib 全稱為 Code Generation Library,是一個強大的高效能,高質量的程式碼生成類庫,
可以在執行期擴充套件 Java 類與實現 Java 介面,CGLib 封裝了 asm,可以再執行期動態生成新
的 class。和 JDK 動態代理相比較:JDK 建立代理有一個限制,就是隻能為介面建立代理例項,
而對於沒有通過介面定義業務方法的類,則可以通過 CGLib 建立動態代理。
6.1.8.2. 實現原理
@Aspect
public class TransactionDemo {
@Pointcut(value=“execution(* com.yangxin.core.service...*(…))”)
public void point(){
}
@Before(value=“point()”)
public void before(){
System.out.println(“transaction begin”);
}
@AfterReturning(value = “point()”)
public void after(){
System.out.println(“transaction commit”);
}
@Around(“point()”)
public void around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println(“transaction begin”);
joinPoint.proceed();
System.out.println(“transaction commit”);
} }13/04/2018 Page 132 of 283
6.1.9. Spring MVC 原理
Spring 的模型-檢視-控制器(MVC)框架是圍繞一個 DispatcherServlet 來設計的,這個 Servlet
會把請求分發給各個處理器,並支援可配置的處理器對映、檢視渲染、本地化、時區與主題渲染
等,甚至還能支援檔案上傳。
6.1.9.1. MVC 流程13/04/2018 Page 133 of 283
Http 請求到 DispatcherServlet
(1) 客戶端請求提交到 DispatcherServlet。
HandlerMapping 尋找處理器
(2) 由 DispatcherServlet 控制器查詢一個或多個 HandlerMapping,找到處理請求的
Controller。
呼叫處理器 Controller
(3) DispatcherServlet 將請求提交到 Controller。
Controller 呼叫業務邏輯處理後,返回 ModelAndView
(4)(5)呼叫業務處理和返回結果:Controller 呼叫業務邏輯處理後,返回 ModelAndView。
DispatcherServlet 查詢 ModelAndView
(6)(7)處理檢視對映並返回模型: DispatcherServlet 查詢一個或多個 ViewResoler 檢視解析器,
找到 ModelAndView 指定的檢視。
ModelAndView 反饋瀏覽器 HTTP
(8) Http 響應:檢視負責將結果顯示到客戶端。
6.1.9.1. MVC 常用註解13/04/2018 Page 134 of 283
6.1.10. Spring Boot 原理
Spring Boot 是由 Pivotal 團隊提供的全新框架,其設計目的是用來簡化新 Spring 應用的初始搭
建以及開發過程。該框架使用了特定的方式來進行配置,從而使開發人員不再需要定義樣板化的
配置。通過這種方式,Spring Boot 致力於在蓬勃發展的快速應用開發領域(rapid application
development)成為領導者。其特點如下: - 建立獨立的 Spring 應用程式
- 嵌入的 Tomcat,無需部署 WAR 檔案
- 簡化 Maven 配置
- 自動配置 Spring
- ᨀ供生產就緒型功能,如指標,健康檢查和外部配置
- 絕對沒有程式碼生成和對 XML 沒有要求配置 [1]
6.1.11. JPA 原理
6.1.11.1. 事務
事務是計算機應用中不可或缺的元件模型,它保證了使用者操作的原子性 ( Atomicity )、一致性
( Consistency )、隔離性 ( Isolation ) 和永續性 ( Durabilily )。
6.1.11.2. 本地事務
緊密依賴於底層資源管理器(例如資料庫連線 ),事務處理侷限在當前事務資源內。此種事務處理
方式不存在對應用伺服器的依賴,因而部署靈活卻無法支援多資料來源的分散式事務。在資料庫連
接中使用本地事務示例如下:
public void transferAccount() {
Connection conn = null;
Statement stmt = null;
try{
conn = getDataSource().getConnection();
// 將自動ᨀ交設定為 false,若設定為 true 則資料庫將會把每一次資料更新認定為一個事務並自動ᨀ交
conn.setAutoCommit(false);
stmt = conn.createStatement();
// 將 A 賬戶中的金額減少 500
stmt.execute(“update t_account set amount = amount - 500 where account_id = ‘A’”);13/04/2018 Page 135 of 283
// 將 B 賬戶中的金額增加 500
stmt.execute(“update t_account set amount = amount + 500 where account_id = ‘B’”);
// ᨀ交事務
conn.commit();
// 事務ᨀ交:轉賬的兩步操作同時成功
} catch(SQLException sqle){
// 發生異常,回滾在本事務中的操做
conn.rollback();
// 事務回滾:轉賬的兩步操作完全撤銷
stmt.close();
conn.close();
}
}
6.1.11.1. 分散式事務
Java 事務程式設計介面(JTA:Java Transaction API)和 Java 事務服務 (JTS;Java Transaction
Service) 為 J2EE 平臺提供了分散式事務服務。分散式事務(Distributed Transaction)包括事務
管理器(Transaction Manager)和一個或多個支援 XA 協議的資源管理器 ( Resource
Manager )。我們可以將資源管理器看做任意型別的持久化資料儲存;事務管理器承擔著所有事務
參與單元的協調與控制。
public void transferAccount() {
UserTransaction userTx = null;
Connection connA = null; Statement stmtA = null;
Connection connB = null; Statement stmtB = null;
try{
// 獲得 Transaction 管理物件
userTx = (UserTransaction)getContext().lookup(“java:comp/UserTransaction”);
connA = getDataSourceA().getConnection();// 從資料庫 A 中取得資料庫連線
connB = getDataSourceB().getConnection();// 從資料庫 B 中取得資料庫連線
userTx.begin(); // 啟動事務
stmtA = connA.createStatement();// 將 A 賬戶中的金額減少 500
stmtA.execute(“update t_account set amount = amount - 500 where account_id = ‘A’”);
// 將 B 賬戶中的金額增加 500
stmtB = connB.createStatement(); 13/04/2018 Page 136 of 283
stmtB.execute(“update t_account set amount = amount + 500 where account_id = ‘B’”);
userTx.commit();// 提交事務
// 事務提交:轉賬的兩步操作同時成功(資料庫 A 和資料庫 B 中的資料被同時更新)
} catch(SQLException sqle){
// 發生異常,回滾在本事務中的操縱
userTx.rollback();// 事務回滾:資料庫 A 和資料庫 B 中的資料更新被同時撤銷
} catch(Exception ne){ }
}
6.1.11.1. 兩階段ᨀ交
兩階段提交主要保證了分散式事務的原子性:即所有結點要麼全做要麼全不做,所謂的兩個階段
是指:第一階段:準備階段;第二階段:提交階段。
1 準備階段
事務協調者(事務管理器)給每個參與者(資源管理器)傳送 Prepare 訊息,每個參與者要麼直接返回
失敗(如許可權驗證失敗),要麼在本地執行事務,寫本地的 redo 和 undo 日誌,但不提交,到達一
種“萬事俱備,只欠東風”的狀態。
2 ᨀ交階段:
如果協調者收到了參與者的失敗訊息或者超時,直接給每個參與者傳送回滾(Rollback)訊息;否則,
傳送提交(Commit)訊息;參與者根據協調者的指令執行提交或者回滾操作,釋放所有事務處理過
程中使用的鎖資源。(注意:必須在最後階段釋放鎖資源)13/04/2018 Page 137 of 283
將提交分成兩階段進行的目的很明確,就是儘可能晚地提交事務,讓事務在提交前儘可能地完成
所有能完成的工作。
6.1.12. Mybatis 快取
Mybatis 中有一級快取和二級快取,預設情況下一級快取是開啟的,而且是不能關閉的。一級快取
是指 SqlSession 級別的快取,當在同一個 SqlSession 中進行相同的 SQL 語句查詢時,第二次以
後的查詢不會從資料庫查詢,而是直接從快取中獲取,一級快取最多快取 1024 條 SQL。二級快取
是指可以跨 SqlSession 的快取。是 mapper 級別的快取,對於 mapper 級別的快取不同的
sqlsession 是可以共享的。13/04/2018 Page 138 of 283
6.1.12.1. Mybatis 的一級快取原理(sqlsession 級別)
第一次發出一個查詢 sql,sql 查詢結果寫入 sqlsession 的一級快取中,快取使用的資料結構是一
個 map。
key:MapperID+offset+limit+Sql+所有的入參
value:使用者資訊
同一個 sqlsession 再次發出相同的 sql,就從快取中取出資料。如果兩次中間出現 commit 操作
(修改、新增、刪除),本 sqlsession 中的一級快取區域全部清空,下次再去快取中查詢不到所
以要從資料庫查詢,從資料庫查詢到再寫入快取。
6.1.12.2. 二級快取原理(mapper 基本)
二級快取的範圍是 mapper 級別(mapper 同一個名稱空間),mapper 以名稱空間為單位建立緩
存資料結構,結構是 map。mybatis 的二級快取是通過 CacheExecutor 實現的。CacheExecutor13/04/2018 Page 139 of 283
其實是 Executor 的代理物件。所有的查詢操作,在 CacheExecutor 中都會先匹配快取中是否存
在,不存在則查詢資料庫。
key:MapperID+offset+limit+Sql+所有的入參
具體使用需要配置: - Mybatis 全域性配置中啟用二級快取配置
- 在對應的 Mapper.xml 中配置 cache 節點
- 在對應的 select 查詢節點中新增 useCache=true
6.1.13. Tomcat 架構
http://www.importnew.com/21112.html13/04/2018 Page 140 of 283 - 微服務
7.1.1. 服務註冊發現
服務註冊就是維護一個登記簿,它管理系統內所有的服務地址。當新的服務啟動後,它會向登記
簿交待自己的地址資訊。服務的依賴方直接向登記簿要 Service Provider 地址就行了。當下用於服
務註冊的工具非常多 ZooKeeper,Consul,Etcd, 還有 Netflix 家的 eureka 等。服務註冊有兩種
形式:客戶端註冊和第三方註冊。
7.1.1.1. 客戶端註冊(zookeeper)
客戶端註冊是服務自身要負責註冊與登出的工作。當服務啟動後向註冊中心註冊自身,當服務下
線時登出自己。期間還需要和註冊中心保持心跳。心跳不一定要客戶端來做,也可以由註冊中心
負責(這個過程叫探活)。這種方式的缺點是註冊工作與服務耦合在一起,不同語言都要實現一
套註冊邏輯。
7.1.1.2. 第三方註冊(獨立的服務 Registrar)
第三方註冊由一個獨立的服務Registrar負責註冊與登出。當服務啟動後以某種方式通知Registrar, 然後 Registrar 負責向註冊中心發起註冊工作。同時註冊中心要維護與服務之間的心跳,當服務不
可用時,向註冊中心登出服務。這種方式的缺點是 Registrar 必須是一個高可用的系統,否則註冊
工作沒法進展。13/04/2018 Page 141 of 283
7.1.1.3. 客戶端發現
客戶端發現是指客戶端負責查詢可用服務地址,以及負載均衡的工作。這種方式最方便直接,而
且也方便做負載均衡。再者一旦發現某個服務不可用立即換另外一個,非常直接。缺點也在於多
語言時的重複工作,每個語言實現相同的邏輯。13/04/2018 Page 142 of 283
7.1.1.4. 服務端發現
服務端發現需要額外的 Router 服務,請求先打到 Router,然後 Router 負責查詢服務與負載均衡。
這種方式雖然沒有客戶端發現的缺點,但是它的缺點是保證 Router 的高可用。
7.1.1.5. Consul
7.1.1.6. Eureka
7.1.1.7. SmartStack
7.1.1.8. Etcd
7.1.2. API 閘道器
API Gateway 是一個伺服器,也可以說是進入系統的唯一節點。這跟物件導向設計模式中的
Facade 模式很像。API Gateway 封裝內部系統的架構,並且提供 API 給各個客戶端。它還可能有
其他功能,如授權、監控、負載均衡、快取、請求分片和管理、靜態響應處理等。下圖展示了一
個適應當前架構的 API Gateway。13/04/2018 Page 143 of 283
API Gateway 負責請求轉發、合成和協議轉換。所有來自客戶端的請求都要先經過 API Gateway,
然後路由這些請求到對應的微服務。API Gateway 將經常通過呼叫多個微服務來處理一個請求以
及聚合多個服務的結果。它可以在 web 協議與內部使用的非 Web 友好型協議間進行轉換,如
HTTP 協議、WebSocket 協議。
7.1.2.1. 請求轉發
服務轉發主要是對客戶端的請求安裝微服務的負載轉發到不同的服務上
7.1.2.2. 響應合併
把業務上需要呼叫多個服務介面才能完成的工作合併成一次呼叫對外統一提供服務。
7.1.2.3. 協議轉換
重點是支援 SOAP,JMS,Rest 間的協議轉換。
7.1.2.4. 資料轉換
重點是支援 XML 和 Json 之間的報文格式轉換能力(可選)13/04/2018 Page 144 of 283
7.1.2.5. 安全認證 - 基於 Token 的客戶端訪問控制和安全策略
- 傳輸資料和報文加密,到服務端解密,需要在客戶端有獨立的 SDK 代理包
- 基於 Https 的傳輸加密,客戶端和服務端數字證照支援
- 基於 OAuth2.0 的服務安全認證(授權碼,客戶端,密碼模式等)
7.1.3. 配置中心
配置中心一般用作系統的引數配置,它需要滿足如下幾個要求:高效獲取、實時感知、分散式訪
問。
7.1.3.1. zookeeper 配置中心
實現的架構圖如下所示,採取資料載入到記憶體方式解決高效獲取的問題,藉助 zookeeper 的節點
監聽機制來實現實時感知。
7.1.3.2. 配置中心資料分類
7.1.4. 事件排程(kafka)
訊息服務和事件的統一排程,常用用 kafka ,activemq 等。
7.1.5. 服務跟蹤(starter-sleuth)
隨著微服務數量不斷增長,需要跟蹤一個請求從一個微服務到下一個微服務的傳播過程, Spring
Cloud Sleuth 正是解決這個問題,它在日誌中引入唯一 ID,以保證微服務呼叫之間的一致性,這
樣你就能跟蹤某個請求是如何從一個微服務傳遞到下一個。13/04/2018 Page 145 of 283 - 為了實現請求跟蹤,當請求傳送到分散式系統的入口端點時,只需要服務跟蹤框架為該請求
建立一個唯一的跟蹤標識,同時在分散式系統內部流轉的時候,框架始終保持傳遞該唯一標
識,直到返回給請求方為止,這個唯一標識就是前文中提到的 Trace ID。通過 Trace ID 的記
錄,我們就能將所有請求過程日誌關聯起來。 - 為了統計各處理單元的時間延遲,當請求達到各個服務元件時,或是處理邏輯到達某個狀態
時,也通過一個唯一標識來標記它的開始、具體過程以及結束,該標識就是我們前文中提到
的 Span ID,對於每個 Span 來說,它必須有開始和結束兩個節點,通過記錄開始 Span 和結
束 Span 的時間戳,就能統計出該 Span 的時間延遲,除了時間戳記錄之外,它還可以包含一
些其他後設資料,比如:事件名稱、請求資訊等。 - 在快速入門示例中,我們輕鬆實現了日誌級別的跟蹤資訊接入,這完全歸功於spring-cloudstarter-sleuth 元件的實現。在 Spring Boot 應用中,通過在工程中引入 spring-cloudstarter-sleuth 依賴之後, 它會自動的為當前應用構建起各通訊通道的跟蹤機制,比如:
ƒ 通過諸如 RabbitMQ、Kafka(或者其他任何 Spring Cloud Stream 繫結器實現的訊息
中介軟體)傳遞的請求。 ƒ 通過 Zuul 代理傳遞的請求。 ƒ 通過 RestTemplate 發起的請求。
7.1.6. 服務熔斷(Hystrix)
在微服務架構中通常會有多個服務層呼叫,基礎服務的故障可能會導致級聯故障,進而造成整個
系統不可用的情況,這種現象被稱為服務雪崩效應。服務雪崩效應是一種因“服務提供者”的不
可用導致“服務消費者”的不可用,並將不可用逐漸放大的過程。
熔斷器的原理很簡單,如同電力過載保護器。它可以實現快速失敗,如果它在一段時間內偵測到
許多類似的錯誤,會強迫其以後的多個呼叫快速失敗,不再訪問遠端伺服器,從而防止應用程式
不斷地嘗試執行可能會失敗的操作,使得應用程式繼續執行而不用等待修正錯誤,或者浪費 CPU
時間去等到長時間的超時產生。熔斷器也可以使應用程式能夠診斷錯誤是否已經修正,如果已經
修正,應用程式會再次嘗試呼叫操作。13/04/2018 Page 146 of 283
7.1.6.1. Hystrix 斷路器機制
斷路器很好理解, 當 Hystrix Command 請求後端服務失敗數量超過一定比例(預設 50%), 斷路器會
切換到開路狀態(Open). 這時所有請求會直接失敗而不會傳送到後端服務. 斷路器保持在開路狀態
一段時間後(預設 5 秒), 自動切換到半開路狀態(HALF-OPEN). 這時會判斷下一次請求的返回情況,
如果請求成功, 斷路器切回閉路狀態(CLOSED), 否則重新切換到開路狀態(OPEN). Hystrix 的斷路器
就像我們家庭電路中的保險絲, 一旦後端服務不可用, 斷路器會直接切斷請求鏈, 避免傳送大量無效
請求影響系統吞吐量, 並且斷路器有自我檢測並恢復的能力。
7.1.7. API 管理
SwaggerAPI 管理工具。13/04/2018 Page 147 of 283 - Netty 與 RPC
8.1.1. Netty 原理
Netty 是一個高效能、非同步事件驅動的 NIO 框架,基於 JAVA NIO 提供的 API 實現。它提供了對
TCP、UDP 和檔案傳輸的支援,作為一個非同步 NIO 框架,Netty 的所有 IO 操作都是非同步非阻塞
的,通過 Future-Listener 機制,使用者可以方便的主動獲取或者通過通知機制獲得 IO 操作結果。
8.1.2. Netty 高效能
在 IO 程式設計過程中,當需要同時處理多個客戶端接入請求時,可以利用多執行緒或者 IO 多路複用技術
進行處理。IO 多路複用技術通過把多個 IO 的阻塞複用到同一個 select 的阻塞上,從而使得系統在
單執行緒的情況下可以同時處理多個客戶端請求。與傳統的多執行緒/多程式模型比,I/O 多路複用的
最大優勢是系統開銷小,系統不需要建立新的額外程式或者執行緒,也不需要維護這些程式和執行緒
的執行,降低了系統的維護工作量,節省了系統資源。
與 Socket 類和 ServerSocket 類相對應,NIO 也提供了 SocketChannel 和 ServerSocketChannel
兩種不同的套接字通道實現。
8.1.2.1. 多路複用通訊方式
Netty 架構按照 Reactor 模式設計和實現,它的服務端通訊序列圖如下:
客戶端通訊序列圖如下:13/04/2018 Page 148 of 283
Netty 的 IO 執行緒 NioEventLoop 由於聚合了多路複用器 Selector,可以同時併發處理成百上千個
客戶端 Channel,由於讀寫操作都是非阻塞的,這就可以充分提升 IO 執行緒的執行效率,避免由於
頻繁 IO 阻塞導致的執行緒掛起。
8.1.2.1. 非同步通訊 NIO
由於 Netty 採用了非同步通訊模式,一個 IO 執行緒可以併發處理 N 個客戶端連線和讀寫操作,這從根
本上解決了傳統同步阻塞 IO 一連線一執行緒模型,架構的效能、彈性伸縮能力和可靠性都得到了極
大的提升。13/04/2018 Page 149 of 283
8.1.2.2. 零拷貝(DIRECT BUFFERS 使用堆外直接記憶體) - Netty 的接收和傳送 ByteBuffer 採用 DIRECT BUFFERS,使用堆外直接記憶體進行 Socket 讀寫,
不需要進行位元組緩衝區的二次拷貝。如果使用傳統的堆記憶體(HEAP BUFFERS)進行 Socket 讀寫,
JVM 會將堆記憶體 Buffer 拷貝一份到直接記憶體中,然後才寫入 Socket 中。相比於堆外直接記憶體,
訊息在傳送過程中多了一次緩衝區的記憶體拷貝。 - Netty 提供了組合 Buffer 物件,可以聚合多個 ByteBuffer 物件,使用者可以像操作一個 Buffer 那樣
方便的對組合 Buffer 進行操作,避免了傳統通過記憶體拷貝的方式將幾個小 Buffer 合併成一個大的
Buffer。 - Netty的檔案傳輸採用了transferTo方法,它可以直接將檔案緩衝區的資料傳送到目標Channel,
避免了傳統通過迴圈 write 方式導致的記憶體拷貝問題
8.1.2.3. 記憶體池(基於記憶體池的緩衝區重用機制)
隨著 JVM 虛擬機器和 JIT 即時編譯技術的發展,物件的分配和回收是個非常輕量級的工作。但是對於緩
衝區 Buffer,情況卻稍有不同,特別是對於堆外直接記憶體的分配和回收,是一件耗時的操作。為了盡
量重用緩衝區,Netty 提供了基於記憶體池的緩衝區重用機制。
8.1.2.4. 高效的 Reactor 執行緒模型
常用的 Reactor 執行緒模型有三種,Reactor 單執行緒模型, Reactor 多執行緒模型, 主從 Reactor 多執行緒模
型。
Reactor 單執行緒模型
Reactor 單執行緒模型,指的是所有的 IO 操作都在同一個 NIO 執行緒上面完成,NIO 執行緒的職責如下:
- 作為 NIO 服務端,接收客戶端的 TCP 連線;
- 作為 NIO 客戶端,向服務端發起 TCP 連線;
- 讀取通訊對端的請求或者應答訊息;
- 向通訊對端傳送訊息請求或者應答訊息。13/04/2018 Page 150 of 283
由於 Reactor 模式使用的是非同步非阻塞 IO,所有的 IO 操作都不會導致阻塞,理論上一個執行緒可以獨
立處理所有 IO 相關的操作。從架構層面看,一個 NIO 執行緒確實可以完成其承擔的職責。例如,通過
Acceptor 接收客戶端的 TCP 連線請求訊息,鏈路建立成功之後,通過 Dispatch 將對應的 ByteBuffer
派發到指定的 Handler 上進行訊息解碼。使用者 Handler 可以通過 NIO 執行緒將訊息傳送給客戶端。
Reactor 多執行緒模型
Rector 多執行緒模型與單執行緒模型最大的區別就是有一組 NIO 執行緒處理 IO 操作。 有專門一個
NIO 執行緒-Acceptor 執行緒用於監聽服務端,接收客戶端的 TCP 連線請求; 網路 IO 操作-讀、寫
等由一個 NIO 執行緒池負責,執行緒池可以採用標準的 JDK 執行緒池實現,它包含一個任務佇列和 N
個可用的執行緒,由這些 NIO 執行緒負責訊息的讀取、解碼、編碼和傳送;
主從 Reactor 多執行緒模型
服務端用於接收客戶端連線的不再是個 1 個單獨的 NIO 執行緒,而是一個獨立的 NIO 執行緒池。
Acceptor 接收到客戶端 TCP 連線請求處理完成後(可能包含接入認證等),將新建立的
SocketChannel 註冊到 IO 執行緒池(sub reactor 執行緒池)的某個 IO 執行緒上,由它負責
SocketChannel 的讀寫和編解碼工作。Acceptor 執行緒池僅僅只用於客戶端的登陸、握手和安全
認證,一旦鏈路建立成功,就將鏈路註冊到後端 subReactor 執行緒池的 IO 執行緒上,由 IO 執行緒負
責後續的 IO 操作。13/04/2018 Page 151 of 283
8.1.2.5. 無鎖設計、執行緒繫結
Netty 採用了序列無鎖化設計,在 IO 執行緒內部進行序列操作,避免多執行緒競爭導致的效能下降。
表面上看,序列化設計似乎 CPU 利用率不高,併發程度不夠。但是,通過調整 NIO 執行緒池的執行緒
引數,可以同時啟動多個序列化的執行緒並行執行,這種區域性無鎖化的序列執行緒設計相比一個佇列-
多個工作執行緒模型效能更優。
Netty 的 NioEventLoop 讀取到訊息之後,直接呼叫 ChannelPipeline 的
fireChannelRead(Object msg),只要使用者不主動切換執行緒,一直會由 NioEventLoop 呼叫
到使用者的 Handler,期間不進行執行緒切換,這種序列化處理方式避免了多執行緒操作導致的鎖
的競爭,從效能角度看是最優的。
8.1.2.6. 高效能的序列化框架
Netty 預設提供了對 Google Protobuf 的支援,通過擴充套件 Netty 的編解碼介面,使用者可以實現其它的
高效能序列化框架,例如 Thrift 的壓縮二進位制編解碼框架。
- SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128K 或者 256K。13/04/2018 Page 152 of 283
小包封大包,防止網路阻塞 - SO_TCPNODELAY:NAGLE 演算法通過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量
小封包的傳送阻塞網路,從而提高網路應用效率。但是對於時延敏感的應用場景需要關閉該優化算
法。
軟中斷 Hash 值和 CPU 繫結 - 軟中斷:開啟 RPS 後可以實現軟中斷,提升網路吞吐量。RPS 根據資料包的源地址,目的地址以
及目的和源埠,計算出一個 hash 值,然後根據這個 hash 值來選擇軟中斷執行的 cpu,從上層
來看,也就是說將每個連線和 cpu 繫結,並通過這個 hash 值,來均衡軟中斷在多個 cpu 上,提升
網路並行處理效能。
8.1.3. Netty RPC 實現
8.1.3.1. 概念
RPC,即 Remote Procedure Call(遠端過程呼叫),呼叫遠端計算機上的服務,就像呼叫本地服務一
樣。RPC 可以很好的解耦系統,如 WebService 就是一種基於 Http 協議的 RPC。這個 RPC 整體框架
如下:
8.1.3.2. 關鍵技術 - 服務釋出與訂閱:服務端使用 Zookeeper 註冊服務地址,客戶端從 Zookeeper 獲取可用的服務
地址。 - 通訊:使用 Netty 作為通訊框架。
- Spring:使用 Spring 配置服務,載入 Bean,掃描註解。
- 動態代理:客戶端使用代理模式透明化服務呼叫。
- 訊息編解碼:使用 Protostuff 序列化和反序列化訊息。
8.1.3.3. 核心流程 - 服務消費方(client)呼叫以本地呼叫方式呼叫服務;13/04/2018 Page 153 of 283
- client stub 接收到呼叫後負責將方法、引數等組裝成能夠進行網路傳輸的訊息體;
- client stub 找到服務地址,並將訊息傳送到服務端;
- server stub 收到訊息後進行解碼;
- server stub 根據解碼結果呼叫本地的服務;
- 本地服務執行並將結果返回給 server stub;
- server stub 將返回結果打包成訊息併傳送至消費方;
- client stub 接收到訊息,並進行解碼;
- 服務消費方得到最終結果。
RPC 的目標就是要 2~8 這些步驟都封裝起來,讓使用者對這些細節透明。JAVA 一般使用動態代
理方式實現遠端呼叫。
8.1.3.1. 訊息編解碼
息資料結構(介面名稱+方法名+引數型別和引數值+超時時間+ requestID)
客戶端的請求訊息結構一般需要包括以下內容: - 介面名稱:在我們的例子裡介面名是“HelloWorldService”,如果不傳,服務端就不知道呼叫哪
個介面了; - 方法名:一個介面內可能有很多方法,如果不傳方法名服務端也就不知道呼叫哪個方法;
- 引數型別和引數值:引數型別有很多,比如有 bool、int、long、double、string、map、list,
甚至如 struct(class);以及相應的引數值; - 超時時間:
- requestID,標識唯一請求 id,在下面一節會詳細描述 requestID 的用處。
- 服務端返回的訊息 : 一般包括以下內容。返回值+狀態 code+requestID13/04/2018 Page 154 of 283
序列化
目前網際網路公司廣泛使用 Protobuf、Thrift、Avro 等成熟的序列化解決方案來搭建 RPC 框架,這
些都是久經考驗的解決方案。
8.1.3.1. 通訊過程
核心問題(執行緒暫停、訊息亂序)
如果使用 netty 的話,一般會用 channel.writeAndFlush()方法來傳送訊息二進位制串,這個方
法呼叫後對於整個遠端呼叫(從發出請求到接收到結果)來說是一個非同步的,即對於當前執行緒來說,
將請求傳送出來後,執行緒就可以往後執行了,至於服務端的結果,是服務端處理完成後,再以訊息
的形式傳送給客戶端的。於是這裡出現以下兩個問題: - 怎麼讓當前執行緒“暫停”,等結果回來後,再向後執行?
- 如果有多個執行緒同時進行遠端方法呼叫,這時建立在 client server 之間的 socket 連線上
會有很多雙方傳送的訊息傳遞,前後順序也可能是隨機的,server 處理完結果後,將結
果訊息傳送給 client,client 收到很多訊息,怎麼知道哪個訊息結果是原先哪個執行緒呼叫
的?如下圖所示,執行緒 A 和執行緒 B 同時向 client socket 傳送請求 requestA 和 requestB,
socket 先後將 requestB 和 requestA 傳送至 server,而 server 可能將 responseB 先返
回,儘管 requestB 請求到達時間更晚。我們需要一種機制保證 responseA 丟給
ThreadA,responseB 丟給 ThreadB。
通訊流程
requestID 生成-AtomicLong - client 執行緒每次通過 socket 呼叫一次遠端介面前,生成一個唯一的 ID,即 requestID
(requestID 必需保證在一個 Socket 連線裡面是唯一的),一般常常使用 AtomicLong
從 0 開始累計數字生成唯一 ID;
存放回撥物件 callback 到全域性 ConcurrentHashMap - 將處理結果的回撥物件 callback ,存放到全域性 ConcurrentHashMap 裡 面
put(requestID, callback);
synchronized 獲取回撥物件 callback 的鎖並自旋 wait - 當執行緒呼叫 channel.writeAndFlush()傳送訊息後,緊接著執行 callback 的 get()方法試
圖獲取遠端返回的結果。在 get()內部,則使用 synchronized 獲取回撥物件 callback 的
鎖,再先檢測是否已經獲取到結果,如果沒有,然後呼叫 callback 的 wait()方法,釋放
callback 上的鎖,讓當前執行緒處於等待狀態。13/04/2018 Page 155 of 283
監聽訊息的執行緒收到訊息,找到 callback 上的鎖並喚醒 - 服務端接收到請求並處理後,將 response 結果(此結果中包含了前面的 requestID)發
送給客戶端,客戶端 socket 連線上專門監聽訊息的執行緒收到訊息,分析結果,取到
requestID ,再從前面的 ConcurrentHashMap 裡 面 get(requestID) ,從而找到
callback 物件,再用 synchronized 獲取 callback 上的鎖,將方法呼叫結果設定到
callback 物件裡,再呼叫 callback.notifyAll()喚醒前面處於等待狀態的執行緒。
public Object get() {
synchronized (this) { // 旋鎖
while (true) { // 是否有結果了
If (!isDone){
wait(); //沒結果釋放鎖,讓當前執行緒處於等待狀態
}else{//獲取資料並處理
}
}
}
}
private void setDone(Response res) {
this.res = res;
isDone = true;
synchronized (this) { //獲取鎖,因為前面 wait()已經釋放了 callback 的鎖了
notifyAll(); // 喚醒處於等待的執行緒
}
}
8.1.4. RMI 實現方式
Java 遠端方法呼叫,即 Java RMI(Java Remote Method Invocation)是 Java 程式語言裡,一種用
於實現遠端過程呼叫的應用程式程式設計介面。它使客戶機上執行的程式可以呼叫遠端伺服器上的物件。遠
程方法呼叫特性使 Java 程式設計人員能夠在網路環境中分佈操作。RMI 全部的宗旨就是儘可能簡化遠端接
口物件的使用。
8.1.4.1. 實現步驟
- 編寫遠端服務介面,該介面必須繼承 java.rmi.Remote 介面,方法必須丟擲
java.rmi.RemoteException 異常; - 編寫遠端介面實現類,該實現類必須繼承 java.rmi.server.UnicastRemoteObject 類;
- 執行 RMI 編譯器(rmic),建立客戶端 stub 類和服務端 skeleton 類;
- 啟動一個 RMI 登錄檔,以便駐留這些服務;13/04/2018 Page 156 of 283
- 在 RMI 登錄檔中註冊服務;
- 客戶端查詢遠端物件,並呼叫遠端方法;
1:建立遠端介面,繼承 java.rmi.Remote 介面
public interface GreetService extends java.rmi.Remote {
String sayHello(String name) throws RemoteException;
}2:實現遠端介面,繼承 java.rmi.server.UnicastRemoteObject 類
public class GreetServiceImpl extends java.rmi.server.UnicastRemoteObject
implements GreetService {
private static final long serialVersionUID = 3434060152387200042L;
public GreetServiceImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
} }
3:生成 Stub 和 Skeleton;
4:執行 rmiregistry 命令註冊服務
5:啟動服務
LocateRegistry.createRegistry(1098);
Naming.bind(“rmi://10.108.1.138:1098/GreetService”, new GreetServiceImpl());
6.客戶端呼叫
GreetService greetService = (GreetService)
Naming.lookup(“rmi://10.108.1.138:1098/GreetService”);
System.out.println(greetService.sayHello(“Jobs”));
8.1.5. Protoclol Buffer
protocol buffer 是 google 的一個開源專案,它是用於結構化資料序列化的靈活、高效、自動的方法,
例如 XML,不過它比 xml 更小、更快、也更簡單。你可以定義自己的資料結構,然後使用程式碼生成器
生成的程式碼來讀寫這個資料結構。你甚至可以在無需重新部署程式的情況下更新資料結構。13/04/2018 Page 157 of 283
8.1.5.1. 特點
Protocol Buffer 的序列化 & 反序列化簡單 & 速度快的原因是: - 編碼 / 解碼 方式簡單(只需要簡單的數學運算 = 位移等等)
- 採用 Protocol Buffer 自身的框架程式碼 和 編譯器 共同完成
Protocol Buffer 的資料壓縮效果好(即序列化後的資料量體積小)的原因是: - a. 採用了獨特的編碼方式,如 Varint、Zigzag 編碼方式等等
- b. 採用 T - L - V 的資料儲存方式:減少了分隔符的使用 & 資料儲存得緊湊
8.1.6. Thrift
Apache Thrift 是 Facebook 實現的一種高效的、支援多種程式語言的遠端服務呼叫的框架。本文將從
Java 開發人員角度詳細介紹 Apache Thrift 的架構、開發和部署,並且針對不同的傳輸協議和服務類
型給出相應的 Java 例項,同時詳細介紹 Thrift 非同步客戶端的實現,最後提出使用 Thrift 需要注意的事
項。
目前流行的服務呼叫方式有很多種,例如基於 SOAP 訊息格式的 Web Service,基於 JSON 訊息格式
的 RESTful 服務等。其中所用到的資料傳輸方式包括 XML,JSON 等,然而 XML 相對體積太大,傳輸
效率低,JSON 體積較小,新穎,但還不夠完善。本文將介紹由 Facebook 開發的遠端服務呼叫框架
Apache Thrift,它採用介面描述語言定義並建立服務,支援可擴充套件的跨語言服務開發,所包含的程式碼
生成引擎可以在多種語言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa,
Smalltalk 等建立高效的、無縫的服務,其傳輸資料採用二進位制格式,相對 XML 和 JSON 體積更小,
對於高併發、大資料量和多語言的環境更有優勢。本文將詳細介紹 Thrift 的使用,並且提供豐富的例項
程式碼加以解釋說明,幫助使用者快速構建服務。
為什麼要 Thrift: 1、多語言開發的需要 2、效能問題13/04/2018 Page 158 of 28313/04/2018 Page 159 of 283 - 網路
9.1.1. 網路 7 層架構
7 層模型主要包括: - 物理層:主要定義物理裝置標準,如網線的介面型別、光纖的介面型別、各種傳輸介質的傳輸速率
等。它的主要作用是傳輸位元流(就是由 1、0 轉化為電流強弱來進行傳輸,到達目的地後在轉化為
1、0,也就是我們常說的模數轉換與數模轉換)。這一層的資料叫做位元。 - 資料鏈路層:主要將從物理層接收的資料進行 MAC 地址(網路卡的地址)的封裝與解封裝。常把這
一層的資料叫做幀。在這一層工作的裝置是交換機,資料通過交換機來傳輸。 - 網路層:主要將從下層接收到的資料進行 IP 地址(例 192.168.0.1)的封裝與解封裝。在這一層工
作的裝置是路由器,常把這一層的資料叫做資料包。 - 傳輸層:定義了一些傳輸資料的協議和埠號(WWW 埠 80 等),如:TCP(傳輸控制協議,
傳輸效率低,可靠性強,用於傳輸可靠性要求高,資料量大的資料),UDP(使用者資料包協議,
與 TCP 特性恰恰相反,用於傳輸可靠性要求不高,資料量小的資料,如 QQ 聊天資料就是通過這
種方式傳輸的)。 主要是將從下層接收的資料進行分段進行傳輸,到達目的地址後在進行重組。
常常把這一層資料叫做段。 - 會話層:通過傳輸層(埠號:傳輸埠與接收埠)建立資料傳輸的通路。主要在你的系統之間
發起會話或或者接受會話請求(裝置之間需要互相認識可以是 IP 也可以是 MAC 或者是主機名) - 表示層:主要是進行對接收的資料進行解釋、加密與解密、壓縮與解壓縮等(也就是把計算機能夠
識別的東西轉換成人能夠能識別的東西(如圖片、聲音等)) - 應用層 主要是一些終端的應用,比如說FTP(各種檔案下載),WEB(IE瀏覽),QQ之類的(你
就把它理解成我們在電腦螢幕上可以看到的東西.就 是終端應用)。13/04/2018 Page 160 of 283
9.1.2. TCP/IP 原理
TCP/IP 協議不是 TCP 和 IP 這兩個協議的合稱,而是指因特網整個 TCP/IP 協議族。從協議分層
模型方面來講,TCP/IP 由四個層次組成:網路介面層、網路層、傳輸層、應用層。
9.1.2.1. 網路訪問層(Network Access Layer) - 網路訪問層(Network Access Layer)在 TCP/IP 參考模型中並沒有詳細描述,只是指出主機
必須使用某種協議與網路相連。
9.1.2.2. 網路層(Internet Layer) - 網路層(Internet Layer)是整個體系結構的關鍵部分,其功能是使主機可以把分組發往任何網
絡,並使分組獨立地傳向目標。這些分組可能經由不同的網路,到達的順序和傳送的順序也
可能不同。高層如果需要順序收發,那麼就必須自行處理對分組的排序。網際網路層使用因特
網協議(IP,Internet Protocol)。
9.1.2.3. 傳輸層(Tramsport Layer-TCP/UDP) - 傳輸層(Tramsport Layer)使源端和目的端機器上的對等實體可以進行會話。在這一層定義了
兩個端到端的協議:傳輸控制協議(TCP,Transmission Control Protocol)和使用者資料包協
議(UDP,User Datagram Protocol)。TCP 是面向連線的協議,它提供可靠的報文傳輸和對
上層應用的連線服務。為此,除了基本的資料傳輸外,它還有可靠性保證、流量控制、多路
複用、優先權和安全性控制等功能。UDP 是面向無連線的不可靠傳輸的協議,主要用於不需
要 TCP 的排序和流量控制等功能的應用程式。
9.1.2.4. 應用層(Application Layer) - 應用層(Application Layer)包含所有的高層協議,包括:虛擬終端協議(TELNET,
TELecommunications NETwork)、檔案傳輸協議(FTP,File Transfer Protocol)、電子郵件
傳輸協議(SMTP,Simple Mail Transfer Protocol)、域名服務(DNS,Domain Name 13/04/2018 Page 161 of 283
Service)、網上新聞傳輸協議(NNTP,Net News Transfer Protocol)和超文字傳送協議
(HTTP,HyperText Transfer Protocol)等。
9.1.3. TCP 三次握手/四次揮手
TCP 在傳輸之前會進行三次溝通,一般稱為“三次握手”,傳完資料斷開的時候要進行四次溝通,一般
稱為“四次揮手”。
9.1.3.1. 資料包說明 - 源埠號( 16 位):它(連同源主機 IP 地址)標識源主機的一個應用程式。
- 目的埠號( 16 位):它(連同目的主機 IP 地址)標識目的主機的一個應用程式。這兩個值
加上 IP 報頭中的源主機 IP 地址和目的主機 IP 地址唯一確定一個 TCP 連線。 - 順序號 seq( 32 位):用來標識從 TCP 源端向 TCP 目的端傳送的資料位元組流,它表示在這個
報文段中的第一個資料位元組的順序號。如果將位元組流看作在兩個應用程式間的單向流動,則
TCP 用順序號對每個位元組進行計數。序號是 32bit 的無符號數,序號到達 2 的 32 次方 - 1 後
又從 0 開始。當建立一個新的連線時, SYN 標誌變 1 ,順序號欄位包含由這個主機選擇的該
連線的初始順序號 ISN ( Initial Sequence Number )。 - 確認號 ack( 32 位):包含傳送確認的一端所期望收到的下一個順序號。因此,確認序號應當
是上次已成功收到資料位元組順序號加 1 。只有 ACK 標誌為 1 時確認序號欄位才有效。 TCP 為
應用層提供全雙工服務,這意味資料能在兩個方向上獨立地進行傳輸。因此,連線的每一端必
須保持每個方向上的傳輸資料順序號。 - TCP 報頭長度( 4 位):給出報頭中 32bit 字的數目,它實際上指明資料從哪裡開始。需要這
個值是因為任選欄位的長度是可變的。這個欄位佔 4bit ,因此 TCP 最多有 60 位元組的首部。然
而,沒有任選欄位,正常的長度是 20 位元組。 - 保留位( 6 位):保留給將來使用,目前必須置為 0 。
- 控制位( control flags , 6 位):在 TCP 報頭中有 6 個標誌位元,它們中的多個可同時被設
置為 1 。依次為:
ƒ URG :為 1 表示緊急指標有效,為 0 則忽略緊急指標值。
ƒ ACK :為 1 表示確認號有效,為 0 表示報文中不包含確認資訊,忽略確認號欄位。
ƒ PSH :為 1 表示是帶有 PUSH 標誌的資料,指示接收方應該儘快將這個報文段交給應用層
而不用等待緩衝區裝滿。 ƒ RST :用於復位由於主機崩潰或其他原因而出現錯誤的連線。它還可以用於拒絕非法的報
文段和拒絕連線請求。一般情況下,如果收到一個 RST 為 1 的報文,那麼一定發生了某些
問題。
ƒ SYN :同步序號,為 1 表示連線請求,用於建立連線和使順序號同步( synchronize )。
ƒ FIN :用於釋放連線,為 1 表示傳送方已經沒有資料傳送了,即關閉本方資料流。 - 視窗大小( 16 位):資料位元組數,表示從確認號開始,本報文的源方可以接收的位元組數,即源
方接收視窗大小。視窗大小是一個 16bit 欄位,因而視窗大小最大為 65535 位元組。 - 校驗和( 16 位):此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 資料,以 16 位字
進行計算所得。這是一個強制性的欄位,一定是由傳送端計算和儲存,並由接收端進行驗證。 - 緊急指標( 16 位):只有當 URG 標誌置 1 時緊急指標才有效。TCP 的緊急方式是傳送端向另
一端傳送緊急資料的一種方式。13/04/2018 Page 162 of 283 - 選項:最常見的可選欄位是最長報文大小,又稱為 MSS(Maximum Segment Size) 。每個連
接方通常都在通訊的第一個報文段(為建立連線而設定 SYN 標誌的那個段)中指明這個選項,
它指明本端所能接收的最大長度的報文段。選項長度不一定是 32 位字的整數倍,所以要加填充
位,使得報頭長度成為整字數。 - 資料: TCP 報文段中的資料部分是可選的。在一個連線建立和一個連線終止時,雙方交換的報
文段僅有 TCP 首部。如果一方沒有資料要傳送,也使用沒有任何資料的首部來確認收到的數
據。在處理超時的許多情況中,也會傳送不帶任何資料的報文段。
9.1.3.2. 三次握手
第一次握手:主機 A 傳送位碼為 syn=1,隨機產生 seq number=1234567 的資料包到伺服器,主機 B 由 SYN=1 知道,A 要求建立聯機;
第二次握手:主機 B 收到請求後要確認聯機資訊,向 A 發 送 ack number=( 主 機 A 的
seq+1),syn=1,ack=1,隨機產生 seq=7654321 的包
第三次握手:主機 A 收到後檢查 ack number 是否正確,即第一次傳送的 seq number+1,以及位碼
ack 是否為 1,若正確,主機 A 會再傳送 ack number=(主機 B 的 seq+1),ack=1,主機 B 收到後確認13/04/2018 Page 163 of 283
seq 值與 ack=1 則連線建立成功。
9.1.3.3. 四次揮手
TCP 建立連線要進行三次握手,而斷開連線要進行四次。這是由於 TCP 的半關閉造成的。因為 TCP 連
接是全雙工的(即資料可在兩個方向上同時傳遞)所以進行關閉時每個方向上都要單獨進行關閉。這個單
方向的關閉就叫半關閉。當一方完成它的資料傳送任務,就傳送一個 FIN 來向另一方通告將要終止這個
方向的連線。
1) 關閉客戶端到伺服器的連線:首先客戶端 A 傳送一個 FIN,用來關閉客戶到伺服器的資料傳送,
然後等待伺服器的確認。其中終止標誌位 FIN=1,序列號 seq=u
2) 伺服器收到這個 FIN,它發回一個 ACK,確認號 ack 為收到的序號加 1。
3) 關閉伺服器到客戶端的連線:也是傳送一個 FIN 給客戶端。
4) 客戶段收到 FIN 後,併發回一個 ACK 報文確認,並將確認序號 seq 設定為收到序號加 1。
首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。13/04/2018 Page 164 of 283
主機 A 傳送 FIN 後,進入終止等待狀態, 伺服器 B 收到主機 A 連線釋放報文段後,就立即
給主機 A 傳送確認,然後伺服器 B 就進入 close-wait 狀態,此時 TCP 伺服器程式就通知高
層應用程式,因而從 A 到 B 的連線就釋放了。此時是“半關閉”狀態。即 A 不可以傳送給
B,但是 B 可以傳送給 A。此時,若 B 沒有資料包要傳送給 A 了,其應用程式就通知 TCP 釋
放連線,然後傳送給 A 連線釋放報文段,並等待確認。A 傳送確認後,進入 time-wait,注
意,此時 TCP 連線還沒有釋放掉,然後經過時間等待計時器設定的 2MSL 後,A 才進入到
close 狀態。
9.1.4. HTTP 原理
HTTP 是一個無狀態的協議。無狀態是指客戶機(Web 瀏覽器)和伺服器之間不需要建立持久的連線,
這意味著當一個客戶端向伺服器端發出請求,然後伺服器返回響應(response),連線就被關閉了,在服
務器端不保留連線的有關資訊.HTTP 遵循請求(Request)/應答(Response)模型。客戶機(瀏覽器)向
伺服器傳送請求,伺服器處理請求並返回適當的應答。所有 HTTP 連線都被構造成一套請求和應答。
9.1.4.1. 傳輸流程
1:地址解析
如用客戶端瀏覽器請求這個頁面:http://localhost.com:8080/index.htm 從中分解出協議名、主機名、
埠、物件路徑等部分,對於我們的這個地址,解析得到的結果如下:
協議名:http
主機名:localhost.com
埠:8080
物件路徑:/index.htm13/04/2018 Page 165 of 283
在這一步,需要域名系統 DNS 解析域名 localhost.com,得主機的 IP 地址。
2:封裝 HTTP 請求資料包
把以上部分結合本機自己的資訊,封裝成一個 HTTP 請求資料包
3:封裝成 TCP 包並建立連線
封裝成 TCP 包,建立 TCP 連線(TCP 的三次握手)
4:客戶機傳送請求命
4)客戶機傳送請求命令:建立連線後,客戶機傳送一個請求給伺服器,請求方式的格式為:統一資
源識別符號(URL)、協議版本號,後邊是 MIME 資訊包括請求修飾符、客戶機資訊和可內容。
5:伺服器響應
伺服器接到請求後,給予相應的響應資訊,其格式為一個狀態行,包括資訊的協議版本號、一個成功或
錯誤的程式碼,後邊是 MIME 資訊包括伺服器資訊、實體資訊和可能的內容。 6:伺服器關閉 TCP 連線
伺服器關閉 TCP 連線:一般情況下,一旦 Web 伺服器向瀏覽器傳送了請求資料,它就要關閉 TCP 連
接,然後如果瀏覽器或者伺服器在其頭資訊加入了這行程式碼 Connection:keep-alive,TCP 連線在傳送
後將仍然保持開啟狀態,於是,瀏覽器可以繼續通過相同的連線傳送請求。保持連線節省了為每個請求
建立新連線所需的時間,還節約了網路頻寬。
9.1.4.2. HTTP 狀態
狀態碼 原因短語
訊息響應
100 Continue(繼續)
101 Switching Protocol(切換協議)13/04/2018 Page 166 of 283
成功響應
200 OK(成功)
201 Created(已建立)
202 Accepted(已建立)
203 Non-Authoritative Information(未授權資訊)
204 No Content(無內容)
205 Reset Content(重置內容)
206 Partial Content(部分內容)
重定向
300 Multiple Choice(多種選擇)
301 Moved Permanently(永久移動)
302 Found(臨時移動)
303 See Other(檢視其他位置)
304 Not Modified(未修改)
305 Use Proxy(使用代理)
306 unused(未使用)
307 Temporary Redirect(臨時重定向)
308 Permanent Redirect(永久重定向)
客戶端錯誤
400 Bad Request(錯誤請求)
401 Unauthorized(未授權)
402 Payment Required(需要付款)
403 Forbidden(禁止訪問)
404 Not Found(未找到)
405 Method Not Allowed(不允許使用該方法)
406 Not Acceptable(無法接受)
407 Proxy Authentication Required(要求代理身份驗證)
408 Request Timeout(請求超時)
409 Conflict(衝突)
410 Gone(已失效)
411 Length Required(需要內容長度頭)
412 Precondition Failed(預處理失敗)
413 Request Entity Too Large(請求實體過長)
414 Request-URI Too Long(請求網址過長)
415 Unsupported Media Type(媒體型別不支援)
416 Requested Range Not Satisfiable(請求範圍不合要求)
417 Expectation Failed(預期結果失敗)
伺服器端錯誤
500 Internal Server Error(內部伺服器錯誤)
501 Implemented(未實現)
502 Bad Gateway(閘道器錯誤)
503 Service Unavailable(服務不可用)
504 Gateway Timeout (閘道器超時)
505 HTTP Version Not Supported(HTTP 版本不受支援)
9.1.4.3. HTTPS
HTTPS(全稱:Hypertext Transfer Protocol over Secure Socket Layer),是以安全為目標的
HTTP 通道,簡單講是 HTTP 的安全版。即 HTTP 下加入 SSL 層,HTTPS 的安全基礎是 SSL。其所用
的埠號是 443。 過程大致如下:13/04/2018 Page 167 of 283
建立連線獲取證照
1) SSL 客戶端通過 TCP 和伺服器建立連線之後(443 埠),並且在一般的 tcp 連線協商(握
手)過程中請求證照。即客戶端發出一個訊息給伺服器,這個訊息裡面包含了自己可實現的算
法列表和其它一些需要的訊息,SSL 的伺服器端會回應一個資料包,這裡面確定了這次通訊所
需要的演算法,然後伺服器向客戶端返回證照。(證照裡面包含了伺服器資訊:域名。申請證照
的公司,公共祕鑰)。
證照驗證
2) Client 在收到伺服器返回的證照後,判斷簽發這個證照的公共簽發機構,並使用這個機構的公
共祕鑰確認簽名是否有效,客戶端還會確保證照中列出的域名就是它正在連線的域名。
資料加密和傳輸
3) 如果確認證照有效,那麼生成對稱祕鑰並使用伺服器的公共祕鑰進行加密。然後傳送給服務
器,伺服器使用它的私鑰對它進行解密,這樣兩臺計算機可以開始進行對稱加密進行通訊。
9.1.5. CDN 原理
CND 一般包含分發服務系統、負載均衡系統和管理系統
9.1.5.1. 分發服務系統
其基本的工作單元就是各個 Cache 伺服器。負責直接響應使用者請求,將內容快速分發到使用者;同時還
負責內容更新,保證和源站內容的同步。13/04/2018 Page 168 of 283
根據內容型別和服務種類的不同,分發服務系統分為多個子服務系統,如:網頁加速服務、流媒體加速
服務、應用加速服務等。每個子服務系統都是一個分散式的服務叢集,由功能類似、地域接近的分佈部
署的 Cache 叢集組成。
在承擔內容同步、更新和響應使用者請求之外,分發服務系統還需要向上層的管理排程系統反饋各個
Cache 裝置的健康狀況、響應情況、內容快取狀況等,以便管理排程系統能夠根據設定的策略決定由
哪個 Cache 裝置來響應使用者的請求。
9.1.5.2. 負載均衡系統:
負載均衡系統是整個 CDN 系統的中樞。負責對所有的使用者請求進行排程,確定提供給使用者的最終訪問
地址。
使用分級實現。最基本的兩極排程體系包括全域性負載均衡(GSLB)和本地負載均衡(SLB)。
GSLB 根據使用者地址和使用者請求的內容,主要根據就近性原則,確定向使用者服務的節點。一般通過 DNS
解析或者應用層重定向(Http 3XX 重定向)的方式實現。
SLB 主要負責節點內部的負載均衡。當使用者請求從 GSLB 排程到 SLB 時,SLB 會根據節點內各個
Cache 裝置的工作狀況和內容分佈情況等對使用者請求重定向。SLB 的實現有四層排程(LVS)、七
相關文章
- JVM--Java核心面試知識整理(一)JVMJava面試
- JVM 面試知識整理JVM面試
- 高階 Java 面試通關知識點整理Java面試
- jQuery面試知識點整理jQuery面試
- 自己整理的php面試知識點PHP面試
- Vue 面試中常問知識點整理Vue面試
- PHP 面試知識點整理歸納PHP面試
- Java知識整理Java
- iOS | 面試知識整理 - OC基礎 (一)iOS面試
- Java 核心知識點整理Java
- Java基礎知識面試題Java面試題
- java面試知識點總結Java面試
- Java 面試知識點總結Java面試
- Java 基礎面試知識點Java面試
- 搞定PHP面試 - HTTP協議知識點整理PHP面試HTTP協議
- [ Java面試題 ]Java 開發崗面試知識點解析Java面試題
- 【Java面試】Java基礎知識面試題—2020最新版!Java面試題
- 搞定PHP面試 - 正規表示式知識點整理PHP面試
- Java 資料庫知識整理Java資料庫
- 《面試補習》- Java鎖知識大梳理面試Java
- Java面試題必備知識之ThreadLocalJava面試題thread
- Java基礎面試知識點總結Java面試
- 常見Java面試知識點總結Java面試
- 面試知識點面試
- Java面試題整理《上》Java面試題
- 一些知識點的整理以及面試題記錄面試題
- Java面試知識總結(一)-- 網路基礎Java面試
- Java容器相關知識點整理Java
- 整理Java基礎知識--Calendar 類Java
- Java基礎知識整理之this用法Java
- 2018前端面試知識整理【上】前端面試
- Java面試整理(精簡版)Java面試
- Java集合類常見面試知識點總結Java面試
- JAVA高階面試必過知識點彙總Java面試
- Java基礎知識整理之註解Java
- java框架之Hibernate框架知識點整理。Java框架
- 前端面試知識點目錄整理前端面試
- 面試必知的web知識點面試Web