Java高級深入與JVM
1.關於HashCode
不能根據hashCode值判斷兩個對象是否相等,但可以直接根據hashCode值判斷兩個對象不相等。
如果兩個對象的hashCode值不等,一定是不同的對象,要判斷兩個對象是否真正相等,必須通過equals()方法
如果調用equals()方法得到的結果爲true,則兩個對象的hashCode值一定相等
如果equals()方法得到的結果爲false,則兩個對象的hashCode值不一定不同
如果兩個對象的hashcode值不等,則equals方法得到的結果必定爲false;
如果兩個對象的hashcode值相等,則equals方法得到的結果未知。
在重寫equals()方法時,必須重寫hashCode方法,讓equals()方法和hashCode()方法始終在邏輯上保持一致性
hashCode方法的主要作用是爲了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable
Java裏靜態語句塊是優先對象存在,也就是優先於構造方法存在,我們通常用來做只創建一次對象使用,類似於單列模式而且執行的順序是:
父類靜態語句塊 -> 子類靜態語句塊 -> 父類非靜態代碼塊、父類構造方法 -> 子類非靜態代碼塊構造方法
靜態語句塊可以看作在類加載的時候執行,且只執行一次
Java對象初始化順序:
靜態代碼塊內容先執行(父>子),接着執行父類非靜態代碼塊和構造方法,然後執行子類非靜態代碼塊和構造方法
首先執行父類靜態的內容,父類靜態的內容執行完畢後,接着去執行子類的靜態的內容,當子類的靜態內容執行完畢之後,再去看父類有沒有非靜態代碼塊,如果有就執行父類的非靜態代碼塊,
父類的非靜態代碼塊執行完畢,接着執行父類的構造方法;父類的構造方法執行完畢之後,它接着去看子類有沒有非靜態代碼塊,如果有就執行子類的非靜態代碼塊。子類的非靜態代碼塊執行完畢再去執行子類的構造方法
2.JVM內存模型
程序計數器
用來指示 執行哪條指令的
如果線程執行的是非native方法,則程序計數器保持的是當前需要執行的指令的地址
如果線程執行的是native方法,程序計數器中的值是undefined
Java棧
存放一個個的棧幀,每個棧幀對應一個被調用的方法。
棧幀中包括:局部變量表、操作數棧、指向當前方法所屬的類的運行時常量池的引用、方法返回地址和一些額外的附加信息
當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓入棧,當方法執行完後,便會將棧幀出棧
線程當前執行的方法所對應的棧幀位於Java棧的頂部
棧幀
局部變量表
操作數棧
指向運行時常量池的引用
方法返回地址
額外的附加信息
堆
存儲對象本身以及數組
方法區 --- 線程共享區域
1、存儲了每個類的信息(包括:類名稱、方法信息、字段信息)、靜態變量、常量以及編譯後的代碼等
2、常量池
本地方法棧
3.Hash表
散列表,能快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字比較來進行查找。
Hash表的設計,採用了函數映射思想,將記錄的存儲位置與記錄的關鍵字關聯起來,從而能夠很快速地進行查找
eg:
張三 13980593357
李四 15828662334
王五 13409821234
張帥 13890583472
Hash表能通過"李四"這個信息直接獲取到該記錄在表中的位置,複雜度爲O(1)
原理:
Hash表採用一個映射函數
f:key --> address
將關鍵字key映射到該記錄在表中的存儲位置
說明:
1.映射關係 稱爲 Hash函數
2.通過Hash函數和關鍵字計算出來的存儲位置(hash表中的存儲位置,不是實際的物理地址) 稱爲 Hash地址
聯繫人信息採用Hash表存儲時,當想找 "李四" 的信息時,直接根據 "李四" 和 Hash函數,計算出Hash地址即可
4.Java線程安全的本質:線程中並沒有存放任何對象數據,而是在執行時,去主內存(堆)中去同步數據,所有的對象數據都存在JVM的堆中,因此需要對資源進行共享鎖!!!
堆 --- JVM的核心數據存儲區 --- 線程共享的主內存
堆中爲JVM的所有對象分配了內存空間用以存儲和維護變量值等
棧 --- 線程私有的內存區,由棧幀(線程棧)組成,存放8中基本數據類型和對象引用
每個線程都會生成一個自有的線程棧,線程棧中用存儲了該線程的基本數據常量,變量值,以及對象長變量的引用
每個線程執行時,根據代碼順序,壓棧 棧幀(棧內存)
對象變量在線程執行時的過程:!!! --- 由JVM內存模型決定
1.線程根據棧中的引用去堆上同步該對象數據下來,然後在線程自己的內存中進行操作
2.操作之後再將線程棧撒花姑娘的運算結果同步到堆(主內存)中
3.多線程時,因爲每個線程都操作自己從主內存(JVM堆)中同步過來的數據,如果不加鎖,會導致線程安全問題(數據提交到主內存時不一致)
5.堆 --- JVM中所有對象的內存空間 分爲: Young Gen, Old Gen
Young Gen 又分爲:Eden區和兩個大小相同的Survivor區(from 和 to)
Eden和Survivor默認比例 8:1 由 -XX:SurvivorRation設置
堆大小 -Xmx -Xms 設置
Young Gen -Xmn 設置
-XX:NewSize和-XX:MaxNewSize
用於設置年輕代的大小,建議設爲整個堆大小的1/3或者1/4,兩個值設爲一樣大。
Minor GC --- 發生在新生代的垃圾回收,Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,
年輕代的GC使用複製算法(將內存分爲兩塊,每次只用其中一塊,當這塊內存用完,就將還活着的對象複製到另外一塊上面,複製算法不會產生內存碎片)
Full GC --- 發生在年老代的GC, Full GC比較慢,儘量避免
新創建對象都會被分配到Eden區(一些大對象特殊處理),當Eden區滿則進行Minor GC,
這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區, 對象在Survivor區中每熬過一次Minor GC,年齡增加1歲,
當年齡增加到一定程度後(默認15歲),會被移動到年老代中,
當年老代滿時,經常Full GC
線程Stack 每個線程獨有的操作數棧局部變量表方法入口 -Xss 設置
方法區 -XX:PermSize和-XX:MaxPermSize設置
6.JVM class類加載加載機制 --- JVM把描述類的數據從class加載到內存,並對數據進行校驗,轉化解析和初始化,最終得到可被虛擬機直接使用的Java類型
類裝載器: 尋找類的字節碼文件, 並構造出類在JVM內部表示的對象組件
JVM類加載器把一個類裝入JVM過程:
(1) 裝載:查找和導入Class文件;
(2) 鏈接:把類的二進制數據合併到JRE中;
(a)校驗:檢查載入Class文件數據的正確性,確保被加載類信息符合JVM規範;
(b)準備:給類的靜態變量分配存儲空間,並將其初始化爲默認值;
(c)解析:將虛擬機常量池中符號引用轉成直接引用;
(3) 初始化:對類的靜態變量,靜態代碼塊執行初始化操作,爲類的靜態變量賦初始值
說明:
1.類加載的雙親委託機制
某個特定的類加載器在接到加載類的請求時,首先將加載任務交給父類加載器,父類加載器又將加載任務向上委託,直到最高層的父類加載器,
如果最高層的父類加載器可以完成類加載任務,就成功返回,否則向下傳遞加載任務,由其子類加載器進行加載
2.初始化步驟
1.如果類沒有被加載和鏈接,則先進行加載和鏈接
2.如果類存在直接父類,並且父類未被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類
3.如果存在static塊,一次執行這些初始化語句
4.static塊,會在類第一次被使用時執行(調用類的靜態變量,初始化對象,Class.forName等)
7.正則表達式總計:
^ 和$表示以字符串開始和以字符串結尾。例:^abc 表示必須以abc開始(如:abcd,abcefd),abc$ 表示必須以abc結尾(如:);^abc$ 只能是abc(abc是個整體,abcabc不匹配) ;abc 表示包含abc的字符串
* 和 + 和 ? 分別表示出現0次或多次,1次或多次,0次或1次。例:abc*表示有0個或多個abc,其他兩個同理
上面的*+?完全可以用範圍代替,abc{2}表示ab後面有至少兩個c,如abcc,dfdabccccc都是符合的;abc{2}$ 只有以abcc結尾的符合,如343abcc
abc{1,2} 表示ab後面跟着1或2個c;
abc{3,} 表示ab後面跟着至少3個c; {,3}這種是不正確的
| 或運算 ab|cd 表示字符串裏有ab或者cd;
. 可以替換任意字符
下面是幾種是需要記住的
"[ab]":表示一個字符串有一個"a"或"b"(相當於"a|b");
"[a-d]":表示一個字符串包含小寫的'a'到'd'中的一個(相當於"a|b|c|d"或者"[abcd]");
"^[a-zA-Z]":表示一個以字母開頭的字符串;
"[0-9]%":表示一個百分號前有一位的數字;
",[a-zA-Z0-9]$":表示一個字符串以一個逗號後面跟着一個字母或數字結束。
下面看看具體的實例,比如我今天做的:一個輸入框,可以輸入數字,也可以輸入多個數字用逗號隔開,或者兩個數字用~分隔。
我寫的正則表達式 : ((^[0-9]+[~]?)?|^([0-9]+[,])+)[0-9]+$
8.JVM中的GC垃圾回收和內存分配策略 ---- JVM高級 參考:
http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
http://www.cnblogs.com/zhguang/tag/Java/
http://blog.csdn.net/column/details/javavirtualmachine.html
http://blog.csdn.net/eric_sunah/article/details/7870906
1.判斷回收對象
1.引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器值+1,當引用失效,計數器值-1,任何時刻計數器爲0的對象就不可能再被使用
缺點:
無法解決對象的循環依賴問題!!!
2.可達性算法 --- JVM使用的GC判斷算法
通過一系列稱爲 GC Roots 的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲 引用鏈,
當一個對象到 GC Roots 沒有任何引用鏈相連時(即:GC Roots到這個對象不可達),則此對象是不可用的
Java中可作爲 GC Roots的對象包括以下幾種:
1.虛擬機棧(棧幀的本地變量表)中引用的對象
2.方法區中類靜態屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧中JNI(Native方法)引用的對象
2.JVM的垃圾回收機制
1.幾個概念:
內存分代 新生代(Eden + 2 Survivor )、老年代、永久代
新創建的對象分配在Eden區,經歷一次Minor GC後被移到 Survivor1區,再經歷一次Minor GC被移到Survivor2區,直到升至老年代
大對象(長字符串或大數組)可能直接存放到老年代
對象創建都在堆上,類信息、常量、靜態變量存儲在方法區,堆和方法區是線性共享的
GC由守護線程執行,在從內存回收一個對象之前,會調用對象的finalize()方法
GC的觸發由JVM堆內存大小決定,System.gc()和Runtime.gc()會向JVM發送執行GC的請求,但JVM不保證一定會執行GC
堆中沒有內存創建新對象時,會拋出 OutOfMemoryError
2.GC回收什麼對象?
可達性算法 --- 根搜索法
--- 通過一系列稱爲GC Roots的對象作爲起始點,從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,證明此對象不可用,可以被回收了
程序把所有的引用關係看做一張圖,從一個節點 GC Roots開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢後,剩餘的節點被認爲是沒有被引用的節點,可被回收
--- 可被作爲GC Roots的對象
1.虛擬機棧中引用的對象(本地變量表)
2.方法靜態屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧用引用的對象(Native對象)
引用計數法
--- 可能有循環依賴問題,導致計數器不爲0,對象永遠無法被回收
多次掃描,而都不可達的對象,經歷幾次掃描後會被回收
3.垃圾回收算法: 參考:http://www.cnblogs.com/sunniest/p/4575144.html
任何垃圾回收算法,只做2件基本事情:
1.發現無用可被回收對象 (可達性算法)
2.回收被無用對象佔用的空間,使得該空間可被再次使用
1.標記-清除算法:
從GC Roots根集合進行掃描,對存活的對象進行標記,標記完成後,在掃描整個空間中未被標記的對象,進行回收
優點:不必移動對象,在存活對象比較多的情況下極爲高效
缺點:GC時會暫停整個應用,容易造成不連續的內存碎片
2.複製算法:
爲了解決內存碎片問題而產生的一種算法。它的整個過程可以描述爲:標記所有的存活對象;通過重新調整存活對象位置來縮並對象圖;更新指向被移動了位置的對象的指針
將內存劃分爲大小相等的2塊,每次只是用其中一塊,當一塊用完後,將還存活的對象複製到另一塊內存上,然後把已使用過的內存空間一次清理掉,
這樣使得每次都是對其中的一塊進行內存回收,不會產生碎片等情況,只要移動堆訂的指針,按順序分配內存即可,實現簡單,運行高效
優點:不會造成內存碎片
缺點:內存縮小爲原來的一半,需要移動對象
3.標記-整理算法:
先進行 標記-清除算法,然後在清理完無用對象後,將所有存活的對象移動到一端,並更新引用其對象的指針
4.分代算法:
基於對象生命週期分析得出的垃圾回收算法。把對象分爲年輕代、年老代、持久代,對不同的生命週期使用不同的算法
年輕代: --- Minor GC, 對象生命週期短 使用: Serial、PraNew、Parallel Scavenge 收集器
1.所有新生成對象,目標是儘快收集掉生命週期短的對象
2.新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
Eden : Survivor0:Survivor1 = 8:1:1
3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)
老年代: --- Full GC 對象生命週期長 使用:Serial Old、Parallel Old、CMS收集器
1.在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。
2.內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
永久代: --- 用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響
4.GC實現 --- 垃圾收集器
Serial --- 複製算法
新生代單線程收集器,標記和清理都是單線程,優點:簡單高效
Serial Old --- 標記-整理算法
老年代單線程收集器
ParNew --- 停止複製算法
Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現
Parallel Scavenge ---
並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景
Parallel Old收集器(停止-複製算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS --- 標記清理算法
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
5.GC執行機制
Minor GC: 新對象在年輕代的Eden區申請內存失敗時,觸發Minor GC --- 年輕代
--- 只對年輕代的Eden去進行,不影響老年代
--- 頻繁進行
--- 選用速度快、效率高的算法,使Eden儘快空閒
Full GC: 對整個堆進行整理,包括 年輕代、老年代、永久代
--- 對整個堆進行回收,速度比 Minor GC 慢
--- 儘量減少 Full GC次數
--- 對象JVM調優過程,大部分是針對Full GC的調節
可能導致Full GC的原因:
1.老年代滿
2.持久代滿
3.System.gc()被顯示調用
4.上一次GC之後Heap的各域分配策略動態變化
6.容易出現泄露的地方:
1.靜態集合類(HashMap、Vector),這些靜態變量生命週期和應用程序一樣,所有的對象也不能被釋放
2.各種連接、數據了連接、網絡連接、IO連接沒被顯示調用close(),不被GC回收導致內存泄露
3.監聽器,在釋放對象的同時沒有相應刪除監聽器時可能導致內存泄露
9.JVM內存模型 http://www.infoq.com/cn/articles/java-memory-model-1
內存模型:描述了程序中各個變量(實例域、靜態域和數組元素)直接的關係,以及在實際計算機系統中將變量存儲到內存、從內存中取出變量這樣的底層細節
JVM中存在一個主存,Java中所有對象成員變量都存儲在主存中,對於所有線程是共享的,每條線程都有自己的工作內存,
工作內存中保存的是主存中某些對象成員變量的拷貝,線程對所有成員變量的操作都是在工作內存中進行,然後同步到主存,線程之間無法相互直接訪問,變量傳遞都需要通過主存
JMM(Java內存模型,Java Memory Model簡稱)是控制Java線程之間、線程和主存之間通信的協議。
JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存(local memory),
本地內存中存儲了該線程以讀/寫共享變量的副本,線程在本地私有內存中操作完後,要將數據同步到主內存
Java內存模型中規定了:
所有變量都存儲在主內存中,每個線程都有自己的工作內存,線程的工作內存中使用到的變量,是主內存的副本拷貝,
線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量,操作完後,要將數據同步到主內存
不同的線程之間工作內存是獨立的,線程間變量值的傳遞均需要在主內存來完成
特別注意:
(根據Java虛擬機規範的規定,volatile變量依然有共享內存的拷貝,但是由於它特殊的操作順序性規定——
從工作內存中讀寫數據前,必須先將主內存中的數據同步到工作內存中,所有看起來如同直接在主內存中讀寫訪問一般,因此這裏的描述對於volatile也不例外)。
不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值得傳遞均需要通過主內存來完成
8種內存間的交互操作:
Java內存模型定義了8種操作來完成主內存與工作內存之間交互的實現細節
1.lock(鎖定)
作用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態
2.unlock(解鎖)
作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
3.read(讀取)
作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
4.load(載入)
作用於主內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
5.use(使用)
作用於工作內存的變量,它把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值得字節碼指令時,將會執行這個操作
6.assign(賦值)
作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
7.store(存儲)
作用於工作內存的變量,它把工作內存中的一個變量的值傳遞到主內存中,一般隨後的write操作使用
8.write(寫入)
作用於主內存的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中
8種基本操作必須滿足的規則:
1.不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行(即:read和load直接、store和write之間是可插入其他指令的)
2.不允許一個線程丟棄它的最近的assign操作,即:變量在工作內存中改變了之後,必須把該變化同步回主內存
3.不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中
4.一個新的變量只能從主內存中"誕生",不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,
即:對一個變量實施use和store操作之前,必須先執行過了assign河load操作
5.一個變量在同一時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個線程重複執行(可重入鎖),
多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖
6.如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值
7.如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量
8.對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)
volatile變量的特殊規則: --- 不能保證原子性
1.保證此變量對所有線程的可見性
2.禁止指令重排序優化
內存模型有哪些規則?
原子性、可見性、可排序性
10.Java反射與動態代理
Java反射,可以知道Java類的內部構造,就可以與它進行交互,包括創建新的對象和調用對象中的方法等。
這種交互方式與直接在源代碼中使用的效果是相同的,但是又額外提供了運行時刻的靈活性。使用反射的一個最大的弊端是性能比較差
每個類被加載進內存後,系統就會爲該類生成一個對應的 java.lang.Class對象,通過該Class對象即可訪問到JVM中的這個類
加載完類之後,在堆內存中會產生一個Class類型的對象(一個類只有一個Class對象),這個對象包含了完整的類結構信息,這個Class對象就像一面鏡子,可以看到類的結構
程序在運行狀態中,可以動態加載一個只有名稱的類(全路徑),對於任意一個已經加載的類,都能夠知道這個類的所有屬性和方法,
對於任意一個對象,都能調用它的任意一個方法和屬性
用法:
作用1:獲取類的內部結構
Java反射API可以獲取程序在運行時刻的內部結構,只需短短十幾行代碼,就可遍歷出一個Java類的內部結構,包括:構造方法、聲明的域和定義的方法等
java.lang.Class類的對象,可以通過其中的方法來獲取到該類中的構造方法、域和方法(getConstructor、getFiled、getMethod)
這三個方法還有相應的getDeclaredXXX版本,區別在於getDeclaredXXX版本的方法只會獲取該類自身所聲明的元素,而不會考慮繼承下來的。
Constructor、Field和Method這三個類分別表示類中的構造方法、域和方法。這些類中的方法可以獲取到所對應結構的元數據。
作用2:運行時刻,對一個Java對象進行操作
動態創建一個Java類的對象,獲取某個屬性的值和調用某個方法
在Java源碼中編寫的對類和對象的操作,都可以在運行時刻通過反射API來實現
特別說明:
clazz.setAccessible(true) --- 可以獲取到類中的private屬性和private方法
Class對象的獲取
1.對象的 getClass()方法
2.類.class屬性 --- 最安全,性能最好
3.Class.forName(String classFullName) 動態加載類 --- classFullName是類的全限定名(包.類名) --- 最常用
從Class中獲取信息
1.獲取信息
構造器、包含方法、包含屬性、包含的Annotation、實現的接口,所在包,類名,簡稱,修飾符等
獲取內容 方法簽名
構造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法 Method getMethod(String name, Class<?>... parameterTypes)
包含的屬性 Field getField(String name)
包含的Annotation <A extends Annotation> A getAnnotation(Class<A> annotationClass)
內部類 Class<?>[] getDeclaredClasses()
外部類 Class<?> getDeclaringClass()
所實現的接口 Class<?>[] getInterfaces()
修飾符 int getModifiers()
所在包 Package getPackage()
類名 String getName()
簡稱 String getSimpleName()
2.判斷信息
註解類型、是否是了指定註解、是否是數組、枚舉、接口等
判斷內容 方法簽名
註解類型? boolean isAnnotation()
使用了該Annotation修飾? boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名類? boolean isAnonymousClass()
數組? boolean isArray()
枚舉? boolean isEnum()
原始類型? boolean isPrimitive()
接口? boolean isInterface()
obj是否是該Class的實例 boolean isInstance(Object obj)
反射生成並操作對象
通過Method對象執行相應的方法
通過Constructor對象調用對應的構造器來創建實例
通過Field對象直接訪問和修改對象的成員變量值
反射創建對象
2種方式:
1.使用Class對象的newInstance()創建該Class對象對應類的實例(要求改Class對應類有默認構造器)
2.先使用Class對象獲取指定的Constructor對象,再調用Constructor對象的newInstance()方法創建該Class對象對應類的實例
--- 該方式,可選擇指定的構造器來創建實例
1.Spring根據配置文件信息(類的全限定名),使用反射創建對象 ---- 方式一,默認構造器創建實例
eg:
模擬Spring,實現一個對象池,對象池根據文件讀取key-value對,然後創建這些對象,並放入Map中,
對象池可以將id作爲key,將對象實例作爲value,可以通過id獲取對象實例
即:
ObjectPool pool = ObjectPool.init("config.json");
User user = (User) pool.getObject("id1");
System.out.println(user);
Bean bean = (Bean) pool.getObject("id2");
System.out.println(bean);
參考:
http://blog.csdn.net/zjf280441589/article/details/50453776
配置文件
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User"
},
{
"id": "id2",
"class": "com.fq.domain.Bean"
}
]
}
ObjectPool對象池
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
private static JSONArray getObjects(String config) throws IOException {
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
// 根據指定的JSON配置文件來初始化對象池
public static ObjectPool init(String config) {
try {
JSONArray objects = getObjects(config);
ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
if (objects != null && objects.size() != 0) {
for (int i = 0; i < objects.size(); ++i) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
pool.putObject(id, getInstance(className));
}
}
return pool;
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Object getObject(String id) {
return pool.get(id);
}
public void putObject(String id, Object object) {
pool.put(id, object);
}
public void clear() {
pool.clear();
}
}
實例類User和Bean
public class User {
private int id;
private String name;
private String password;
}
public class Bean {
private Boolean usefull;
private BigDecimal rate;
private String name;
}
反射調用方法
獲取到某個類對應的Class對象後,可通過該Class對象的getMethod()來獲取一個Method數組或Method對象,
每個Method對象對應一個方法,在獲取Method對象後,可通過調用invoke()方法調用該Method對象對應的方法
eg:
通過動態調用對象方法 + 配置文件,來給對象設置值 --- 根據屬性創建對象(調用setter方法,可設置屬性和依賴的對象)
1.json格式的配置文件,用來定義對象、屬性值及其依賴關係 --- config.json
注意:
其中fields代表該Bean所包含的屬性, name爲屬性名稱, value爲屬性值(屬性類型爲JSON支持的類型),
ref代表引用一個對象(也就是屬性類型爲Object,但是一定要引用一個已經存在了的對象)
這裏定義了一個User對象,設置其3個屬性
定義一個ComplexBean對象,設置其name屬性爲complex-bean-name,並設置其引用的對象是id2
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User",
"fields": [
{
"name": "id",
"value": 101
},
{
"name": "name",
"value": "feiqing"
},
{
"name": "password",
"value": "ICy5YqxZB1uWSwcVLSNLcA=="
}
]
},
{
"id": "id2",
"class": "com.fq.domain.Bean",
"fields": [
{
"name": "usefull",
"value": true
},
{
"name": "rate",
"value": 3.14
},
{
"name": "name",
"value": "bean-name"
}
]
},
{
"id": "id3",
"class": "com.fq.domain.ComplexBean",
"fields": [
{
"name": "name",
"value": "complex-bean-name"
},
{
"name": "refBean",
"ref": "id2"
}
]
}
]
}
2.定義對象池,用於創建對象,並調用其方法給對象賦值
參考:反射.ObjectPool 的實現
package com.jay.advanced.java.反射;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模擬Spring,創建一個對象池
* Created by hetiewei on 2017/2/17.
*/
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
//從指定文件,讀取配置信息,返回解析後json數組
private static JSONArray getObjects(String config) throws IOException {
//獲取輸入流
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
//讀取輸入流內容,變成json數組返回
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
//根據類名,獲取類的對象實例
private static Object getIntance(String className, JSONArray fields) throws ClassNotFoundException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//配置的Class
Class<?> clazz = Class.forName(className);
//目標Class的實例對象
Object targetObject = clazz.newInstance();
//遍歷屬性,賦值給實例對象 --- 注意區分,直接賦值,還是賦值引用對象
if (fields != null && fields.size() != 0) {
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
//需要設置的成員變量名
String filedName = field.getString("name");
//需要設置的成員變量的值
Object fieldValue;
//如果是8種基本類型或String類型,直接獲取value,得到屬性賦值
if (field.containsKey("value")) {
fieldValue = field.get("value");
} else if (field.containsKey("ref")) {
//如果是引用類型, 先獲得引用對象的id,然後根據id,從對象池中得到引用對象
String refBeanId = field.getString("ref");
fieldValue = OBJECTPOOL.getObject(refBeanId);
} else {
throw new RuntimeException("沒有value 或 ref 引用");
}
//構造setterXxx
String setterName = "set" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
//需要設置成員變量的setter方法
Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
//調用setter方法設置值
setterMethod.invoke(targetObject, fieldValue);
}
}
return targetObject;
}
private static ObjectPool OBJECTPOOL;
//創建一個對象池實例(保存多線程安全)
private static void initSingletonPool() {
if (OBJECTPOOL == null) {
synchronized (ObjectPool.class) {
if (OBJECTPOOL == null) {
OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
}
}
}
}
//指定根據的JSON配置文件來初始化對象池
public static ObjectPool init(String config) {
//初始化pool
initSingletonPool();
//解析Json配置文件
try {
JSONArray objects = getObjects(config);
for (int i = 0; i < objects.size(); i++) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
//初始化Bean,並放入對象池中
OBJECTPOOL.putObject(id, getIntance(className, object.getJSONArray("fields")));
}
return OBJECTPOOL;
} catch (IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void putObject(String id, Object obj) {
pool.put(id, obj);
}
public Object getObject(String id) {
return pool.get(id);
}
public void clear() {
pool.clear();
}
}
3.客戶端使用對象池
//初始化對象池
ObjectPool pool = ObjectPool.init("config.json");
//從對象池中根據id獲取對象實例
User user = (User) pool.getObject("id1");
Bean bean = (Bean) pool.getObject("id2");
ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
4.ComplexBean類
public class ComplexBean {
private String name;
private Bean refBean;
}
反射訪問並操作成員變量
通過Class對象的getField()方法可獲取該類所包含的全部或指定的成員變量Field,
getDeclaredXxx方法可以獲取所有的成員變量,無論private/public;
Field提供了讀取和設置成員變量值的方法
getXxx(Object obj)
獲取obj對象的該成員變量的值,此處的Xxx對應8中基本類型,如果該成員變量的類型是引用類型, 則取消get後面的Xxx;
setXxx(Object obj, Xxx val)
將obj對象的該成員變量值設置成val值.此處的Xxx對應8種基本類型, 如果該成員類型是引用類型, 則取消set後面的Xxx;
eg:
通過反射,設置成員變量值
User user = new User();
//反射獲取對象的指定屬性
Field idField = User.class.getDeclaredFiled("id");
//設置該屬性即使爲private也可被訪問
idField.setAccessible(true);
//將對象的常用變量,設置爲指定值 --- 這裏將user對象的id屬性,設置爲30
idField.setInt(user, 30);
private void setAccessible(AccessibleObject object) {
object.setAccessible(true);
}
反射獲取註解信息
只需要獲取到Class Method Filed等這些實現了AnnotatedElement接口的類實例, 就可以獲取到我們想要的註解信息
eg:
獲取client()方法上的註解信息
Annotation[] annotations = this.getClass().getMethod("client").getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType().getName());
}
eg:
獲取某個註解中的元數據,需要強轉成所需的註解類型,然通過註解對象的抽象方法來訪問這些元數據
@Tag(name = "client")
public class Client {
@Test
public void client() throws NoSuchMethodException {
Annotation[] annotations = this.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Tag) {
Tag tag = (Tag) annotation;
System.out.println("name: " + tag.name());
System.out.println("description: " + tag.description());
}
}
}
}
反射獲取泛型信息
爲了通過反射操作泛型,Java新增了4種類型來代表不能歸一到Class了下,但又和原始類型同樣重要的類型
類型 含義
ParameterizedType 一種參數化類型, 比如Collection<String>
GenericArrayType 一種元素類型是參數化類型或者類型變量的數組類型
TypeVariable 各種類型變量的公共接口
WildcardType 一種通配符類型表達式, 如? ? extends Number ? super Integer
動態代理:
代理模式:
代理對象和被代理對象一般實現相同的接口,調用者與代理對象進行交互。代理的存在對於調用者來說是透明的,調用者看到的只是接口。
代理對象則可以封裝一些內部的處理邏輯,如訪問控制、遠程通信、日誌、緩存等。比如一個對象訪問代理就可以在普通的訪問機制之上添加緩存的支持,
傳統的代理模式的實現,需要在源代碼中添加一些附加的類。這些類一般是手寫或是通過工具來自動生成
動態代理:
JDK5引入了動態代理機制,允許開發人員在運行時刻動態的創建出代理類及其對象。
在運行時刻,可以動態創建出一個實現了多個接口的代理類,每個代理類的對象都會關聯一個表示內部處理邏輯的InvocationHandler接口的實現,
當使用者調用代理對象所代理的接口中的方法時,這個調用信息被傳遞個InvocationHandler的invoke()方法,
在invoke()方法參數中可以獲取到代理對象、方法對應的Method對象和調用的實際參數,invoke()方法的返回值被返回給使用者。
相當於對方法調用進行了攔截 --- 這是一個不需要依賴AspectJ等AOP框架的一種AOP實現方式
11.常用的內存調節參數
-Xms 初始堆大小,默認是物理內存1/64(<1G)
-Xmx 最大堆大小
-Xmn 新生代的內存空間大小(eden+ 2 survivor space),
整個堆大小=新生代大小 + 老生代大小 + 永久代大小
在保證堆大小不變的情況下,增大新生代後,將會減小老生代大小。此值對系統性能影響較大,Sun官方推薦新生代配置爲整個堆的3/8
-XX:SurvivorRation 新生代Eden區和Survivor區容量比,默認是8, 兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10
-Xss 每個線程的堆棧大小, 推薦 256K
-XX:PermSize 持久代(非堆內存)初始值, 默認是物理內存 1/64
-XX:MaxPermSize 持久代(非堆內存)最大值,默認物理內存1/4
-XX:+UseParallelGC 多核處理器,配置後提示GC效率
eg:
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=256M
-vmargs 說明後面是VM的參數,所以後面的其實都是JVM的參數了
-Xms128m JVM初始分配的堆內存
-Xmx512m JVM最大允許分配的堆內存,按需分配
-XX:PermSize=64M JVM初始分配的非堆內存
-XX:MaxPermSize=128M JVM最大允許分配的非堆內存,按需分配
說明:
Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在 Java 虛擬機啓動時創建的。”“在JVM中堆之外的內存稱爲非堆內存(Non-heap memory)”。
JVM主要管理兩種類型的內存:堆和非堆。簡單來說堆就是Java代碼可及的內存,是留給開發人員使用的;非堆就是JVM留給自己用的,
方法區,JVM內存處理貨優化所需的內存、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中
內存分配方法:
1.堆上分配
2.棧上分配
12.JVM內存管理
程序計數器
方法區
堆
棧(JVM棧 + 本地方法棧)
說明:
GC主要發生在堆上,方法區也會發生GC, 棧與寄存器是線程私有的,不會GC
方法區:
存放內容:類信息、類的static屬性、方法、常量池
已經加載的類的信息(名稱、修飾符等)
類中的static變量
類中的field信息
類中定義爲final常量
類中的方法信息
運行時常量池:編譯器生成的各種字面量和符號引用(編譯期)存儲在class文件的常量池中,這部分內容會在類加載之後進入運行時常量池
使用實例: 反射
在程序中通過Class對象調用getName等方法獲取信息數據時,這些信息數據來自方法區
調節參數:
-XX:PermSize 指定方法區最小值,默認16M
-XX:MaxPermSize 指定方法區最大值,默認64M
所拋異常:
方法區使用內存超過最大值,拋出 OutOfMemoryError
GC操作:
對類的卸載
針對常量池的回收
總結:
企業開發中, -XX:PermSize==-XX:MaxPermSize,都設置爲256M
eg:
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
類中的static變量會在方法區分配內存,但是類中的實例變量不會(類中的實例變量會隨着對象實例的創建一起分配在堆中,當然若是基本數據類型的話,會隨着對象的創建直接壓入操作數棧)
關於方法區的存放內容,可以這樣去想所有的通過Class對象可以反射獲取的都是從方法區獲取的(包括Class對象也是方法區的,Class是該類下所有其他信息的訪問入口)
堆:
存放內容:
對象實例(類中的實例變量會隨着對象實例的創建一起分配在堆中,當然若是基本數據類型的話,會隨着對象的創建直接壓入操作數棧)
數組值
使用實例:
new創建的對象都在這裏分配
調節參數:
-Xmx:最大堆內存,默認爲物理內存的1/4但小於1G
-Xms:最小堆內存,默認爲物理內存的1/64但小於1G
-XX:MinHeapFreeRatio,默認當空餘堆內存小於最小堆內存的40%時,堆內存增大到-Xmx
-XX:MaxHeapFreeRatio,當空餘堆內存大於最大堆內存的70%時,堆內存減小到-Xms
注意:
在實際使用中,-Xmx與-Xms配置成相等的,這樣,堆內存就不會頻繁的進行調整了!!!
所拋錯誤:
OutOfMemoryError:Java heap space
堆內存劃分:
新生代:
Eden + from + to from 與 to 大小相等
-Xmn:整個新生代大小
-XX:SurvivorRation : 調整Eden:from(to)的比例,默認是 8:1 即: eden:from:to = 8:1:1
老年代:
新建對象直接分配到老年代的2種情況:
大對象:-XX:PretenureSizeThreshold(單位:字節)參數來指定大對象的標準
大數組:數據中的元素沒有引用任何外部的對象
總結:
企業開發中 -Xmx==-Xms
eg:
<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年輕代大小-Xmn==512m,這樣,年老代大小就是2048-512==1536m,這個比率值得記住,
在企業開發中,年輕代:年老代==1:3,而此時,我們配置的-XX:MaxTenuringThreshold=15(這也是默認值),年輕代對象經過15次的複製後進入到年老代
年輕代分爲Eden和2個Survivor(from+to),默認 Eden:from:to==8:1:1
1.新產生的對象有效分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的對象會直接進入年老代)
2.當Eden區滿了或放不下時,其中存活的對象會複製到from區(注意:如果存活下來的對象from區放不下,則這些存活下來的對象全部進入老年代),之後Eden區的內存全部回收掉
注意:如果Eden區沒有滿,但來了一個小對象Eden區放不下,這時候Eden區存活對象複製到from區後,清空Eden區,之後剛纔的小對象再進入Eden區
3.之後產生的對象繼續分配在Eden區,當Eden區滿時,會把Eden區和from區存活下來的對象複製到to(同理,如果存活下來的對象to區都放不下,則這些存活下來的對象全部進入年老代),之後回收掉Eden區和from區的所有內存;
4)如上這樣,會有很多對象會被複制很多次(每複製一次,對象的年齡就+1),默認情況下,當對象被複制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了
5)當年老代滿了或者存放不下將要進入年老代的存活對象的時候,就會發生一次Full GC(這個是我們最需要減少的,因爲耗時很嚴重)
棧:
注意點:
每個線程都會分配一個棧,每個棧中有多個棧幀(每個方法對應一個棧幀) 每個方法在執行的同時都會創建一個棧幀,每個棧幀用於存儲當前方法的局部變量表、操作數棧等,
每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程,說的更明白一點,就是方法執行時創建棧幀,方法結束時釋放棧幀所佔內存
存放內容:
局部變量表: 8種基本數據類型、對象引用, 該空間在編譯期已經分配好,運行期不變
參數調節:
-Xss:設置棧大小,通常設置爲1m就好
eg:
<jvm-arg>-Xss1m</jvm-arg>
所拋異常:
StackOverFlowError 線程請求的棧深度大於虛擬機所允許的深度
eg:
沒有終止調節的遞歸(遞歸基於棧)
每個方法的棧深度在javac編譯之後就已經確定了
OutOfMemoryError: 虛擬機棧可以動態擴展,如果擴展時無法申請到足夠內存,則拋該異常
注意:
棧可以動態擴展,但棧中的局部變量表不可以
C寄存器(程序計數器)
概念: 當前線程所執行的字節碼的行號指示器,用於字節碼解釋器對字節碼指令的執行。
多線程:通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條線程中的指令,
爲了線程切換後能恢復到正確的執行位置,每條線程都要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲。
內存分配概念:
在類加載完成後,一個對象所需的內存大小就可以完全確定了,具體的情況查看對象的內存佈局。
爲對象分配空間,即把一塊兒確定大小(上述確定下來的對象內存大小)的內存從Java堆中劃分出來
Java的對象內存佈局 3 大塊
對象頭
存儲對象自身的運行時數據:Mark Word(在32位和64位JVM上長度分別爲32bit和64bit),包含信息如下:
對象hashCode
對象GC分代年齡
鎖狀態標識
線程持有的鎖
偏向鎖相關:偏向鎖、自旋鎖、輕量級鎖以及其他的一些鎖優化策略是JDK1.6加入的,這些優化使得Synchronized的性能與ReentrantLock的性能持平,
在Synchronized可以滿足要求的情況下,優先使用Synchronized,除非是使用一些ReentrantLock獨有的功能,例如指定時間等待等。
類型指針: 對象指向類元數據的指針
JVM通過這個指針來確定這個對象是哪個類的實例(根據對象確定其Class的指針)
實例數據
對象真正存儲的有效信息
對齊填充
JVM要求對象的大小必須是8的整數倍,若不夠,需要補位對齊
注意:
1.Mark Word是非固定的數據結構,以便在極小空間內存儲儘量多的信息
2.如果對象是一個數組,對象頭必須有一塊用來記錄數組長度的數據,JVM可以通過Java對象的元數據確定對象長度,但對於數組不行
3.基本數據類型和對象包裝類所在的內存大小(字節)
boolean 1字節
byte 1字節
short 2字節
char 2字節
int 4字節
float 4字節
long 8字節
double 8字節
引用類型 在32位和64位系統上長度分別爲4bit和8bit
內存分配的 2 種方式:
1.指針碰撞
適用場合:
堆內存規整(即:沒有內存碎片,有大塊完整內存)的情況下
原理:
用過的內存全部整合到一邊,沒用過的內存放在另一邊,中間有個分界值指針,只需要向着沒有用過的內存方向將該指針移動新創建的對象內存大小位置即可
GC收集器:
Serial、ParNew
2.空閒列表
適用場合:
堆內存不規整情況
原理:
JVM虛擬機會維護一個列表,該列表會記錄哪些內存塊是可用的,在分配時,找一塊足夠大的內存來劃分給new的對象實例,最後更新列表記錄
GC收集器:
CMS
注意:
1.2種內存分配方式,取決於Java堆內存是否規整
2.Java堆內存是否規整,取決於GC收集器的算法是 "標記-清除" 還是 "標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製算法內存也是規整的
創建一個真正對象的基本過程:5步
1.類加載機制檢查
JVM首先檢查一個new指定的參數是否能在常量池中定位到一個符號引用,並且檢查該符號應用代表的類是否已被加載、解析和初始化過
實際就是在檢查new的對象所屬的類是否已經執行過類加載機制,如果沒有,則先進行加載機制加載類
2.分配內存
把一塊確定大小的內存從堆中劃分出來
3.初始化零值
對象的實例字段不需要賦初始值也可以直接通過其默認零值
每種類型的對象都有不同的默認零值
4.設置對象頭
5.執行<init>
爲對象字符賦值(第3步只是初始化了零值,這裏會根據參數,給實例賦值)
13.JVM 內存回收GC
1.內存回收區域
堆:GC主要區域
方法區: 回收無用的類和廢氣的常量
注意:
棧和PC寄存器是線程私有,不會發生GC
2.判斷對象是否存活
1.引用計數法
原理:
給對象添加一個引用計數器,每當有一個地方使用它,計數器值+1,引用失效時,計數器值-1
缺點:
1.每次爲對象賦值時,都要進行計數器值得加減,消耗較大
2.對於循環依賴無法處理
2.可達性分析(跟蹤收集)
原理:
從根集合(GC Root)開始向下掃描,根集合中的節點可以到達的節點就是存活節點,根集合中的節點到達不了的節點就是要被回收的的節點
GC Root節點: 全局性的引用(常量和靜態屬性)和棧引用
1.Java棧中的對象引用
2.方法區中, 常量+靜態變量
3. 3 種引用類型
強引用: A a = new A();//a是常引用
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題
軟引用: 內存不足時,是否弱引用所引用的對象,當內存足夠時,就是一個普通對象(強引用)
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a); //軟引用
弱引用: 弱引用對象只能存活到下一次垃圾會話之前,一旦發生垃圾回收,立刻被回收掉
4.GC回收算法
1.標記-清楚算法 --- 年老代
2.標記-整理算法(標記-壓縮) --- 年老代
3.複製算法 --- 年輕代
標記-清楚算法 --- 年老代
原理:
從根集合點掃描,標記處所有的存活對象,最後掃描整個內存空間,並清除沒有標記的對象(即:死亡對象)
使用場合:
存活對象較多的情況下比較高效
適用於年老代
缺點:
容易產生內存碎片,再來一個比較大的對象時(典型情況:該對象的大小大於空閒表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
注意:
在該情況下,內存不規整,對象的內存分配採用"空閒列表法"
標記-整理算法(標記-壓縮) --- 年老代
原理:
從根集合節點進行掃描,標記出所有的存活對象,最後掃描整個內存空間並清除沒有標記的對象(即死亡對象)(可以發現前邊這些就是標記-清除算法的原理),清除完之後,將所有的存活對象左移到一起。
適用場合:
用於年老代(即舊生代)
缺點:
需要移動對象,若對象非常多而且標記回收後的內存非常不完整,可能移動這個動作也會耗費一定時間
掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
優點:
不會產生內存碎片
注意:
在該情況下,內存規整,對象的內存分配採用"指針碰撞法"
複製算法 --- 年輕代
原理:
從根集合節點進行掃描,標記出所有的存活對象,並將這些存活的對象複製到一塊兒新的內存(圖中下邊的那一塊兒內存)上去,之後將原來的那一塊兒內存(圖中上邊的那一塊兒內存)全部回收掉
適用場合:
存活對象較少的情況下比較高效
掃描了整個空間一次(標記存活對象並複製移動)
適用於年輕代(即新生代):基本上98%的對象是"朝生夕死"的,存活下來的會很少
缺點:
需要一塊兒空的內存空間
需要複製移動對象
注意:
在該情況下,內存規整,對象的內存分配採用"指針碰撞法",見《第二章 JVM內存分配》
以空間換時間:通過一塊兒空內存的使用,減少了一次掃描
14.關於Set --- HashSet、TreeSet、LinkedHashSet -- 都是去重的,都可用Iterator或foreach進行遍歷
參考:http://blog.csdn.net/speedme/article/details/22661671
HashSet --- 去重,無序, add()時會調用hashcode和equals,所以存儲在HashSet中的對象需要重寫這兩個方法,非同步的,元素只能放一個null
即:
HashSet:數據結構式哈希表,線程非同步。保證元素唯一性的原理,判斷hashCode是否相同,如果相同,判斷元素的equals方法
TreeSet --- 去重,可按照某種順序排序, add()會將對象轉爲Comparable,然後調用compareTo()方法,所以存儲在TreeSet中的對象必須實現Comparable,重寫compareTo()方法
底層數據結構是 二叉樹,保證元素唯一性的依據
支持2種排序:自然排序、定製排序
TreeSet判斷兩個對象不相等的方式是兩個對象通過equals方法返回false,或者通過CompareTo方法比較沒有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。
Java提供了一個Comparable接口,該接口裏定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該接口的對象就可以比較大小。
obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個對象相等,如果返回一個正數,則表明obj1大於obj2,如果是 負數,則表明obj1小於obj2。
如果我們將兩個對象的equals方法總是返回true,則這兩個對象的compareTo方法返回應該返回0
定製排序
自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator接口,實現 int compare(T o1,T o2)方法
2種排序方式比較:
方式一:讓集合中的元素自身具有比較性,這就讓加入到TreeSet集合中的對象必須實現comparable接口重寫compareTo(Object obj)方法
這種方式也成爲元素的自然排序或默認排序。(但是如果排序的元素不是本人寫的,別人寫的沒有實現comparable接口時想排序使用第二種方式)
方式二:讓集合容器具有比較性,自定義一個比較器實現comparator接口,重寫compare(Object o1,Object o2)方法,在初始化TreeSet容器對象將這個
自定義的比較器作參數傳給容器的構造函數,使得集合容器具有比較性,使用這種方式的優先級高於方式一,
LinkedHashSet --- HashSet的子類,去重,並保留存儲順序
HashSet 工作原理: 每次存儲對象時,調用對象的hashCode(),計算一個hash值,在集合中查找是否包含hash值相同的元素
如果沒有hash值相同的元素,根據hashCode值,來決定該對象的存儲位置,直接存入,
如果有hash值相同的元素,逐個使用equals()方法比較,
比較結果全爲false就存入.
如果比較結果有true則不存.
如何將自定義類對象存入HashSet進行去重複
* 類中必須重寫hashCode()方法和equals()方法
* equals()方法中比較所有屬性
* hashCode()方法要保證屬性相同的對象返回值相同, 屬性不同的對象儘量不同
TreeSet 工作原理:存儲對象時,add()內部會自動調用compareTo()方法進行比較,根據比較結果使用二叉樹形式進行存儲 --- 二叉樹實現存儲
參考:http://blog.csdn.net/jinhuoxingkong/article/details/51191106
TreeSet使用二叉樹原理,對新add()的對象安裝指定的順序排序(升序、降序),每增加一個對象都會進行排序,將對象插入二叉樹指定的位置
Integer和String都是按默認的TreeSet排序,自定義的對象,必須實現Comparable接口,重寫compareTo(),指定比較規則
在重寫compare()方法時,要返回相應的值才能使TreeSet按照一定規則來排序,升序是:比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整數、零或正整數。
如果想把自定義類的對象存入TreeSet進行排序, 那麼必須實現Comparable接口
* 在類上implement Comparable
* 重寫compareTo()方法
* 在方法內定義比較算法, 根據大小關係, 返回正數負數或零
TreeSet實現排序的2種比較方式:
1.自定義類類 實現 Comparable接口,重寫其 compareTo()方法 ---- 類排序
2.給TreeSet定義一個實現Comparator接口的比較器,重寫其 compare()方法 ---- 比較器排序
* a.自然順序(Comparable)
* TreeSet類的add()方法中會把存入的對象提升爲Comparable類型
* 調用對象的compareTo()方法和集合中的對象比較
* 根據compareTo()方法返回的結果進行存儲
* b.比較器順序(Comparator)
* 創建TreeSet的時候可以制定 一個Comparator
* 如果傳入了Comparator的子類對象, 那麼TreeSet就會按照比較器中的順序排序
* add()方法內部會自動調用Comparator接口中compare()方法排序
* 調用的對象是compare方法的第一個參數,集合中的對象是compare方法的第二個參數
* c.兩種方式的區別
* TreeSet構造函數什麼都不傳, 默認按照類中Comparable的順序(沒有就報錯ClassCastException)
* TreeSet如果傳入Comparator, 就優先按照Comparator
15.關於Map --- HashMap、TreeMap
原理:http://blog.csdn.net/chenssy/article/details/26668941
HashMap
按照key的hashCode實現,無序
TreeMap
基於紅黑樹實現,映射根據其key的自然順序進行排序,或根據創建映射時提供的Comparator進行排序,具體取決於使用的構造方法
TreeMap只能依據key來排序,不能根據value排序
如果想對value排序,可以把TreeMap的EntrySet轉換成list,然後使用Collections.sort排序 -- 參考:http://blog.csdn.net/liuxiao723846/article/details/50454622
http://blog.csdn.net/xiaoyu714543065/article/details/38519817
eg:value是String或引用類型的值,按照指定規則對value進行排序
public static Map sortTreeMapByValue(Map map){
List<Map.Entry> list = new ArrayList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry>() {
//升序排
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().toString().compareTo(o2.getValue().toString());
}
});
for (Map.Entry<String, String> e: list) {
System.out.println(e.getKey()+":"+e.getValue());
}
return map;
}
16.關於ThreadLocal
當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本
3個重要方法:
void set(T value)、T get()以及T initialValue()
使用場景:
多線程中,每個線程需要獨享這個變量,且每個線程用的變量最初都是一樣的,可以通過ThreadLocal處理該變量
原理:
ThreadLocal如何爲每個線程維護變量的副本?
ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中key爲線程對象,value爲線程的變量副本
eg:
public class JavaTest {
// 創建一個Integer型的線程本地變量, 並通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];// 創建5個線程
for (int j = 0; j < 5; j++) {
threads[j] = new Thread(new Runnable() {
@Override
public void run() {
// 獲取當前線程的本地變量,然後累加5次
int num = local.get();// 返回當前線程的線程本地變量值,若對應的thread不存在,則會調用initialValue初始化
for (int i = 0; i < 5; i++) {
num++;
}
// 重新設置累加後的本地變量
local.set(num);
System.out.println(Thread.currentThread().getName() + " : "
+ local.get());
}
}, "Thread-" + j);
}
for (Thread thread : threads) {// 啓動線程
thread.start();
}
}
}
運行後結果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個線程累加後的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響
17.自定義註解
元註解
Java提供了4種元註解,專門負責註解其他註解使用
@Retention 表示需要在什麼級別保存該註釋信息(生命週期)
可選參數:
RetentionPolicy.SOURCE: 停留在java源文件,編譯器被丟掉
RetentionPolicy.CLASS:停留在class文件中,但會被VM丟棄(默認)
RetentionPolicy.RUNTIME:內存中的字節碼,VM將在運行時也保留註解,因此可以通過反射機制讀取註解的信息 --- 最常用
@Target 表示該註解用於什麼地方
可選參數:
ElementType.CONSTRUCTOR: 構造器聲明
ElementType.FIELD: 成員變量、對象、屬性(包括enum實例)
ElementType.LOCAL_VARIABLE: 局部變量聲明
ElementType.METHOD: 方法聲明
ElementType.PACKAGE: 包聲明
ElementType.PARAMETER: 參數聲明
ElementType.TYPE: 類、接口(包括註解類型)或enum聲明
@Documented 將註解包含在JavaDoc中
@Inheried 運行子類型繼承父類中的註解
自定義註解:
eg:
自定義註解 --- MyAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定義註解
* 作用於方法和類
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
//爲註解添加屬性
String color();
String value() default "我是xxx";//爲註解屬性提供默認值
int[] array() default {1,2,3};
Gender gender() default Gender.MAN; // 添加一個枚舉
// 添加枚舉屬性
MetaAnnotation metaAnnotation() default @MetaAnnotation(birthday = "我的出身日期爲1988-2-18");
}
定義一個枚舉類 --- Gender
public enum Gender{
MAN{
public String getName(){
return "男";
}
},
WOMEN{
public String getName(){
return "女";
}
};
}
定義註解類 --- MetaAnnotation
public @interface MetaAnnotation{
String birthday();
}
解析註解:
/**
* 調用註解並賦值
* Created by hetiewei on 2016/10/12.
*/
@MyAnnotation(metaAnnotation = @MetaAnnotation(birthday = "我的出身日期爲1991-2-27"),
color = "red", array = {23, 26 })
public class Test {
public static void main(String args[]){
//檢查類Test中是否包含@MyAnnotation註解
if (Test.class.isAnnotationPresent(MyAnnotation.class)){
//若存在則獲取註解,並解析
MyAnnotation annotation = Test.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
//解析註解中的內容
//1.獲取註解屬性
System.out.println(annotation.color());
System.out.println(annotation.value());
//2.獲取屬性數組
int[] arrs = annotation.array();
System.out.println(arrs.toString());
//3.獲取枚舉
Gender gender = annotation.gender();
System.out.println("性別:"+gender);
//4.獲取註解屬性
MetaAnnotation meta = annotation.metaAnnotation();
System.out.println(meta.birthday());
}
}
}
18.關於枚舉 枚舉類是一種特殊的類,它一樣有自己的Field,方法,可以實現一個或者多個接口,也可以定義自己的構造器
枚舉與普通類有如下簡單區別:
(1). 枚舉類可以實現一個或者多個接口,使用enum定義的枚舉類默認繼承了java.lang.Enum類,而不是繼承Object類。其中java.lang.Enum類實現了java.lang.Serializable和java.lang.Comparable接口。
(2). 使用enum定義,非抽象的枚舉類默認會使用final修飾,因此枚舉類不能派生子類。
(3). 枚舉類的構造器只能使用private訪問控制符,如果省略了構造器的訪問控制符,則默認使用private修飾;如果強制指定訪問控制符,則只能指定private修飾符。
(4). 枚舉類的所有實例必須在枚舉類的第一行顯示列出,否則這個枚舉類永遠不能產生實例。列出這些實例時,系統會自動添加public static final修飾,無須程序員顯示添加。
所有的枚舉類都提供了一個values方法,該方法可以很方便的遍歷所有的枚舉值
(5) 枚舉常用方法
name() ,toString() --- 返回此枚舉實例名稱,優先使用 toString()
ordinal() --- 返回枚舉實例的索引位置,第一個枚舉值索引爲0
public static T valueOf(Class enumType, String name)
--- 返回指定枚舉類中指定名稱的枚舉值,名稱必須與在該枚舉類中聲明枚舉值時所用的標識符完全匹配,不允許使用額外的空白字符
eg:
public enum SeasonEnum{
//列出4個枚舉實例
SPRING,SUMMER,FALL,WINTER;
}
解析:
1.枚舉值之間以英文逗號(,)隔開,枚舉值列舉結束後以英文分號作爲結束
2.枚舉值代表了該枚舉類的所有可能實例
3.使用枚舉值 EnumClass.variable eg: SeasonEnum.SPRING
4.枚舉可作爲switch條件表達式
5.獲取所有枚舉值 EnumClass[] enums = EnumClass.values();
枚舉類的屬性Field、方法和構造器
1.枚舉類也是一種類,只是它是一種比較特殊的類,因此它一樣可以定義Field,方法
19.幾種集合類解析
1.HashMap 底層 數組+鏈表,計算出hash值後,得到元素在數組中存放的位置索引,
若不同元素hash值相同,即:有相同的存放位置,則在相同位置建立鏈表,採用頭插入法依次保存元素
工作原理:
數組+鏈表 以 Entry[]數組實現的哈希桶數組,用Key的哈希值取模數組的大小得到數組的下標
如果多個key佔有同一個下標(碰撞),則使用鏈表將相同的key串起來
通過hash方法,通過put和get存儲和獲取對象,存儲對象時,將K/V傳給put()方法時,它調用hashCode()計算hash值得到bucket位置,
進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過加載因子,容量擴展爲2倍)。
獲取對象時,通過K,調用hashCode()得到bucket位置,並進一步調用equals()方法確定鍵值對。
如果發生碰撞時,HashMap通過鏈表將產生碰撞的元素組織起來,在Java8中,如果一個bucket中碰撞的元素超過某個限制(,默認8個),
則使用紅黑樹來替換鏈表,從而提高速度
2.HashSet 底層 是HashMap實現, 優點:利用哈希表提供查詢效率, 缺點:元素不能重複
由於HashSet不允許元素重複,故需要判斷元素是否相同,
用hash表判斷元素是否相同的方法,即需要hashCode和equals兩個方法,對於hashSet,先通過hashCode判斷元素在哈希表中的位置是否相同,在通過equals方法判斷元素內容是否相同
哈希表如何判斷元素是否相同?
1> 哈希值是否相同 :判斷的其實是對象的hashCode()方法是否相同(看對象是否在哈希表的同一個位置)
2>內容是否相同:用equals方法判斷對象是否相同。
規則:若hash值不同,不必判斷對象內容,返回false;若hash值相同,有必要判斷對象內容,若在相同,返回true,否則false。
3.TreeSet 使用元素的自然順序,對象集合中元素進行排序,添加的元素需要自己實現Comparable接口,以便默認排序時調用其CompareTo()進行比較
2中自定義排序方式
1.元素的類,實現Comparable接口,實現compareTo()
2.給ThreeSet傳遞一個實現Comparator接口的參數對象
20.關於線程池
參考:
http://www.codeceo.com/article/java-thread-pool-deep-learn.html
http://www.codeceo.com/article/java-threadpoolexecutor.html
1.核心類:
ThreadPoolExecutor extends AbstractExecutorService implement ExecutorService 提供4個構造器
構造器參數:
corePoolSize: 核心池大小
默認情況,創建線程池後,池中線程數爲0,當有任務來時,創建一個線程去執行任務,當線程池中線程數目達到corePoolSize後,會把任務放入緩存隊列、
maxPoolSize: 線程池最大線程數
keepAliveTime:表示線程沒有任務執行時最多保持多久會終止
默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize
unit: 參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
workQueue: 阻塞隊列,用來存儲等待執行的任務
可選的阻塞隊列:
ArrayBlockingQueue
LinkedBlockingQueue --- 默認,用的最多
SynchronousQueue
PriorityBlockingQueue
threadFactory: 線程工廠,主要用來創建線程
handler: 表示當拒絕處理任務時的策略,
4種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
2.線程池原理:
1.線程池狀態:
ThreadPoolExecutor中定義了一個Volatile變量runState表示當前線程池的狀態,使用volatile來保證線程之間的可見性
線程池的4種狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
解析:
1.創建線程池後,初始時,線程池處於RUNNING狀態
2.如果調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不能接受新任務,但會等待所有任務執行完畢
3.如果調用了shutdownNow()方法,線程池處於STOP狀態,此時線程池不能接受新任務,並且會嘗試終止正在執行的任務
4.當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態
2.任務的執行:
線程任務保存在BlockingQueue中,通過execute(Runnable )來調用執行,
21.Java的類加載器
類加載器 --- 一個用來加載類文件的類
Java源碼通過javac編譯成類文件,然後JVM來執行類文件中的字節碼,類加載器負責加載文件系統、網絡或其他來源的類文件
JVM中類加載器的樹狀層次結構
Bootstrap ClassLoader 引導類加載器, 加載Java的核心庫(jre/lib/rt.jar),用C++代碼實現,不繼承子java.lang.ClassLoader
Extension ClassLoader 擴展類加載器, 加載Java的擴展庫(jre/ext/*.jar), Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類
System ClassLoader 系統類加載器, 根據Java應用的類路徑(classpath)來加載Java類,
Java應用的類都是由它來完成加載的,可通過ClassLoader.getSystemClassLoader()獲取系統類加載器
自定義類加載器 通過繼承java.lang.ClassLoader類,實現自己的類加載器
Java類加載器的3個機制:
委託機制:
將加載器的請求交給父加載器,如果父類加載器找不到或不能加載這個類,則當前類加載器再加載它
可見性機制:
子類的加載器可以看見所有父類加載器加載的類,而父類加載器看不到子類加載器加載的類
單一性機制:
類僅被加載一次, 由 委託機制 確保子類加載器不會再次加載父類加載器加載過的類
類加載過程 3個步驟:
裝載:
鏈接:(驗證、準備、解析)
初始化:
裝載:
查找並加載類的二進制數據
鏈接:
驗證:確保被加載類信息符合JVM規範、沒有安全方面問題
準備:爲類的靜態變量分配內存,並將其初始化爲默認值
解析:把虛擬機常量池中的符號引用轉換爲直接引用
初始化:
爲類的靜態變量賦予正確的初始值
說明:
1.JVM會爲每個加載的類維護一個常量池
2.類的初始化步驟:
1.如果這個類沒被加載和鏈接,則先進行加載和鏈接
2.如果這個類存在父類,如果類未初始化,則先初始化其父類
3.如果類中存在static塊,一次執行這些初始化語句
java.lang.ClassLoader類
根據一個指定的類名稱,找到或生成其對於的字節代碼,然後從這些字節碼中定義出一個Java類,即:java.lang.Class類的一個實例
ClassLoader中的方法:
getParent() 返回該類加載器的父類加載器
loadClass(name) 加載名稱爲name的類,返回結果是java.lang.Class的實例
findClass(name) 查找名稱爲name的類,返回結果是java.lang.Class的實例
findLoadedClass(name) 查找名稱爲name的已被加載過的類,返回結果是java.lang.Class的實例
resolveClass(Class<?> c) 鏈接指定的Java類
Java中的類加載過程
加載(可自定義類加載器) 連接 ( 驗證 準備 解析 ) 初始化
加載:
獲取二進制字節流 --> 將字節流靜態存儲結構轉換爲方法區的運行時數據結構 --> 在堆中生成Class對象
連接:
驗證:
文件格式驗證: 1.參照Class文件格式規範驗證
2.此階段基於字節流經過此驗證後,字節流纔會進入方法區,後面的驗證都依賴與方法區的驗證
元數據驗證: Java語法驗證,eg:該類是否繼承了不該繼承的類
字節碼驗證: 運行時安全性檢查
符號引用驗證: 確保類中引用的類,字段,方法都是可訪問的
準備:
設置類變量初始值 --- static類變量 初始值 , 注意:final比較特別!!!
1.設置類變量 --- static變量
2.設置變量初始值 (注意:非代碼中定義的值,8種基本數據類型都有初始值 eg: static int a = 10, 準備階段會把a初始值賦值爲0,初始化時,再賦值爲10 )
3.對於final的值,設爲代碼中的值(eg:final static int a = 10 , 準備階段直接把 a 賦值爲10)
解析:
將符號引用轉換爲直接引用
1.符號引用: 用符號來表示所引用的目標
2.直接引用: 一個指向內存中目標對象地址的句柄
初始化:
1.根據代碼實現初始化類變量及其他資源 (準備階段,static類變量還是初始值,這裏賦值爲代碼中指定的值)
2.執行子類初始化方法時,先執行父類的初始化方法(static變量賦值,static代碼段執行,先父類後子類)
22.Java反射 增加 裝飾模式 的適用性
裝飾模式:在不必改變原類文件和使用繼承的情況下,動態地擴展一個對象的功能,它是通過創建一個包裝對象來包裹真實的對象,比生產子類更加靈活,
使用Java的動態代理實現裝飾模式,會具有更強的靈活性和適用性
裝飾模式有什麼特點呢?
1、裝飾對象和真實對象有相同的接口。這樣調用者就能以和真實對象相同的方式和裝飾對象交互。
2、裝飾對象包含一個真實對象的引用(即上面例子中的Ability接口)。
3、裝飾對象接受所有來調用者的請求,並把這些請求轉發給真實的對象。
4、裝飾對象可以在調用者的方法以前或以後增加一些附加功能。這樣就確保了在運行時,不用修改給定對象的結構就可以在外部增加附加的功能。
什麼樣的地方使用裝飾模式呢?
1、需要動態擴展一個類的功能,或給一個類添加附加職責。
2、需要動態的給一個對象添加功能,這些功能可以再動態的撤銷。
3、需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
4、 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因爲類定義被隱藏,或類定義不能用於生成子類。
23.JVM的運行時棧幀 --- JVM運行程序的過程!!! --- 方法的運行過程 !!!
1.每個方法的執行,在JVM中都是對應的棧幀在JVM棧中的入棧到出棧的過程!!!
2.每個在JVM中運行的程序,都是由許多的幀切換產生的結果
參考:
http://blog.csdn.net/column/details/14217.html
棧幀: --- 線程安全!!!每個線程的棧幀相互獨立 ---> 局部變量在多線程環境下線程安全的原因!!!
存放方法的局部變量表、操作數棧、動態鏈接,方法返回值和一些額外的附加信息
當前棧:
一個方法的調用鏈可能很長,當調用一個方法時,可能會有很多方法處於執行狀態,但對於執行引擎,置於JVM棧頂的棧幀纔是有效的,這個棧幀稱爲 當前棧
當前棧所關聯的方法稱爲當前方法,執行引擎的所有指令都是針對當前棧幀進行操作的
局部變量表:
內容: 存放方法的局部變量
eg:方法參數,方法內定義的局部變量,對象引用,returnAddress類型
在Java程序被編譯爲class文件時,這個表的容量最大值已經確定
訪問:
虛擬機利用索引編號的遞增來對局部變量表中定義的變量進行一次訪問(從0開始),而對於實例方法(非static方法),其局部變量表的第0個索引是this,
這是可以在實例方法中使用this.name ......的原因
動態連接:
參考:http://blog.csdn.net/eric_sunah/article/details/8014865
方法的調用過程:
在虛擬機運行時,運行時常量池會保存大量符號引用,這些符號引用可以看做每個方法的間接引用,
如果代表棧幀A的方法要調用代表棧幀B的方法,則這個虛擬機的方法調用指令就會以B方法的符號引用作爲參數,
但因爲符號引用並不是直接指向代表B方法的內存位置,所有在調用之前還必須要將符號引用轉換爲直接引用,然後通過直接引用訪問到真正的方法!
注意:
靜態解析:
如果符號引用在類加載階段或第一次使用時轉化爲直接引用,則這種轉換成爲靜態解析
動態連接:
如果在運行期間轉換爲直接引用,這種轉換稱爲動態連接
棧幀A 常量池 棧幀B
局部變量表 局部變量表
A方法的符號引用
操作數棧 操作數棧
B方法的符號引用
動態連接 動態連接
字符串常量等
返回地址 返回地址
方法返回地址
1.正常退出:根據方法定義決定是否要返回值給上層調用者
2.異常退出:不會傳遞返回值給上層調用者
注意:
1. 不管那種方式結束,在退出當前方法時,都會跳轉到當前方法被調用的位置!!!
如果正常退出,則調用者的PC計數器的值可作爲返回地址,
如果異常退出,則需要通過異常處理表來確定
2. 方法的一次調用對應着棧幀在虛擬機中的一次入棧出棧操作!!!
方法退出時做的事情:
恢復上層方法的局部變量表以及操作數棧,如果有返回值,就把返回值壓入到調用者棧幀的操作數棧中,
還會把PC計數器的值調整爲方法調用入口的下一條指令
24.JVM的內存溢出分析和參數調優
1.JVM調優 http://blog.csdn.net/eric_sunah/article/details/7862114
1.JVM內存參數調優
-Xms 設置初始化堆的內存
-Xmx 設置堆最大使用內存
-Xss 設置每個線程的棧大小
-Xmn 設置年輕代大小
eg:
java -Xmx4096m -Xms4096m -Xmn2g -Xss128k
或
java -Xmx4g -Xms4g -Xmn2g -Xss128k
設置JVM堆最大可以內存爲4096M,初始內存爲4096M(-Xms設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存)
設置JVM年輕代大小爲2G JVM堆內存 = 年輕代大小 + 年老代大小 + 持久代大小
持久代一般固定大小爲64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,推薦配置爲整個堆的3/8
設置每個線程的棧大小爲128k
JDK5+ 線程棧默認1M, 相同物理內存下,減小該值能生成更多線程,但操作系統對一個進程內的線程數有限制,最好不超過5000
如果方法遞歸太深,則可能耗盡線程棧,報出 StackOverflow !!! 線程棧內存溢出 <--- 方法調用太深
eg:設置堆內存中的內存分配比例
java -Xmx4g -Xms4g -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=64m -XX:MaxTenuringThreshold=0
-Xmn2g 設置年輕代大小爲2G
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆的1/5
-XX:SurvivorRatio=4 設置年輕代中from和to區的比例,Eden區(from)與Survivor區(to)的大小比值爲4:1:1,即 一個 Survivor 佔 年輕代的1/6
特別注意:
上面的比值 4 <=等價=> 1:4
-XX:MaxPermSize=64m 設置持久代大小爲64M
-XX:MaxTenuringThreshold=0:設置垃圾最大年齡
小結:
1,整個堆包括年輕代,老年代和持久代。其中年輕代又包括一個Eden區和兩個Survivor區。
2,年輕代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
2,持久代:
-XX:PermSize
-XX:MaxPermSize
3,年輕代和老年代的比例:
-XX:NewRatio(年輕代和老年代的比值,年輕代多,除去持久代)
當設置了-XX:+UseConcMarkSweepGC後,會使-XX:NewRatio=4失效,此時需要使用-Xmn設置年輕代大小
4,Eden與Survivor的比例
-XX:SurvivorRatio(Eden區與兩個Survivor區的比值,Eden區多)
2.GC參數設置
並行收集器 --- 吞吐量優先,適合後臺處理
eg:
-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
解析:
-XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集
-XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等
-XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集,JDK6.0支持對年老代並行收集
併發收集器 --- 響應時間優先,保證系統響應時間,減少垃圾收集時的停頓時間,適合應用服務器和典型領域等
eg:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設置。
-XX:+UseParNewGC:設置年輕代爲並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,所以無需再設置此值。
3.常見配置彙總
堆設置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設置年輕代大小
-XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:MaxPermSize=n:設置持久代大小
收集器設置
-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置並行收集器
-XX:+UseParalledlOldGC:設置並行年老代收集器
-XX:+UseConcMarkSweepGC:設置併發收集器
垃圾回收統計信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設置
-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)
併發收集器設置
-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的CPU數。並行收集線程數。
4.調優總結
年輕代大小選擇
響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。
吞吐量優先的應用:儘可能的設置大,可能到達Gbit的程度。因爲對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設置,一般要考慮併發會話率和會話持續時間等一些參數。
如果堆設置小了,可以會造成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;
如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下數據獲得:
併發垃圾收集信息
持久代併發收集次數
傳統GC信息
花在年輕代和年老代回收上的時間比例
減少年輕代和年老代花費的時間,一般會提高應用的效率
吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期對象,
減少中期的對象,而年老代盡存放長期存活對象。
較小堆引起的碎片問題
因爲年老代的併發收集器使用標記、清除算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的對象。
但是,當堆空間較小時,運行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,
然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啓對年老代的壓縮。
-XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的情況下,這裏設置多少次Full GC後,對年老代進行壓縮
2.JVM內存溢出分析 --- OutOfMemoryError 不屬於Exception,繼承自Throwable
1.堆內存溢出 --- 不斷創建對象,並且不釋放,導致GC無法回收,堆內存移除 Java heap space
eg: 製造堆內存溢出的程序
1.降低修改虛擬機堆內存大小 -Xms20m -Xmx20m
2.不斷創建強引用對象,方式GC回收
public static void main(String[] args) {
headOutOfMemory();
}
/*
* -verbose:gc -XX:+PrintGCDetails -verbose:gc
* -XX:+HeapDumpOnOutOfMemoryError
*
* -Xms20m -Xms20m
*
*/
static void headOutOfMemory() {
long count = 0;
try {
List<Object> objects = new ArrayList<Object>();
while (true) {
count++;
objects.add(new Object());
}
} catch (Throwable ex) {
System.out.println(count);
ex.printStackTrace();
}
}
}
異常信息:
java.lang.OutOfMemoryError: Java heap space
2.棧內存溢出 --- 棧主要存放棧幀(局部變量表(基本數據類型,對象引用,returnAddress類型),操作數棧,動態鏈接,方法出口信息),
1.StackOverflowError ---- 當線程棧的空間大於虛擬機所允許時,拋出 StackOverflowError
線程棧,因遞歸或方法調用太深,導致超過線程棧設定時,拋 StackOverflowError
eg:
自定義線程棧溢出
1.降低線程棧大小 -Xss4k
2.方法循環遞歸
public class JVMStackSOF {
/**
* (1) 在hotspot虛擬機中不區分虛擬機棧(-Xss)和本地方法棧(-Xoss),且只有對Xss參數的設置,纔對棧的分配有影響
*
* (2)
* 由於StackOverflowError->VirtualMachineError->Error
* ->Throwable,所以catch的時候如果用Exception的話將捕獲不到異常 Stack length 會隨着-Xss的減少而相應的變小
*/
private int stackNumber1 = 1;
public void stackLeck1() {
stackNumber1++;
stackLeck1();
}
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeck1();
} catch (Throwable ex) {
System.out.println("Stack length:" + jvmStackSOF.stackNumber1);
ex.printStackTrace();
}
}
}
異常信息:
java.lang.StackOverflowError
2.OutOfMemoryError ---- 棧空間不足,拋出OutOfMemoryError
JVM棧,整體內存不足時,拋OutOfMemoryError
eg:
自定義JVM棧溢出
1.JVM中除了堆和方法區,剩餘的內存基本都由棧佔用
2.每個線程都有獨立的棧空間(堆、方法區是線程公用)
3.如果-Xss調大每個線程的棧空間,可建立的線程數量必然減少
public class JVMStackOOM {
/**
* (1)不停的創建線程,因爲OS提供給每個進程的內存是有限的,且虛擬機棧+本地方法棧=(總內存-最大堆容量(X模型)-最大方法區容量(
* MaxPermSize)),於是可以推斷出,當每個線程的棧越大時,那麼可以分配的線程數量的就越少,當沒有足夠的內存來分配線程所需要的棧空間時,
* 就會拋出OutOfMemoryException
* (2)由於在window平臺的虛擬機中,java的線程是隱射到操作系統的內核線程上的,所以運行一下代碼時,會導致操作系統假死(我就嚐到了血的代價)
*/
private static volatile int threadNumber = 0;
public void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
threadNumber++;
while (true) {
System.out.println(Thread.currentThread());
}
}
}.start();
}
}
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
try {
jvmStackOOM.stackLeakByThread();
} catch (Throwable ex) {
System.out.println(JVMStackOOM.threadNumber);
ex.printStackTrace();
}
}
}
異常信息如下:
java.lang.OutOfMemoryError:unable to create new native thread
3.方法區溢出
方法區:存放JVM加載的類信息,常量,運行時常量池,靜態變量,編譯期編譯後的代碼等
異常信息:
java.lang.OutOfMemoryError: PermGen space
eg:
自定義方法區溢出代碼
1.通過不斷產生類信息來佔用方法區內存
2.調整 -XX:PermSize=10m --XX:MaxPermSize=10m,來降低方法區內存大小
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 利用CGLib技術不斷的生成動態Class,這些Class的信息會被存放在方法區中,如果方法區不是很大會造成方法去的OOM
*
*
* -XX:PermSize=10m -XX:MaxPermSize=10m
* */
public class MethodAreaOOM {
static class Test {}
public static void main(String[] args) {
try{
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(arg0, arg2);
}
});
System.out.println(enhancer.create());
}
}catch(Throwable th){
th.printStackTrace();
}
}
}
異常信息:java.lang.OutOfMemoryError: PermGen space
25.JVM內存分配策略與回收
1.分配策略
1.對象優先在Eden區上分配
2.大對象直接分配在老年區
-XX:PretenureSizeThreshold 參數設置直接放入老年區的對象大小
3.長期存活的對象直接進入老年區
-XX:MaxTenuringThreshold 參數設置對象年齡,經歷幾次gc可以進入老年區
JVM爲每個對象定義了年齡計數器,
如果對象在Eden出生並經過第一次Minor GC後任然存活,並能被Survivor容納,將被移到Survivor空間中,
並將對象年齡設爲1,對象在Survivor區每熬過一次Minor GC,年齡+1,
當年齡達到一定程度(默認15,可參數-XX:MaxTenuringThreshold設置)時,進入到老年代中
2.內存回收
1.Minor GC 發生在年輕代的GC,當JVM無法爲一個新對象分配空間時,觸發Minor GC,清理年輕代內存,大多數對象生命週期短,所以Minor GC 非常頻繁,而且速度較快
觸發條件:
Eden區滿時,觸發Minor GC
2.Full GC 發生在年老代的GC
觸發條件:
1.調用System.gc(),系統建議執行Full GC,但不一定執行
2.老年代空間不足
3.方法區空間不足
4.通過Minor GC後進入老年代的對象平均大小,大於老年代可用內存空間
5.由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
26.AOP應用和實現
AOP優化改造:http://blog.csdn.net/xvshu/article/details/46288953
1.關於Aspectj
獲取目標方法信息 --- JoinPoint Spring只支持方法執行的JoinPoint
JoinPoint裏包含了如下幾個常用的方法:
Object[] getArgs:返回目標方法的參數
Signature getSignature:返回目標方法的簽名
Object getTarget:返回被織入增強處理的目標對象
Object getThis:返回AOP框架爲目標對象生成的代理對象
注意:當使用@Around處理時,我們需要將第一個參數定義爲ProceedingJoinPoint類型,該類是JoinPoint的子類
2.5種增強處理
Before、 在某個連接點JoinPoint之前執行, 不能阻止連接點前的執行
Around、 包圍一個連接點的通知,可以在方法調用前後完成自定義的行爲
AfterReturning、 在某連接點正常完成後執行的通知,不包括拋出異常的情況
After、 某連接點退出時執行(不同正常返回還是異常退出,都會執行的通知,類似finally功能,可以用於釋放連接和資源)
AfterThrowing 在方法拋出異常退出時執行的通知
--- 5種增強處理中,織入增強處理的目標方法、目標方法的參數和被織入增強處理的目標對象等
任何一種織入的增強處理中,都可以獲取目標方法的信息
切點表達式:
1.execution()表達式
eg:
execution (* com.sample.service.impl..*. *(..))
1.execution() 表達式主體
2.第一個* : 表示返回類型, * 表示所有類型
3.包名: 要攔截的包,後面2個句點表示當前包和當前包的所有子包,即:com.sample.service.impl包、及其子孫包下所有類的方法
4.第二個* : 表示類名, * 表示所有類
5.第三個* : 表示方法名, * 表示所有方法, *(..) 中的2個句點表示方法參數, 2個句點表示任何參數
2.自定義註解
1.定義切點方法
//自定義註解方式
@Pointcut("annotation(com.jay.annotation.MyAnnotation)")
//@Pointcut("execution (* com.gcx.controller..*.*(..))") --- execution()切點表達式方式
public void controllerAspect(){}
2.在增強方法上,使用切點方法
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint){
}
eg:
使用5種增強處理
1.類上添加 @Aspect註解
2.方法上添加 4中增強處理註解
3.特別注意:
@Around中參數是 ProceedJoinPoint, 其他4類增強用 JoinPoint
@Aspect
public class AdviceTest {
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模擬權限檢查...");
System.out.println("@Before:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:參數爲:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被織入的目標對象爲:" + point.getTarget());
}
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:執行目標方法之前...");
//訪問目標方法的參數:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改變後的參數1";
}
//用改變後的參數執行目標方法
Object returnValue = point.proceed(args);
System.out.println("@Around:執行目標方法之後...");
System.out.println("@Around:被織入的目標對象爲:" + point.getTarget());
return "原返回值:" + returnValue + ",這是返回結果的後綴";
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模擬日誌記錄功能...");
System.out.println("@AfterReturning:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:參數爲:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值爲:" + returnValue);
System.out.println("@AfterReturning:被織入的目標對象爲:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模擬釋放資源...");
System.out.println("@After:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:參數爲:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被織入的目標對象爲:" + point.getTarget());
}
//標註該方法體爲異常通知,當目標方法出現異常時,執行該方法體
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")
public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){
...
}
}
3.動態代理實現
1.靜態代理 --- 代理模式
兩個類實現同一個接口,在代理類的接口方法中,調用被代理對象的接口方法,同時在代理類的接口方法中,調用被代理類方法前後添加邏輯
2.動態代理 --- JDK動態代理 和 cglib 動態代理(字節碼增強技術,效率高)
參考:http://blog.csdn.net/zpf336/article/details/52086180
http://blog.csdn.net/wenbo20182/article/details/52021096
2種動態代理區別:
1.JDK動態代理要求被代理類要實現接口,而cglib不需要
2.cglib能根據內存中爲其創建子類(代理對象)
1.JDK的動態代理 --- 通過創建一個實現InvocationHandler接口的中間對象,實現動態代理
優點:不必要求代理者和被代理者實現相同接口
缺點:僅支持接口代理, JDK動態代理需要實現類通過接口定義業務方法,對於沒有接口的類,無法代理
eg:
自定義的基於JDK的動態代理類
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用JDK動態代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy()
greeting.sayHello("Jack");
2.cglib動態代理 --- 字節碼增強技術,效率高
原理:
通過字節碼技術,爲一個類創建子類,並在子類中採用方法攔截的技術,攔截所有父類方法的調用,順勢織入橫切邏輯
優點:字節碼增強技術,CGLib創建的動態代理對象性能比JDK創建的動態代理對象的性能高不少,
但是CGLib在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因爲無需頻繁創建對象,用CGLib合適,
反之,使用JDK方式要更爲合適一些
缺點:CGLib由於是採用動態創建子類的方法,對於final方法,無法進行代理
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用cglib動態代理
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
27.關於JVM架構
參考:http://www.ityouknow.com/java/2017/03/01/jvm-overview.html
1.Java內存模型
內存模型:
描述程序中各個變量(實例域、靜態域、數組元素)之間的關係,以及在計算機系統中將變量存儲到內存,從內存取出變量這樣的底層細節
內存模型規則:
原子性:
約定了:訪問存儲單元內任何類型字段的值以及對其進行更新操作時,必須保證其是原子的
即:獲得或初始化某一些值時(),該值的原子性在JVM內部是必須得到保證的
可見性:
一個線程修改的狀態,對另一個線程是可見的,
即:一個線程修改的結果,另一個線程馬上就能看到
eg:
volatile修飾的變量具有可見性,不允許線程內部緩存和重排序,但不保證原子性
可見性規則約束下,定義了一個線程在哪種情況下可以訪問或影響另外一個線程,以及從另外一個線程的可見區域讀取相關數據、將數據寫入到另外一個線程內
可排序性:
爲了提高性能,編譯器和處理器可能會對指令做重排序
volatile修飾的變量不允許線程內部緩存和重排序
Java內存模型: --- JMM Java Memory Model
JMM: 是控制Java線程之間、線程和主存直接通向的協議
JMM定義了線程和主存之間的抽象關係:
線程之間的共享變量存儲在主存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存存儲了該線程以讀/寫共享變量的副本
2.JVM實例內部結構
子系統:
類加載器作爲JVM的子系統,針對Class文件進行檢測來加載對應的類
執行引擎:
負責代碼的解釋和執行
內存區域:
存儲字節碼、類信息、對象、參數、變量等
方法區: --- 線程共享
存儲類的裝載信息(類信息),
靜態變量(類變量),
運行時常量池
堆: --- 線程共享
對象、數組內存對分配:
當一個Java程序創建一個對象或者一個數組時,JVM實例會針對該對象和數組分配一個新的內存堆空間。在JVM實例內部,只存在一個內存堆的實例,所有的依賴該JVM的Java程序都共享該實例
進程內存堆分配:
多個Java進程啓動時,會得到JVM分配給自己的對空間,多個Java進程的堆空間時相互獨立的
JVM棧: --- 線程私有
對於線程內存棧分配:
當一個新線程啓動時,JVM爲其創建獨立內存棧,
內存棧由棧幀構成,棧幀有2中操作:出棧和入棧
當前線程方法 --- 正在被線程執行的方法, 該方法的棧幀稱爲 當前幀
對於方法:
當一個線程調用某個方法時,JVM創建並將一個新幀壓入到內存棧中,這個幀稱爲當前棧幀,
當該方法執行時,JVM使用內存棧來存儲 參數引用、局部引用變量、基本類型數值、返回值等相關數據
無論方法正常結束還是異常結束,JVM都彈出或丟棄該棧幀,上一幀方法成爲當前幀
本地方法棧:
保存了本地Java方法調用狀態,其狀態包括局部變量、被調用的參數、它的返回值、以及中間計算結果
程序計數器: --- 線程私有
每個線程都有自己的PC寄存器,通過計數器來指示下一條指令執行
3.JVM內存分配策略
靜態存儲:
編譯時,能確定每個數據在運行時需要的存儲空間,因而在編譯時就給它們分配固定的內存空間
此分配策略,要求代碼中不允許有可變數據結構存在,也不允許嵌套或遞歸結構出現(無法計算需要內存空間)
eg:
static final 全局常量
棧式存儲:
動態存儲分配,由一個類似堆棧的運行棧來實現,按先進後出原則分配
程序對數據所需內存空間未知,只有到運行時才能知道佔用空間
堆式存儲:
專門負責在編譯時或運行時,無法確定存儲要求的數據結構的內存分配
eg:
可變字符串和對象實例
4.對象分配規則:
1.對象優先分配在年輕代的Eden區,如果Eden區沒有足夠空間,JVM執行一次 Minor GC
2.大對象直接進入老年代(大對象:需要大量連續內存空間的對象, 可參數設置大對象大小),目的:避免在Eden區和2個Survivor區直接進行大量的內存拷貝 <--- 新生代採用複製算法收集內存
3.長期存活的對象進入老年代, JVM爲每個對象定義了一個年齡計數器,如果對象經過1次Minor GC,對象進入Survivor區,之後沒經過一次Minor GC,對象年齡+1, 直到達到閾值,才進入老年代 (可參數設置年齡計數器大小)
4.動態判斷對象年齡。 如果Survivor區中相同年齡的所有對象大小總和,大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代
5.空間分配擔保。 每次進行Minor GC時, JVM會計算Survivor區移至老年代的對象的平均大小,如果這個值大於老年區剩餘值,則進行一個Full GC
5.GC算法 垃圾回收
對象存活判斷:
引用計數 --- 每個對象有應用計數屬性,新增一個引用,計數+1, 應用釋放,計數-1,計數爲0,可回收 ---- 無法解決對象循環引用問題!
可達性算法(根搜索算法) --- 從GC Roots開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到 GC Roots沒有任何引用鏈可達時, 證明此對象不再用,可回收
可作爲GC Roots的對象:
虛擬機棧中引用的對象
方法區中的靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI引用的對象
特別注意:
GC管理的主要是 Java 堆,一般情況下只針對堆,進行垃圾回收,
方法區、棧、本地方法區不被GC鎖管理,因而選擇這些區域內的對象作爲GC Roots,
被GC Roots引用的對象不被GC回收
GC算法: 3種基礎算法
參考:http://blog.csdn.net/java2000_wl/article/details/8022293
標記-清除算法
分"標記" 和 "清除" 兩個階段
首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象
缺點:
標記和清除過程效率低
標記清除後,產生大量不連續的內存碎片
複製算法
將可用內存按容量分爲相等的2塊,每次只使用其中的一塊,
當這塊內存用完了,就將還存活的對象複製到另外一塊上面,然後把已使用過的內存空間一次清理掉
缺點:
內存縮小爲原來的一半
優點:
不會有內存碎片,
只需要移動堆的內存指針,按順序分配內存即可,實現簡單,運行高效
標記-壓縮(整理)算法
標記與第一種算法一樣類似,但後續操作不是直接清理對象,而是讓所有存活對象都向一端移動,
並更新引用其對象的指針,然後直接清理掉端邊界意外的內存
缺點:
在標記-清除的基礎上,需要進行對象的移動,成本相對較高
優點:
不會產生內存碎片
JVM分代收集算法
JVM把堆內存分爲新生代和老年代, 根據各個代的特點,採用合適的收集算法
新生代 --- 複製算法 !!!
每次垃圾收集都有大批對象死去,只有少量存活,適合複製算法,只需付出少量存活對象的複製成本就可完成收集
老年代 --- 標記-整理算法 !!!
老年代中對象存活率高,沒有額外空間對它進行分配擔保,必須使用 "標記-壓縮" 算法進行回收
垃圾回收器:
Serial收集器 --- 串行收集器,最古老,最穩定,效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收
ParNew收集器 --- Serial收集器的多線程版本
Parallel收集器 --- 類似ParNew,更關注系統的吞吐量
Parallel Old收集器 --- 使用多線程 和 標記-整理 算法
CMS收集器 --- 是一種以獲取最短回收停頓時間爲目標的收集器
G1收集器 --- 面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵
28.哈希表 和 HashMap原理
哈希表和哈希算法原理:
http://blog.csdn.net/duan19920101/article/details/51579136
http://blog.csdn.net/tanggao1314/article/details/51457585
一種根據關鍵字直接訪問內存存儲位置的數據結構
哈希函數: 通過哈希表,數據元素的存放位置和數據元素關鍵字之間建立起某種對應關係, 關鍵字到內存的映射
hash表以及HashMap原理:
http://www.code123.cc/258.html
HashMap原理:
HashMap基於哈希函數,使用put(key,value)存儲對象,使用get(key)獲取對象,
put傳遞鍵值對,先對鍵調用hashCode()方法,返回hashCode()用於找到bucket位置來存儲Entry對象,
HashMap是在bucket中存儲鍵對象和值對象,作爲 Map.Entry
bucket存儲LinkedList,而LinkedList中存儲的是key-value對象 --> Map.Entry
HashMap在Map.Entry靜態內部類中實現存儲key-value對,
HashMap使用哈希算法,在put和get方法中,使用hashCode()和equals()方法,
通過傳遞key-value對,調用put方法時,HashMap調用鍵對象(key)的hashCode()計算hashCode,然後bucket位置來存儲對象(根據key查找value存放地址),
如果多個key的hashCode相同,則是發生碰撞,對象將存儲在同一個bucket位置的LinkedList的下一個節點中(鍵值對對象 <==> Map.Entry,Entry存儲在LinkedList中)
兩個對象的hashCode相同會發生什麼?
1.兩個對象的hashCode相同,但它們可能並不相等 hashCode() 和 equals()
2.HashMap的key-value抽象成Map.Entry, Entry存儲在LinkedList中, 每個bucket位置對應一個LinkedList,用於解決哈希碰撞
3.如果hashCode相同,則它們的bucket位置相同, 存儲時會發生碰撞,
如果發送碰撞,後一個添加的對象,會存放在bucket位置對應的LinkedList的下個節點中
如果兩個鍵的hashCode相同,如何獲取值對象?
1.get()根據key的hashCode去查找bucket位置,然後獲取對應的value對象
2.如果兩個key的hashCode相同,則兩個key對應的Entry都存儲在同一個bucket位置的LinkedList中
3.HashMap的LinkedList中存儲的是鍵值對Map.Entry對象
4.根據key的hashCode找到bucket位置後,調用key.equals()方法找到LinkedList中正確的節點,最終找到要查的value值對象
5.注意:
hashCode()用於直接定位bucket位置
equals() 用於獲取值對象時使用
如果HashMap大小超過了負載因子定義的容量,怎麼辦?
1.默認負載因子是0.75,即:一個map填滿75%的bucket時,將會創建原來HashMap大小的2倍的bucket數組,來重新調整map的大小,
並將原來的對象放入新的的bucket數組中 --- 這個過程稱爲 rehashing --- 因爲它調用hash方法找到新的bucket位置
重新調整HashMap大小時,存在什麼問題?
多線程下,可能產生條件競爭
eg:
如果2個線程都發現HashMap需要重新調整大小,它們會同時嘗試調整大小。
在調整大小過程中,存儲在LinkedList中的元素的次序將會反過來。
因爲移動到新的bucket位置時,HashMap並不會將元素放到LinkedList的尾部,而是放在頭部,這是爲了避免尾部遍歷,
如果條件競爭發生了,就死循環了
多線程環境下,使用HashTable或ConcurrentHashMap替代HashMap
注意:
1.String、Integer適合作爲HashMap的key
原因:
String是不可變的,final的,已經重寫了equals()和hashCode()方法
如果兩個不相等的對象返回不同的hashCode,碰撞的機率就會小,進而提高HashMap的性能
2.只要遵循equals()和hashCode()的定義規則,並且當對象插入到Map中之後不會再改變了,可以自定義對象作爲key,
3.使用ConcurrentHashMap替代HashTable
HashTable是同步的,線程安全的,但ConcurrentHashMap同步性能更好,其採用了分段鎖
4.HashTable和ConcurrentHashMap的區別
1.HashTable每次同步執行是,都要鎖住整個結構
2.ConcurrentHashMap將hash表分爲16個bucket桶(默認值),採用分段鎖,get、put、remove等操作只鎖當前需要用到的桶
eg:
原先只能一個線程進入,現在卻能同時16個寫線程進入(寫線程才需要鎖定,讀線程不受限制),提升了併發性
總結:
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然後返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。
當兩個不同的鍵對象的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對
HashTable實現原理:
實現原理與HashMap類似,但爲了線程安全,HashTable中幾乎所有的public方法都用synchronized做了同步處理,有些方法也是在內部通過 synchronized 代碼塊來實現
Hashtable 與 HashMap 的簡單比較
HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減少實現此接口所需的工作。
HashMap 的 key 和 value 都允許爲 null,而 Hashtable 的 key 和 value 都不允許爲 null。HashMap 遇到 key 爲 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現。
所以有人一般都建議如果是涉及到多線程同步時採用 HashTable,沒有涉及就採用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的 Map 對象,並把它作爲一個封裝的對象來返回。
不考慮性能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap)來替換HashMap,這兩種方式基本都是對整個 hash 表結構做鎖定操作的
ConcurrentHashMap實現原理: --- 依賴於Java內存模型
參考:http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
ConcurrentHashMap結果中包含Segment的數組,默認併發基本,創建包含16個Segment對象的數組,
每個Segment又包含若干個散列表的桶,每個桶是由HashEntry連接起來的一個鏈表,
如果key能夠均勻散列,每個Segment大約守護整個散列表桶總數的1/16
併發讀些操作:
執行 put 方法的時候,會需要加鎖來完成,但加鎖操作是針對的hash值對應的Segment,而不是整個ConcurrentHashMap,因爲put操作只是在某個Segment中完成,並不需要對整個ConcurrentHashMap加鎖,
此時,其他線程可以對另外的Segment進行put操作,雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖
同時,讀線程並不會因爲本線程的加鎖而阻塞
在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行併發寫操作(如果併發級別設置爲 16),及任意數量線程的讀操作
總結:
散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個併發散列映射表的實現,它允許完全併發的讀取,並且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。
在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也導致對容器的訪問變成串行化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
用分離鎖實現多個線程間的更深層次的共享訪問。
用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。
通過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高
29.關於Comparable和Comparator接口
1.Comparable和Comparator接口被用來對對象集合或者數組進行排序
2.內部排序,對象類實現Comparable接口,重寫 CompareTo()方法
2.外部集合排序,調用Collections.sort(collection, Comparator<T>)對集合元素排序
eg:
Collections.sort(list,new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
if (user1.getName().equals(user2.getName())) {
return user1.getAge() - user2.getAge();
} else {
return user1.getName().compareTo(user2.getName());
}
}
});
30.OAuth認證流程
參考:http://www.code123.cc/1671.html
31.Java NIO
參考:http://blog.csdn.net/hxpjava1/article/details/56282385
概念:
專門爲提高I/O吞吐量而設計,NIO通過Reactor模式的事件驅動機制來達到 Non Blocking
Reactor -- 反應器
將事件註冊到Reactor中,當有相應的事件發生時,Reactor告訴我們哪些事情發生了,我們根據具體的事件去做相應的處理
通道和緩衝區: 標準IO基於字節流和字符流進行操作,而NIO基於通道Channel和緩衝區Buffer進行操作,數據總是從通道讀取到緩衝區,或從緩衝區寫入到通道中
異步IO:NIO可以異步的使用IO,當線程從通道讀取數據到緩衝區時,線程還可進行其他事情,當數據被寫入到緩衝區時,線程可以繼續處理它。從緩衝區寫入通道也類似
Selectors選擇器:選擇器用於監聽多個通道的事件(eg:連接打開,數據到達等),因此,單線程可以監聽多個數據通道
eg:
Selector運行單線程處理多個Channel,如果應用打開了多個連接(通道),但每個連接流量都很低,使用Selector就會很方便, 如:在一個聊天服務器中
優點:
舊IO對文件操作只能一個字節一個字節或一行一行的讀,對Socket IO會阻塞,可以爲每個Socket創建一個Thread,但開銷太大
NIO 對Socket IO可以實現非阻塞,可用單線程管理多個通道,
NIO使用緩衝區,File IO和Socket IO都是和Buffer緩衝區交互讀取
NIO將通道數據讀到緩衝區中再進行操作,避免逐字節或逐行讀取的性能開銷
從Channel讀取到Buffer -->
Channel Buffer
<--從Buffer寫入到Channel
NIO和IO如何影響程序的設計?
對NIO和IO類的API調用
數據處理
用來處理數據的線程數
使用場景:
1.聊天服務器 需要管理同時打開的成千上萬個連接,但這些連接每次只發送少量的數據
2.P2P網絡中, 需要維持許多打開的連接到其他計算機上,使用一個單獨的線程來管理所有出戰連接
3.其他流量小, 連接多的場景
不適合場景:
1.少量連接佔用高帶寬,一次發送大量數據 --- 文件服務器(適合IO實現,一個連接通過一個線程處理)
NIO核心模塊:
Selector(選擇器):
1.Selector允許單線程處理多個Channel pk 舊IO的多線程處理, 性能更高
一個單線程選擇器可監控多個通道的多個事件(eg:連接打開,數據到達等事件)
2.使用Selector,需要向它註冊一個Channel,然後輪詢調用它的select()方法,該方法將阻塞,
當註冊的某個通道準備好要進行IO操作時,返回已選擇鍵的個數,
此時通過selectedKeys獲得已選擇的鍵,就可進行相關的IO操作,
選擇鍵SelectionKey 是用來連接 Selector 和 Channel
直到這裏註冊中的Channels中有一個準備好的事件,
一旦這個方法返回,這個線程將會執行這些事件
事件的實例是進來的連接,接收到的數據等等
3. 要使用Selector,得向Selector註冊Channel,然後調用它的select()方法,
該方法會一直阻塞到某個註冊的通道有事件就緒,一旦這個方法返回,線程就可以處理這些事件(新連接進來,數據接收等)
---> Channel
Thread ---> Selector ---> Channel
---> Channel
4.NIO的選擇器允許一個單獨的線程來監視多個輸入通道,可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來 "選擇" 通道,
這些通道里已經有可以處理的輸入,或選擇已準備寫入的通道, 這種機制,使得一個單獨線程很容易來管理多個通道
Channel(通道):
1.4種Channel
FileChannel 文件IO
ServerSocketChannel TCP IO
SocketChannel TCP IO
DatagramChannel UDP IO
Buffer(緩衝區):
緩衝區 --- 內存中預留指定字節數的內存空間,用來對輸入、輸出的數據作臨時存儲, IO操作中數據的中轉站
緩衝區直接爲通道Channel服務,寫入數據到通道,或從通道讀取數據
8種緩衝區類:
對應boolean之外的7基本數據類型 + MappedByteBuffer(專門用於內存映射的ByteBuffer)
ByteBuffer <--繼承 -- MappedByteBuffer 用於表示內存映射文件
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer讀寫數據的4個步驟:
寫入數據到Buffer
調用flip()方法
從Buffer中讀取數據
調用clear()或 compact()方法
說明:
1.當向Buffer寫入數據時,buffer會記錄下寫了多少數據,
一旦要讀取數據,調用flip()方法,將Buffer從寫模式切換到讀模式,在讀模式下,可以讀取之前寫入到Buffer的所有數據
2.一旦讀完所有數據,就要清空緩衝區,讓它可再次被寫入,2種清空緩衝區方式:
clear() --- 清空整個緩衝區
compact() --- 只清除已讀過的數據,任何未讀的數據都將被移到緩衝區的起始處,新寫入的數據,將放到緩衝區未讀數據的後面
緩衝區的 4 個屬性: capacity>=limit>=position>=mark>=0
capacity
可容納的最大數量,緩衝區創建時被設定,不能改變
limit
上界,緩衝區中當前數據量
讀模式下:
表示最多能讀到多少數據,當切換Buffer到讀模式時,limit被設置爲寫模式下的position值,
即:能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
寫模式下:
表示最多能往Buffer裏寫多少數據,limit等於Buffer的capacity
position
位置,下一個要被讀、寫的元素的索引
寫數據到Buffer時,position表示當前的位置,初始位置爲0,當數據寫入到Buffer後,position向前移動到下一個可插入數據的Buffer單元
讀數據時,同某個特定位置讀,當將Buffer從寫模式切換到讀模式,position會被重置爲0,當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置
初始:爲 0,最大:capacity-1
mark
標記,調用mark()來設置mark=position,再調用reset()可以讓position回覆到標記的位置,即:position=mark
初始:爲 -1
創建緩衝區
緩衝區類都是抽象的,不能new方式實例化,每個緩衝區類都有一個靜態工廠方法,用於創建相應緩衝區對象
格式:
XxBuffer buf = XxBuffer.allocate(capacity)
eg:
//創建一個容量爲10的byte緩衝區和char緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(10);
CharBuffer buf2 = CharBuffer.allocate(10);
如果想用一個指定大小的數組,作爲緩衝區的數據存儲器,可用wrap()方法創建緩衝區
eg:
//使用一個指定數組,作爲緩衝區存儲器
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes);
解析:
1.緩衝區數據會存在bytes數組中,bytes數組或buf緩衝區任何一方中數據的改動都會影響另一方
2.還可創建指定初始位置(position)和上界(limit)的緩衝區
//使用指定數組作爲緩衝區存儲器,並創建一個position=3,limit=8,capacity=10的緩衝區
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes, 3, 8);
操作緩衝區
1.存取
get() 從緩衝區取數據
put(xx) 向緩衝區存數據
channel.read(buf) 從Channel寫到Buffer
channel.write(buf) 從Buffer讀取數據到Channel
Buffer.rewind() 將position設回0, 可以重讀Buffer中的所有數據,limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
eg:
ByteBuffer buf = ByteBuffer.allocate(10);
//存3個數據
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'D');
//反轉緩衝區,即從寫模式切換到讀模式,從頭開始讀,最多讀取已寫入個數的數據
buf.flip();
//讀取2次數據
System.out.println((char)buf.get());
System.out.println((char)buf.get());
//返回當前位置到上界的數據元素數量
System.out.println(buf.remaining());
//從當前位置到上界是否還有數據元素
System.out.println(buf.hasRemaining());
解析:
1.調用put()或get()時,每調用一次,position值 +1,指示下次存或取開始的位置
2.buf.flip() 從寫模式切換到讀模式,可以從頭開始讀取最多已存入個數的數據
3.Buffer.remaining():返回從當前位置到上界的數據元素數量;
Buffer.hasRemaining():告訴我們從當前位置到上界是否有數據元素;
2.反轉 flip()
將一個處於存數據狀態的緩衝區變爲一個處於準備讀取數據的狀態
反轉緩衝區,
即:將緩衝字節數組的指針設置爲數組的開始序列,即:數組下標0,這樣才能從buffer開頭,對buffer進行遍歷(讀取)
即:調用flip()後,讀寫指針知道緩衝區頭部,並且設置了最多隻能讀取已已寫入的數據長度
如果不調用flip(),就會從文件最好開始讀取
特別注意:
buffer.flip() 作用:將Buffer從寫模式切換到讀模式,調用flip()方法設置這個position的值爲0,以及設置這個limit的值爲剛纔position的值,
換句話說,position現在標記了讀的位置,limit標記了有多少個Buffer的字節,字符等等被寫入到buffer。限制有多少個字節,字符可以去讀取的。
flip源碼:
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;
}
3.清空數據
1.清空緩衝區內所有數據 --- clear()
2.清空已讀取的數據 --- compact()
在buffer中仍然有未讀取的數據,並且你想稍後讀取,調用compact(),
compact()方法拷貝所有未讀取的數據到buffer的開頭,然後設置position值爲最後未讀取數據元素的後面,
再寫數據時,從已有數據後面繼續寫
4.標記 --- mark()
記住當前位置,之後可以將位置恢復到標記處(使用reset()方法恢復)
通過調用Buffer.mark(),可以標記Buffer中的一個特定的position,
之後可通過調用Buffer.reset() 恢復到這個position
eg:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
5.比較2個緩衝區是否相等
6.批量移動緩衝區的數據
public static void batchMove(){
byte[] bytes = "hello nio".getBytes();
/**這裏,可以直接把數據交給數組來保存
ByteBuffer buf = ByteBuffer.wrap(bytes);
*/
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
//將byte數據寫入緩衝區 <=等價=> buf.put(bytes);
buf.put(bytes, 0, bytes.length);
//反轉緩衝區,變爲讀模式
buf.flip();
//輪詢判斷是否有數據,有則將緩衝區數據批量讀到array中
byte[] array = new byte[bytes.length];
while (buf.hasRemaining()){
buf.get(array, 0, buf.remaining());
}
//輸出緩衝區讀出的數據
System.out.println(new String(array));
}
7.複製緩衝區
通道之間的數據傳輸:
NIO中,如果2個通道中有一個是FileChannel, 則可以直接將數據從一個Channel傳輸到另外一個Channel
transferFrom() --- 對目標Channel調用
FileChannel的transferFrom()方法可將數據從源通道傳入到FileChannel中
兩種方式:
toChannel.transferFrom(fromChannel, position, count)
toChannel.transferFrom(position, count, fromChannel)
transferTo() --- 對源Channel調用
將數據從FileChannel傳輸到其他Channel中
eg:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Selector選擇器:
NIO能檢測到一到多個通道,並能知道通道的各類事件,一個單獨的線程可以管理多個Channel,從而管理多個網絡連接
1.Selector創建
Selector selector = Selector.open();
2.向Selector中註冊通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ)
注意:
1.與Selector一起使用時,Channel必須處於非阻塞模式下,所以:不能將FileChannel於Selector一起使用,因爲FileChannel不能切換到非阻塞模式
2.Channel通過register()方法註冊到Selector,並標明它感興趣的事件,之後通過Selector的select()判斷是否有感興趣的事件發生,如果有,通過selectedKeys()獲得興趣事件的集合
3.register(selector, Key)中,Key 表示通過Selector監聽Channel時,對什麼事件感興趣
4種不同類型事件:
Connect
Accept
Read
Write
3.SelectionKey
向Selector註冊Channel時,register()方法返回一個SelectionKey對象,這個對象包含感興趣的屬性:
interest集合
ready集合
Channel
Selector
附加的對象(可選)
4.通過Selector選擇通道
select() 方法 返回 "感興趣事件(連接,接受,讀,寫)" 已經就緒的那些通道
NIO聊天室參考:
http://blog.csdn.net/kindz_cn/article/details/51512672
http://blog.csdn.net/abc_key/article/details/29029879
32.Java中線程池原理和實現
JDK 線程池
Executors -創建-> 4種線程池 -實現-> ExecutorService(接口) -繼承-> Executor(接口)
4種線程池
newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
--- 單線程執行,按提交的線程順序依次執行
newFixedThreadPool
創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待
--- 推薦方式,超過線程池最大容量,線程等待被調用,
--- 定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
創建一個定長線程池,支持定時及週期性任務執行,延遲執行
eg:
/**
* 使用線程池定時延遲調度
*/
private static void test2(){
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延遲3秒後執行
scheduledExecutorService.schedule(getThread(),3, TimeUnit.SECONDS);
//延遲1秒後,每3秒執行一次
scheduledExecutorService.scheduleAtFixedRate(getThread(),1,3, TimeUnit.SECONDS);
}
newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程(空閒線程默認60s會被回收),若無可回收,則新建線程
--- 不推薦,如果一次提交過多線程,而且每個線程比較耗時耗內存,可能瞬間擠爆JVM內存!!!
1.創建
ExecutorService executorService = Executors.newXxThreadExecutor()
2.調用
1.不要返回值
void executorService(Runnable)
2.需要返回值
Future<T> submit(Callable<T>)
3.如果需要 定時或 延遲 執行線程, 使用 ScheduledExecutorService的schedule()和scheduleAtFixedRate() 調用線程
3.ThreadPoolExecutor是Executors類的底層實現,
ThreadPoolExecutor的構造器:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所保存的線程數,包括空閒線程。
maximumPoolSize-池中允許的最大線程數。
keepAliveTime - 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
unit - keepAliveTime 參數的時間單位。
workQueue - 執行前用於保持任務的隊列。此隊列僅保持由 execute方法提交的 Runnable任務。
threadFactory - 執行程序創建新線程時使用的工廠。
handler - 由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。
4.關於線程等待隊列
1.newFixedThreadPool和newSingleThreadExecutor 線程等待隊列是 LinkedBlockingQueue --- 無界阻塞隊列
2.newCachedThreadPool 線程等待隊列是 SynchronousQueue --- 同步阻塞隊列 --- 每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0
3.ScheduledThreadPoolExecutor 線程等待隊列是 DelayedWorkQueue --- 延遲阻塞隊列
如何選擇線程阻塞隊列?
1.直接提交的無界隊列 SynchronousQueue
2.無界隊列 LinkedBlockingQueue
3.有界隊列 ArrayBlockingQueue
關於SynchronousQueue?
1.該Queue,在某次添加元素後,必須等待其他線程取走後才能繼續添加 (類似,進棧的元素,必須出棧,纔能有新的元素被放入)
2.可避免在處理具有內部依賴性的請求集時出現鎖
eg:
如果你的任務A1,A2有內部關聯,A1需要先運行,那麼先提交A1,再提交A2,當使用SynchronousQueue我們可以保證,
A1必定先被執行,在A1麼有被執行前,A2不可能添加入queue中
5.線程池的4個組成:
1.線程池管理器(ThreadPool) --- 創建並管理線程池,包括:創建線程池,銷燬線程池,添加新任務
2.工作線程(PoolWorker) --- 線程池中線程,沒有任務時處於等待狀態,可以循環執行任務
3.任務接口(Task) --- 每個任務必須實現的接口,以便工作線程調度 --- Runnable
4.任務隊列(taskQueue) --- 存放沒有處理的任務,提供一種緩衝機制
6.JDK線程池源碼分析:http://www.cnblogs.com/exe19/p/5359885.html
33.JDK動態代理和cglib字節碼技術代理的區別?
1.JDK動態代理:
1.靜態代理 --- 代理對象和目標對象實現了相同的接口,目標對象作爲代理對象的一個屬性,
具體接口實現中,可以調用目標對象相應方法前後加上其他業務處理邏輯
2.JDK動態代理只能針對實現了接口的類生成代理
2.CGLIB代理 --- 通過字節碼技術,爲目標對象生成一個與其功能一樣的子類
1.針對類實現代理
2.主要是對指定的類生產一個子類,覆蓋其中的所有方法
3.被代理類或方法不能聲明爲final
3.區別:
1.JDK動態代理只能對實現了接口的類生成代理, 動態代理只能對於接口進行代理
2.cglib針對類實現代理,主要是對指定的類生成一個子類,覆蓋中的方法,因爲是繼承,所以該類或方法最好不要聲明成final ,final可以阻止繼承和多態
3.Spring實現中,如果有接口,默認使用JDK動態代理,如果目標對象沒有實現接口,使用cglib代理,
如果目標對象實現了接口,可以強制使用CGLIB實現代理(添加CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
4.動態代理的應用
AOP(Aspect-OrientedProgramming,面向切面編程),AOP包括切面(aspect)、通知(advice)、連接點(joinpoint),實現方式就是通過對目標對象的代理在連接點前後加入通知,完成統一的切面操作。
實現AOP的技術,主要分爲兩大類:
一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;
二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。
Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。
默認的策略是如果目標類是接口,則使用JDK動態代理技術,如果目標對象沒有實現接口,則默認會採用CGLIB代理。
如果目標對象實現了接口,可以強制使用CGLIB實現代理(添加CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
5.參考:http://www.cnblogs.com/linghu-java/p/5714769.html
34.Spring的事務傳播級別
Spring中定義了7種傳播行爲:
參考:https://yq.aliyun.com/articles/71303?spm=5176.8067842.tagmain.14.VE7RJr
35.關於Spring 聲明式事務的原理
參考:http://yemengying.com/2016/11/14/something-about-spring-transaction/
Spring的聲明式事務:
1.JavaConfig方法 --- 在需要管理事務的類或方法上添加 @Transactional註解,然後在配置類上添加 @EnableTransactionManagement註解
2.Xml方式 --- 添加 <tx:annotation-driven />
Spring會利用Aop在相關方法調用的前後進行事務管理
問題:
public class JayServiceImpl implements JayService {
public void A(List<Giraffe> giraffes) {
for (Giraffe giraffe : giraffes) {
B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
說明:
Service中A方法調用B方法,方法A沒有事務管理,方法B採用聲明式事務,通過在方法上聲明 @Transactional註解來做事務管理
問題:
Junit 測試方法 A 的時候發現方法 B 的事務並沒有開啓, 而直接調用方法 B 事務是正常開啓的???
// 沒有開啓事務
@Test
public void testA() {
giraffeService.A();
}
// 正常開啓事務
@Test
public void testB() {
giraffeService.B();
}
}
原理分析:
Spring在加載目標Bean時,會爲聲明瞭@Transactional的Bean創建一個代理類,而目標類本身並不能感知到代理類的存在,
調用通過Spring上下文注入的Bean的方法,而不是直接調用目標類的方法
即:
先調用代理類的方法,代理類再調用目標類的方法
Calling Code
--call--> Proxy --->foo()
---> Pojo --> pojo.foo()
對於加了@Transactional註解的方法,在調用代理類方法時,會先通過攔截器 TransactionInterceptor開啓事務,
然後再調用目標類的方法,最後在調用結束後, TransactionInterceptor會提交或回滾事務
問題解析:
對於第一段的代碼,我在方法 A 中調用方法 B,實際上是通過“this”的引用,也就是直接調用了目標類的方法,而非通過 Spring 上下文獲得的代理類,所以。。。事務是不會開啓滴
解決方法:
通過實現ApplicationContextAware接口獲得 Spring 的上下文,(或自動注入Context對象),然後獲得目標類的代理類,通過代理類的對象,調用方法 B,即可
public class GiraffeServiceImpl implements GiraffeService,ApplicationContextAware{
@Setter
private ApplicationContext applicationContext;
public void A(List<Giraffe> giraffes) {
GiraffeService service = applicationContext.getBean(GiraffeService.class);
for (Giraffe giraffe : giraffes) {
service.B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
36.Java類加載機制
裝載 ---> 鏈接(驗證 --> 準備 --> 解析) ---> 初始化
1.JVM類加載機制:
裝載:
1.找到該類型的class文件,產生一個該類型的class文件二進制數據流(ClassLoader需要實現的loadClassData()方法)
2.解析該二進制數據流爲方法區內的數據結構
3.創建一個該類型的java.lang.Class實例
最終:通過defineClass()創建一個Java類型對象(Class對象)
找到二進制字節碼,並加載到JVM中
JVM通過類全限定名(包名.類名) + 類加載器 完成類的加載,生成類對應的Class對象
鏈接:
驗證:
負責對二進制字節碼進行校驗、類信息是否符合JVM規範,有沒有安全問題、對class文件長度和類型進行檢查
參考:http://www.importnew.com/17105.html
準備:
初始化類中靜態變量、並將其初始化爲默認值 --- 只初始化靜態變量默認值 !!!,給其類變量賦值發生在初始化階段!!!
對於final類型的變量,準備階段直接賦初始值
該內存分配發生在方法區
解析:
解析類中調用的接口、類、字段、方法的符號引用,把虛擬機常量池中的符號引用轉換爲直接引用
初始化:
1.對static類變量指定初始值!!!(2種方式:一種是通過類變量的初始化語句,一種是靜態初始化語句)
2.一個類的初始化需要先初始化其父類,並遞歸初始化其祖先類
2.JVM必須在每個類或接口主動使用時進行初始化:
主動使用的情況:
1.創建類的實例(無論是new、還是反射、克隆、序列化創建的)
2.使用某個類的靜態方法
3.訪問某個類或即可的靜態字段
4.調用Java API中的某些反射方法
5.初始化某個類的子類(先初始化其父類)
6.啓動某個標明爲啓動類的類(含main()方法)
主動使用會導致類的初始化,其超類均將在該類的初始化之前被初始化,但通過子類訪問父類的靜態字段或方法時,對於子類(或子接口、接口的實現類)來說,這種訪問就是被動訪問,或者說訪問了該類(接口)中的不在該類(接口)中聲明的靜態成員
3.創建對象時,類中各成員的執行順序:
父靜態塊 <-- 子靜態塊 <-- 父普通代碼塊 <-- 父構造器 <-- 子普通代碼塊 <-- 子構造器
1.父類靜態成員和靜態初始化快,按在代碼中出現的順序依次執行。
2.子類靜態成員和靜態初始化塊,按在代碼中出現的順序依次執行。
3. 父類的實例成員和實例初始化塊,按在代碼中出現的順序依次執行。
4.執行父類的構造方法。
5.子類實例成員和實例初始化塊,按在代碼中出現的順序依次執行。
6.執行子類的構造方法。
eg:
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}
class Parent{
{
System.out.println("parent中的初始化塊");
}
static{
System.out.println("parent中static初始化塊");
}
public Parent(){
System.out.println("parent構造方法");
}
}
class Son extends Parent{
{
System.out.println("son中的初始化塊");
}
static{
System.out.println("son中的static初始化塊");
}
public Son(){
System.out.println("son構造方法");
}
}
結果:
parent中static初始化塊
son中的static初始化塊
parent中的初始化塊
parent構造方法
son中的初始化塊
son構造方法
37.MySQL性能優化
參考:
MySQL性能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存儲引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事務處理,爲每個表創建3個文件,分別存儲不同內容
支持表級鎖,表的寫操作會阻塞其他用戶對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他用戶對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他用戶對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是串行的
eg:
tb_Demo表,那麼就會生成以下三個文件:
1.tb_demo.frm,存儲表定義;
2.tb_demo.MYD,存儲數據;
3.tb_demo.MYI,存儲索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量數據時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入數據,適合管理:郵件或Web服務器日誌數據
總結:
1.適合做count的計算 (注意:不含where條件的統計,因爲MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 默認引擎
支持事務處理
引入了行級鎖(併發高)和外鍵約束
不支持全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外鍵約束 --- MySQL支持外鍵的存儲引擎只有InnoDB
5.支持自動增加列 Auto_INCREMNET屬性
6.InnoDB是爲處理巨大數據量時的最大性能設計
總結:
可靠性要求高,需要事務支持,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多性能開銷,而它的強項在於多線程的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定數據引擎的創建
細節和具體實現的差別:
1.InnoDB不支持FULLTEXT類型的索引。
2.InnoDB 中不保存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT類型的字段,InnoDB中必須包含只有該字段的索引,但是在MyISAM表中,可以和其他字段一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,導入數據後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務類型來選擇合適的表類型,才能最大的發揮MySQL的性能優勢。
存儲引擎選擇依據?
是否需要支持事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外鍵支持;
是否需要全文索引
經常使用什麼樣的查詢模式
數據量大小
eg:
需要事務和外鍵約束 -- InnoDB
需要全文索引 -- MyISAM
數據量大,傾向於InnoDB,因爲它支持事務處理和故障恢復,InnoDB可以利用事務日誌進行數據恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
操作數據表的習慣,也會影響性能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因爲InnoDB不保存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致性能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB性能的方法:
InnoDB支持事務,存儲過程,視圖和行級鎖,在高併發下,表現比MyISAM強很多
影響性能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設置爲1的話,那麼每次插入數據的時候都會自動提交,導致性能急劇下降,應該是跟刷新日誌有關係,設置爲0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設置達到好的性能
設置innodb_buffer_pool_size能夠提升InnoDB的性能
設置查詢緩存
2.配置文件my.ini參數優化
1.max_connections --- 最大併發連接數,允許的同時客戶連接數, 默認100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢緩存,用於緩存select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設置query_cache_size大於0,可以極大改善查詢效率。而如果表數據頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 緩存的最大線程數
5.sort_buffer --- 每個需要進行排序的線程分配該大小的一個緩衝區
6.wait_timeout --- 默認是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連接在8小時內沒有活動,就會自動斷開該連接。 不要設置太長,建議 7200
7.default-storage-engine=INNODB # 創建新表時將使用的默認存儲引擎
配置示例,2G內存,針對站多,抗壓型的設置,最佳:
table_cache=1024 物理內存越大,設置就越大.默認爲2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 默認爲2M
innodb_flush_log_at_trx_commit=1
(設置爲0就是等到innodb_log_buffer_size列隊滿後再統一儲存,默認爲1)
innodb_log_buffer_size=2M 默認爲1M
innodb_thread_concurrency=8 你的服務器CPU有幾個就設置爲幾,建議用默認一般爲8
key_buffer_size=256M 默認爲218 調到128最佳
tmp_table_size=64M 默認爲16M 調到64-256最掛
read_buffer_size=4M 默認爲64K
read_rnd_buffer_size=16M 默認爲256K
sort_buffer_size=32M 默認爲256K
max_connections=1024 默認爲1210
thread_cache_size=120 默認爲60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 查看執行效率,定位優化對象的性能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連接代替子查詢
7.當只要一行數據時,使用limit 1
8.爲搜索字段建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啓查詢緩存,併爲查詢緩存優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢緩存,
2.像NOW()和RAND()或是其它的諸如此類的SQL函數都不會開啓查詢緩存,因爲這些函數的返回是會不定的易變的。所以,你所需要的就是用一個變量來代替MySQL的函數,從而開啓緩存
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變爲一個不衣變的值
38.JVM性能優化
1.
2.Java代碼性能優化
1.沒必要儘量不要使用靜態變量
2.充分利用單例機制減少對資源的加載,縮短運行的時間,提高系統效率
單例適用場景:
1. 控制資源的使用,通過線程同步來控制資源的併發訪問;
2. 控制實例的產生,以達到節約資源的目的
3.減少對象創建,最大限度的重用對象
儘量避免在經常調用的方法中循環使用new對象 --- 享元模式(可減少對象多次創建)
4.使用final修飾符
5.儘量使用局部變量
調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在分配給改方法的棧(Stack)中,速度較快。其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢
6.學會用StringBuilder和StringBuffer,並儘量確定其容量
單線程使用StringBuilder,多線程情況下使用StringBuffer,這樣性能會有很大提升
7.儘量使用基本數據類型代替對象 eg:字符串創建
8.使用HashMa、ArrayList,HashTable、Vector等使用在多線程的場合,內部使用了同步機制,這個會降低程序的性能
9.深入理解HashMap原理
當你要創建一個比較大的hashMap時,充分利用另一個構造函數
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次進行了hash重構,擴容是一件很耗費性能的事,在默認initialCapacity只有16,
而 loadFactor是 0.75,需要多大的容量,你最好能準確的估計你所需要的最佳大小,同樣的Hashtable,Vectors也是一樣的道理
10.儘量在finally塊中釋放資源
11.儘早釋放無用對象的引用
12.儘量避免使用split,split由於支持正則表達式,所以效率比較低,考慮使用apache的 StringUtils.split(string,char),頻繁split的可以緩存結果
13.儘量使用System.arraycopy ()代替通過來循環複製數組
System.arraycopy()要比通過循環來複制數組快的多
14..儘量緩存經常使用的對象 推薦:redis緩存
15.儘量避免非常大的內存分配
參考:http://developer.51cto.com/art/201511/496263.htm
61.MySQL性能優化
參考:
MySQL性能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存儲引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事務處理,爲每個表創建3個文件,分別存儲不同內容
支持表級鎖,表的寫操作會阻塞其他用戶對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他用戶對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他用戶對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是串行的
eg:
tb_Demo表,那麼就會生成以下三個文件:
1.tb_demo.frm,存儲表定義;
2.tb_demo.MYD,存儲數據;
3.tb_demo.MYI,存儲索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量數據時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入數據,適合管理:郵件或Web服務器日誌數據
總結:
1.適合做count的計算 (注意:不含where條件的統計,因爲MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 默認引擎
支持事務處理
引入了行級鎖(併發高)和外鍵約束
不支持全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外鍵約束 --- MySQL支持外鍵的存儲引擎只有InnoDB
5.支持自動增加列 Auto_INCREMNET屬性
6.InnoDB是爲處理巨大數據量時的最大性能設計
總結:
可靠性要求高,需要事務支持,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多性能開銷,而它的強項在於多線程的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定數據引擎的創建
細節和具體實現的差別:
1.InnoDB不支持FULLTEXT類型的索引。
2.InnoDB 中不保存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT類型的字段,InnoDB中必須包含只有該字段的索引,但是在MyISAM表中,可以和其他字段一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,導入數據後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務類型來選擇合適的表類型,才能最大的發揮MySQL的性能優勢。
存儲引擎選擇依據?
是否需要支持事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外鍵支持;
是否需要全文索引
經常使用什麼樣的查詢模式
數據量大小
eg:
需要事務和外鍵約束 -- InnoDB
需要全文索引 -- MyISAM
數據量大,傾向於InnoDB,因爲它支持事務處理和故障恢復,InnoDB可以利用事務日誌進行數據恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
操作數據表的習慣,也會影響性能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因爲InnoDB不保存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致性能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB性能的方法:
InnoDB支持事務,存儲過程,視圖和行級鎖,在高併發下,表現比MyISAM強很多
影響性能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設置爲1的話,那麼每次插入數據的時候都會自動提交,導致性能急劇下降,應該是跟刷新日誌有關係,設置爲0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設置達到好的性能
設置innodb_buffer_pool_size能夠提升InnoDB的性能
設置查詢緩存
2.配置文件my.ini參數優化
1.max_connections --- 最大併發連接數,允許的同時客戶連接數, 默認100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢緩存,用於緩存select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設置query_cache_size大於0,可以極大改善查詢效率。而如果表數據頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 緩存的最大線程數
5.sort_buffer --- 每個需要進行排序的線程分配該大小的一個緩衝區
6.wait_timeout --- 默認是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連接在8小時內沒有活動,就會自動斷開該連接。 不要設置太長,建議 7200
7.default-storage-engine=INNODB # 創建新表時將使用的默認存儲引擎
配置示例,2G內存,針對站多,抗壓型的設置,最佳:
table_cache=1024 物理內存越大,設置就越大.默認爲2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 默認爲2M
innodb_flush_log_at_trx_commit=1
(設置爲0就是等到innodb_log_buffer_size列隊滿後再統一儲存,默認爲1)
innodb_log_buffer_size=2M 默認爲1M
innodb_thread_concurrency=8 你的服務器CPU有幾個就設置爲幾,建議用默認一般爲8
key_buffer_size=256M 默認爲218 調到128最佳
tmp_table_size=64M 默認爲16M 調到64-256最掛
read_buffer_size=4M 默認爲64K
read_rnd_buffer_size=16M 默認爲256K
sort_buffer_size=32M 默認爲256K
max_connections=1024 默認爲1210
thread_cache_size=120 默認爲60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 查看執行效率,定位優化對象的性能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連接代替子查詢
7.當只要一行數據時,使用limit 1
8.爲搜索字段建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啓查詢緩存,併爲查詢緩存優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢緩存,
2.像NOW()和RAND()或是其它的諸如此類的SQL函數都不會開啓查詢緩存,因爲這些函數的返回是會不定的易變的。所以,你所需要的就是用一個變量來代替MySQL的函數,從而開啓緩存
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變爲一個不衣變的值
62.Java常見的鎖類型有哪些?請簡述其特點。
1、synchronized對象同步鎖:synchronized是對對象加鎖,可作用於對象、方法(相當於對this對象加鎖)、靜態方法(相當於對Class實例對象加鎖,鎖住的該類的所有對象)以保證併發環境的線程安全。同一時刻只有一個線程可以獲得鎖。
其底層實現是通過使用對象監視器Monitor,每個對象都有一個監視器,當線程試圖獲取Synchronized鎖定的對象時,就會去請求對象監視器(Monitor.Enter()方法),如果監視器空閒,則請求成功,會獲取執行鎖定代碼的權利;如果監視器已被其他線程持有,線程進入同步隊列等待。
2、Lock同步鎖:與synchronized功能類似,可從Lock與synchronized區別進行分析:
1、Lock可以通過tryLock()方法非阻塞地獲取鎖而。如果獲取了鎖即立刻返回true,否則立刻返回false。這個方法還有加上定時等待的重載方法tryLock(long time, TimeUnit unit)方法,在定時期間內,如果獲取了鎖立刻返回true,否則在定時結束後返回false。在定時等待期間可以被中斷,拋出InterruptException異常。而Synchronized在獲得鎖的過程中是不可被中斷的。
2、Lock可以通過lockInterrupt()方法可中斷的獲取鎖,與lock()方法不同的是等待時可以響應中斷,拋出InterruptException異常。
3、Synchronized是隱式的加鎖解鎖,而Lock必須顯示的加鎖解鎖,而且解鎖應放到finnally中,保證一定會被解鎖,而Synchronized在出現異常時也會自動解鎖。但也因爲這樣,Lock更加靈活。
4、Synchronized是JVM層面上的設計,對對象加鎖,基於對象監視器。Lock是代碼實現的。
3、可重入鎖:ReentrantLock與Synchronized都是可重入鎖。可重入意味着,獲得鎖的線程可遞歸的再次獲取鎖。當所有鎖釋放後,其他線程纔可以獲取鎖。
4、公平鎖與非公平鎖:“公平性”是指是否等待最久的線程就會獲得資源。如果獲得鎖的順序是順序的,那麼就是公平的。不公平鎖一般效率高於公平鎖。ReentrantLock可以通過構造函數參數控制鎖是否公平。
5、ReentrantReadWriteLock讀寫鎖:是一種非排它鎖, 一般的鎖都是排他鎖,就是同一時刻只有一個線程可以訪問,比如Synchronized和Lock。讀寫鎖就多個線程可以同時獲取讀鎖讀資源,當有寫操作的時候,獲取寫鎖,寫操作之後的讀寫操作都將被阻塞,直到寫鎖釋放。讀寫鎖適合寫操作較多的場景,效率較高。
6、樂觀鎖與悲觀鎖:在Java中的實際應用類並不多,大多用在數據庫鎖上,可參看:http://blog.csdn.net/sdyy321/article/details/6183412
7、死鎖:是當兩個線程互相等待獲取對方的對象監視器時就會發生死鎖。一旦出現死鎖,整個程序既不會出現異常也不會有提示,但所有線程都處於阻塞狀態。死鎖一般出現於多個同步監視器的情況。
63.volatile與automicInteger是什麼?如何使用?
在併發環境中有三個因素需要慎重考量,原子性、可見性、有序性。
voatile 保證了有序性(防止指令衝排序)和變量的內存可見性(每次都強制取主存數據),每次取到volatile變量一定是最新的
volatile主要用於解決可見性,它修飾變量,相當於對當前語句前後加上了“內存柵欄”。使當前代碼之前的代碼不會被重排到當前代碼之後,當前代碼之後的指令不會被重排到當前代碼之前,一定程度保證了有序性。而volatile最主要的作用是使修改volatile修飾的變量值時會使所有線程中的緩存失效,並強制寫入公共主存,保證了各個線程的一致。可以看做是輕量級的Synchronized。詳情可參看:http://www.cnblogs.com/dolphin0520/p/3920373.html。
automicXXX主要用於解決原子性,有一個很經典的問題:i++是原子性的操作碼?答案是不是,它其實是兩步操作,一步是取i的值,一步是++。在取值之後如果有另外的線程去修改這個值,那麼當前線程的i值就是舊數據,會影響最後的運算結果。使用automicXXX就可以非阻塞、保證原子性的對數據進行增減操作。詳情可參看:http://ifeve.com/java-atomic/
注:在此列舉的只是Java多線程最基礎的知識,也是面試官最常問到的,先打牢基礎,再去探討底層原理或者高級用法,除了這十個問題,在此再推薦一些其他的資料:
JVM底層又是如何實現synchronized的:http://www.open-open.com/lib/view/open1352431526366.html
Java線程池詳解:http://blog.csdn.net/zhangliangzi/article/details/52389766
Java線程池深度解析:http://www.cnblogs.com/dolphin0520/p/3932921.html
ConcurrentHashMap原理分析:http://www.cnblogs.com/ITtangtang/p/3948786.html
Java阻塞隊列詳解:http://ifeve.com/java-blocking-queue/
64.MyBatis中timestamp時間類型,在Spring MVC出參時無法轉爲正確的時間類型?
在xml中配置: javaType爲 java.sql.Timestamp
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" javaType="java.sql.Timestamp"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" javaType="java.sql.Timestamp"/>
65.Datatable自定義搜索
//自定義搜索,每次只能根據一個維度進行搜索(按渠道或產品類型)
$("#channel_select,#brand_select").change(function(){
var tsval = $(this).val()
table.search(tsval, false, false).draw();
});
66.synchronized和Lock的底層實現原理?
參考:http://www.open-open.com/lib/view/open1352431526366.html
鎖原理:http://blog.csdn.net/Luxia_24/article/details/52403033
synchronized 在軟件層面依賴JVM
Lock 在硬件層面依賴特殊的CPU指令 --- CAS + JNI調用CPU指令來實現
synchronized 可以吧任何一個非 null 對象作爲 "鎖",
作用於方法上時,鎖住的是對象實例this,
作用於靜態方法,鎖住的是對象對應的Class實例,因爲Class數據存儲在永久帶,因此靜態方法鎖相當於該類的全局鎖,
作用於某個對象實例,鎖住的是對應的代碼塊
HotSpot JVM中,鎖 --- 對象監視器(對象來監視線程的互斥) --- synchronized的實現原理
對象監視器,設置幾種狀態來區分請求的線程:
Contention Set: 所有請求鎖的線程,被首先放置到該競爭隊列 --- 先進後出的虛擬隊列,會被線程併發訪問
Entry Set: 等待獲取鎖的線程(來自Contention Set)排隊隊列 --- 等待獲取對象鎖運行
Wait Set: 獲取鎖後,調用wait()方法,被阻塞的線程隊列 --- 等待再次獲取對象鎖
OnDeck: 任何時刻最多只能有一個線程正在競爭鎖,該線程稱爲OnDeck
Owner: 獲得鎖的線程稱爲Owner
!Owner: 釋放鎖的線程
說明:
1.Entry Set 和Contention Set 同屬等待隊列,
2.Contention Set會被線程併發訪問,爲了降低對Contention Set隊尾的爭用(爲了減少加入與取出兩個線程對於contentionList的競爭),而建立Entry Set,如果Entry Set爲空,則從Contention Set隊尾取出節點
3.Owner線程在unlock時,會從Contention Set中遷移線程到Entry Set,並會指定Entry Set中的某個線程(一般爲Head)爲Read(OnDeck)線程
4.Owner線程並不是把鎖傳遞給OnDeck線程,只是把競爭鎖的權利交給OnDeck,OnDeck線程需要重新競爭鎖
5.OnDeck線程獲得鎖喉變爲Owner線程,無法獲得鎖的線程依然留在Entry Set中
6.如果Owner線程被wait()方法阻塞,則轉移後WaitSet中,如果某個時刻被notify/notifyAll喚醒,則再次轉移到EntrySet
線程的互斥,其實是線程對同一個對象的監視器monitor的操作:
每個對象都有一個監視器(monitor)!,當monitor被佔就會處於鎖定狀態,線程執行monitorentry指令時嘗試獲取monitor的所有權
1.如果monitor的進入數爲0,則線程進入monitor,然後將進入數設置爲1,線程即爲monitor的所有者
2.如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數 +1 --- 鎖可重入 --- ReentrantLock 和synchronized 都是 可重入鎖
3.其他線程已經佔用了monitor,則該線程進入阻塞狀態,知道monitor的進入數爲0,再嘗試獲取monitor的所有權
4.線程調用一次unlock()釋放鎖,monitor的進入數就 -1 (只有monitor進入數爲0,才能被其他線程搶佔)
5.一個線程獲取多少次鎖,就必須釋放多少次鎖,對於synchronized內置鎖 ,每一次進入和離開synchronized方法(代碼塊),就是一個完整的鎖獲取和釋放
sleep()不會釋放鎖,等待指定時間後繼續運行
wait()會釋放鎖,進入對象監視器的 Wait Set隊列,等待被喚醒,被喚醒後,需要重新獲取鎖
wait()和notify/notifyAll必須成對出現,而且必須放在synchronized中,
yield() 不會釋放鎖, 只是讓當前線程讓出CPU佔用權
Synchronized底層優化 --- 偏向鎖、輕量級鎖
參考:http://www.cnblogs.com/paddix/p/5405678.html
http://www.jianshu.com/p/5dbb07c8d5d5
Synchronized效率低的原因?
Synchronized是通過對象內部的對象監視器鎖(monitor)來實現的,monitor本質是依賴於底層的操作系統的Mutex Lock(互斥鎖)來實現,
操作系統實現線程間切換需要從用戶態轉到內核態(JVM轉到操作系統內核),這個成本非常高,狀態轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因
Synchronized底層優化:
1.鎖的4種狀態:
無鎖狀態、偏向鎖、輕量級鎖、重量級鎖
隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖(鎖升級只能從低到高升級,不會出現鎖的降級)
JDK1.6默認開啓偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖
鎖的狀態保存在對象的頭文件中
重量級鎖 --- 依賴於操作系統Mutex Lock所實現的鎖, 需要從JVM轉到操作系統內核,進行互斥操作
輕量級鎖 --- 並不是用來代替重量級鎖,本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗
輕量級鎖目的:
爲了在線程交替執行同步塊時提高性能 !!!
輕量級鎖適用場景:
線程交替執行同步塊的情況 ---- 鎖競爭不激烈的情況!
如果存在同一時間訪問同一個鎖,就會導致輕量級鎖升級爲重量級鎖
偏向鎖 --- 爲了在無多線程競爭的情況下,儘量減少不必要的輕量級鎖執行路徑
一旦線程第一次獲得了監視對象,之後讓監視對象 "偏向"這個線程,在該線程重複獲取鎖時,避免CAS操作
即:
設置一個變量,如果發現是true,無需再走加鎖、解鎖的流程!
偏向鎖目的:
解決無競爭下的鎖性能問題,在只有一個線程執行同步塊時,進一步提高性能
總結:
ynchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量 !!!
67.Spring如何解決Bean的循環依賴? --- 只支持Singleton作用域的, setter方式的循環依賴!!!
參考:https://my.oschina.net/yibuliushen/blog/737640
http://blog.csdn.net/caomiao2006/article/details/46511123
Spring容器循環依賴包括構造器循環依賴和setter循環依賴
如果是構造器循環依賴,Spring容器將無法啓動,報循環依賴異常BeanCurrentlInCreationException
解決方式:
將構造器注入方式改爲屬性注入方式 --- setter
Spring 支持setter方法注入屬性方式的循環依賴
Spring中將循環依賴的處理分3中情況:
1.構造器循環依賴 --- 原理, Spring 不支持構造器方式的循環依賴
通過構造器注入構成的循環依賴是無法解決的,只能在容器啓動時拋出BeanCurrentlInCreationException異常 --- 表示循環依賴
Spring容器將每一個正在創建的Bean的標識符(id)放到 "當前創建bean池" 中,bean標識符在創建過程中將一直保持在這個池中,
如果在創建bean過程中,發現自己已經在 "當前創建bean池"裏時,將拋出 BeanCurrentlInCreationException異常,表示循環依賴,
而對於創建完畢的bean將從 "當前創建bean池"中清除掉
eg:
如在創建TestA類時,構造器需要TestB類,那將去創建TestB,在創建TestB類時又發現需要TestC類,則又去創建TestC,
最終在創建TestC時發現又需要TestA,從而形成一個環,沒辦法創建 --- 循環依賴,拋出 BeanCurrentlInCreationException異常
配置文件;
<bean id="testA" class="com.bean.TestA">
<constructor-arg index="0" ref="testB"/>
</bean>
<bean id="testB" class="com.bean.TestB">
<constructor-arg index="0" ref="testC"/>
</bean>
<bean id="testC" class="com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>
測試用例:
@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test.xml");
} catch (Exception e) {
//因爲要在創建testC時拋出;
Throwable ee1 = e.getCause().getCause().getCause();
throw e1;
}
}
分析:
Spring容器創建"testA"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testB",並將"testA"標識符放到"當前創建bean池"。
Spring容器創建"testB"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testC",並將"testB"標識符放到"當前創建bean池"。
Spring容器創建"testC"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testA",並將"testC"標識符放到"當前創建Bean池"。
到此爲止Spring容器要去創建"testA"bean,發現該bean標識符在"當前創建bean池"中,因爲表示循環依賴,拋出BeanCurrentlyInCreationException。
說明:
Spring中bean默認是單例的,對於singleton作用於的Bean,可通過setAllowCircularReferences(false)來禁用循環引用
2.Setter循環依賴 --- 原理, Spring支持setter方式注入屬性的循環依賴!
setter注入方式構成的循環依賴,通過Spring容器提前暴露剛完成構造器注入但未完成其他步驟(eg:setter注入)的bean來完成的,
而且只能解決Singleton單例作用域的bean循環依賴,通過提前暴露一個單例工廠方法,從而使其他bean能引用到該bean(注意:此時僅僅只是生了一個bean,該bean還未調用其他方法,如setter注入)
對單例Bean循環依賴的處理:通過遞歸方法,找出當前Bean的所有依賴Bean,然後提前緩存起來
原理:
創建Bean A時,先通過無參構造器創建一個A實例,此時屬性都是空的,但對象引用已經創建創建出來,然後把Bean A的引用提前暴露出來,
然後setter B屬性時,創建B對象,此時同樣通過無參構造器,構造一個B對象的引用,並將B對象引用暴露出來。
接着B執行setter方法,去池中找到A(因爲此時,A已經暴露出來,有指向該對象的引用了),這樣依賴B就構造完成,也初始化完成,然後A接着初始化完成,
循環依賴就這麼解決了!!!
總結:
先創建對象引用,再通過setter()方式,給屬性賦值,層層創建對象 !!!
Bean A初始化時,先對其依賴B進行初始化,同時,通過默認無參構造器,生成自己的引用,而不調用其setter()方法,
當B對象創建時,如果還依賴C,則也通過無參構造器,生成B的引用,
C對象創建時,如果引用了A,則去對象池中查到A的引用,然後調用setter()方式,注入A,完成C對象的創建
C創建完成後,B使用setter()方式,注入C,完成B對象創建,
B對象場景完成後,A使用setter()方式,注入B,完成A對象創建,
最終,完成setter()方式的循環依賴!
如果循環依賴的都是單例對象(都是通過setter方式注入屬性的),那麼這個肯定沒問題,放心使用即可!!!
如果一個是單例,一個是原型,那麼一定要保證單例對象能提前暴露出來,纔可以正常注入屬性!!!
3.prototype範圍的依賴處理
對於"prototype"作用域bean,Spring容器無法完成依賴注入,因爲Spring容器不進行緩存"prototype"作用域的bean,因此無法提前暴露一個創建中的bean
這個spring也無能爲力,因爲是原型對象,A創建的時候不會提前暴露出來,所以,每次都是要創建,創建的時候,發現有相同的對象正在創建,同樣報錯,循環依賴錯誤
4.Spring創建Bean的源碼解釋:
1.創建Bean的入口
AbstractBeanFactory-->doGetBean()
Object sharedInstance = getSingleton(beanName); //從緩存中查找,或者如果當前創建池中有並且已經暴露出來了,就返回這個對象
2.創建單例Bean方法
DefaultSingletonBeanRegistry-->getSingleton(String beanName, ObjectFactory<?> singletonFactory)
3.創建真正對象
AbstractAutowireCapableBeanFactory-->doCreateBean
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
} 注意這一步很關鍵,是調用構造方法創建一個實例對象,如果這個構造方法有參數,而且就是循環依賴的參數,那麼這個對象就無法創建了,
因爲到這裏對象沒有創建,也沒有暴露當前對象,如果是無參的構造方法,那麼就可以,先創建一個對象,儘管所有的屬性都爲空
68.Spring事務管理的原理?
參考:http://www.codeceo.com/article/spring-transactions.html
聲明式事務管理,在Service之上或Service的方法之上,添加 @Transactional註解
@Transactional如何工作?
Spring在啓動時,會去解析生成相關的Bean,這是會查看擁有相關注解的類和方法,
並且爲這些類和方法生成代理,並根據 @Transactional的相關參數進行相關配置注入,
這樣就在代理中把相關的事務處理掉了(開啓正常提交事務,異常回滾事務)
真正的數據庫層,事務提交和回滾是通過binlog和redo log實現的
Spring事務管理機制實現原理:
參考:
http://www.jianshu.com/p/4312162b1458
http://www.cnblogs.com/duanxz/p/3750845.html
http://www.92to.com/bangong/2016/11-05/12533010.html
在調用一個需要事務的組件時,管理器首先判斷當前調用(即:當前線程)有沒有事務,如果沒有事務則啓動一個事務,並把事務與當前線程綁定,
Spring使用TransactionSynchronizationManager的bindResource方法將當前線程與一個事務綁定,採用的方式就是ThreadLocal,
參考:DataSourceTransactionManager的啓動事務用的代碼 doBegin()
通過動態代理或AOP方式,對所有需要事務管理的Bean進行加載,生成代理對象,並根據配置在invoke()方法中對當前調用的方法名進行判定,
並在method.invoke()方法前後爲其加上合適的事務管理代碼,根據method.invoke()執行結果,正常提交事務,異常回滾事務
實現了EntityManager接口的持久化上下文代理,包含3個組成部分:
1.EntityManager Proxy本身
2.事務的切面
3.事務管理器
遇到過的問題:
參考:59,爲何聲明式事務沒有生效?
69.Spring如何處理高併發?高併發下,如何保證性能?
1.單例模式 + ThreadLocal
單例模式大大節省了對象的創建和銷燬,有利於性能提高,ThreadLocal用來保證線程安全性
Spring單例模式下,用ThreadLocal來切換不同線程直接的參數,用ThreadLocal是爲了保證線程安全,實際上,ThreadLocal的key就是當前線程的Thread實例
單例模式下,Spring把每個線程可能存在線程安全問題的參數值放進了ThreadLocal,雖然是一個實例,但在不同線程下的數據是相互隔離的,
因爲運行時創建和銷燬的bean大大減少了,所以大多數場景下,這種方式對內存資源的消耗較少,並且併發越高,優勢越明顯
2.ThreadLocal
相比同步機制,ThreadLocal則從另一個角度來解決多線程的併發訪問。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。
因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,
在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal !!!
3.Spring MVC在併發訪問時,是否會存在線程安全問題?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
http://blog.csdn.net/a236209186/article/details/61460211
Struts2是基於類的攔截(每次處理請求,都會實例化一個對象Action,不會有線程安全問題)
Spring MVC 是基於方法的攔截,粒度更細,而Spring的Controller默認是Singleton的,即:每個request請求,系統都會用同一個Controller去處理,
Spring MVC和Servlet都是方法級別的線程安全,如果單例的Controller或Servlet中存在實例變量,都是線程不安全的,而Struts2確實是線程安全的
優點:
不用每次創建Controller,減少了對象創建和銷燬
缺點:
Controller是單例的,Controller裏面的變量線程不安全
解決方案:
1.在Controller中使用ThreadLocal變量,把不安全的變量封裝進ThreadLocal,使用ThreadLocal來保存類變量,將類變量保存在線程的變量域中,讓不同的請求隔離開來
2.聲明Controller爲原型 scope="prototype",每個請求都創建新的Controller
3.Controller中不使用實例變量
Spring MVC 如何保證request對象線程安全?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
InvocationHandler接口:這是springmvc保證request對象線程安全的核心。
通過實現該接口,開發者能夠在Java對象方法執行時進行干預,搭配Threadlocal就能夠實現線程安全
問題:判斷一下程序是否線程安全?
@Controller
public class UserController{
@Autowired
private HttpSession session
@RequestMapping(xxxxxxx)
public void getUser{
session.get ...
session.set...
....
}
}
結論:
該程序是線程安全的
解析:
項目啓動和運行時,Controller對象中的HttpSession並不是HttpSession實例,而是一個代理,
是org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler代理了HttpSession ,可通過這個代碼求證:System.out.println(Proxy.getInvocationHandler(session));
只要當你真正調用HttpSession中的非java.lang.Object方法時纔會真真去調用被代理的HttpSession裏面的方法
說一下session.get ...過程:首先從對象工廠從Threadlocal中取得HttpSession實例,然後通過反射調用該實例的set方法
特別注意:
1.Spring 應該是在請求進來的時候ThreadLocal.set(Session),然後在請求的生命週期中都是一個Thread ,執行完後ThreadLocal.remove(Session)
2.一個請求使用一個ThreadLocal,綁定對應的HttpSession,所以是線程安全的
3.對於 "注入" 到Controller中的單例對象, 都是由Spring統一管理的,Spring對注入Controller的對象使用了ThreadLocal + 代理機制,保證了線程安全
4.但是,對於在Controller中直接定義的實例變量,是線程不安全的!!!
eg:
@RestController
@RequestMapping("/test1")
public class ControllerTest1 {
private int i = 0;
@GetMapping("/count")
public void test1(){
System.out.println(i++);
}
//驗證Controller中的實例變量,線程不安全
@GetMapping("/t1")
public void test3(){
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i=0;i<1000;i++) {
service.execute(new Runnable() {
@Override
public void run() {
HttpUtils.sendGet("http://localhost:8080/test1/count", null);
}
});
}
}
}
調用:http://localhost:8080/test1/t1 方法,使用多線程對i進行操作,發現i的結果不是999,證明Controller中的實例變量是線程不安全的!
結論:
1.對於單例的Controller,Service中定義的實例變量,都不是線程安全的!!!
2.儘量避免在Controller和Service中定義多線程共享的實例變量
3.Spring使用ThreadLocal + InvocationHandler(動態代理)提供了高併發訪問的性能
4.對於Controller和Service中的實例變量,多線程訪問時,需要加鎖處理 或 設置 scope = "prototype"爲每個請求創一個對象
5.對於 @Autowire注入的HttpServletRequest和HttpSession,Spring進行了特殊處理,不會有線程安全問題
70.ConcurrentLinkedQueue與BlockingQueue
2類線程安全的隊列:
1.阻塞隊列 --- 阻塞算法 --- 隊列使用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式實現
2.同步隊列 --- 非阻塞算法 --- 使用循環CAS方式實現
LinkedBlockingQueue 線程安全的阻塞隊列,實現了BlockingQueue接口,BlockingQueue繼承自java.util.Queue接口,
並在接口基礎上增加了take()和put()方法, 這2個方法正式隊列操作的阻塞版本
先進先出,可以指定容量,默認最大是Integer.MAX_VALUE;其中主要用到put和take方法,put方法在隊列滿的時候會阻塞直到有隊列成員被消費,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來!!!
put() 向隊列中放數據 take() 從隊列中取數據
ConcurrentLinkedQueue 是Queue的一個安全實現.Queue中元素按FIFO原則進行排序.採用CAS操作,來保證元素的一致性。
非阻塞方式實現的無界線程安全隊列 !!!
offer()添加元素, poll()獲取元素 isEmpty()判斷隊列是否爲空 (特別注意,不用size(),效率低,會遍歷隊列,儘量要避免用size而改用isEmpty())
採用CAS操作,允許多個線程併發執行,並不會因爲你加鎖而阻塞線程,使得併發性能更好!!!
使用:http://www.cnblogs.com/dmir/p/4907515.html
http://blog.csdn.net/sunxianghuang/article/details/52046150
ConcurrentLinkedQueue源碼解析:http://ifeve.com/concurrentlinkedqueue/
總結:
多數生產消費模型的首選數據結構就是隊列(先進先出)。Java提供的線程安全的Queue可以分爲阻塞隊列和非阻塞隊列,
其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞隊列或者非阻塞隊列
Java中的7種阻塞隊列
阻塞隊列: --- 阻塞的是線程操作(拿和取元素)
常用於生產者和消費者場景,是生產者用來存放元素、消費者用來獲取元素的容器
put()阻塞:隊列滿時,阻塞插入元素的線程,直到隊列不滿
take()阻塞:隊列空時,阻塞獲取元素的線程,直到隊列不空
ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。 生產者和消費者直接傳遞數據,不對數據作緩存,生產者和消費者通過在隊列裏排隊的方式來阻塞和喚醒 --- 速度快
線程數少時,使用SynchronousQueue 速度更快!!!
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的區別和使用場景?
區別:
1.三者都是線程安全的
2.2個BlockingQueue是阻塞的,ConcurrentLinkedQueue是併發的
3.2個BlockingQueue使用鎖機制實現阻塞和線程安全(通過ReentrantLock + Condition阻塞容量爲空時的取操作和容量滿時的寫操作),
ConcurrentLinkedQueue使用cas算法保證線程安全
4.ArrayBlockingQueue使用一個鎖(lock + 2個Condition),而LinkedBlockingQueue使用2個鎖(鎖分離,取用takeLock + Condition,寫用putLock+Condition),所以LinkedBlockingQueue的吞吐量大,併發性能比Array高
LinkedBlockingQueue,對頭和尾採用不同的鎖,提高了吞吐量,適合 "消費者生產者" 模式
ArrayBlockingQueue, 數組實現,使用一把全局鎖並行對queue的讀寫操作,同時使用2個Condition阻塞容量爲空時的讀操作和容量滿時的寫操作
5.正因爲LinkedBlockingQueue使用兩個獨立的鎖控制數據同步,所以可以使存取兩種操作並行執行,從而提高併發效率。
而ArrayBlockingQueue使用一把鎖,造成在存取兩種操作爭搶一把鎖,而使得性能相對低下。LinkedBlockingQueue可以不設置隊列容量,默認爲Integer.MAX_VALUE.其容易造成內存溢出,一般要設置其值
使用場景:
阻塞隊列優點:
多線程操作不需要同步,
隊列會自動平衡負載,即:生產和消費兩邊,處理快了會被阻塞,減少兩邊的處理速度差距,
自動平衡負載特性,造成它能被用於多生產者隊列,隊列滿了就要阻塞等着,直到消費者使隊列不滿才能繼續生產
ConcurrentLinkedQueue:
允許多線程共享訪問一個集合,多用於消息隊列!!!
多消費者消費同一個 用 ConcurrentLinkedQueue:
BlockingQueueue:
多線程共享時阻塞,多用於任務隊列!!!
單消費者用 BlockingQueueue:
總結: 單個消費者用LinkedBlockignQueue, 多消費者用ConcurrentLinkedQueue !!!
單生產者,單消費者 用 LinkedBlockingqueue
多生產者,單消費者 用 LinkedBlockingqueue
單生產者 ,多消費者 用 ConcurrentLinkedQueue
多生產者 ,多消費者 用 ConcurrentLinkedQueue
71.Java多線程同步機制:3種類型
volatile 變量:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性保證,不提供原子性。 --- 只保證可見性,不保證原子性,不絕對線程安全!!!
CAS 原子指令:輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性和原子化更新保證。
內部鎖(synchronized)和顯式鎖(各種Lock):重量級多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性和原子性。
參考:
非阻塞算法在併發容器中的實現:ConcurrentLinkedQueue https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html
72.CAS在JDK中的實現
參考:http://blog.csdn.net/canot/article/details/50759424
1.Synchronized鎖機制存在的問題:
(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險
優化:
偏向鎖、輕量級鎖、減小鎖粒度
2.鎖分類
悲觀鎖 --- 獨佔鎖 --- synchronized是獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖
樂觀鎖 --- 每次不加鎖,而是假設沒有衝突,而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止
3.CAS原理 --- 實現樂觀鎖
CAS操作:
CAS有3個操作數:
V 內存值
A 舊的預期值
B 要修改的新值
當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做!!!
非阻塞算法: 一個線程的失敗或掛起,不應該影響其他線程的失敗或掛起的算法
CAS的硬件基礎和實現原理:
現代的CPU提供了特殊指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,
而compareAndSet()就用這些代替了鎖定, compareAndSet利用JNI,藉助調用的C語言來完成CPU指令的操作
eg:
AtomicInteger 如何實現無鎖下的線程安全?
//在沒有鎖的機制下可能需要藉助volatile原語,保證線程間的數據是可見的(共享的)。這樣才獲取變量的值的時候才能直接讀取。
private volatile int value;
public final int get(){
return value;
}
//i++操作, 每次從內存中讀取數據,然後將此數據和 +1 後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功爲止
public final int incrementAndGet(){
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
39.Redis性能優化
40.MongoDB性能優化
41.MQ性能優化和對比
42.一次搞定Java多線程併發編程
參考:http://blog.csdn.net/liuguangqiang/article/details/52137188
Java併發編程:CountDownLatch、CyclicBarrier和Semaphore
http://blog.csdn.net/zheng0518/article/details/42297259
http://www.cnblogs.com/dongguacai/p/6023028.html
線程安全:
1.Synchronized 線程同步
2.Lock + ReentrantLock 線程安全
線程通信與協作:
0.wait()、notify()、notifyAll() 每個對象都有的3個方法,通道Monitor、waitSet、enterSet用來監聽鎖,存放線程隊列
1.ReentrantLock + Condition 併發控制多路複用
每個ReentrantLock可以創建多個Condition,每個Condition都可以通過控制一個對象鎖,來實現多個線程通信
Condition方法:
線程1中調用await()後,線程1將釋放鎖,等待被喚醒
線程2獲取到鎖後,執行完畢,調用signal()方法,喚醒線程1
eg:
打印1到9這9個數字,A線程打印1,2,3,然後B線程打印4,5,6,,然後再A線程打印 7,8,9
2.ReadWriteLock 讀寫鎖
3.CountDownLatch
4.CyclicBarrier
5.Semaphore
效率提升:
6.線程池 + 阻塞隊列
不能根據hashCode值判斷兩個對象是否相等,但可以直接根據hashCode值判斷兩個對象不相等。
如果兩個對象的hashCode值不等,一定是不同的對象,要判斷兩個對象是否真正相等,必須通過equals()方法
如果調用equals()方法得到的結果爲true,則兩個對象的hashCode值一定相等
如果equals()方法得到的結果爲false,則兩個對象的hashCode值不一定不同
如果兩個對象的hashcode值不等,則equals方法得到的結果必定爲false;
如果兩個對象的hashcode值相等,則equals方法得到的結果未知。
在重寫equals()方法時,必須重寫hashCode方法,讓equals()方法和hashCode()方法始終在邏輯上保持一致性
hashCode方法的主要作用是爲了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable
Java裏靜態語句塊是優先對象存在,也就是優先於構造方法存在,我們通常用來做只創建一次對象使用,類似於單列模式而且執行的順序是:
父類靜態語句塊 -> 子類靜態語句塊 -> 父類非靜態代碼塊、父類構造方法 -> 子類非靜態代碼塊構造方法
靜態語句塊可以看作在類加載的時候執行,且只執行一次
Java對象初始化順序:
靜態代碼塊內容先執行(父>子),接着執行父類非靜態代碼塊和構造方法,然後執行子類非靜態代碼塊和構造方法
首先執行父類靜態的內容,父類靜態的內容執行完畢後,接着去執行子類的靜態的內容,當子類的靜態內容執行完畢之後,再去看父類有沒有非靜態代碼塊,如果有就執行父類的非靜態代碼塊,
父類的非靜態代碼塊執行完畢,接着執行父類的構造方法;父類的構造方法執行完畢之後,它接着去看子類有沒有非靜態代碼塊,如果有就執行子類的非靜態代碼塊。子類的非靜態代碼塊執行完畢再去執行子類的構造方法
2.JVM內存模型
程序計數器
用來指示 執行哪條指令的
如果線程執行的是非native方法,則程序計數器保持的是當前需要執行的指令的地址
如果線程執行的是native方法,程序計數器中的值是undefined
Java棧
存放一個個的棧幀,每個棧幀對應一個被調用的方法。
棧幀中包括:局部變量表、操作數棧、指向當前方法所屬的類的運行時常量池的引用、方法返回地址和一些額外的附加信息
當線程執行一個方法時,就會隨之創建一個對應的棧幀,並將建立的棧幀壓入棧,當方法執行完後,便會將棧幀出棧
線程當前執行的方法所對應的棧幀位於Java棧的頂部
棧幀
局部變量表
操作數棧
指向運行時常量池的引用
方法返回地址
額外的附加信息
堆
存儲對象本身以及數組
方法區 --- 線程共享區域
1、存儲了每個類的信息(包括:類名稱、方法信息、字段信息)、靜態變量、常量以及編譯後的代碼等
2、常量池
本地方法棧
3.Hash表
散列表,能快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字比較來進行查找。
Hash表的設計,採用了函數映射思想,將記錄的存儲位置與記錄的關鍵字關聯起來,從而能夠很快速地進行查找
eg:
張三 13980593357
李四 15828662334
王五 13409821234
張帥 13890583472
Hash表能通過"李四"這個信息直接獲取到該記錄在表中的位置,複雜度爲O(1)
原理:
Hash表採用一個映射函數
f:key --> address
將關鍵字key映射到該記錄在表中的存儲位置
說明:
1.映射關係 稱爲 Hash函數
2.通過Hash函數和關鍵字計算出來的存儲位置(hash表中的存儲位置,不是實際的物理地址) 稱爲 Hash地址
聯繫人信息採用Hash表存儲時,當想找 "李四" 的信息時,直接根據 "李四" 和 Hash函數,計算出Hash地址即可
4.Java線程安全的本質:線程中並沒有存放任何對象數據,而是在執行時,去主內存(堆)中去同步數據,所有的對象數據都存在JVM的堆中,因此需要對資源進行共享鎖!!!
堆 --- JVM的核心數據存儲區 --- 線程共享的主內存
堆中爲JVM的所有對象分配了內存空間用以存儲和維護變量值等
棧 --- 線程私有的內存區,由棧幀(線程棧)組成,存放8中基本數據類型和對象引用
每個線程都會生成一個自有的線程棧,線程棧中用存儲了該線程的基本數據常量,變量值,以及對象長變量的引用
每個線程執行時,根據代碼順序,壓棧 棧幀(棧內存)
對象變量在線程執行時的過程:!!! --- 由JVM內存模型決定
1.線程根據棧中的引用去堆上同步該對象數據下來,然後在線程自己的內存中進行操作
2.操作之後再將線程棧撒花姑娘的運算結果同步到堆(主內存)中
3.多線程時,因爲每個線程都操作自己從主內存(JVM堆)中同步過來的數據,如果不加鎖,會導致線程安全問題(數據提交到主內存時不一致)
5.堆 --- JVM中所有對象的內存空間 分爲: Young Gen, Old Gen
Young Gen 又分爲:Eden區和兩個大小相同的Survivor區(from 和 to)
Eden和Survivor默認比例 8:1 由 -XX:SurvivorRation設置
堆大小 -Xmx -Xms 設置
Young Gen -Xmn 設置
-XX:NewSize和-XX:MaxNewSize
用於設置年輕代的大小,建議設爲整個堆大小的1/3或者1/4,兩個值設爲一樣大。
Minor GC --- 發生在新生代的垃圾回收,Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快,
年輕代的GC使用複製算法(將內存分爲兩塊,每次只用其中一塊,當這塊內存用完,就將還活着的對象複製到另外一塊上面,複製算法不會產生內存碎片)
Full GC --- 發生在年老代的GC, Full GC比較慢,儘量避免
新創建對象都會被分配到Eden區(一些大對象特殊處理),當Eden區滿則進行Minor GC,
這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區, 對象在Survivor區中每熬過一次Minor GC,年齡增加1歲,
當年齡增加到一定程度後(默認15歲),會被移動到年老代中,
當年老代滿時,經常Full GC
線程Stack 每個線程獨有的操作數棧局部變量表方法入口 -Xss 設置
方法區 -XX:PermSize和-XX:MaxPermSize設置
6.JVM class類加載加載機制 --- JVM把描述類的數據從class加載到內存,並對數據進行校驗,轉化解析和初始化,最終得到可被虛擬機直接使用的Java類型
類裝載器: 尋找類的字節碼文件, 並構造出類在JVM內部表示的對象組件
JVM類加載器把一個類裝入JVM過程:
(1) 裝載:查找和導入Class文件;
(2) 鏈接:把類的二進制數據合併到JRE中;
(a)校驗:檢查載入Class文件數據的正確性,確保被加載類信息符合JVM規範;
(b)準備:給類的靜態變量分配存儲空間,並將其初始化爲默認值;
(c)解析:將虛擬機常量池中符號引用轉成直接引用;
(3) 初始化:對類的靜態變量,靜態代碼塊執行初始化操作,爲類的靜態變量賦初始值
說明:
1.類加載的雙親委託機制
某個特定的類加載器在接到加載類的請求時,首先將加載任務交給父類加載器,父類加載器又將加載任務向上委託,直到最高層的父類加載器,
如果最高層的父類加載器可以完成類加載任務,就成功返回,否則向下傳遞加載任務,由其子類加載器進行加載
2.初始化步驟
1.如果類沒有被加載和鏈接,則先進行加載和鏈接
2.如果類存在直接父類,並且父類未被初始化(注意:在一個類加載器中,類只能初始化一次),那就初始化直接的父類
3.如果存在static塊,一次執行這些初始化語句
4.static塊,會在類第一次被使用時執行(調用類的靜態變量,初始化對象,Class.forName等)
7.正則表達式總計:
^ 和$表示以字符串開始和以字符串結尾。例:^abc 表示必須以abc開始(如:abcd,abcefd),abc$ 表示必須以abc結尾(如:);^abc$ 只能是abc(abc是個整體,abcabc不匹配) ;abc 表示包含abc的字符串
* 和 + 和 ? 分別表示出現0次或多次,1次或多次,0次或1次。例:abc*表示有0個或多個abc,其他兩個同理
上面的*+?完全可以用範圍代替,abc{2}表示ab後面有至少兩個c,如abcc,dfdabccccc都是符合的;abc{2}$ 只有以abcc結尾的符合,如343abcc
abc{1,2} 表示ab後面跟着1或2個c;
abc{3,} 表示ab後面跟着至少3個c; {,3}這種是不正確的
| 或運算 ab|cd 表示字符串裏有ab或者cd;
. 可以替換任意字符
下面是幾種是需要記住的
"[ab]":表示一個字符串有一個"a"或"b"(相當於"a|b");
"[a-d]":表示一個字符串包含小寫的'a'到'd'中的一個(相當於"a|b|c|d"或者"[abcd]");
"^[a-zA-Z]":表示一個以字母開頭的字符串;
"[0-9]%":表示一個百分號前有一位的數字;
",[a-zA-Z0-9]$":表示一個字符串以一個逗號後面跟着一個字母或數字結束。
下面看看具體的實例,比如我今天做的:一個輸入框,可以輸入數字,也可以輸入多個數字用逗號隔開,或者兩個數字用~分隔。
我寫的正則表達式 : ((^[0-9]+[~]?)?|^([0-9]+[,])+)[0-9]+$
8.JVM中的GC垃圾回收和內存分配策略 ---- JVM高級 參考:
http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html
http://www.cnblogs.com/zhguang/tag/Java/
http://blog.csdn.net/column/details/javavirtualmachine.html
http://blog.csdn.net/eric_sunah/article/details/7870906
1.判斷回收對象
1.引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器值+1,當引用失效,計數器值-1,任何時刻計數器爲0的對象就不可能再被使用
缺點:
無法解決對象的循環依賴問題!!!
2.可達性算法 --- JVM使用的GC判斷算法
通過一系列稱爲 GC Roots 的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲 引用鏈,
當一個對象到 GC Roots 沒有任何引用鏈相連時(即:GC Roots到這個對象不可達),則此對象是不可用的
Java中可作爲 GC Roots的對象包括以下幾種:
1.虛擬機棧(棧幀的本地變量表)中引用的對象
2.方法區中類靜態屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧中JNI(Native方法)引用的對象
2.JVM的垃圾回收機制
1.幾個概念:
內存分代 新生代(Eden + 2 Survivor )、老年代、永久代
新創建的對象分配在Eden區,經歷一次Minor GC後被移到 Survivor1區,再經歷一次Minor GC被移到Survivor2區,直到升至老年代
大對象(長字符串或大數組)可能直接存放到老年代
對象創建都在堆上,類信息、常量、靜態變量存儲在方法區,堆和方法區是線性共享的
GC由守護線程執行,在從內存回收一個對象之前,會調用對象的finalize()方法
GC的觸發由JVM堆內存大小決定,System.gc()和Runtime.gc()會向JVM發送執行GC的請求,但JVM不保證一定會執行GC
堆中沒有內存創建新對象時,會拋出 OutOfMemoryError
2.GC回收什麼對象?
可達性算法 --- 根搜索法
--- 通過一系列稱爲GC Roots的對象作爲起始點,從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,證明此對象不可用,可以被回收了
程序把所有的引用關係看做一張圖,從一個節點 GC Roots開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢後,剩餘的節點被認爲是沒有被引用的節點,可被回收
--- 可被作爲GC Roots的對象
1.虛擬機棧中引用的對象(本地變量表)
2.方法靜態屬性引用的對象
3.方法區中常量引用的對象
4.本地方法棧用引用的對象(Native對象)
引用計數法
--- 可能有循環依賴問題,導致計數器不爲0,對象永遠無法被回收
多次掃描,而都不可達的對象,經歷幾次掃描後會被回收
3.垃圾回收算法: 參考:http://www.cnblogs.com/sunniest/p/4575144.html
任何垃圾回收算法,只做2件基本事情:
1.發現無用可被回收對象 (可達性算法)
2.回收被無用對象佔用的空間,使得該空間可被再次使用
1.標記-清除算法:
從GC Roots根集合進行掃描,對存活的對象進行標記,標記完成後,在掃描整個空間中未被標記的對象,進行回收
優點:不必移動對象,在存活對象比較多的情況下極爲高效
缺點:GC時會暫停整個應用,容易造成不連續的內存碎片
2.複製算法:
爲了解決內存碎片問題而產生的一種算法。它的整個過程可以描述爲:標記所有的存活對象;通過重新調整存活對象位置來縮並對象圖;更新指向被移動了位置的對象的指針
將內存劃分爲大小相等的2塊,每次只是用其中一塊,當一塊用完後,將還存活的對象複製到另一塊內存上,然後把已使用過的內存空間一次清理掉,
這樣使得每次都是對其中的一塊進行內存回收,不會產生碎片等情況,只要移動堆訂的指針,按順序分配內存即可,實現簡單,運行高效
優點:不會造成內存碎片
缺點:內存縮小爲原來的一半,需要移動對象
3.標記-整理算法:
先進行 標記-清除算法,然後在清理完無用對象後,將所有存活的對象移動到一端,並更新引用其對象的指針
4.分代算法:
基於對象生命週期分析得出的垃圾回收算法。把對象分爲年輕代、年老代、持久代,對不同的生命週期使用不同的算法
年輕代: --- Minor GC, 對象生命週期短 使用: Serial、PraNew、Parallel Scavenge 收集器
1.所有新生成對象,目標是儘快收集掉生命週期短的對象
2.新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(一般而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,然後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另一個survivor1區,然後清空eden和這個survivor0區,此時survivor0區是空的,然後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。
Eden : Survivor0:Survivor1 = 8:1:1
3.當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。若是老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收
4.新生代發生的GC也叫做Minor GC,MinorGC發生頻率比較高(不一定等Eden區滿了才觸發)
老年代: --- Full GC 對象生命週期長 使用:Serial Old、Parallel Old、CMS收集器
1.在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。
2.內存比新生代也大很多(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。
永久代: --- 用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響
4.GC實現 --- 垃圾收集器
Serial --- 複製算法
新生代單線程收集器,標記和清理都是單線程,優點:簡單高效
Serial Old --- 標記-整理算法
老年代單線程收集器
ParNew --- 停止複製算法
Serial收集器的多線程版本,在多核CPU環境下有着比Serial更好的表現
Parallel Scavenge ---
並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般爲99%, 吞吐量= 用戶線程時間/(用戶線程時間+GC線程時間)。適合後臺應用等對交互相應要求不高的場景
Parallel Old收集器(停止-複製算法)
Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先
CMS --- 標記清理算法
高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇
5.GC執行機制
Minor GC: 新對象在年輕代的Eden區申請內存失敗時,觸發Minor GC --- 年輕代
--- 只對年輕代的Eden去進行,不影響老年代
--- 頻繁進行
--- 選用速度快、效率高的算法,使Eden儘快空閒
Full GC: 對整個堆進行整理,包括 年輕代、老年代、永久代
--- 對整個堆進行回收,速度比 Minor GC 慢
--- 儘量減少 Full GC次數
--- 對象JVM調優過程,大部分是針對Full GC的調節
可能導致Full GC的原因:
1.老年代滿
2.持久代滿
3.System.gc()被顯示調用
4.上一次GC之後Heap的各域分配策略動態變化
6.容易出現泄露的地方:
1.靜態集合類(HashMap、Vector),這些靜態變量生命週期和應用程序一樣,所有的對象也不能被釋放
2.各種連接、數據了連接、網絡連接、IO連接沒被顯示調用close(),不被GC回收導致內存泄露
3.監聽器,在釋放對象的同時沒有相應刪除監聽器時可能導致內存泄露
9.JVM內存模型 http://www.infoq.com/cn/articles/java-memory-model-1
內存模型:描述了程序中各個變量(實例域、靜態域和數組元素)直接的關係,以及在實際計算機系統中將變量存儲到內存、從內存中取出變量這樣的底層細節
JVM中存在一個主存,Java中所有對象成員變量都存儲在主存中,對於所有線程是共享的,每條線程都有自己的工作內存,
工作內存中保存的是主存中某些對象成員變量的拷貝,線程對所有成員變量的操作都是在工作內存中進行,然後同步到主存,線程之間無法相互直接訪問,變量傳遞都需要通過主存
JMM(Java內存模型,Java Memory Model簡稱)是控制Java線程之間、線程和主存之間通信的協議。
JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存(local memory),
本地內存中存儲了該線程以讀/寫共享變量的副本,線程在本地私有內存中操作完後,要將數據同步到主內存
Java內存模型中規定了:
所有變量都存儲在主內存中,每個線程都有自己的工作內存,線程的工作內存中使用到的變量,是主內存的副本拷貝,
線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量,操作完後,要將數據同步到主內存
不同的線程之間工作內存是獨立的,線程間變量值的傳遞均需要在主內存來完成
特別注意:
(根據Java虛擬機規範的規定,volatile變量依然有共享內存的拷貝,但是由於它特殊的操作順序性規定——
從工作內存中讀寫數據前,必須先將主內存中的數據同步到工作內存中,所有看起來如同直接在主內存中讀寫訪問一般,因此這裏的描述對於volatile也不例外)。
不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值得傳遞均需要通過主內存來完成
8種內存間的交互操作:
Java內存模型定義了8種操作來完成主內存與工作內存之間交互的實現細節
1.lock(鎖定)
作用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態
2.unlock(解鎖)
作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定
3.read(讀取)
作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
4.load(載入)
作用於主內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
5.use(使用)
作用於工作內存的變量,它把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值得字節碼指令時,將會執行這個操作
6.assign(賦值)
作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
7.store(存儲)
作用於工作內存的變量,它把工作內存中的一個變量的值傳遞到主內存中,一般隨後的write操作使用
8.write(寫入)
作用於主內存的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中
8種基本操作必須滿足的規則:
1.不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行(即:read和load直接、store和write之間是可插入其他指令的)
2.不允許一個線程丟棄它的最近的assign操作,即:變量在工作內存中改變了之後,必須把該變化同步回主內存
3.不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中
4.一個新的變量只能從主內存中"誕生",不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,
即:對一個變量實施use和store操作之前,必須先執行過了assign河load操作
5.一個變量在同一時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個線程重複執行(可重入鎖),
多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖
6.如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值
7.如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量
8.對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)
volatile變量的特殊規則: --- 不能保證原子性
1.保證此變量對所有線程的可見性
2.禁止指令重排序優化
內存模型有哪些規則?
原子性、可見性、可排序性
10.Java反射與動態代理
Java反射,可以知道Java類的內部構造,就可以與它進行交互,包括創建新的對象和調用對象中的方法等。
這種交互方式與直接在源代碼中使用的效果是相同的,但是又額外提供了運行時刻的靈活性。使用反射的一個最大的弊端是性能比較差
每個類被加載進內存後,系統就會爲該類生成一個對應的 java.lang.Class對象,通過該Class對象即可訪問到JVM中的這個類
加載完類之後,在堆內存中會產生一個Class類型的對象(一個類只有一個Class對象),這個對象包含了完整的類結構信息,這個Class對象就像一面鏡子,可以看到類的結構
程序在運行狀態中,可以動態加載一個只有名稱的類(全路徑),對於任意一個已經加載的類,都能夠知道這個類的所有屬性和方法,
對於任意一個對象,都能調用它的任意一個方法和屬性
用法:
作用1:獲取類的內部結構
Java反射API可以獲取程序在運行時刻的內部結構,只需短短十幾行代碼,就可遍歷出一個Java類的內部結構,包括:構造方法、聲明的域和定義的方法等
java.lang.Class類的對象,可以通過其中的方法來獲取到該類中的構造方法、域和方法(getConstructor、getFiled、getMethod)
這三個方法還有相應的getDeclaredXXX版本,區別在於getDeclaredXXX版本的方法只會獲取該類自身所聲明的元素,而不會考慮繼承下來的。
Constructor、Field和Method這三個類分別表示類中的構造方法、域和方法。這些類中的方法可以獲取到所對應結構的元數據。
作用2:運行時刻,對一個Java對象進行操作
動態創建一個Java類的對象,獲取某個屬性的值和調用某個方法
在Java源碼中編寫的對類和對象的操作,都可以在運行時刻通過反射API來實現
特別說明:
clazz.setAccessible(true) --- 可以獲取到類中的private屬性和private方法
Class對象的獲取
1.對象的 getClass()方法
2.類.class屬性 --- 最安全,性能最好
3.Class.forName(String classFullName) 動態加載類 --- classFullName是類的全限定名(包.類名) --- 最常用
從Class中獲取信息
1.獲取信息
構造器、包含方法、包含屬性、包含的Annotation、實現的接口,所在包,類名,簡稱,修飾符等
獲取內容 方法簽名
構造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法 Method getMethod(String name, Class<?>... parameterTypes)
包含的屬性 Field getField(String name)
包含的Annotation <A extends Annotation> A getAnnotation(Class<A> annotationClass)
內部類 Class<?>[] getDeclaredClasses()
外部類 Class<?> getDeclaringClass()
所實現的接口 Class<?>[] getInterfaces()
修飾符 int getModifiers()
所在包 Package getPackage()
類名 String getName()
簡稱 String getSimpleName()
2.判斷信息
註解類型、是否是了指定註解、是否是數組、枚舉、接口等
判斷內容 方法簽名
註解類型? boolean isAnnotation()
使用了該Annotation修飾? boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名類? boolean isAnonymousClass()
數組? boolean isArray()
枚舉? boolean isEnum()
原始類型? boolean isPrimitive()
接口? boolean isInterface()
obj是否是該Class的實例 boolean isInstance(Object obj)
反射生成並操作對象
通過Method對象執行相應的方法
通過Constructor對象調用對應的構造器來創建實例
通過Field對象直接訪問和修改對象的成員變量值
反射創建對象
2種方式:
1.使用Class對象的newInstance()創建該Class對象對應類的實例(要求改Class對應類有默認構造器)
2.先使用Class對象獲取指定的Constructor對象,再調用Constructor對象的newInstance()方法創建該Class對象對應類的實例
--- 該方式,可選擇指定的構造器來創建實例
1.Spring根據配置文件信息(類的全限定名),使用反射創建對象 ---- 方式一,默認構造器創建實例
eg:
模擬Spring,實現一個對象池,對象池根據文件讀取key-value對,然後創建這些對象,並放入Map中,
對象池可以將id作爲key,將對象實例作爲value,可以通過id獲取對象實例
即:
ObjectPool pool = ObjectPool.init("config.json");
User user = (User) pool.getObject("id1");
System.out.println(user);
Bean bean = (Bean) pool.getObject("id2");
System.out.println(bean);
參考:
http://blog.csdn.net/zjf280441589/article/details/50453776
配置文件
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User"
},
{
"id": "id2",
"class": "com.fq.domain.Bean"
}
]
}
ObjectPool對象池
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
return Class.forName(className).newInstance();
}
private static JSONArray getObjects(String config) throws IOException {
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
// 根據指定的JSON配置文件來初始化對象池
public static ObjectPool init(String config) {
try {
JSONArray objects = getObjects(config);
ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
if (objects != null && objects.size() != 0) {
for (int i = 0; i < objects.size(); ++i) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
pool.putObject(id, getInstance(className));
}
}
return pool;
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public Object getObject(String id) {
return pool.get(id);
}
public void putObject(String id, Object object) {
pool.put(id, object);
}
public void clear() {
pool.clear();
}
}
實例類User和Bean
public class User {
private int id;
private String name;
private String password;
}
public class Bean {
private Boolean usefull;
private BigDecimal rate;
private String name;
}
反射調用方法
獲取到某個類對應的Class對象後,可通過該Class對象的getMethod()來獲取一個Method數組或Method對象,
每個Method對象對應一個方法,在獲取Method對象後,可通過調用invoke()方法調用該Method對象對應的方法
eg:
通過動態調用對象方法 + 配置文件,來給對象設置值 --- 根據屬性創建對象(調用setter方法,可設置屬性和依賴的對象)
1.json格式的配置文件,用來定義對象、屬性值及其依賴關係 --- config.json
注意:
其中fields代表該Bean所包含的屬性, name爲屬性名稱, value爲屬性值(屬性類型爲JSON支持的類型),
ref代表引用一個對象(也就是屬性類型爲Object,但是一定要引用一個已經存在了的對象)
這裏定義了一個User對象,設置其3個屬性
定義一個ComplexBean對象,設置其name屬性爲complex-bean-name,並設置其引用的對象是id2
{
"objects": [
{
"id": "id1",
"class": "com.fq.domain.User",
"fields": [
{
"name": "id",
"value": 101
},
{
"name": "name",
"value": "feiqing"
},
{
"name": "password",
"value": "ICy5YqxZB1uWSwcVLSNLcA=="
}
]
},
{
"id": "id2",
"class": "com.fq.domain.Bean",
"fields": [
{
"name": "usefull",
"value": true
},
{
"name": "rate",
"value": 3.14
},
{
"name": "name",
"value": "bean-name"
}
]
},
{
"id": "id3",
"class": "com.fq.domain.ComplexBean",
"fields": [
{
"name": "name",
"value": "complex-bean-name"
},
{
"name": "refBean",
"ref": "id2"
}
]
}
]
}
2.定義對象池,用於創建對象,並調用其方法給對象賦值
參考:反射.ObjectPool 的實現
package com.jay.advanced.java.反射;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 模擬Spring,創建一個對象池
* Created by hetiewei on 2017/2/17.
*/
public class ObjectPool {
private Map<String, Object> pool;
private ObjectPool(Map<String, Object> pool) {
this.pool = pool;
}
//從指定文件,讀取配置信息,返回解析後json數組
private static JSONArray getObjects(String config) throws IOException {
//獲取輸入流
Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
//讀取輸入流內容,變成json數組返回
return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
}
//根據類名,獲取類的對象實例
private static Object getIntance(String className, JSONArray fields) throws ClassNotFoundException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//配置的Class
Class<?> clazz = Class.forName(className);
//目標Class的實例對象
Object targetObject = clazz.newInstance();
//遍歷屬性,賦值給實例對象 --- 注意區分,直接賦值,還是賦值引用對象
if (fields != null && fields.size() != 0) {
for (int i = 0; i < fields.size(); i++) {
JSONObject field = fields.getJSONObject(i);
//需要設置的成員變量名
String filedName = field.getString("name");
//需要設置的成員變量的值
Object fieldValue;
//如果是8種基本類型或String類型,直接獲取value,得到屬性賦值
if (field.containsKey("value")) {
fieldValue = field.get("value");
} else if (field.containsKey("ref")) {
//如果是引用類型, 先獲得引用對象的id,然後根據id,從對象池中得到引用對象
String refBeanId = field.getString("ref");
fieldValue = OBJECTPOOL.getObject(refBeanId);
} else {
throw new RuntimeException("沒有value 或 ref 引用");
}
//構造setterXxx
String setterName = "set" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1);
//需要設置成員變量的setter方法
Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
//調用setter方法設置值
setterMethod.invoke(targetObject, fieldValue);
}
}
return targetObject;
}
private static ObjectPool OBJECTPOOL;
//創建一個對象池實例(保存多線程安全)
private static void initSingletonPool() {
if (OBJECTPOOL == null) {
synchronized (ObjectPool.class) {
if (OBJECTPOOL == null) {
OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
}
}
}
}
//指定根據的JSON配置文件來初始化對象池
public static ObjectPool init(String config) {
//初始化pool
initSingletonPool();
//解析Json配置文件
try {
JSONArray objects = getObjects(config);
for (int i = 0; i < objects.size(); i++) {
JSONObject object = objects.getJSONObject(i);
if (object == null || object.size() == 0) {
continue;
}
String id = object.getString("id");
String className = object.getString("class");
//初始化Bean,並放入對象池中
OBJECTPOOL.putObject(id, getIntance(className, object.getJSONArray("fields")));
}
return OBJECTPOOL;
} catch (IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException |
NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void putObject(String id, Object obj) {
pool.put(id, obj);
}
public Object getObject(String id) {
return pool.get(id);
}
public void clear() {
pool.clear();
}
}
3.客戶端使用對象池
//初始化對象池
ObjectPool pool = ObjectPool.init("config.json");
//從對象池中根據id獲取對象實例
User user = (User) pool.getObject("id1");
Bean bean = (Bean) pool.getObject("id2");
ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
4.ComplexBean類
public class ComplexBean {
private String name;
private Bean refBean;
}
反射訪問並操作成員變量
通過Class對象的getField()方法可獲取該類所包含的全部或指定的成員變量Field,
getDeclaredXxx方法可以獲取所有的成員變量,無論private/public;
Field提供了讀取和設置成員變量值的方法
getXxx(Object obj)
獲取obj對象的該成員變量的值,此處的Xxx對應8中基本類型,如果該成員變量的類型是引用類型, 則取消get後面的Xxx;
setXxx(Object obj, Xxx val)
將obj對象的該成員變量值設置成val值.此處的Xxx對應8種基本類型, 如果該成員類型是引用類型, 則取消set後面的Xxx;
eg:
通過反射,設置成員變量值
User user = new User();
//反射獲取對象的指定屬性
Field idField = User.class.getDeclaredFiled("id");
//設置該屬性即使爲private也可被訪問
idField.setAccessible(true);
//將對象的常用變量,設置爲指定值 --- 這裏將user對象的id屬性,設置爲30
idField.setInt(user, 30);
private void setAccessible(AccessibleObject object) {
object.setAccessible(true);
}
反射獲取註解信息
只需要獲取到Class Method Filed等這些實現了AnnotatedElement接口的類實例, 就可以獲取到我們想要的註解信息
eg:
獲取client()方法上的註解信息
Annotation[] annotations = this.getClass().getMethod("client").getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType().getName());
}
eg:
獲取某個註解中的元數據,需要強轉成所需的註解類型,然通過註解對象的抽象方法來訪問這些元數據
@Tag(name = "client")
public class Client {
@Test
public void client() throws NoSuchMethodException {
Annotation[] annotations = this.getClass().getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof Tag) {
Tag tag = (Tag) annotation;
System.out.println("name: " + tag.name());
System.out.println("description: " + tag.description());
}
}
}
}
反射獲取泛型信息
爲了通過反射操作泛型,Java新增了4種類型來代表不能歸一到Class了下,但又和原始類型同樣重要的類型
類型 含義
ParameterizedType 一種參數化類型, 比如Collection<String>
GenericArrayType 一種元素類型是參數化類型或者類型變量的數組類型
TypeVariable 各種類型變量的公共接口
WildcardType 一種通配符類型表達式, 如? ? extends Number ? super Integer
動態代理:
代理模式:
代理對象和被代理對象一般實現相同的接口,調用者與代理對象進行交互。代理的存在對於調用者來說是透明的,調用者看到的只是接口。
代理對象則可以封裝一些內部的處理邏輯,如訪問控制、遠程通信、日誌、緩存等。比如一個對象訪問代理就可以在普通的訪問機制之上添加緩存的支持,
傳統的代理模式的實現,需要在源代碼中添加一些附加的類。這些類一般是手寫或是通過工具來自動生成
動態代理:
JDK5引入了動態代理機制,允許開發人員在運行時刻動態的創建出代理類及其對象。
在運行時刻,可以動態創建出一個實現了多個接口的代理類,每個代理類的對象都會關聯一個表示內部處理邏輯的InvocationHandler接口的實現,
當使用者調用代理對象所代理的接口中的方法時,這個調用信息被傳遞個InvocationHandler的invoke()方法,
在invoke()方法參數中可以獲取到代理對象、方法對應的Method對象和調用的實際參數,invoke()方法的返回值被返回給使用者。
相當於對方法調用進行了攔截 --- 這是一個不需要依賴AspectJ等AOP框架的一種AOP實現方式
11.常用的內存調節參數
-Xms 初始堆大小,默認是物理內存1/64(<1G)
-Xmx 最大堆大小
-Xmn 新生代的內存空間大小(eden+ 2 survivor space),
整個堆大小=新生代大小 + 老生代大小 + 永久代大小
在保證堆大小不變的情況下,增大新生代後,將會減小老生代大小。此值對系統性能影響較大,Sun官方推薦新生代配置爲整個堆的3/8
-XX:SurvivorRation 新生代Eden區和Survivor區容量比,默認是8, 兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10
-Xss 每個線程的堆棧大小, 推薦 256K
-XX:PermSize 持久代(非堆內存)初始值, 默認是物理內存 1/64
-XX:MaxPermSize 持久代(非堆內存)最大值,默認物理內存1/4
-XX:+UseParallelGC 多核處理器,配置後提示GC效率
eg:
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=256M
-vmargs 說明後面是VM的參數,所以後面的其實都是JVM的參數了
-Xms128m JVM初始分配的堆內存
-Xmx512m JVM最大允許分配的堆內存,按需分配
-XX:PermSize=64M JVM初始分配的非堆內存
-XX:MaxPermSize=128M JVM最大允許分配的非堆內存,按需分配
說明:
Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。堆是在 Java 虛擬機啓動時創建的。”“在JVM中堆之外的內存稱爲非堆內存(Non-heap memory)”。
JVM主要管理兩種類型的內存:堆和非堆。簡單來說堆就是Java代碼可及的內存,是留給開發人員使用的;非堆就是JVM留給自己用的,
方法區,JVM內存處理貨優化所需的內存、每個類結構(如運行時常數池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中
內存分配方法:
1.堆上分配
2.棧上分配
12.JVM內存管理
程序計數器
方法區
堆
棧(JVM棧 + 本地方法棧)
說明:
GC主要發生在堆上,方法區也會發生GC, 棧與寄存器是線程私有的,不會GC
方法區:
存放內容:類信息、類的static屬性、方法、常量池
已經加載的類的信息(名稱、修飾符等)
類中的static變量
類中的field信息
類中定義爲final常量
類中的方法信息
運行時常量池:編譯器生成的各種字面量和符號引用(編譯期)存儲在class文件的常量池中,這部分內容會在類加載之後進入運行時常量池
使用實例: 反射
在程序中通過Class對象調用getName等方法獲取信息數據時,這些信息數據來自方法區
調節參數:
-XX:PermSize 指定方法區最小值,默認16M
-XX:MaxPermSize 指定方法區最大值,默認64M
所拋異常:
方法區使用內存超過最大值,拋出 OutOfMemoryError
GC操作:
對類的卸載
針對常量池的回收
總結:
企業開發中, -XX:PermSize==-XX:MaxPermSize,都設置爲256M
eg:
<jvm-arg>-XX:PermSize=256M</jvm-arg>
<jvm-arg>-XX:MaxPermSize=256M</jvm-arg>
類中的static變量會在方法區分配內存,但是類中的實例變量不會(類中的實例變量會隨着對象實例的創建一起分配在堆中,當然若是基本數據類型的話,會隨着對象的創建直接壓入操作數棧)
關於方法區的存放內容,可以這樣去想所有的通過Class對象可以反射獲取的都是從方法區獲取的(包括Class對象也是方法區的,Class是該類下所有其他信息的訪問入口)
堆:
存放內容:
對象實例(類中的實例變量會隨着對象實例的創建一起分配在堆中,當然若是基本數據類型的話,會隨着對象的創建直接壓入操作數棧)
數組值
使用實例:
new創建的對象都在這裏分配
調節參數:
-Xmx:最大堆內存,默認爲物理內存的1/4但小於1G
-Xms:最小堆內存,默認爲物理內存的1/64但小於1G
-XX:MinHeapFreeRatio,默認當空餘堆內存小於最小堆內存的40%時,堆內存增大到-Xmx
-XX:MaxHeapFreeRatio,當空餘堆內存大於最大堆內存的70%時,堆內存減小到-Xms
注意:
在實際使用中,-Xmx與-Xms配置成相等的,這樣,堆內存就不會頻繁的進行調整了!!!
所拋錯誤:
OutOfMemoryError:Java heap space
堆內存劃分:
新生代:
Eden + from + to from 與 to 大小相等
-Xmn:整個新生代大小
-XX:SurvivorRation : 調整Eden:from(to)的比例,默認是 8:1 即: eden:from:to = 8:1:1
老年代:
新建對象直接分配到老年代的2種情況:
大對象:-XX:PretenureSizeThreshold(單位:字節)參數來指定大對象的標準
大數組:數據中的元素沒有引用任何外部的對象
總結:
企業開發中 -Xmx==-Xms
eg:
<jvm-arg>-Xms2048m</jvm-arg>
<jvm-arg>-Xmx2048m</jvm-arg>
<jvm-arg>-Xmn512m</jvm-arg>
<jvm-arg>-XX:SurvivorRatio=8</jvm-arg>
<jvm-arg>-XX:MaxTenuringThreshold=15</jvm-arg>
可以看到,-Xms==-Xmx==2048m,年輕代大小-Xmn==512m,這樣,年老代大小就是2048-512==1536m,這個比率值得記住,
在企業開發中,年輕代:年老代==1:3,而此時,我們配置的-XX:MaxTenuringThreshold=15(這也是默認值),年輕代對象經過15次的複製後進入到年老代
年輕代分爲Eden和2個Survivor(from+to),默認 Eden:from:to==8:1:1
1.新產生的對象有效分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的對象會直接進入年老代)
2.當Eden區滿了或放不下時,其中存活的對象會複製到from區(注意:如果存活下來的對象from區放不下,則這些存活下來的對象全部進入老年代),之後Eden區的內存全部回收掉
注意:如果Eden區沒有滿,但來了一個小對象Eden區放不下,這時候Eden區存活對象複製到from區後,清空Eden區,之後剛纔的小對象再進入Eden區
3.之後產生的對象繼續分配在Eden區,當Eden區滿時,會把Eden區和from區存活下來的對象複製到to(同理,如果存活下來的對象to區都放不下,則這些存活下來的對象全部進入年老代),之後回收掉Eden區和from區的所有內存;
4)如上這樣,會有很多對象會被複制很多次(每複製一次,對象的年齡就+1),默認情況下,當對象被複制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了
5)當年老代滿了或者存放不下將要進入年老代的存活對象的時候,就會發生一次Full GC(這個是我們最需要減少的,因爲耗時很嚴重)
棧:
注意點:
每個線程都會分配一個棧,每個棧中有多個棧幀(每個方法對應一個棧幀) 每個方法在執行的同時都會創建一個棧幀,每個棧幀用於存儲當前方法的局部變量表、操作數棧等,
每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程,說的更明白一點,就是方法執行時創建棧幀,方法結束時釋放棧幀所佔內存
存放內容:
局部變量表: 8種基本數據類型、對象引用, 該空間在編譯期已經分配好,運行期不變
參數調節:
-Xss:設置棧大小,通常設置爲1m就好
eg:
<jvm-arg>-Xss1m</jvm-arg>
所拋異常:
StackOverFlowError 線程請求的棧深度大於虛擬機所允許的深度
eg:
沒有終止調節的遞歸(遞歸基於棧)
每個方法的棧深度在javac編譯之後就已經確定了
OutOfMemoryError: 虛擬機棧可以動態擴展,如果擴展時無法申請到足夠內存,則拋該異常
注意:
棧可以動態擴展,但棧中的局部變量表不可以
C寄存器(程序計數器)
概念: 當前線程所執行的字節碼的行號指示器,用於字節碼解釋器對字節碼指令的執行。
多線程:通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器(也就是一個核)只能執行一條線程中的指令,
爲了線程切換後能恢復到正確的執行位置,每條線程都要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲。
內存分配概念:
在類加載完成後,一個對象所需的內存大小就可以完全確定了,具體的情況查看對象的內存佈局。
爲對象分配空間,即把一塊兒確定大小(上述確定下來的對象內存大小)的內存從Java堆中劃分出來
Java的對象內存佈局 3 大塊
對象頭
存儲對象自身的運行時數據:Mark Word(在32位和64位JVM上長度分別爲32bit和64bit),包含信息如下:
對象hashCode
對象GC分代年齡
鎖狀態標識
線程持有的鎖
偏向鎖相關:偏向鎖、自旋鎖、輕量級鎖以及其他的一些鎖優化策略是JDK1.6加入的,這些優化使得Synchronized的性能與ReentrantLock的性能持平,
在Synchronized可以滿足要求的情況下,優先使用Synchronized,除非是使用一些ReentrantLock獨有的功能,例如指定時間等待等。
類型指針: 對象指向類元數據的指針
JVM通過這個指針來確定這個對象是哪個類的實例(根據對象確定其Class的指針)
實例數據
對象真正存儲的有效信息
對齊填充
JVM要求對象的大小必須是8的整數倍,若不夠,需要補位對齊
注意:
1.Mark Word是非固定的數據結構,以便在極小空間內存儲儘量多的信息
2.如果對象是一個數組,對象頭必須有一塊用來記錄數組長度的數據,JVM可以通過Java對象的元數據確定對象長度,但對於數組不行
3.基本數據類型和對象包裝類所在的內存大小(字節)
boolean 1字節
byte 1字節
short 2字節
char 2字節
int 4字節
float 4字節
long 8字節
double 8字節
引用類型 在32位和64位系統上長度分別爲4bit和8bit
內存分配的 2 種方式:
1.指針碰撞
適用場合:
堆內存規整(即:沒有內存碎片,有大塊完整內存)的情況下
原理:
用過的內存全部整合到一邊,沒用過的內存放在另一邊,中間有個分界值指針,只需要向着沒有用過的內存方向將該指針移動新創建的對象內存大小位置即可
GC收集器:
Serial、ParNew
2.空閒列表
適用場合:
堆內存不規整情況
原理:
JVM虛擬機會維護一個列表,該列表會記錄哪些內存塊是可用的,在分配時,找一塊足夠大的內存來劃分給new的對象實例,最後更新列表記錄
GC收集器:
CMS
注意:
1.2種內存分配方式,取決於Java堆內存是否規整
2.Java堆內存是否規整,取決於GC收集器的算法是 "標記-清除" 還是 "標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製算法內存也是規整的
創建一個真正對象的基本過程:5步
1.類加載機制檢查
JVM首先檢查一個new指定的參數是否能在常量池中定位到一個符號引用,並且檢查該符號應用代表的類是否已被加載、解析和初始化過
實際就是在檢查new的對象所屬的類是否已經執行過類加載機制,如果沒有,則先進行加載機制加載類
2.分配內存
把一塊確定大小的內存從堆中劃分出來
3.初始化零值
對象的實例字段不需要賦初始值也可以直接通過其默認零值
每種類型的對象都有不同的默認零值
4.設置對象頭
5.執行<init>
爲對象字符賦值(第3步只是初始化了零值,這裏會根據參數,給實例賦值)
13.JVM 內存回收GC
1.內存回收區域
堆:GC主要區域
方法區: 回收無用的類和廢氣的常量
注意:
棧和PC寄存器是線程私有,不會發生GC
2.判斷對象是否存活
1.引用計數法
原理:
給對象添加一個引用計數器,每當有一個地方使用它,計數器值+1,引用失效時,計數器值-1
缺點:
1.每次爲對象賦值時,都要進行計數器值得加減,消耗較大
2.對於循環依賴無法處理
2.可達性分析(跟蹤收集)
原理:
從根集合(GC Root)開始向下掃描,根集合中的節點可以到達的節點就是存活節點,根集合中的節點到達不了的節點就是要被回收的的節點
GC Root節點: 全局性的引用(常量和靜態屬性)和棧引用
1.Java棧中的對象引用
2.方法區中, 常量+靜態變量
3. 3 種引用類型
強引用: A a = new A();//a是常引用
強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題
軟引用: 內存不足時,是否弱引用所引用的對象,當內存足夠時,就是一個普通對象(強引用)
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a); //軟引用
弱引用: 弱引用對象只能存活到下一次垃圾會話之前,一旦發生垃圾回收,立刻被回收掉
4.GC回收算法
1.標記-清楚算法 --- 年老代
2.標記-整理算法(標記-壓縮) --- 年老代
3.複製算法 --- 年輕代
標記-清楚算法 --- 年老代
原理:
從根集合點掃描,標記處所有的存活對象,最後掃描整個內存空間,並清除沒有標記的對象(即:死亡對象)
使用場合:
存活對象較多的情況下比較高效
適用於年老代
缺點:
容易產生內存碎片,再來一個比較大的對象時(典型情況:該對象的大小大於空閒表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
注意:
在該情況下,內存不規整,對象的內存分配採用"空閒列表法"
標記-整理算法(標記-壓縮) --- 年老代
原理:
從根集合節點進行掃描,標記出所有的存活對象,最後掃描整個內存空間並清除沒有標記的對象(即死亡對象)(可以發現前邊這些就是標記-清除算法的原理),清除完之後,將所有的存活對象左移到一起。
適用場合:
用於年老代(即舊生代)
缺點:
需要移動對象,若對象非常多而且標記回收後的內存非常不完整,可能移動這個動作也會耗費一定時間
掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
優點:
不會產生內存碎片
注意:
在該情況下,內存規整,對象的內存分配採用"指針碰撞法"
複製算法 --- 年輕代
原理:
從根集合節點進行掃描,標記出所有的存活對象,並將這些存活的對象複製到一塊兒新的內存(圖中下邊的那一塊兒內存)上去,之後將原來的那一塊兒內存(圖中上邊的那一塊兒內存)全部回收掉
適用場合:
存活對象較少的情況下比較高效
掃描了整個空間一次(標記存活對象並複製移動)
適用於年輕代(即新生代):基本上98%的對象是"朝生夕死"的,存活下來的會很少
缺點:
需要一塊兒空的內存空間
需要複製移動對象
注意:
在該情況下,內存規整,對象的內存分配採用"指針碰撞法",見《第二章 JVM內存分配》
以空間換時間:通過一塊兒空內存的使用,減少了一次掃描
14.關於Set --- HashSet、TreeSet、LinkedHashSet -- 都是去重的,都可用Iterator或foreach進行遍歷
參考:http://blog.csdn.net/speedme/article/details/22661671
HashSet --- 去重,無序, add()時會調用hashcode和equals,所以存儲在HashSet中的對象需要重寫這兩個方法,非同步的,元素只能放一個null
即:
HashSet:數據結構式哈希表,線程非同步。保證元素唯一性的原理,判斷hashCode是否相同,如果相同,判斷元素的equals方法
TreeSet --- 去重,可按照某種順序排序, add()會將對象轉爲Comparable,然後調用compareTo()方法,所以存儲在TreeSet中的對象必須實現Comparable,重寫compareTo()方法
底層數據結構是 二叉樹,保證元素唯一性的依據
支持2種排序:自然排序、定製排序
TreeSet判斷兩個對象不相等的方式是兩個對象通過equals方法返回false,或者通過CompareTo方法比較沒有返回0
自然排序
自然排序使用要排序元素的CompareTo(Object obj)方法來比較元素之間大小關係,然後將元素按照升序排列。
Java提供了一個Comparable接口,該接口裏定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現了該接口的對象就可以比較大小。
obj1.compareTo(obj2)方法如果返回0,則說明被比較的兩個對象相等,如果返回一個正數,則表明obj1大於obj2,如果是 負數,則表明obj1小於obj2。
如果我們將兩個對象的equals方法總是返回true,則這兩個對象的compareTo方法返回應該返回0
定製排序
自然排序是根據集合元素的大小,以升序排列,如果要定製排序,應該使用Comparator接口,實現 int compare(T o1,T o2)方法
2種排序方式比較:
方式一:讓集合中的元素自身具有比較性,這就讓加入到TreeSet集合中的對象必須實現comparable接口重寫compareTo(Object obj)方法
這種方式也成爲元素的自然排序或默認排序。(但是如果排序的元素不是本人寫的,別人寫的沒有實現comparable接口時想排序使用第二種方式)
方式二:讓集合容器具有比較性,自定義一個比較器實現comparator接口,重寫compare(Object o1,Object o2)方法,在初始化TreeSet容器對象將這個
自定義的比較器作參數傳給容器的構造函數,使得集合容器具有比較性,使用這種方式的優先級高於方式一,
LinkedHashSet --- HashSet的子類,去重,並保留存儲順序
HashSet 工作原理: 每次存儲對象時,調用對象的hashCode(),計算一個hash值,在集合中查找是否包含hash值相同的元素
如果沒有hash值相同的元素,根據hashCode值,來決定該對象的存儲位置,直接存入,
如果有hash值相同的元素,逐個使用equals()方法比較,
比較結果全爲false就存入.
如果比較結果有true則不存.
如何將自定義類對象存入HashSet進行去重複
* 類中必須重寫hashCode()方法和equals()方法
* equals()方法中比較所有屬性
* hashCode()方法要保證屬性相同的對象返回值相同, 屬性不同的對象儘量不同
TreeSet 工作原理:存儲對象時,add()內部會自動調用compareTo()方法進行比較,根據比較結果使用二叉樹形式進行存儲 --- 二叉樹實現存儲
參考:http://blog.csdn.net/jinhuoxingkong/article/details/51191106
TreeSet使用二叉樹原理,對新add()的對象安裝指定的順序排序(升序、降序),每增加一個對象都會進行排序,將對象插入二叉樹指定的位置
Integer和String都是按默認的TreeSet排序,自定義的對象,必須實現Comparable接口,重寫compareTo(),指定比較規則
在重寫compare()方法時,要返回相應的值才能使TreeSet按照一定規則來排序,升序是:比較此對象與指定對象的順序。如果該對象小於、等於或大於指定對象,則分別返回負整數、零或正整數。
如果想把自定義類的對象存入TreeSet進行排序, 那麼必須實現Comparable接口
* 在類上implement Comparable
* 重寫compareTo()方法
* 在方法內定義比較算法, 根據大小關係, 返回正數負數或零
TreeSet實現排序的2種比較方式:
1.自定義類類 實現 Comparable接口,重寫其 compareTo()方法 ---- 類排序
2.給TreeSet定義一個實現Comparator接口的比較器,重寫其 compare()方法 ---- 比較器排序
* a.自然順序(Comparable)
* TreeSet類的add()方法中會把存入的對象提升爲Comparable類型
* 調用對象的compareTo()方法和集合中的對象比較
* 根據compareTo()方法返回的結果進行存儲
* b.比較器順序(Comparator)
* 創建TreeSet的時候可以制定 一個Comparator
* 如果傳入了Comparator的子類對象, 那麼TreeSet就會按照比較器中的順序排序
* add()方法內部會自動調用Comparator接口中compare()方法排序
* 調用的對象是compare方法的第一個參數,集合中的對象是compare方法的第二個參數
* c.兩種方式的區別
* TreeSet構造函數什麼都不傳, 默認按照類中Comparable的順序(沒有就報錯ClassCastException)
* TreeSet如果傳入Comparator, 就優先按照Comparator
15.關於Map --- HashMap、TreeMap
原理:http://blog.csdn.net/chenssy/article/details/26668941
HashMap
按照key的hashCode實現,無序
TreeMap
基於紅黑樹實現,映射根據其key的自然順序進行排序,或根據創建映射時提供的Comparator進行排序,具體取決於使用的構造方法
TreeMap只能依據key來排序,不能根據value排序
如果想對value排序,可以把TreeMap的EntrySet轉換成list,然後使用Collections.sort排序 -- 參考:http://blog.csdn.net/liuxiao723846/article/details/50454622
http://blog.csdn.net/xiaoyu714543065/article/details/38519817
eg:value是String或引用類型的值,按照指定規則對value進行排序
public static Map sortTreeMapByValue(Map map){
List<Map.Entry> list = new ArrayList<>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry>() {
//升序排
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getValue().toString().compareTo(o2.getValue().toString());
}
});
for (Map.Entry<String, String> e: list) {
System.out.println(e.getKey()+":"+e.getValue());
}
return map;
}
16.關於ThreadLocal
當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本
3個重要方法:
void set(T value)、T get()以及T initialValue()
使用場景:
多線程中,每個線程需要獨享這個變量,且每個線程用的變量最初都是一樣的,可以通過ThreadLocal處理該變量
原理:
ThreadLocal如何爲每個線程維護變量的副本?
ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中key爲線程對象,value爲線程的變量副本
eg:
public class JavaTest {
// 創建一個Integer型的線程本地變量, 並通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];// 創建5個線程
for (int j = 0; j < 5; j++) {
threads[j] = new Thread(new Runnable() {
@Override
public void run() {
// 獲取當前線程的本地變量,然後累加5次
int num = local.get();// 返回當前線程的線程本地變量值,若對應的thread不存在,則會調用initialValue初始化
for (int i = 0; i < 5; i++) {
num++;
}
// 重新設置累加後的本地變量
local.set(num);
System.out.println(Thread.currentThread().getName() + " : "
+ local.get());
}
}, "Thread-" + j);
}
for (Thread thread : threads) {// 啓動線程
thread.start();
}
}
}
運行後結果:
Thread-0 : 5
Thread-4 : 5
Thread-2 : 5
Thread-1 : 5
Thread-3 : 5
我們看到,每個線程累加後的結果都是5,各個線程處理自己的本地變量值,線程之間互不影響
17.自定義註解
元註解
Java提供了4種元註解,專門負責註解其他註解使用
@Retention 表示需要在什麼級別保存該註釋信息(生命週期)
可選參數:
RetentionPolicy.SOURCE: 停留在java源文件,編譯器被丟掉
RetentionPolicy.CLASS:停留在class文件中,但會被VM丟棄(默認)
RetentionPolicy.RUNTIME:內存中的字節碼,VM將在運行時也保留註解,因此可以通過反射機制讀取註解的信息 --- 最常用
@Target 表示該註解用於什麼地方
可選參數:
ElementType.CONSTRUCTOR: 構造器聲明
ElementType.FIELD: 成員變量、對象、屬性(包括enum實例)
ElementType.LOCAL_VARIABLE: 局部變量聲明
ElementType.METHOD: 方法聲明
ElementType.PACKAGE: 包聲明
ElementType.PARAMETER: 參數聲明
ElementType.TYPE: 類、接口(包括註解類型)或enum聲明
@Documented 將註解包含在JavaDoc中
@Inheried 運行子類型繼承父類中的註解
自定義註解:
eg:
自定義註解 --- MyAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定義註解
* 作用於方法和類
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface MyAnnotation {
//爲註解添加屬性
String color();
String value() default "我是xxx";//爲註解屬性提供默認值
int[] array() default {1,2,3};
Gender gender() default Gender.MAN; // 添加一個枚舉
// 添加枚舉屬性
MetaAnnotation metaAnnotation() default @MetaAnnotation(birthday = "我的出身日期爲1988-2-18");
}
定義一個枚舉類 --- Gender
public enum Gender{
MAN{
public String getName(){
return "男";
}
},
WOMEN{
public String getName(){
return "女";
}
};
}
定義註解類 --- MetaAnnotation
public @interface MetaAnnotation{
String birthday();
}
解析註解:
/**
* 調用註解並賦值
* Created by hetiewei on 2016/10/12.
*/
@MyAnnotation(metaAnnotation = @MetaAnnotation(birthday = "我的出身日期爲1991-2-27"),
color = "red", array = {23, 26 })
public class Test {
public static void main(String args[]){
//檢查類Test中是否包含@MyAnnotation註解
if (Test.class.isAnnotationPresent(MyAnnotation.class)){
//若存在則獲取註解,並解析
MyAnnotation annotation = Test.class.getAnnotation(MyAnnotation.class);
System.out.println(annotation);
//解析註解中的內容
//1.獲取註解屬性
System.out.println(annotation.color());
System.out.println(annotation.value());
//2.獲取屬性數組
int[] arrs = annotation.array();
System.out.println(arrs.toString());
//3.獲取枚舉
Gender gender = annotation.gender();
System.out.println("性別:"+gender);
//4.獲取註解屬性
MetaAnnotation meta = annotation.metaAnnotation();
System.out.println(meta.birthday());
}
}
}
18.關於枚舉 枚舉類是一種特殊的類,它一樣有自己的Field,方法,可以實現一個或者多個接口,也可以定義自己的構造器
枚舉與普通類有如下簡單區別:
(1). 枚舉類可以實現一個或者多個接口,使用enum定義的枚舉類默認繼承了java.lang.Enum類,而不是繼承Object類。其中java.lang.Enum類實現了java.lang.Serializable和java.lang.Comparable接口。
(2). 使用enum定義,非抽象的枚舉類默認會使用final修飾,因此枚舉類不能派生子類。
(3). 枚舉類的構造器只能使用private訪問控制符,如果省略了構造器的訪問控制符,則默認使用private修飾;如果強制指定訪問控制符,則只能指定private修飾符。
(4). 枚舉類的所有實例必須在枚舉類的第一行顯示列出,否則這個枚舉類永遠不能產生實例。列出這些實例時,系統會自動添加public static final修飾,無須程序員顯示添加。
所有的枚舉類都提供了一個values方法,該方法可以很方便的遍歷所有的枚舉值
(5) 枚舉常用方法
name() ,toString() --- 返回此枚舉實例名稱,優先使用 toString()
ordinal() --- 返回枚舉實例的索引位置,第一個枚舉值索引爲0
public static T valueOf(Class enumType, String name)
--- 返回指定枚舉類中指定名稱的枚舉值,名稱必須與在該枚舉類中聲明枚舉值時所用的標識符完全匹配,不允許使用額外的空白字符
eg:
public enum SeasonEnum{
//列出4個枚舉實例
SPRING,SUMMER,FALL,WINTER;
}
解析:
1.枚舉值之間以英文逗號(,)隔開,枚舉值列舉結束後以英文分號作爲結束
2.枚舉值代表了該枚舉類的所有可能實例
3.使用枚舉值 EnumClass.variable eg: SeasonEnum.SPRING
4.枚舉可作爲switch條件表達式
5.獲取所有枚舉值 EnumClass[] enums = EnumClass.values();
枚舉類的屬性Field、方法和構造器
1.枚舉類也是一種類,只是它是一種比較特殊的類,因此它一樣可以定義Field,方法
19.幾種集合類解析
1.HashMap 底層 數組+鏈表,計算出hash值後,得到元素在數組中存放的位置索引,
若不同元素hash值相同,即:有相同的存放位置,則在相同位置建立鏈表,採用頭插入法依次保存元素
工作原理:
數組+鏈表 以 Entry[]數組實現的哈希桶數組,用Key的哈希值取模數組的大小得到數組的下標
如果多個key佔有同一個下標(碰撞),則使用鏈表將相同的key串起來
通過hash方法,通過put和get存儲和獲取對象,存儲對象時,將K/V傳給put()方法時,它調用hashCode()計算hash值得到bucket位置,
進一步存儲,HashMap會根據當前bucket的佔用情況自動調整容量(超過加載因子,容量擴展爲2倍)。
獲取對象時,通過K,調用hashCode()得到bucket位置,並進一步調用equals()方法確定鍵值對。
如果發生碰撞時,HashMap通過鏈表將產生碰撞的元素組織起來,在Java8中,如果一個bucket中碰撞的元素超過某個限制(,默認8個),
則使用紅黑樹來替換鏈表,從而提高速度
2.HashSet 底層 是HashMap實現, 優點:利用哈希表提供查詢效率, 缺點:元素不能重複
由於HashSet不允許元素重複,故需要判斷元素是否相同,
用hash表判斷元素是否相同的方法,即需要hashCode和equals兩個方法,對於hashSet,先通過hashCode判斷元素在哈希表中的位置是否相同,在通過equals方法判斷元素內容是否相同
哈希表如何判斷元素是否相同?
1> 哈希值是否相同 :判斷的其實是對象的hashCode()方法是否相同(看對象是否在哈希表的同一個位置)
2>內容是否相同:用equals方法判斷對象是否相同。
規則:若hash值不同,不必判斷對象內容,返回false;若hash值相同,有必要判斷對象內容,若在相同,返回true,否則false。
3.TreeSet 使用元素的自然順序,對象集合中元素進行排序,添加的元素需要自己實現Comparable接口,以便默認排序時調用其CompareTo()進行比較
2中自定義排序方式
1.元素的類,實現Comparable接口,實現compareTo()
2.給ThreeSet傳遞一個實現Comparator接口的參數對象
20.關於線程池
參考:
http://www.codeceo.com/article/java-thread-pool-deep-learn.html
http://www.codeceo.com/article/java-threadpoolexecutor.html
1.核心類:
ThreadPoolExecutor extends AbstractExecutorService implement ExecutorService 提供4個構造器
構造器參數:
corePoolSize: 核心池大小
默認情況,創建線程池後,池中線程數爲0,當有任務來時,創建一個線程去執行任務,當線程池中線程數目達到corePoolSize後,會把任務放入緩存隊列、
maxPoolSize: 線程池最大線程數
keepAliveTime:表示線程沒有任務執行時最多保持多久會終止
默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize
unit: 參數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
workQueue: 阻塞隊列,用來存儲等待執行的任務
可選的阻塞隊列:
ArrayBlockingQueue
LinkedBlockingQueue --- 默認,用的最多
SynchronousQueue
PriorityBlockingQueue
threadFactory: 線程工廠,主要用來創建線程
handler: 表示當拒絕處理任務時的策略,
4種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
2.線程池原理:
1.線程池狀態:
ThreadPoolExecutor中定義了一個Volatile變量runState表示當前線程池的狀態,使用volatile來保證線程之間的可見性
線程池的4種狀態:
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;
static final int STOP = 2;
static final int TERMINATED = 3;
解析:
1.創建線程池後,初始時,線程池處於RUNNING狀態
2.如果調用了shutdown()方法,則線程池處於SHUTDOWN狀態,此時線程池不能接受新任務,但會等待所有任務執行完畢
3.如果調用了shutdownNow()方法,線程池處於STOP狀態,此時線程池不能接受新任務,並且會嘗試終止正在執行的任務
4.當線程池處於SHUTDOWN或STOP狀態,並且所有工作線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態
2.任務的執行:
線程任務保存在BlockingQueue中,通過execute(Runnable )來調用執行,
21.Java的類加載器
類加載器 --- 一個用來加載類文件的類
Java源碼通過javac編譯成類文件,然後JVM來執行類文件中的字節碼,類加載器負責加載文件系統、網絡或其他來源的類文件
JVM中類加載器的樹狀層次結構
Bootstrap ClassLoader 引導類加載器, 加載Java的核心庫(jre/lib/rt.jar),用C++代碼實現,不繼承子java.lang.ClassLoader
Extension ClassLoader 擴展類加載器, 加載Java的擴展庫(jre/ext/*.jar), Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類
System ClassLoader 系統類加載器, 根據Java應用的類路徑(classpath)來加載Java類,
Java應用的類都是由它來完成加載的,可通過ClassLoader.getSystemClassLoader()獲取系統類加載器
自定義類加載器 通過繼承java.lang.ClassLoader類,實現自己的類加載器
Java類加載器的3個機制:
委託機制:
將加載器的請求交給父加載器,如果父類加載器找不到或不能加載這個類,則當前類加載器再加載它
可見性機制:
子類的加載器可以看見所有父類加載器加載的類,而父類加載器看不到子類加載器加載的類
單一性機制:
類僅被加載一次, 由 委託機制 確保子類加載器不會再次加載父類加載器加載過的類
類加載過程 3個步驟:
裝載:
鏈接:(驗證、準備、解析)
初始化:
裝載:
查找並加載類的二進制數據
鏈接:
驗證:確保被加載類信息符合JVM規範、沒有安全方面問題
準備:爲類的靜態變量分配內存,並將其初始化爲默認值
解析:把虛擬機常量池中的符號引用轉換爲直接引用
初始化:
爲類的靜態變量賦予正確的初始值
說明:
1.JVM會爲每個加載的類維護一個常量池
2.類的初始化步驟:
1.如果這個類沒被加載和鏈接,則先進行加載和鏈接
2.如果這個類存在父類,如果類未初始化,則先初始化其父類
3.如果類中存在static塊,一次執行這些初始化語句
java.lang.ClassLoader類
根據一個指定的類名稱,找到或生成其對於的字節代碼,然後從這些字節碼中定義出一個Java類,即:java.lang.Class類的一個實例
ClassLoader中的方法:
getParent() 返回該類加載器的父類加載器
loadClass(name) 加載名稱爲name的類,返回結果是java.lang.Class的實例
findClass(name) 查找名稱爲name的類,返回結果是java.lang.Class的實例
findLoadedClass(name) 查找名稱爲name的已被加載過的類,返回結果是java.lang.Class的實例
resolveClass(Class<?> c) 鏈接指定的Java類
Java中的類加載過程
加載(可自定義類加載器) 連接 ( 驗證 準備 解析 ) 初始化
加載:
獲取二進制字節流 --> 將字節流靜態存儲結構轉換爲方法區的運行時數據結構 --> 在堆中生成Class對象
連接:
驗證:
文件格式驗證: 1.參照Class文件格式規範驗證
2.此階段基於字節流經過此驗證後,字節流纔會進入方法區,後面的驗證都依賴與方法區的驗證
元數據驗證: Java語法驗證,eg:該類是否繼承了不該繼承的類
字節碼驗證: 運行時安全性檢查
符號引用驗證: 確保類中引用的類,字段,方法都是可訪問的
準備:
設置類變量初始值 --- static類變量 初始值 , 注意:final比較特別!!!
1.設置類變量 --- static變量
2.設置變量初始值 (注意:非代碼中定義的值,8種基本數據類型都有初始值 eg: static int a = 10, 準備階段會把a初始值賦值爲0,初始化時,再賦值爲10 )
3.對於final的值,設爲代碼中的值(eg:final static int a = 10 , 準備階段直接把 a 賦值爲10)
解析:
將符號引用轉換爲直接引用
1.符號引用: 用符號來表示所引用的目標
2.直接引用: 一個指向內存中目標對象地址的句柄
初始化:
1.根據代碼實現初始化類變量及其他資源 (準備階段,static類變量還是初始值,這裏賦值爲代碼中指定的值)
2.執行子類初始化方法時,先執行父類的初始化方法(static變量賦值,static代碼段執行,先父類後子類)
22.Java反射 增加 裝飾模式 的適用性
裝飾模式:在不必改變原類文件和使用繼承的情況下,動態地擴展一個對象的功能,它是通過創建一個包裝對象來包裹真實的對象,比生產子類更加靈活,
使用Java的動態代理實現裝飾模式,會具有更強的靈活性和適用性
裝飾模式有什麼特點呢?
1、裝飾對象和真實對象有相同的接口。這樣調用者就能以和真實對象相同的方式和裝飾對象交互。
2、裝飾對象包含一個真實對象的引用(即上面例子中的Ability接口)。
3、裝飾對象接受所有來調用者的請求,並把這些請求轉發給真實的對象。
4、裝飾對象可以在調用者的方法以前或以後增加一些附加功能。這樣就確保了在運行時,不用修改給定對象的結構就可以在外部增加附加的功能。
什麼樣的地方使用裝飾模式呢?
1、需要動態擴展一個類的功能,或給一個類添加附加職責。
2、需要動態的給一個對象添加功能,這些功能可以再動態的撤銷。
3、需要增加由一些基本功能的排列組合而產生的非常大量的功能,從而使繼承關係變的不現實。
4、 當不能採用生成子類的方法進行擴充時。一種情況是,可能有大量獨立的擴展,爲支持每一種組合將產生大量的子類,使得子類數目呈爆炸性增長。另一種情況可能是因爲類定義被隱藏,或類定義不能用於生成子類。
23.JVM的運行時棧幀 --- JVM運行程序的過程!!! --- 方法的運行過程 !!!
1.每個方法的執行,在JVM中都是對應的棧幀在JVM棧中的入棧到出棧的過程!!!
2.每個在JVM中運行的程序,都是由許多的幀切換產生的結果
參考:
http://blog.csdn.net/column/details/14217.html
棧幀: --- 線程安全!!!每個線程的棧幀相互獨立 ---> 局部變量在多線程環境下線程安全的原因!!!
存放方法的局部變量表、操作數棧、動態鏈接,方法返回值和一些額外的附加信息
當前棧:
一個方法的調用鏈可能很長,當調用一個方法時,可能會有很多方法處於執行狀態,但對於執行引擎,置於JVM棧頂的棧幀纔是有效的,這個棧幀稱爲 當前棧
當前棧所關聯的方法稱爲當前方法,執行引擎的所有指令都是針對當前棧幀進行操作的
局部變量表:
內容: 存放方法的局部變量
eg:方法參數,方法內定義的局部變量,對象引用,returnAddress類型
在Java程序被編譯爲class文件時,這個表的容量最大值已經確定
訪問:
虛擬機利用索引編號的遞增來對局部變量表中定義的變量進行一次訪問(從0開始),而對於實例方法(非static方法),其局部變量表的第0個索引是this,
這是可以在實例方法中使用this.name ......的原因
動態連接:
參考:http://blog.csdn.net/eric_sunah/article/details/8014865
方法的調用過程:
在虛擬機運行時,運行時常量池會保存大量符號引用,這些符號引用可以看做每個方法的間接引用,
如果代表棧幀A的方法要調用代表棧幀B的方法,則這個虛擬機的方法調用指令就會以B方法的符號引用作爲參數,
但因爲符號引用並不是直接指向代表B方法的內存位置,所有在調用之前還必須要將符號引用轉換爲直接引用,然後通過直接引用訪問到真正的方法!
注意:
靜態解析:
如果符號引用在類加載階段或第一次使用時轉化爲直接引用,則這種轉換成爲靜態解析
動態連接:
如果在運行期間轉換爲直接引用,這種轉換稱爲動態連接
棧幀A 常量池 棧幀B
局部變量表 局部變量表
A方法的符號引用
操作數棧 操作數棧
B方法的符號引用
動態連接 動態連接
字符串常量等
返回地址 返回地址
方法返回地址
1.正常退出:根據方法定義決定是否要返回值給上層調用者
2.異常退出:不會傳遞返回值給上層調用者
注意:
1. 不管那種方式結束,在退出當前方法時,都會跳轉到當前方法被調用的位置!!!
如果正常退出,則調用者的PC計數器的值可作爲返回地址,
如果異常退出,則需要通過異常處理表來確定
2. 方法的一次調用對應着棧幀在虛擬機中的一次入棧出棧操作!!!
方法退出時做的事情:
恢復上層方法的局部變量表以及操作數棧,如果有返回值,就把返回值壓入到調用者棧幀的操作數棧中,
還會把PC計數器的值調整爲方法調用入口的下一條指令
24.JVM的內存溢出分析和參數調優
1.JVM調優 http://blog.csdn.net/eric_sunah/article/details/7862114
1.JVM內存參數調優
-Xms 設置初始化堆的內存
-Xmx 設置堆最大使用內存
-Xss 設置每個線程的棧大小
-Xmn 設置年輕代大小
eg:
java -Xmx4096m -Xms4096m -Xmn2g -Xss128k
或
java -Xmx4g -Xms4g -Xmn2g -Xss128k
設置JVM堆最大可以內存爲4096M,初始內存爲4096M(-Xms設置與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配內存)
設置JVM年輕代大小爲2G JVM堆內存 = 年輕代大小 + 年老代大小 + 持久代大小
持久代一般固定大小爲64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,推薦配置爲整個堆的3/8
設置每個線程的棧大小爲128k
JDK5+ 線程棧默認1M, 相同物理內存下,減小該值能生成更多線程,但操作系統對一個進程內的線程數有限制,最好不超過5000
如果方法遞歸太深,則可能耗盡線程棧,報出 StackOverflow !!! 線程棧內存溢出 <--- 方法調用太深
eg:設置堆內存中的內存分配比例
java -Xmx4g -Xms4g -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=64m -XX:MaxTenuringThreshold=0
-Xmn2g 設置年輕代大小爲2G
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設置爲4,則年輕代與年老代所佔比值爲1:4,年輕代佔整個堆的1/5
-XX:SurvivorRatio=4 設置年輕代中from和to區的比例,Eden區(from)與Survivor區(to)的大小比值爲4:1:1,即 一個 Survivor 佔 年輕代的1/6
特別注意:
上面的比值 4 <=等價=> 1:4
-XX:MaxPermSize=64m 設置持久代大小爲64M
-XX:MaxTenuringThreshold=0:設置垃圾最大年齡
小結:
1,整個堆包括年輕代,老年代和持久代。其中年輕代又包括一個Eden區和兩個Survivor區。
2,年輕代:
-XX:NewSize (for 1.3/1.4) ,
-XX:MaxNewSize (for 1.3/1.4) ,
-Xmn
2,持久代:
-XX:PermSize
-XX:MaxPermSize
3,年輕代和老年代的比例:
-XX:NewRatio(年輕代和老年代的比值,年輕代多,除去持久代)
當設置了-XX:+UseConcMarkSweepGC後,會使-XX:NewRatio=4失效,此時需要使用-Xmn設置年輕代大小
4,Eden與Survivor的比例
-XX:SurvivorRatio(Eden區與兩個Survivor區的比值,Eden區多)
2.GC參數設置
並行收集器 --- 吞吐量優先,適合後臺處理
eg:
-XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
解析:
-XX:+UseParallelGC:選擇垃圾收集器爲並行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用併發收集,而年老代仍舊使用串行收集
-XX:ParallelGCThreads=20:配置並行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等
-XX:+UseParallelOldGC:配置年老代垃圾收集方式爲並行收集,JDK6.0支持對年老代並行收集
併發收集器 --- 響應時間優先,保證系統響應時間,減少垃圾收集時的停頓時間,適合應用服務器和典型領域等
eg:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:設置年老代爲併發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。所以,此時年輕代大小最好用-Xmn設置。
-XX:+UseParNewGC:設置年輕代爲並行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設置,所以無需再設置此值。
3.常見配置彙總
堆設置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:設置年輕代大小
-XX:NewRatio=n:設置年輕代和年老代的比值。如:爲3,表示年輕代與年老代比值爲1:3,年輕代佔整個年輕代年老代和的1/4
-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5
-XX:MaxPermSize=n:設置持久代大小
收集器設置
-XX:+UseSerialGC:設置串行收集器
-XX:+UseParallelGC:設置並行收集器
-XX:+UseParalledlOldGC:設置並行年老代收集器
-XX:+UseConcMarkSweepGC:設置併發收集器
垃圾回收統計信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
並行收集器設置
-XX:ParallelGCThreads=n:設置並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n:設置並行收集最大暫停時間
-XX:GCTimeRatio=n:設置垃圾回收時間佔程序運行時間的百分比。公式爲1/(1+n)
併發收集器設置
-XX:+CMSIncrementalMode:設置爲增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n:設置併發收集器年輕代收集方式爲並行收集時,使用的CPU數。並行收集線程數。
4.調優總結
年輕代大小選擇
響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。
吞吐量優先的應用:儘可能的設置大,可能到達Gbit的程度。因爲對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
年老代大小選擇
響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設置,一般要考慮併發會話率和會話持續時間等一些參數。
如果堆設置小了,可以會造成內存碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;
如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下數據獲得:
併發垃圾收集信息
持久代併發收集次數
傳統GC信息
花在年輕代和年老代回收上的時間比例
減少年輕代和年老代花費的時間,一般會提高應用的效率
吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期對象,
減少中期的對象,而年老代盡存放長期存活對象。
較小堆引起的碎片問題
因爲年老代的併發收集器使用標記、清除算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的對象。
但是,當堆空間較小時,運行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,
然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:
-XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啓對年老代的壓縮。
-XX:CMSFullGCsBeforeCompaction=0:上面配置開啓的情況下,這裏設置多少次Full GC後,對年老代進行壓縮
2.JVM內存溢出分析 --- OutOfMemoryError 不屬於Exception,繼承自Throwable
1.堆內存溢出 --- 不斷創建對象,並且不釋放,導致GC無法回收,堆內存移除 Java heap space
eg: 製造堆內存溢出的程序
1.降低修改虛擬機堆內存大小 -Xms20m -Xmx20m
2.不斷創建強引用對象,方式GC回收
public static void main(String[] args) {
headOutOfMemory();
}
/*
* -verbose:gc -XX:+PrintGCDetails -verbose:gc
* -XX:+HeapDumpOnOutOfMemoryError
*
* -Xms20m -Xms20m
*
*/
static void headOutOfMemory() {
long count = 0;
try {
List<Object> objects = new ArrayList<Object>();
while (true) {
count++;
objects.add(new Object());
}
} catch (Throwable ex) {
System.out.println(count);
ex.printStackTrace();
}
}
}
異常信息:
java.lang.OutOfMemoryError: Java heap space
2.棧內存溢出 --- 棧主要存放棧幀(局部變量表(基本數據類型,對象引用,returnAddress類型),操作數棧,動態鏈接,方法出口信息),
1.StackOverflowError ---- 當線程棧的空間大於虛擬機所允許時,拋出 StackOverflowError
線程棧,因遞歸或方法調用太深,導致超過線程棧設定時,拋 StackOverflowError
eg:
自定義線程棧溢出
1.降低線程棧大小 -Xss4k
2.方法循環遞歸
public class JVMStackSOF {
/**
* (1) 在hotspot虛擬機中不區分虛擬機棧(-Xss)和本地方法棧(-Xoss),且只有對Xss參數的設置,纔對棧的分配有影響
*
* (2)
* 由於StackOverflowError->VirtualMachineError->Error
* ->Throwable,所以catch的時候如果用Exception的話將捕獲不到異常 Stack length 會隨着-Xss的減少而相應的變小
*/
private int stackNumber1 = 1;
public void stackLeck1() {
stackNumber1++;
stackLeck1();
}
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeck1();
} catch (Throwable ex) {
System.out.println("Stack length:" + jvmStackSOF.stackNumber1);
ex.printStackTrace();
}
}
}
異常信息:
java.lang.StackOverflowError
2.OutOfMemoryError ---- 棧空間不足,拋出OutOfMemoryError
JVM棧,整體內存不足時,拋OutOfMemoryError
eg:
自定義JVM棧溢出
1.JVM中除了堆和方法區,剩餘的內存基本都由棧佔用
2.每個線程都有獨立的棧空間(堆、方法區是線程公用)
3.如果-Xss調大每個線程的棧空間,可建立的線程數量必然減少
public class JVMStackOOM {
/**
* (1)不停的創建線程,因爲OS提供給每個進程的內存是有限的,且虛擬機棧+本地方法棧=(總內存-最大堆容量(X模型)-最大方法區容量(
* MaxPermSize)),於是可以推斷出,當每個線程的棧越大時,那麼可以分配的線程數量的就越少,當沒有足夠的內存來分配線程所需要的棧空間時,
* 就會拋出OutOfMemoryException
* (2)由於在window平臺的虛擬機中,java的線程是隱射到操作系統的內核線程上的,所以運行一下代碼時,會導致操作系統假死(我就嚐到了血的代價)
*/
private static volatile int threadNumber = 0;
public void stackLeakByThread() {
while (true) {
new Thread() {
public void run() {
threadNumber++;
while (true) {
System.out.println(Thread.currentThread());
}
}
}.start();
}
}
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
try {
jvmStackOOM.stackLeakByThread();
} catch (Throwable ex) {
System.out.println(JVMStackOOM.threadNumber);
ex.printStackTrace();
}
}
}
異常信息如下:
java.lang.OutOfMemoryError:unable to create new native thread
3.方法區溢出
方法區:存放JVM加載的類信息,常量,運行時常量池,靜態變量,編譯期編譯後的代碼等
異常信息:
java.lang.OutOfMemoryError: PermGen space
eg:
自定義方法區溢出代碼
1.通過不斷產生類信息來佔用方法區內存
2.調整 -XX:PermSize=10m --XX:MaxPermSize=10m,來降低方法區內存大小
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/*
* 利用CGLib技術不斷的生成動態Class,這些Class的信息會被存放在方法區中,如果方法區不是很大會造成方法去的OOM
*
*
* -XX:PermSize=10m -XX:MaxPermSize=10m
* */
public class MethodAreaOOM {
static class Test {}
public static void main(String[] args) {
try{
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Test.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object arg0, Method method, Object[] arg2, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(arg0, arg2);
}
});
System.out.println(enhancer.create());
}
}catch(Throwable th){
th.printStackTrace();
}
}
}
異常信息:java.lang.OutOfMemoryError: PermGen space
25.JVM內存分配策略與回收
1.分配策略
1.對象優先在Eden區上分配
2.大對象直接分配在老年區
-XX:PretenureSizeThreshold 參數設置直接放入老年區的對象大小
3.長期存活的對象直接進入老年區
-XX:MaxTenuringThreshold 參數設置對象年齡,經歷幾次gc可以進入老年區
JVM爲每個對象定義了年齡計數器,
如果對象在Eden出生並經過第一次Minor GC後任然存活,並能被Survivor容納,將被移到Survivor空間中,
並將對象年齡設爲1,對象在Survivor區每熬過一次Minor GC,年齡+1,
當年齡達到一定程度(默認15,可參數-XX:MaxTenuringThreshold設置)時,進入到老年代中
2.內存回收
1.Minor GC 發生在年輕代的GC,當JVM無法爲一個新對象分配空間時,觸發Minor GC,清理年輕代內存,大多數對象生命週期短,所以Minor GC 非常頻繁,而且速度較快
觸發條件:
Eden區滿時,觸發Minor GC
2.Full GC 發生在年老代的GC
觸發條件:
1.調用System.gc(),系統建議執行Full GC,但不一定執行
2.老年代空間不足
3.方法區空間不足
4.通過Minor GC後進入老年代的對象平均大小,大於老年代可用內存空間
5.由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
26.AOP應用和實現
AOP優化改造:http://blog.csdn.net/xvshu/article/details/46288953
1.關於Aspectj
獲取目標方法信息 --- JoinPoint Spring只支持方法執行的JoinPoint
JoinPoint裏包含了如下幾個常用的方法:
Object[] getArgs:返回目標方法的參數
Signature getSignature:返回目標方法的簽名
Object getTarget:返回被織入增強處理的目標對象
Object getThis:返回AOP框架爲目標對象生成的代理對象
注意:當使用@Around處理時,我們需要將第一個參數定義爲ProceedingJoinPoint類型,該類是JoinPoint的子類
2.5種增強處理
Before、 在某個連接點JoinPoint之前執行, 不能阻止連接點前的執行
Around、 包圍一個連接點的通知,可以在方法調用前後完成自定義的行爲
AfterReturning、 在某連接點正常完成後執行的通知,不包括拋出異常的情況
After、 某連接點退出時執行(不同正常返回還是異常退出,都會執行的通知,類似finally功能,可以用於釋放連接和資源)
AfterThrowing 在方法拋出異常退出時執行的通知
--- 5種增強處理中,織入增強處理的目標方法、目標方法的參數和被織入增強處理的目標對象等
任何一種織入的增強處理中,都可以獲取目標方法的信息
切點表達式:
1.execution()表達式
eg:
execution (* com.sample.service.impl..*. *(..))
1.execution() 表達式主體
2.第一個* : 表示返回類型, * 表示所有類型
3.包名: 要攔截的包,後面2個句點表示當前包和當前包的所有子包,即:com.sample.service.impl包、及其子孫包下所有類的方法
4.第二個* : 表示類名, * 表示所有類
5.第三個* : 表示方法名, * 表示所有方法, *(..) 中的2個句點表示方法參數, 2個句點表示任何參數
2.自定義註解
1.定義切點方法
//自定義註解方式
@Pointcut("annotation(com.jay.annotation.MyAnnotation)")
//@Pointcut("execution (* com.gcx.controller..*.*(..))") --- execution()切點表達式方式
public void controllerAspect(){}
2.在增強方法上,使用切點方法
@Before("controllerAspect()")
public void doBefore(JoinPoint joinPoint){
}
eg:
使用5種增強處理
1.類上添加 @Aspect註解
2.方法上添加 4中增強處理註解
3.特別注意:
@Around中參數是 ProceedJoinPoint, 其他4類增強用 JoinPoint
@Aspect
public class AdviceTest {
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模擬權限檢查...");
System.out.println("@Before:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:參數爲:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被織入的目標對象爲:" + point.getTarget());
}
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:執行目標方法之前...");
//訪問目標方法的參數:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改變後的參數1";
}
//用改變後的參數執行目標方法
Object returnValue = point.proceed(args);
System.out.println("@Around:執行目標方法之後...");
System.out.println("@Around:被織入的目標對象爲:" + point.getTarget());
return "原返回值:" + returnValue + ",這是返回結果的後綴";
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模擬日誌記錄功能...");
System.out.println("@AfterReturning:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:參數爲:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值爲:" + returnValue);
System.out.println("@AfterReturning:被織入的目標對象爲:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模擬釋放資源...");
System.out.println("@After:目標方法爲:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:參數爲:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被織入的目標對象爲:" + point.getTarget());
}
//標註該方法體爲異常通知,當目標方法出現異常時,執行該方法體
@AfterThrowing(pointcut="within(com.abchina.irms..*) && @annotation(rl)", throwing="ex")
public void addLog(JoinPoint jp, rmpfLog rl, BusinessException ex){
...
}
}
3.動態代理實現
1.靜態代理 --- 代理模式
兩個類實現同一個接口,在代理類的接口方法中,調用被代理對象的接口方法,同時在代理類的接口方法中,調用被代理類方法前後添加邏輯
2.動態代理 --- JDK動態代理 和 cglib 動態代理(字節碼增強技術,效率高)
參考:http://blog.csdn.net/zpf336/article/details/52086180
http://blog.csdn.net/wenbo20182/article/details/52021096
2種動態代理區別:
1.JDK動態代理要求被代理類要實現接口,而cglib不需要
2.cglib能根據內存中爲其創建子類(代理對象)
1.JDK的動態代理 --- 通過創建一個實現InvocationHandler接口的中間對象,實現動態代理
優點:不必要求代理者和被代理者實現相同接口
缺點:僅支持接口代理, JDK動態代理需要實現類通過接口定義業務方法,對於沒有接口的類,無法代理
eg:
自定義的基於JDK的動態代理類
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用JDK動態代理
Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy()
greeting.sayHello("Jack");
2.cglib動態代理 --- 字節碼增強技術,效率高
原理:
通過字節碼技術,爲一個類創建子類,並在子類中採用方法攔截的技術,攔截所有父類方法的調用,順勢織入橫切邏輯
優點:字節碼增強技術,CGLib創建的動態代理對象性能比JDK創建的動態代理對象的性能高不少,
但是CGLib在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因爲無需頻繁創建對象,用CGLib合適,
反之,使用JDK方式要更爲合適一些
缺點:CGLib由於是採用動態創建子類的方法,對於final方法,無法進行代理
public class CGLibDynamicProxy implements MethodInterceptor {
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
private CGLibDynamicProxy() {
}
public static CGLibDynamicProxy getInstance() {
return instance;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> cls) {
return (T) Enhancer.create(cls, this);
}
@Override
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
}
//客戶端使用cglib動態代理
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
greeting.sayHello("Jack");
27.關於JVM架構
參考:http://www.ityouknow.com/java/2017/03/01/jvm-overview.html
1.Java內存模型
內存模型:
描述程序中各個變量(實例域、靜態域、數組元素)之間的關係,以及在計算機系統中將變量存儲到內存,從內存取出變量這樣的底層細節
內存模型規則:
原子性:
約定了:訪問存儲單元內任何類型字段的值以及對其進行更新操作時,必須保證其是原子的
即:獲得或初始化某一些值時(),該值的原子性在JVM內部是必須得到保證的
可見性:
一個線程修改的狀態,對另一個線程是可見的,
即:一個線程修改的結果,另一個線程馬上就能看到
eg:
volatile修飾的變量具有可見性,不允許線程內部緩存和重排序,但不保證原子性
可見性規則約束下,定義了一個線程在哪種情況下可以訪問或影響另外一個線程,以及從另外一個線程的可見區域讀取相關數據、將數據寫入到另外一個線程內
可排序性:
爲了提高性能,編譯器和處理器可能會對指令做重排序
volatile修飾的變量不允許線程內部緩存和重排序
Java內存模型: --- JMM Java Memory Model
JMM: 是控制Java線程之間、線程和主存直接通向的協議
JMM定義了線程和主存之間的抽象關係:
線程之間的共享變量存儲在主存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存存儲了該線程以讀/寫共享變量的副本
2.JVM實例內部結構
子系統:
類加載器作爲JVM的子系統,針對Class文件進行檢測來加載對應的類
執行引擎:
負責代碼的解釋和執行
內存區域:
存儲字節碼、類信息、對象、參數、變量等
方法區: --- 線程共享
存儲類的裝載信息(類信息),
靜態變量(類變量),
運行時常量池
堆: --- 線程共享
對象、數組內存對分配:
當一個Java程序創建一個對象或者一個數組時,JVM實例會針對該對象和數組分配一個新的內存堆空間。在JVM實例內部,只存在一個內存堆的實例,所有的依賴該JVM的Java程序都共享該實例
進程內存堆分配:
多個Java進程啓動時,會得到JVM分配給自己的對空間,多個Java進程的堆空間時相互獨立的
JVM棧: --- 線程私有
對於線程內存棧分配:
當一個新線程啓動時,JVM爲其創建獨立內存棧,
內存棧由棧幀構成,棧幀有2中操作:出棧和入棧
當前線程方法 --- 正在被線程執行的方法, 該方法的棧幀稱爲 當前幀
對於方法:
當一個線程調用某個方法時,JVM創建並將一個新幀壓入到內存棧中,這個幀稱爲當前棧幀,
當該方法執行時,JVM使用內存棧來存儲 參數引用、局部引用變量、基本類型數值、返回值等相關數據
無論方法正常結束還是異常結束,JVM都彈出或丟棄該棧幀,上一幀方法成爲當前幀
本地方法棧:
保存了本地Java方法調用狀態,其狀態包括局部變量、被調用的參數、它的返回值、以及中間計算結果
程序計數器: --- 線程私有
每個線程都有自己的PC寄存器,通過計數器來指示下一條指令執行
3.JVM內存分配策略
靜態存儲:
編譯時,能確定每個數據在運行時需要的存儲空間,因而在編譯時就給它們分配固定的內存空間
此分配策略,要求代碼中不允許有可變數據結構存在,也不允許嵌套或遞歸結構出現(無法計算需要內存空間)
eg:
static final 全局常量
棧式存儲:
動態存儲分配,由一個類似堆棧的運行棧來實現,按先進後出原則分配
程序對數據所需內存空間未知,只有到運行時才能知道佔用空間
堆式存儲:
專門負責在編譯時或運行時,無法確定存儲要求的數據結構的內存分配
eg:
可變字符串和對象實例
4.對象分配規則:
1.對象優先分配在年輕代的Eden區,如果Eden區沒有足夠空間,JVM執行一次 Minor GC
2.大對象直接進入老年代(大對象:需要大量連續內存空間的對象, 可參數設置大對象大小),目的:避免在Eden區和2個Survivor區直接進行大量的內存拷貝 <--- 新生代採用複製算法收集內存
3.長期存活的對象進入老年代, JVM爲每個對象定義了一個年齡計數器,如果對象經過1次Minor GC,對象進入Survivor區,之後沒經過一次Minor GC,對象年齡+1, 直到達到閾值,才進入老年代 (可參數設置年齡計數器大小)
4.動態判斷對象年齡。 如果Survivor區中相同年齡的所有對象大小總和,大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代
5.空間分配擔保。 每次進行Minor GC時, JVM會計算Survivor區移至老年代的對象的平均大小,如果這個值大於老年區剩餘值,則進行一個Full GC
5.GC算法 垃圾回收
對象存活判斷:
引用計數 --- 每個對象有應用計數屬性,新增一個引用,計數+1, 應用釋放,計數-1,計數爲0,可回收 ---- 無法解決對象循環引用問題!
可達性算法(根搜索算法) --- 從GC Roots開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到 GC Roots沒有任何引用鏈可達時, 證明此對象不再用,可回收
可作爲GC Roots的對象:
虛擬機棧中引用的對象
方法區中的靜態屬性引用的對象
方法區中常量引用的對象
本地方法棧中JNI引用的對象
特別注意:
GC管理的主要是 Java 堆,一般情況下只針對堆,進行垃圾回收,
方法區、棧、本地方法區不被GC鎖管理,因而選擇這些區域內的對象作爲GC Roots,
被GC Roots引用的對象不被GC回收
GC算法: 3種基礎算法
參考:http://blog.csdn.net/java2000_wl/article/details/8022293
標記-清除算法
分"標記" 和 "清除" 兩個階段
首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象
缺點:
標記和清除過程效率低
標記清除後,產生大量不連續的內存碎片
複製算法
將可用內存按容量分爲相等的2塊,每次只使用其中的一塊,
當這塊內存用完了,就將還存活的對象複製到另外一塊上面,然後把已使用過的內存空間一次清理掉
缺點:
內存縮小爲原來的一半
優點:
不會有內存碎片,
只需要移動堆的內存指針,按順序分配內存即可,實現簡單,運行高效
標記-壓縮(整理)算法
標記與第一種算法一樣類似,但後續操作不是直接清理對象,而是讓所有存活對象都向一端移動,
並更新引用其對象的指針,然後直接清理掉端邊界意外的內存
缺點:
在標記-清除的基礎上,需要進行對象的移動,成本相對較高
優點:
不會產生內存碎片
JVM分代收集算法
JVM把堆內存分爲新生代和老年代, 根據各個代的特點,採用合適的收集算法
新生代 --- 複製算法 !!!
每次垃圾收集都有大批對象死去,只有少量存活,適合複製算法,只需付出少量存活對象的複製成本就可完成收集
老年代 --- 標記-整理算法 !!!
老年代中對象存活率高,沒有額外空間對它進行分配擔保,必須使用 "標記-壓縮" 算法進行回收
垃圾回收器:
Serial收集器 --- 串行收集器,最古老,最穩定,效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收
ParNew收集器 --- Serial收集器的多線程版本
Parallel收集器 --- 類似ParNew,更關注系統的吞吐量
Parallel Old收集器 --- 使用多線程 和 標記-整理 算法
CMS收集器 --- 是一種以獲取最短回收停頓時間爲目標的收集器
G1收集器 --- 面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵
28.哈希表 和 HashMap原理
哈希表和哈希算法原理:
http://blog.csdn.net/duan19920101/article/details/51579136
http://blog.csdn.net/tanggao1314/article/details/51457585
一種根據關鍵字直接訪問內存存儲位置的數據結構
哈希函數: 通過哈希表,數據元素的存放位置和數據元素關鍵字之間建立起某種對應關係, 關鍵字到內存的映射
hash表以及HashMap原理:
http://www.code123.cc/258.html
HashMap原理:
HashMap基於哈希函數,使用put(key,value)存儲對象,使用get(key)獲取對象,
put傳遞鍵值對,先對鍵調用hashCode()方法,返回hashCode()用於找到bucket位置來存儲Entry對象,
HashMap是在bucket中存儲鍵對象和值對象,作爲 Map.Entry
bucket存儲LinkedList,而LinkedList中存儲的是key-value對象 --> Map.Entry
HashMap在Map.Entry靜態內部類中實現存儲key-value對,
HashMap使用哈希算法,在put和get方法中,使用hashCode()和equals()方法,
通過傳遞key-value對,調用put方法時,HashMap調用鍵對象(key)的hashCode()計算hashCode,然後bucket位置來存儲對象(根據key查找value存放地址),
如果多個key的hashCode相同,則是發生碰撞,對象將存儲在同一個bucket位置的LinkedList的下一個節點中(鍵值對對象 <==> Map.Entry,Entry存儲在LinkedList中)
兩個對象的hashCode相同會發生什麼?
1.兩個對象的hashCode相同,但它們可能並不相等 hashCode() 和 equals()
2.HashMap的key-value抽象成Map.Entry, Entry存儲在LinkedList中, 每個bucket位置對應一個LinkedList,用於解決哈希碰撞
3.如果hashCode相同,則它們的bucket位置相同, 存儲時會發生碰撞,
如果發送碰撞,後一個添加的對象,會存放在bucket位置對應的LinkedList的下個節點中
如果兩個鍵的hashCode相同,如何獲取值對象?
1.get()根據key的hashCode去查找bucket位置,然後獲取對應的value對象
2.如果兩個key的hashCode相同,則兩個key對應的Entry都存儲在同一個bucket位置的LinkedList中
3.HashMap的LinkedList中存儲的是鍵值對Map.Entry對象
4.根據key的hashCode找到bucket位置後,調用key.equals()方法找到LinkedList中正確的節點,最終找到要查的value值對象
5.注意:
hashCode()用於直接定位bucket位置
equals() 用於獲取值對象時使用
如果HashMap大小超過了負載因子定義的容量,怎麼辦?
1.默認負載因子是0.75,即:一個map填滿75%的bucket時,將會創建原來HashMap大小的2倍的bucket數組,來重新調整map的大小,
並將原來的對象放入新的的bucket數組中 --- 這個過程稱爲 rehashing --- 因爲它調用hash方法找到新的bucket位置
重新調整HashMap大小時,存在什麼問題?
多線程下,可能產生條件競爭
eg:
如果2個線程都發現HashMap需要重新調整大小,它們會同時嘗試調整大小。
在調整大小過程中,存儲在LinkedList中的元素的次序將會反過來。
因爲移動到新的bucket位置時,HashMap並不會將元素放到LinkedList的尾部,而是放在頭部,這是爲了避免尾部遍歷,
如果條件競爭發生了,就死循環了
多線程環境下,使用HashTable或ConcurrentHashMap替代HashMap
注意:
1.String、Integer適合作爲HashMap的key
原因:
String是不可變的,final的,已經重寫了equals()和hashCode()方法
如果兩個不相等的對象返回不同的hashCode,碰撞的機率就會小,進而提高HashMap的性能
2.只要遵循equals()和hashCode()的定義規則,並且當對象插入到Map中之後不會再改變了,可以自定義對象作爲key,
3.使用ConcurrentHashMap替代HashTable
HashTable是同步的,線程安全的,但ConcurrentHashMap同步性能更好,其採用了分段鎖
4.HashTable和ConcurrentHashMap的區別
1.HashTable每次同步執行是,都要鎖住整個結構
2.ConcurrentHashMap將hash表分爲16個bucket桶(默認值),採用分段鎖,get、put、remove等操作只鎖當前需要用到的桶
eg:
原先只能一個線程進入,現在卻能同時16個寫線程進入(寫線程才需要鎖定,讀線程不受限制),提升了併發性
總結:
HashMap的工作原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然後返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。
當兩個不同的鍵對象的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對
HashTable實現原理:
實現原理與HashMap類似,但爲了線程安全,HashTable中幾乎所有的public方法都用synchronized做了同步處理,有些方法也是在內部通過 synchronized 代碼塊來實現
Hashtable 與 HashMap 的簡單比較
HashTable 基於 Dictionary 類,而 HashMap 是基於 AbstractMap。Dictionary 是任何可將鍵映射到相應值的類的抽象父類,而 AbstractMap 是基於 Map 接口的實現,它以最大限度地減少實現此接口所需的工作。
HashMap 的 key 和 value 都允許爲 null,而 Hashtable 的 key 和 value 都不允許爲 null。HashMap 遇到 key 爲 null 的時候,調用 putForNullKey 方法進行處理,而對 value 沒有處理;Hashtable遇到 null,直接返回 NullPointerException。
Hashtable 方法是同步,而HashMap則不是。我們可以看一下源碼,Hashtable 中的幾乎所有的 public 的方法都是 synchronized 的,而有些方法也是在內部通過 synchronized 代碼塊來實現。
所以有人一般都建議如果是涉及到多線程同步時採用 HashTable,沒有涉及就採用 HashMap,但是在 Collections 類中存在一個靜態方法:synchronizedMap(),該方法創建了一個線程安全的 Map 對象,並把它作爲一個封裝的對象來返回。
不考慮性能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap)來替換HashMap,這兩種方式基本都是對整個 hash 表結構做鎖定操作的
ConcurrentHashMap實現原理: --- 依賴於Java內存模型
參考:http://wiki.jikexueyuan.com/project/java-collection/concurrenthashmap.html
ConcurrentHashMap結果中包含Segment的數組,默認併發基本,創建包含16個Segment對象的數組,
每個Segment又包含若干個散列表的桶,每個桶是由HashEntry連接起來的一個鏈表,
如果key能夠均勻散列,每個Segment大約守護整個散列表桶總數的1/16
併發讀些操作:
執行 put 方法的時候,會需要加鎖來完成,但加鎖操作是針對的hash值對應的Segment,而不是整個ConcurrentHashMap,因爲put操作只是在某個Segment中完成,並不需要對整個ConcurrentHashMap加鎖,
此時,其他線程可以對另外的Segment進行put操作,雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖
同時,讀線程並不會因爲本線程的加鎖而阻塞
在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行併發寫操作(如果併發級別設置爲 16),及任意數量線程的讀操作
總結:
散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發性能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個併發散列映射表的實現,它允許完全併發的讀取,並且支持給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。
在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的併發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全併發訪問,但同時也導致對容器的訪問變成串行化的了。
ConcurrentHashMap 的高併發性主要來自於三個方面:
用分離鎖實現多個線程間的更深層次的共享訪問。
用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。
使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。
通過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高
29.關於Comparable和Comparator接口
1.Comparable和Comparator接口被用來對對象集合或者數組進行排序
2.內部排序,對象類實現Comparable接口,重寫 CompareTo()方法
2.外部集合排序,調用Collections.sort(collection, Comparator<T>)對集合元素排序
eg:
Collections.sort(list,new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
if (user1.getName().equals(user2.getName())) {
return user1.getAge() - user2.getAge();
} else {
return user1.getName().compareTo(user2.getName());
}
}
});
30.OAuth認證流程
參考:http://www.code123.cc/1671.html
31.Java NIO
參考:http://blog.csdn.net/hxpjava1/article/details/56282385
概念:
專門爲提高I/O吞吐量而設計,NIO通過Reactor模式的事件驅動機制來達到 Non Blocking
Reactor -- 反應器
將事件註冊到Reactor中,當有相應的事件發生時,Reactor告訴我們哪些事情發生了,我們根據具體的事件去做相應的處理
通道和緩衝區: 標準IO基於字節流和字符流進行操作,而NIO基於通道Channel和緩衝區Buffer進行操作,數據總是從通道讀取到緩衝區,或從緩衝區寫入到通道中
異步IO:NIO可以異步的使用IO,當線程從通道讀取數據到緩衝區時,線程還可進行其他事情,當數據被寫入到緩衝區時,線程可以繼續處理它。從緩衝區寫入通道也類似
Selectors選擇器:選擇器用於監聽多個通道的事件(eg:連接打開,數據到達等),因此,單線程可以監聽多個數據通道
eg:
Selector運行單線程處理多個Channel,如果應用打開了多個連接(通道),但每個連接流量都很低,使用Selector就會很方便, 如:在一個聊天服務器中
優點:
舊IO對文件操作只能一個字節一個字節或一行一行的讀,對Socket IO會阻塞,可以爲每個Socket創建一個Thread,但開銷太大
NIO 對Socket IO可以實現非阻塞,可用單線程管理多個通道,
NIO使用緩衝區,File IO和Socket IO都是和Buffer緩衝區交互讀取
NIO將通道數據讀到緩衝區中再進行操作,避免逐字節或逐行讀取的性能開銷
從Channel讀取到Buffer -->
Channel Buffer
<--從Buffer寫入到Channel
NIO和IO如何影響程序的設計?
對NIO和IO類的API調用
數據處理
用來處理數據的線程數
使用場景:
1.聊天服務器 需要管理同時打開的成千上萬個連接,但這些連接每次只發送少量的數據
2.P2P網絡中, 需要維持許多打開的連接到其他計算機上,使用一個單獨的線程來管理所有出戰連接
3.其他流量小, 連接多的場景
不適合場景:
1.少量連接佔用高帶寬,一次發送大量數據 --- 文件服務器(適合IO實現,一個連接通過一個線程處理)
NIO核心模塊:
Selector(選擇器):
1.Selector允許單線程處理多個Channel pk 舊IO的多線程處理, 性能更高
一個單線程選擇器可監控多個通道的多個事件(eg:連接打開,數據到達等事件)
2.使用Selector,需要向它註冊一個Channel,然後輪詢調用它的select()方法,該方法將阻塞,
當註冊的某個通道準備好要進行IO操作時,返回已選擇鍵的個數,
此時通過selectedKeys獲得已選擇的鍵,就可進行相關的IO操作,
選擇鍵SelectionKey 是用來連接 Selector 和 Channel
直到這裏註冊中的Channels中有一個準備好的事件,
一旦這個方法返回,這個線程將會執行這些事件
事件的實例是進來的連接,接收到的數據等等
3. 要使用Selector,得向Selector註冊Channel,然後調用它的select()方法,
該方法會一直阻塞到某個註冊的通道有事件就緒,一旦這個方法返回,線程就可以處理這些事件(新連接進來,數據接收等)
---> Channel
Thread ---> Selector ---> Channel
---> Channel
4.NIO的選擇器允許一個單獨的線程來監視多個輸入通道,可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來 "選擇" 通道,
這些通道里已經有可以處理的輸入,或選擇已準備寫入的通道, 這種機制,使得一個單獨線程很容易來管理多個通道
Channel(通道):
1.4種Channel
FileChannel 文件IO
ServerSocketChannel TCP IO
SocketChannel TCP IO
DatagramChannel UDP IO
Buffer(緩衝區):
緩衝區 --- 內存中預留指定字節數的內存空間,用來對輸入、輸出的數據作臨時存儲, IO操作中數據的中轉站
緩衝區直接爲通道Channel服務,寫入數據到通道,或從通道讀取數據
8種緩衝區類:
對應boolean之外的7基本數據類型 + MappedByteBuffer(專門用於內存映射的ByteBuffer)
ByteBuffer <--繼承 -- MappedByteBuffer 用於表示內存映射文件
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
Buffer讀寫數據的4個步驟:
寫入數據到Buffer
調用flip()方法
從Buffer中讀取數據
調用clear()或 compact()方法
說明:
1.當向Buffer寫入數據時,buffer會記錄下寫了多少數據,
一旦要讀取數據,調用flip()方法,將Buffer從寫模式切換到讀模式,在讀模式下,可以讀取之前寫入到Buffer的所有數據
2.一旦讀完所有數據,就要清空緩衝區,讓它可再次被寫入,2種清空緩衝區方式:
clear() --- 清空整個緩衝區
compact() --- 只清除已讀過的數據,任何未讀的數據都將被移到緩衝區的起始處,新寫入的數據,將放到緩衝區未讀數據的後面
緩衝區的 4 個屬性: capacity>=limit>=position>=mark>=0
capacity
可容納的最大數量,緩衝區創建時被設定,不能改變
limit
上界,緩衝區中當前數據量
讀模式下:
表示最多能讀到多少數據,當切換Buffer到讀模式時,limit被設置爲寫模式下的position值,
即:能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)
寫模式下:
表示最多能往Buffer裏寫多少數據,limit等於Buffer的capacity
position
位置,下一個要被讀、寫的元素的索引
寫數據到Buffer時,position表示當前的位置,初始位置爲0,當數據寫入到Buffer後,position向前移動到下一個可插入數據的Buffer單元
讀數據時,同某個特定位置讀,當將Buffer從寫模式切換到讀模式,position會被重置爲0,當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置
初始:爲 0,最大:capacity-1
mark
標記,調用mark()來設置mark=position,再調用reset()可以讓position回覆到標記的位置,即:position=mark
初始:爲 -1
創建緩衝區
緩衝區類都是抽象的,不能new方式實例化,每個緩衝區類都有一個靜態工廠方法,用於創建相應緩衝區對象
格式:
XxBuffer buf = XxBuffer.allocate(capacity)
eg:
//創建一個容量爲10的byte緩衝區和char緩衝區
ByteBuffer buf1 = ByteBuffer.allocate(10);
CharBuffer buf2 = CharBuffer.allocate(10);
如果想用一個指定大小的數組,作爲緩衝區的數據存儲器,可用wrap()方法創建緩衝區
eg:
//使用一個指定數組,作爲緩衝區存儲器
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes);
解析:
1.緩衝區數據會存在bytes數組中,bytes數組或buf緩衝區任何一方中數據的改動都會影響另一方
2.還可創建指定初始位置(position)和上界(limit)的緩衝區
//使用指定數組作爲緩衝區存儲器,並創建一個position=3,limit=8,capacity=10的緩衝區
byte[] bytes = new byte[10];
ByteBuffer buf = ByteBuffer.wrap(bytes, 3, 8);
操作緩衝區
1.存取
get() 從緩衝區取數據
put(xx) 向緩衝區存數據
channel.read(buf) 從Channel寫到Buffer
channel.write(buf) 從Buffer讀取數據到Channel
Buffer.rewind() 將position設回0, 可以重讀Buffer中的所有數據,limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
eg:
ByteBuffer buf = ByteBuffer.allocate(10);
//存3個數據
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'D');
//反轉緩衝區,即從寫模式切換到讀模式,從頭開始讀,最多讀取已寫入個數的數據
buf.flip();
//讀取2次數據
System.out.println((char)buf.get());
System.out.println((char)buf.get());
//返回當前位置到上界的數據元素數量
System.out.println(buf.remaining());
//從當前位置到上界是否還有數據元素
System.out.println(buf.hasRemaining());
解析:
1.調用put()或get()時,每調用一次,position值 +1,指示下次存或取開始的位置
2.buf.flip() 從寫模式切換到讀模式,可以從頭開始讀取最多已存入個數的數據
3.Buffer.remaining():返回從當前位置到上界的數據元素數量;
Buffer.hasRemaining():告訴我們從當前位置到上界是否有數據元素;
2.反轉 flip()
將一個處於存數據狀態的緩衝區變爲一個處於準備讀取數據的狀態
反轉緩衝區,
即:將緩衝字節數組的指針設置爲數組的開始序列,即:數組下標0,這樣才能從buffer開頭,對buffer進行遍歷(讀取)
即:調用flip()後,讀寫指針知道緩衝區頭部,並且設置了最多隻能讀取已已寫入的數據長度
如果不調用flip(),就會從文件最好開始讀取
特別注意:
buffer.flip() 作用:將Buffer從寫模式切換到讀模式,調用flip()方法設置這個position的值爲0,以及設置這個limit的值爲剛纔position的值,
換句話說,position現在標記了讀的位置,limit標記了有多少個Buffer的字節,字符等等被寫入到buffer。限制有多少個字節,字符可以去讀取的。
flip源碼:
public final Buffer flip(){
limit = position;
position = 0;
mark = -1;
return this;
}
3.清空數據
1.清空緩衝區內所有數據 --- clear()
2.清空已讀取的數據 --- compact()
在buffer中仍然有未讀取的數據,並且你想稍後讀取,調用compact(),
compact()方法拷貝所有未讀取的數據到buffer的開頭,然後設置position值爲最後未讀取數據元素的後面,
再寫數據時,從已有數據後面繼續寫
4.標記 --- mark()
記住當前位置,之後可以將位置恢復到標記處(使用reset()方法恢復)
通過調用Buffer.mark(),可以標記Buffer中的一個特定的position,
之後可通過調用Buffer.reset() 恢復到這個position
eg:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
5.比較2個緩衝區是否相等
6.批量移動緩衝區的數據
public static void batchMove(){
byte[] bytes = "hello nio".getBytes();
/**這裏,可以直接把數據交給數組來保存
ByteBuffer buf = ByteBuffer.wrap(bytes);
*/
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
//將byte數據寫入緩衝區 <=等價=> buf.put(bytes);
buf.put(bytes, 0, bytes.length);
//反轉緩衝區,變爲讀模式
buf.flip();
//輪詢判斷是否有數據,有則將緩衝區數據批量讀到array中
byte[] array = new byte[bytes.length];
while (buf.hasRemaining()){
buf.get(array, 0, buf.remaining());
}
//輸出緩衝區讀出的數據
System.out.println(new String(array));
}
7.複製緩衝區
通道之間的數據傳輸:
NIO中,如果2個通道中有一個是FileChannel, 則可以直接將數據從一個Channel傳輸到另外一個Channel
transferFrom() --- 對目標Channel調用
FileChannel的transferFrom()方法可將數據從源通道傳入到FileChannel中
兩種方式:
toChannel.transferFrom(fromChannel, position, count)
toChannel.transferFrom(position, count, fromChannel)
transferTo() --- 對源Channel調用
將數據從FileChannel傳輸到其他Channel中
eg:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
Selector選擇器:
NIO能檢測到一到多個通道,並能知道通道的各類事件,一個單獨的線程可以管理多個Channel,從而管理多個網絡連接
1.Selector創建
Selector selector = Selector.open();
2.向Selector中註冊通道
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ)
注意:
1.與Selector一起使用時,Channel必須處於非阻塞模式下,所以:不能將FileChannel於Selector一起使用,因爲FileChannel不能切換到非阻塞模式
2.Channel通過register()方法註冊到Selector,並標明它感興趣的事件,之後通過Selector的select()判斷是否有感興趣的事件發生,如果有,通過selectedKeys()獲得興趣事件的集合
3.register(selector, Key)中,Key 表示通過Selector監聽Channel時,對什麼事件感興趣
4種不同類型事件:
Connect
Accept
Read
Write
3.SelectionKey
向Selector註冊Channel時,register()方法返回一個SelectionKey對象,這個對象包含感興趣的屬性:
interest集合
ready集合
Channel
Selector
附加的對象(可選)
4.通過Selector選擇通道
select() 方法 返回 "感興趣事件(連接,接受,讀,寫)" 已經就緒的那些通道
NIO聊天室參考:
http://blog.csdn.net/kindz_cn/article/details/51512672
http://blog.csdn.net/abc_key/article/details/29029879
32.Java中線程池原理和實現
JDK 線程池
Executors -創建-> 4種線程池 -實現-> ExecutorService(接口) -繼承-> Executor(接口)
4種線程池
newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
--- 單線程執行,按提交的線程順序依次執行
newFixedThreadPool
創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待
--- 推薦方式,超過線程池最大容量,線程等待被調用,
--- 定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()
newScheduledThreadPool
創建一個定長線程池,支持定時及週期性任務執行,延遲執行
eg:
/**
* 使用線程池定時延遲調度
*/
private static void test2(){
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延遲3秒後執行
scheduledExecutorService.schedule(getThread(),3, TimeUnit.SECONDS);
//延遲1秒後,每3秒執行一次
scheduledExecutorService.scheduleAtFixedRate(getThread(),1,3, TimeUnit.SECONDS);
}
newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程(空閒線程默認60s會被回收),若無可回收,則新建線程
--- 不推薦,如果一次提交過多線程,而且每個線程比較耗時耗內存,可能瞬間擠爆JVM內存!!!
1.創建
ExecutorService executorService = Executors.newXxThreadExecutor()
2.調用
1.不要返回值
void executorService(Runnable)
2.需要返回值
Future<T> submit(Callable<T>)
3.如果需要 定時或 延遲 執行線程, 使用 ScheduledExecutorService的schedule()和scheduleAtFixedRate() 調用線程
3.ThreadPoolExecutor是Executors類的底層實現,
ThreadPoolExecutor的構造器:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) .
corePoolSize - 池中所保存的線程數,包括空閒線程。
maximumPoolSize-池中允許的最大線程數。
keepAliveTime - 當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。
unit - keepAliveTime 參數的時間單位。
workQueue - 執行前用於保持任務的隊列。此隊列僅保持由 execute方法提交的 Runnable任務。
threadFactory - 執行程序創建新線程時使用的工廠。
handler - 由於超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序。
4.關於線程等待隊列
1.newFixedThreadPool和newSingleThreadExecutor 線程等待隊列是 LinkedBlockingQueue --- 無界阻塞隊列
2.newCachedThreadPool 線程等待隊列是 SynchronousQueue --- 同步阻塞隊列 --- 每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作。因此此隊列內部其 實沒有任何一個元素,或者說容量是0
3.ScheduledThreadPoolExecutor 線程等待隊列是 DelayedWorkQueue --- 延遲阻塞隊列
如何選擇線程阻塞隊列?
1.直接提交的無界隊列 SynchronousQueue
2.無界隊列 LinkedBlockingQueue
3.有界隊列 ArrayBlockingQueue
關於SynchronousQueue?
1.該Queue,在某次添加元素後,必須等待其他線程取走後才能繼續添加 (類似,進棧的元素,必須出棧,纔能有新的元素被放入)
2.可避免在處理具有內部依賴性的請求集時出現鎖
eg:
如果你的任務A1,A2有內部關聯,A1需要先運行,那麼先提交A1,再提交A2,當使用SynchronousQueue我們可以保證,
A1必定先被執行,在A1麼有被執行前,A2不可能添加入queue中
5.線程池的4個組成:
1.線程池管理器(ThreadPool) --- 創建並管理線程池,包括:創建線程池,銷燬線程池,添加新任務
2.工作線程(PoolWorker) --- 線程池中線程,沒有任務時處於等待狀態,可以循環執行任務
3.任務接口(Task) --- 每個任務必須實現的接口,以便工作線程調度 --- Runnable
4.任務隊列(taskQueue) --- 存放沒有處理的任務,提供一種緩衝機制
6.JDK線程池源碼分析:http://www.cnblogs.com/exe19/p/5359885.html
33.JDK動態代理和cglib字節碼技術代理的區別?
1.JDK動態代理:
1.靜態代理 --- 代理對象和目標對象實現了相同的接口,目標對象作爲代理對象的一個屬性,
具體接口實現中,可以調用目標對象相應方法前後加上其他業務處理邏輯
2.JDK動態代理只能針對實現了接口的類生成代理
2.CGLIB代理 --- 通過字節碼技術,爲目標對象生成一個與其功能一樣的子類
1.針對類實現代理
2.主要是對指定的類生產一個子類,覆蓋其中的所有方法
3.被代理類或方法不能聲明爲final
3.區別:
1.JDK動態代理只能對實現了接口的類生成代理, 動態代理只能對於接口進行代理
2.cglib針對類實現代理,主要是對指定的類生成一個子類,覆蓋中的方法,因爲是繼承,所以該類或方法最好不要聲明成final ,final可以阻止繼承和多態
3.Spring實現中,如果有接口,默認使用JDK動態代理,如果目標對象沒有實現接口,使用cglib代理,
如果目標對象實現了接口,可以強制使用CGLIB實現代理(添加CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
4.動態代理的應用
AOP(Aspect-OrientedProgramming,面向切面編程),AOP包括切面(aspect)、通知(advice)、連接點(joinpoint),實現方式就是通過對目標對象的代理在連接點前後加入通知,完成統一的切面操作。
實現AOP的技術,主要分爲兩大類:
一是採用動態代理技術,利用截取消息的方式,對該消息進行裝飾,以取代原有對象行爲的執行;
二是採用靜態織入的方式,引入特定的語法創建“方面”,從而使得編譯器可以在編譯期間織入有關“方面”的代碼。
Spring提供了兩種方式來生成代理對象: JDKProxy和Cglib,具體使用哪種方式生成由AopProxyFactory根據AdvisedSupport對象的配置來決定。
默認的策略是如果目標類是接口,則使用JDK動態代理技術,如果目標對象沒有實現接口,則默認會採用CGLIB代理。
如果目標對象實現了接口,可以強制使用CGLIB實現代理(添加CGLIB庫,並在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
5.參考:http://www.cnblogs.com/linghu-java/p/5714769.html
34.Spring的事務傳播級別
Spring中定義了7種傳播行爲:
參考:https://yq.aliyun.com/articles/71303?spm=5176.8067842.tagmain.14.VE7RJr
35.關於Spring 聲明式事務的原理
參考:http://yemengying.com/2016/11/14/something-about-spring-transaction/
Spring的聲明式事務:
1.JavaConfig方法 --- 在需要管理事務的類或方法上添加 @Transactional註解,然後在配置類上添加 @EnableTransactionManagement註解
2.Xml方式 --- 添加 <tx:annotation-driven />
Spring會利用Aop在相關方法調用的前後進行事務管理
問題:
public class JayServiceImpl implements JayService {
public void A(List<Giraffe> giraffes) {
for (Giraffe giraffe : giraffes) {
B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
說明:
Service中A方法調用B方法,方法A沒有事務管理,方法B採用聲明式事務,通過在方法上聲明 @Transactional註解來做事務管理
問題:
Junit 測試方法 A 的時候發現方法 B 的事務並沒有開啓, 而直接調用方法 B 事務是正常開啓的???
// 沒有開啓事務
@Test
public void testA() {
giraffeService.A();
}
// 正常開啓事務
@Test
public void testB() {
giraffeService.B();
}
}
原理分析:
Spring在加載目標Bean時,會爲聲明瞭@Transactional的Bean創建一個代理類,而目標類本身並不能感知到代理類的存在,
調用通過Spring上下文注入的Bean的方法,而不是直接調用目標類的方法
即:
先調用代理類的方法,代理類再調用目標類的方法
Calling Code
--call--> Proxy --->foo()
---> Pojo --> pojo.foo()
對於加了@Transactional註解的方法,在調用代理類方法時,會先通過攔截器 TransactionInterceptor開啓事務,
然後再調用目標類的方法,最後在調用結束後, TransactionInterceptor會提交或回滾事務
問題解析:
對於第一段的代碼,我在方法 A 中調用方法 B,實際上是通過“this”的引用,也就是直接調用了目標類的方法,而非通過 Spring 上下文獲得的代理類,所以。。。事務是不會開啓滴
解決方法:
通過實現ApplicationContextAware接口獲得 Spring 的上下文,(或自動注入Context對象),然後獲得目標類的代理類,通過代理類的對象,調用方法 B,即可
public class GiraffeServiceImpl implements GiraffeService,ApplicationContextAware{
@Setter
private ApplicationContext applicationContext;
public void A(List<Giraffe> giraffes) {
GiraffeService service = applicationContext.getBean(GiraffeService.class);
for (Giraffe giraffe : giraffes) {
service.B(giraffe);
}
}
@Transactional("transactionManager")
public void B(Giraffe giraffe) {
// Step 1: update something
// Step 2: insert something
// Step 3: update something
}
}
36.Java類加載機制
裝載 ---> 鏈接(驗證 --> 準備 --> 解析) ---> 初始化
1.JVM類加載機制:
裝載:
1.找到該類型的class文件,產生一個該類型的class文件二進制數據流(ClassLoader需要實現的loadClassData()方法)
2.解析該二進制數據流爲方法區內的數據結構
3.創建一個該類型的java.lang.Class實例
最終:通過defineClass()創建一個Java類型對象(Class對象)
找到二進制字節碼,並加載到JVM中
JVM通過類全限定名(包名.類名) + 類加載器 完成類的加載,生成類對應的Class對象
鏈接:
驗證:
負責對二進制字節碼進行校驗、類信息是否符合JVM規範,有沒有安全問題、對class文件長度和類型進行檢查
參考:http://www.importnew.com/17105.html
準備:
初始化類中靜態變量、並將其初始化爲默認值 --- 只初始化靜態變量默認值 !!!,給其類變量賦值發生在初始化階段!!!
對於final類型的變量,準備階段直接賦初始值
該內存分配發生在方法區
解析:
解析類中調用的接口、類、字段、方法的符號引用,把虛擬機常量池中的符號引用轉換爲直接引用
初始化:
1.對static類變量指定初始值!!!(2種方式:一種是通過類變量的初始化語句,一種是靜態初始化語句)
2.一個類的初始化需要先初始化其父類,並遞歸初始化其祖先類
2.JVM必須在每個類或接口主動使用時進行初始化:
主動使用的情況:
1.創建類的實例(無論是new、還是反射、克隆、序列化創建的)
2.使用某個類的靜態方法
3.訪問某個類或即可的靜態字段
4.調用Java API中的某些反射方法
5.初始化某個類的子類(先初始化其父類)
6.啓動某個標明爲啓動類的類(含main()方法)
主動使用會導致類的初始化,其超類均將在該類的初始化之前被初始化,但通過子類訪問父類的靜態字段或方法時,對於子類(或子接口、接口的實現類)來說,這種訪問就是被動訪問,或者說訪問了該類(接口)中的不在該類(接口)中聲明的靜態成員
3.創建對象時,類中各成員的執行順序:
父靜態塊 <-- 子靜態塊 <-- 父普通代碼塊 <-- 父構造器 <-- 子普通代碼塊 <-- 子構造器
1.父類靜態成員和靜態初始化快,按在代碼中出現的順序依次執行。
2.子類靜態成員和靜態初始化塊,按在代碼中出現的順序依次執行。
3. 父類的實例成員和實例初始化塊,按在代碼中出現的順序依次執行。
4.執行父類的構造方法。
5.子類實例成員和實例初始化塊,按在代碼中出現的順序依次執行。
6.執行子類的構造方法。
eg:
public class Test {
public static void main(String[] args) {
Son s = new Son();
}
}
class Parent{
{
System.out.println("parent中的初始化塊");
}
static{
System.out.println("parent中static初始化塊");
}
public Parent(){
System.out.println("parent構造方法");
}
}
class Son extends Parent{
{
System.out.println("son中的初始化塊");
}
static{
System.out.println("son中的static初始化塊");
}
public Son(){
System.out.println("son構造方法");
}
}
結果:
parent中static初始化塊
son中的static初始化塊
parent中的初始化塊
parent構造方法
son中的初始化塊
son構造方法
37.MySQL性能優化
參考:
MySQL性能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存儲引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事務處理,爲每個表創建3個文件,分別存儲不同內容
支持表級鎖,表的寫操作會阻塞其他用戶對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他用戶對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他用戶對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是串行的
eg:
tb_Demo表,那麼就會生成以下三個文件:
1.tb_demo.frm,存儲表定義;
2.tb_demo.MYD,存儲數據;
3.tb_demo.MYI,存儲索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量數據時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入數據,適合管理:郵件或Web服務器日誌數據
總結:
1.適合做count的計算 (注意:不含where條件的統計,因爲MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 默認引擎
支持事務處理
引入了行級鎖(併發高)和外鍵約束
不支持全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外鍵約束 --- MySQL支持外鍵的存儲引擎只有InnoDB
5.支持自動增加列 Auto_INCREMNET屬性
6.InnoDB是爲處理巨大數據量時的最大性能設計
總結:
可靠性要求高,需要事務支持,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多性能開銷,而它的強項在於多線程的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定數據引擎的創建
細節和具體實現的差別:
1.InnoDB不支持FULLTEXT類型的索引。
2.InnoDB 中不保存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT類型的字段,InnoDB中必須包含只有該字段的索引,但是在MyISAM表中,可以和其他字段一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,導入數據後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務類型來選擇合適的表類型,才能最大的發揮MySQL的性能優勢。
存儲引擎選擇依據?
是否需要支持事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外鍵支持;
是否需要全文索引
經常使用什麼樣的查詢模式
數據量大小
eg:
需要事務和外鍵約束 -- InnoDB
需要全文索引 -- MyISAM
數據量大,傾向於InnoDB,因爲它支持事務處理和故障恢復,InnoDB可以利用事務日誌進行數據恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
操作數據表的習慣,也會影響性能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因爲InnoDB不保存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致性能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB性能的方法:
InnoDB支持事務,存儲過程,視圖和行級鎖,在高併發下,表現比MyISAM強很多
影響性能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設置爲1的話,那麼每次插入數據的時候都會自動提交,導致性能急劇下降,應該是跟刷新日誌有關係,設置爲0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設置達到好的性能
設置innodb_buffer_pool_size能夠提升InnoDB的性能
設置查詢緩存
2.配置文件my.ini參數優化
1.max_connections --- 最大併發連接數,允許的同時客戶連接數, 默認100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢緩存,用於緩存select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設置query_cache_size大於0,可以極大改善查詢效率。而如果表數據頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 緩存的最大線程數
5.sort_buffer --- 每個需要進行排序的線程分配該大小的一個緩衝區
6.wait_timeout --- 默認是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連接在8小時內沒有活動,就會自動斷開該連接。 不要設置太長,建議 7200
7.default-storage-engine=INNODB # 創建新表時將使用的默認存儲引擎
配置示例,2G內存,針對站多,抗壓型的設置,最佳:
table_cache=1024 物理內存越大,設置就越大.默認爲2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 默認爲2M
innodb_flush_log_at_trx_commit=1
(設置爲0就是等到innodb_log_buffer_size列隊滿後再統一儲存,默認爲1)
innodb_log_buffer_size=2M 默認爲1M
innodb_thread_concurrency=8 你的服務器CPU有幾個就設置爲幾,建議用默認一般爲8
key_buffer_size=256M 默認爲218 調到128最佳
tmp_table_size=64M 默認爲16M 調到64-256最掛
read_buffer_size=4M 默認爲64K
read_rnd_buffer_size=16M 默認爲256K
sort_buffer_size=32M 默認爲256K
max_connections=1024 默認爲1210
thread_cache_size=120 默認爲60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 查看執行效率,定位優化對象的性能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連接代替子查詢
7.當只要一行數據時,使用limit 1
8.爲搜索字段建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啓查詢緩存,併爲查詢緩存優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢緩存,
2.像NOW()和RAND()或是其它的諸如此類的SQL函數都不會開啓查詢緩存,因爲這些函數的返回是會不定的易變的。所以,你所需要的就是用一個變量來代替MySQL的函數,從而開啓緩存
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變爲一個不衣變的值
38.JVM性能優化
1.
2.Java代碼性能優化
1.沒必要儘量不要使用靜態變量
2.充分利用單例機制減少對資源的加載,縮短運行的時間,提高系統效率
單例適用場景:
1. 控制資源的使用,通過線程同步來控制資源的併發訪問;
2. 控制實例的產生,以達到節約資源的目的
3.減少對象創建,最大限度的重用對象
儘量避免在經常調用的方法中循環使用new對象 --- 享元模式(可減少對象多次創建)
4.使用final修飾符
5.儘量使用局部變量
調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在分配給改方法的棧(Stack)中,速度較快。其他變量,如靜態變量、實例變量等,都在堆(Heap)中創建,速度較慢
6.學會用StringBuilder和StringBuffer,並儘量確定其容量
單線程使用StringBuilder,多線程情況下使用StringBuffer,這樣性能會有很大提升
7.儘量使用基本數據類型代替對象 eg:字符串創建
8.使用HashMa、ArrayList,HashTable、Vector等使用在多線程的場合,內部使用了同步機制,這個會降低程序的性能
9.深入理解HashMap原理
當你要創建一個比較大的hashMap時,充分利用另一個構造函數
public HashMap(int initialCapacity, float loadFactor)避免HashMap多次進行了hash重構,擴容是一件很耗費性能的事,在默認initialCapacity只有16,
而 loadFactor是 0.75,需要多大的容量,你最好能準確的估計你所需要的最佳大小,同樣的Hashtable,Vectors也是一樣的道理
10.儘量在finally塊中釋放資源
11.儘早釋放無用對象的引用
12.儘量避免使用split,split由於支持正則表達式,所以效率比較低,考慮使用apache的 StringUtils.split(string,char),頻繁split的可以緩存結果
13.儘量使用System.arraycopy ()代替通過來循環複製數組
System.arraycopy()要比通過循環來複制數組快的多
14..儘量緩存經常使用的對象 推薦:redis緩存
15.儘量避免非常大的內存分配
參考:http://developer.51cto.com/art/201511/496263.htm
61.MySQL性能優化
參考:
MySQL性能優化總結:http://www.cnblogs.com/luxiaoxun/p/4694144.html
http://blog.chinaunix.net/uid-29435603-id-4275475.html
1.存儲引擎選擇
參考:http://www.jb51.net/article/38178.htm
MyISAM:
不支持事務處理,爲每個表創建3個文件,分別存儲不同內容
支持表級鎖,表的寫操作會阻塞其他用戶對同一個表的讀和寫操作,併發度低
1.myISAM表的讀操作,不會阻塞其他用戶對同一個表的讀請求,但會阻塞對同一個表的寫請求。
2.myISAM表的寫操作,會阻塞其他用戶對同一個表的讀和寫操作。
3.myISAM表的讀、寫操作之間、以及寫操作之間是串行的
eg:
tb_Demo表,那麼就會生成以下三個文件:
1.tb_demo.frm,存儲表定義;
2.tb_demo.MYD,存儲數據;
3.tb_demo.MYI,存儲索引
適合場景:
1.選擇密集型表 --- MyISAM引擎在篩選大量數據時非常迅速 --- 查詢快
2.插入密集型表 --- 併發插入特性允許同時選擇和插入數據,適合管理:郵件或Web服務器日誌數據
總結:
1.適合做count的計算 (注意:不含where條件的統計,因爲MyISAM會記錄表的行數)
2.插入不頻繁,查詢非常頻繁
3.沒有事務需求
InnoDB: 默認引擎
支持事務處理
引入了行級鎖(併發高)和外鍵約束
不支持全文索引
適合場景:
1.更新密集型表 --- 特別適合處理多重併發的更新請求
2.事務
3.自動災難恢復 --- InnoDB表能夠自動從災難中恢復
4.外鍵約束 --- MySQL支持外鍵的存儲引擎只有InnoDB
5.支持自動增加列 Auto_INCREMNET屬性
6.InnoDB是爲處理巨大數據量時的最大性能設計
總結:
可靠性要求高,需要事務支持,並有較高的併發讀取頻率,適合InnoDB
行鎖機制必然決定了寫入時的更多性能開銷,而它的強項在於多線程的併發處理
表更新和查詢都相當的頻繁,並且表鎖定的機會比較大的情況指定數據引擎的創建
細節和具體實現的差別:
1.InnoDB不支持FULLTEXT類型的索引。
2.InnoDB 中不保存表的具體行數,也就是說,執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可。注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的。
3.對於AUTO_INCREMENT類型的字段,InnoDB中必須包含只有該字段的索引,但是在MyISAM表中,可以和其他字段一起建立聯合索引。
4.DELETE FROM table時,InnoDB不會重新建立表,而是一行一行的刪除。
5.LOAD TABLE FROM MASTER操作對InnoDB是不起作用的,解決方法是首先把InnoDB表改成MyISAM表,導入數據後再改成InnoDB表,但是對於使用的額外的InnoDB特性(例如外鍵)的表不適用。
另外,InnoDB表的行鎖也不是絕對的,如果在執行一個SQL語句時MySQL不能確定要掃描的範圍,InnoDB表同樣會鎖全表,例如update table set num=1 where name like “%aaa%”
任何一種表都不是萬能的,只用恰當的針對業務類型來選擇合適的表類型,才能最大的發揮MySQL的性能優勢。
存儲引擎選擇依據?
是否需要支持事務;
是否需要使用熱備;
崩潰恢復:能否接受崩潰;
是否需要外鍵支持;
是否需要全文索引
經常使用什麼樣的查詢模式
數據量大小
eg:
需要事務和外鍵約束 -- InnoDB
需要全文索引 -- MyISAM
數據量大,傾向於InnoDB,因爲它支持事務處理和故障恢復,InnoDB可以利用事務日誌進行數據恢復,這會比較快。而MyISAM可能會需要幾個小時甚至幾天來幹這些事,InnoDB只需要幾分鐘
操作數據表的習慣,也會影響性能
eg:
COUNT() 在 MyISAM 表中會非常快,而在InnoDB 表下可能會很痛苦(
因爲InnoDB不保存表的行數,即:執行select count(*) from table時,InnoDB要掃描一遍整個表來計算有多少行,但是MyISAM只要簡單的讀出保存好的行數即可,
注意的是,當count(*)語句包含 where條件時,兩種表的操作是一樣的)
主鍵查詢在InnoDB下非常快,但如果主鍵太長也會導致性能問題
大批的inserts語句在MyISAM下回快一些,但updates語句在InnoDB下更快(尤其在併發量大的時候)
提示InnoDB性能的方法:
InnoDB支持事務,存儲過程,視圖和行級鎖,在高併發下,表現比MyISAM強很多
影響性能的配置:
innodb_flush_log_at_trx_commit 這個選項,如果設置爲1的話,那麼每次插入數據的時候都會自動提交,導致性能急劇下降,應該是跟刷新日誌有關係,設置爲0效率能夠看到明顯提升
當然,同 樣你可以SQL中提交“SET AUTOCOMMIT = 0”來設置達到好的性能
設置innodb_buffer_pool_size能夠提升InnoDB的性能
設置查詢緩存
2.配置文件my.ini參數優化
1.max_connections --- 最大併發連接數,允許的同時客戶連接數, 默認100, 建議根據需求設定,eg:1024
2.query_cache_size=0 --- 查詢緩存,用於緩存select 查詢結果,如果有許多返回相同查詢結果的SELECT查詢,並且很少改變表,可以設置query_cache_size大於0,可以極大改善查詢效率。而如果表數據頻繁變化,就不要使用這個,會適得其反
3.table_cache=256
4.thread_cache_size --- 緩存的最大線程數
5.sort_buffer --- 每個需要進行排序的線程分配該大小的一個緩衝區
6.wait_timeout --- 默認是28800秒,也就是說一個connection空閒超過8個小時,Mysql將自動斷開該connection,通俗的講就是一個連接在8小時內沒有活動,就會自動斷開該連接。 不要設置太長,建議 7200
7.default-storage-engine=INNODB # 創建新表時將使用的默認存儲引擎
配置示例,2G內存,針對站多,抗壓型的設置,最佳:
table_cache=1024 物理內存越大,設置就越大.默認爲2402,調到512-1024最佳
innodb_additional_mem_pool_size=4M 默認爲2M
innodb_flush_log_at_trx_commit=1
(設置爲0就是等到innodb_log_buffer_size列隊滿後再統一儲存,默認爲1)
innodb_log_buffer_size=2M 默認爲1M
innodb_thread_concurrency=8 你的服務器CPU有幾個就設置爲幾,建議用默認一般爲8
key_buffer_size=256M 默認爲218 調到128最佳
tmp_table_size=64M 默認爲16M 調到64-256最掛
read_buffer_size=4M 默認爲64K
read_rnd_buffer_size=16M 默認爲256K
sort_buffer_size=32M 默認爲256K
max_connections=1024 默認爲1210
thread_cache_size=120 默認爲60
query_cache_size=64M
一般:
table_cache=512
innodb_additional_mem_pool_size=8M
innodb_flush_log_at_trx_commit=0
innodb_log_buffer_size=4M
innodb_thread_concurrency=8
key_buffer_size=128M
tmp_table_size=128M
read_buffer_size=4M
read_rnd_buffer_size=16M
sort_buffer_size=32M
max_connections=1024
更多參考:
http://www.cnblogs.com/adolfmc/p/6056392.html
3.Query查詢優化
1.explain sql 查看執行效率,定位優化對象的性能瓶頸
2.永遠用小結果驅動大的結果集
3.儘可能在索引中完成排序
4.只取出自己需要的column,而不是*
5.使用最有效的過濾條件
6.用表連接代替子查詢
7.當只要一行數據時,使用limit 1
8.爲搜索字段建立索引
9.千萬不要ORDER BY RAND(),避免select *
10.儘可能使用NOT NULL
11.開啓查詢緩存,併爲查詢緩存優化查詢語句
eg:
select username from user where add_time >= now()
注意:
1.這樣的語句不會使用查詢緩存,
2.像NOW()和RAND()或是其它的諸如此類的SQL函數都不會開啓查詢緩存,因爲這些函數的返回是會不定的易變的。所以,你所需要的就是用一個變量來代替MySQL的函數,從而開啓緩存
3.修改, 對now()進行處理,只取年月日 yyyy-MM-dd,變爲一個不衣變的值
62.Java常見的鎖類型有哪些?請簡述其特點。
1、synchronized對象同步鎖:synchronized是對對象加鎖,可作用於對象、方法(相當於對this對象加鎖)、靜態方法(相當於對Class實例對象加鎖,鎖住的該類的所有對象)以保證併發環境的線程安全。同一時刻只有一個線程可以獲得鎖。
其底層實現是通過使用對象監視器Monitor,每個對象都有一個監視器,當線程試圖獲取Synchronized鎖定的對象時,就會去請求對象監視器(Monitor.Enter()方法),如果監視器空閒,則請求成功,會獲取執行鎖定代碼的權利;如果監視器已被其他線程持有,線程進入同步隊列等待。
2、Lock同步鎖:與synchronized功能類似,可從Lock與synchronized區別進行分析:
1、Lock可以通過tryLock()方法非阻塞地獲取鎖而。如果獲取了鎖即立刻返回true,否則立刻返回false。這個方法還有加上定時等待的重載方法tryLock(long time, TimeUnit unit)方法,在定時期間內,如果獲取了鎖立刻返回true,否則在定時結束後返回false。在定時等待期間可以被中斷,拋出InterruptException異常。而Synchronized在獲得鎖的過程中是不可被中斷的。
2、Lock可以通過lockInterrupt()方法可中斷的獲取鎖,與lock()方法不同的是等待時可以響應中斷,拋出InterruptException異常。
3、Synchronized是隱式的加鎖解鎖,而Lock必須顯示的加鎖解鎖,而且解鎖應放到finnally中,保證一定會被解鎖,而Synchronized在出現異常時也會自動解鎖。但也因爲這樣,Lock更加靈活。
4、Synchronized是JVM層面上的設計,對對象加鎖,基於對象監視器。Lock是代碼實現的。
3、可重入鎖:ReentrantLock與Synchronized都是可重入鎖。可重入意味着,獲得鎖的線程可遞歸的再次獲取鎖。當所有鎖釋放後,其他線程纔可以獲取鎖。
4、公平鎖與非公平鎖:“公平性”是指是否等待最久的線程就會獲得資源。如果獲得鎖的順序是順序的,那麼就是公平的。不公平鎖一般效率高於公平鎖。ReentrantLock可以通過構造函數參數控制鎖是否公平。
5、ReentrantReadWriteLock讀寫鎖:是一種非排它鎖, 一般的鎖都是排他鎖,就是同一時刻只有一個線程可以訪問,比如Synchronized和Lock。讀寫鎖就多個線程可以同時獲取讀鎖讀資源,當有寫操作的時候,獲取寫鎖,寫操作之後的讀寫操作都將被阻塞,直到寫鎖釋放。讀寫鎖適合寫操作較多的場景,效率較高。
6、樂觀鎖與悲觀鎖:在Java中的實際應用類並不多,大多用在數據庫鎖上,可參看:http://blog.csdn.net/sdyy321/article/details/6183412
7、死鎖:是當兩個線程互相等待獲取對方的對象監視器時就會發生死鎖。一旦出現死鎖,整個程序既不會出現異常也不會有提示,但所有線程都處於阻塞狀態。死鎖一般出現於多個同步監視器的情況。
63.volatile與automicInteger是什麼?如何使用?
在併發環境中有三個因素需要慎重考量,原子性、可見性、有序性。
voatile 保證了有序性(防止指令衝排序)和變量的內存可見性(每次都強制取主存數據),每次取到volatile變量一定是最新的
volatile主要用於解決可見性,它修飾變量,相當於對當前語句前後加上了“內存柵欄”。使當前代碼之前的代碼不會被重排到當前代碼之後,當前代碼之後的指令不會被重排到當前代碼之前,一定程度保證了有序性。而volatile最主要的作用是使修改volatile修飾的變量值時會使所有線程中的緩存失效,並強制寫入公共主存,保證了各個線程的一致。可以看做是輕量級的Synchronized。詳情可參看:http://www.cnblogs.com/dolphin0520/p/3920373.html。
automicXXX主要用於解決原子性,有一個很經典的問題:i++是原子性的操作碼?答案是不是,它其實是兩步操作,一步是取i的值,一步是++。在取值之後如果有另外的線程去修改這個值,那麼當前線程的i值就是舊數據,會影響最後的運算結果。使用automicXXX就可以非阻塞、保證原子性的對數據進行增減操作。詳情可參看:http://ifeve.com/java-atomic/
注:在此列舉的只是Java多線程最基礎的知識,也是面試官最常問到的,先打牢基礎,再去探討底層原理或者高級用法,除了這十個問題,在此再推薦一些其他的資料:
JVM底層又是如何實現synchronized的:http://www.open-open.com/lib/view/open1352431526366.html
Java線程池詳解:http://blog.csdn.net/zhangliangzi/article/details/52389766
Java線程池深度解析:http://www.cnblogs.com/dolphin0520/p/3932921.html
ConcurrentHashMap原理分析:http://www.cnblogs.com/ITtangtang/p/3948786.html
Java阻塞隊列詳解:http://ifeve.com/java-blocking-queue/
64.MyBatis中timestamp時間類型,在Spring MVC出參時無法轉爲正確的時間類型?
在xml中配置: javaType爲 java.sql.Timestamp
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" javaType="java.sql.Timestamp"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" javaType="java.sql.Timestamp"/>
65.Datatable自定義搜索
//自定義搜索,每次只能根據一個維度進行搜索(按渠道或產品類型)
$("#channel_select,#brand_select").change(function(){
var tsval = $(this).val()
table.search(tsval, false, false).draw();
});
66.synchronized和Lock的底層實現原理?
參考:http://www.open-open.com/lib/view/open1352431526366.html
鎖原理:http://blog.csdn.net/Luxia_24/article/details/52403033
synchronized 在軟件層面依賴JVM
Lock 在硬件層面依賴特殊的CPU指令 --- CAS + JNI調用CPU指令來實現
synchronized 可以吧任何一個非 null 對象作爲 "鎖",
作用於方法上時,鎖住的是對象實例this,
作用於靜態方法,鎖住的是對象對應的Class實例,因爲Class數據存儲在永久帶,因此靜態方法鎖相當於該類的全局鎖,
作用於某個對象實例,鎖住的是對應的代碼塊
HotSpot JVM中,鎖 --- 對象監視器(對象來監視線程的互斥) --- synchronized的實現原理
對象監視器,設置幾種狀態來區分請求的線程:
Contention Set: 所有請求鎖的線程,被首先放置到該競爭隊列 --- 先進後出的虛擬隊列,會被線程併發訪問
Entry Set: 等待獲取鎖的線程(來自Contention Set)排隊隊列 --- 等待獲取對象鎖運行
Wait Set: 獲取鎖後,調用wait()方法,被阻塞的線程隊列 --- 等待再次獲取對象鎖
OnDeck: 任何時刻最多只能有一個線程正在競爭鎖,該線程稱爲OnDeck
Owner: 獲得鎖的線程稱爲Owner
!Owner: 釋放鎖的線程
說明:
1.Entry Set 和Contention Set 同屬等待隊列,
2.Contention Set會被線程併發訪問,爲了降低對Contention Set隊尾的爭用(爲了減少加入與取出兩個線程對於contentionList的競爭),而建立Entry Set,如果Entry Set爲空,則從Contention Set隊尾取出節點
3.Owner線程在unlock時,會從Contention Set中遷移線程到Entry Set,並會指定Entry Set中的某個線程(一般爲Head)爲Read(OnDeck)線程
4.Owner線程並不是把鎖傳遞給OnDeck線程,只是把競爭鎖的權利交給OnDeck,OnDeck線程需要重新競爭鎖
5.OnDeck線程獲得鎖喉變爲Owner線程,無法獲得鎖的線程依然留在Entry Set中
6.如果Owner線程被wait()方法阻塞,則轉移後WaitSet中,如果某個時刻被notify/notifyAll喚醒,則再次轉移到EntrySet
線程的互斥,其實是線程對同一個對象的監視器monitor的操作:
每個對象都有一個監視器(monitor)!,當monitor被佔就會處於鎖定狀態,線程執行monitorentry指令時嘗試獲取monitor的所有權
1.如果monitor的進入數爲0,則線程進入monitor,然後將進入數設置爲1,線程即爲monitor的所有者
2.如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數 +1 --- 鎖可重入 --- ReentrantLock 和synchronized 都是 可重入鎖
3.其他線程已經佔用了monitor,則該線程進入阻塞狀態,知道monitor的進入數爲0,再嘗試獲取monitor的所有權
4.線程調用一次unlock()釋放鎖,monitor的進入數就 -1 (只有monitor進入數爲0,才能被其他線程搶佔)
5.一個線程獲取多少次鎖,就必須釋放多少次鎖,對於synchronized內置鎖 ,每一次進入和離開synchronized方法(代碼塊),就是一個完整的鎖獲取和釋放
sleep()不會釋放鎖,等待指定時間後繼續運行
wait()會釋放鎖,進入對象監視器的 Wait Set隊列,等待被喚醒,被喚醒後,需要重新獲取鎖
wait()和notify/notifyAll必須成對出現,而且必須放在synchronized中,
yield() 不會釋放鎖, 只是讓當前線程讓出CPU佔用權
Synchronized底層優化 --- 偏向鎖、輕量級鎖
參考:http://www.cnblogs.com/paddix/p/5405678.html
http://www.jianshu.com/p/5dbb07c8d5d5
Synchronized效率低的原因?
Synchronized是通過對象內部的對象監視器鎖(monitor)來實現的,monitor本質是依賴於底層的操作系統的Mutex Lock(互斥鎖)來實現,
操作系統實現線程間切換需要從用戶態轉到內核態(JVM轉到操作系統內核),這個成本非常高,狀態轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因
Synchronized底層優化:
1.鎖的4種狀態:
無鎖狀態、偏向鎖、輕量級鎖、重量級鎖
隨着鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖(鎖升級只能從低到高升級,不會出現鎖的降級)
JDK1.6默認開啓偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖
鎖的狀態保存在對象的頭文件中
重量級鎖 --- 依賴於操作系統Mutex Lock所實現的鎖, 需要從JVM轉到操作系統內核,進行互斥操作
輕量級鎖 --- 並不是用來代替重量級鎖,本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗
輕量級鎖目的:
爲了在線程交替執行同步塊時提高性能 !!!
輕量級鎖適用場景:
線程交替執行同步塊的情況 ---- 鎖競爭不激烈的情況!
如果存在同一時間訪問同一個鎖,就會導致輕量級鎖升級爲重量級鎖
偏向鎖 --- 爲了在無多線程競爭的情況下,儘量減少不必要的輕量級鎖執行路徑
一旦線程第一次獲得了監視對象,之後讓監視對象 "偏向"這個線程,在該線程重複獲取鎖時,避免CAS操作
即:
設置一個變量,如果發現是true,無需再走加鎖、解鎖的流程!
偏向鎖目的:
解決無競爭下的鎖性能問題,在只有一個線程執行同步塊時,進一步提高性能
總結:
ynchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量 !!!
67.Spring如何解決Bean的循環依賴? --- 只支持Singleton作用域的, setter方式的循環依賴!!!
參考:https://my.oschina.net/yibuliushen/blog/737640
http://blog.csdn.net/caomiao2006/article/details/46511123
Spring容器循環依賴包括構造器循環依賴和setter循環依賴
如果是構造器循環依賴,Spring容器將無法啓動,報循環依賴異常BeanCurrentlInCreationException
解決方式:
將構造器注入方式改爲屬性注入方式 --- setter
Spring 支持setter方法注入屬性方式的循環依賴
Spring中將循環依賴的處理分3中情況:
1.構造器循環依賴 --- 原理, Spring 不支持構造器方式的循環依賴
通過構造器注入構成的循環依賴是無法解決的,只能在容器啓動時拋出BeanCurrentlInCreationException異常 --- 表示循環依賴
Spring容器將每一個正在創建的Bean的標識符(id)放到 "當前創建bean池" 中,bean標識符在創建過程中將一直保持在這個池中,
如果在創建bean過程中,發現自己已經在 "當前創建bean池"裏時,將拋出 BeanCurrentlInCreationException異常,表示循環依賴,
而對於創建完畢的bean將從 "當前創建bean池"中清除掉
eg:
如在創建TestA類時,構造器需要TestB類,那將去創建TestB,在創建TestB類時又發現需要TestC類,則又去創建TestC,
最終在創建TestC時發現又需要TestA,從而形成一個環,沒辦法創建 --- 循環依賴,拋出 BeanCurrentlInCreationException異常
配置文件;
<bean id="testA" class="com.bean.TestA">
<constructor-arg index="0" ref="testB"/>
</bean>
<bean id="testB" class="com.bean.TestB">
<constructor-arg index="0" ref="testC"/>
</bean>
<bean id="testC" class="com.bean.TestC">
<constructor-arg index="0" ref="testA"/>
</bean>
測試用例:
@Test(expected = BeanCurrentlyInCreationException.class)
public void testCircleByConstructor() throws Throwable {
try {
new ClassPathXmlApplicationContext("test.xml");
} catch (Exception e) {
//因爲要在創建testC時拋出;
Throwable ee1 = e.getCause().getCause().getCause();
throw e1;
}
}
分析:
Spring容器創建"testA"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testB",並將"testA"標識符放到"當前創建bean池"。
Spring容器創建"testB"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testC",並將"testB"標識符放到"當前創建bean池"。
Spring容器創建"testC"bean,首先去"當前創建bean池"查找是否當前bean正在創建,如果沒發現,則繼續準備其需要的構造器參數"testA",並將"testC"標識符放到"當前創建Bean池"。
到此爲止Spring容器要去創建"testA"bean,發現該bean標識符在"當前創建bean池"中,因爲表示循環依賴,拋出BeanCurrentlyInCreationException。
說明:
Spring中bean默認是單例的,對於singleton作用於的Bean,可通過setAllowCircularReferences(false)來禁用循環引用
2.Setter循環依賴 --- 原理, Spring支持setter方式注入屬性的循環依賴!
setter注入方式構成的循環依賴,通過Spring容器提前暴露剛完成構造器注入但未完成其他步驟(eg:setter注入)的bean來完成的,
而且只能解決Singleton單例作用域的bean循環依賴,通過提前暴露一個單例工廠方法,從而使其他bean能引用到該bean(注意:此時僅僅只是生了一個bean,該bean還未調用其他方法,如setter注入)
對單例Bean循環依賴的處理:通過遞歸方法,找出當前Bean的所有依賴Bean,然後提前緩存起來
原理:
創建Bean A時,先通過無參構造器創建一個A實例,此時屬性都是空的,但對象引用已經創建創建出來,然後把Bean A的引用提前暴露出來,
然後setter B屬性時,創建B對象,此時同樣通過無參構造器,構造一個B對象的引用,並將B對象引用暴露出來。
接着B執行setter方法,去池中找到A(因爲此時,A已經暴露出來,有指向該對象的引用了),這樣依賴B就構造完成,也初始化完成,然後A接着初始化完成,
循環依賴就這麼解決了!!!
總結:
先創建對象引用,再通過setter()方式,給屬性賦值,層層創建對象 !!!
Bean A初始化時,先對其依賴B進行初始化,同時,通過默認無參構造器,生成自己的引用,而不調用其setter()方法,
當B對象創建時,如果還依賴C,則也通過無參構造器,生成B的引用,
C對象創建時,如果引用了A,則去對象池中查到A的引用,然後調用setter()方式,注入A,完成C對象的創建
C創建完成後,B使用setter()方式,注入C,完成B對象創建,
B對象場景完成後,A使用setter()方式,注入B,完成A對象創建,
最終,完成setter()方式的循環依賴!
如果循環依賴的都是單例對象(都是通過setter方式注入屬性的),那麼這個肯定沒問題,放心使用即可!!!
如果一個是單例,一個是原型,那麼一定要保證單例對象能提前暴露出來,纔可以正常注入屬性!!!
3.prototype範圍的依賴處理
對於"prototype"作用域bean,Spring容器無法完成依賴注入,因爲Spring容器不進行緩存"prototype"作用域的bean,因此無法提前暴露一個創建中的bean
這個spring也無能爲力,因爲是原型對象,A創建的時候不會提前暴露出來,所以,每次都是要創建,創建的時候,發現有相同的對象正在創建,同樣報錯,循環依賴錯誤
4.Spring創建Bean的源碼解釋:
1.創建Bean的入口
AbstractBeanFactory-->doGetBean()
Object sharedInstance = getSingleton(beanName); //從緩存中查找,或者如果當前創建池中有並且已經暴露出來了,就返回這個對象
2.創建單例Bean方法
DefaultSingletonBeanRegistry-->getSingleton(String beanName, ObjectFactory<?> singletonFactory)
3.創建真正對象
AbstractAutowireCapableBeanFactory-->doCreateBean
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
} 注意這一步很關鍵,是調用構造方法創建一個實例對象,如果這個構造方法有參數,而且就是循環依賴的參數,那麼這個對象就無法創建了,
因爲到這裏對象沒有創建,也沒有暴露當前對象,如果是無參的構造方法,那麼就可以,先創建一個對象,儘管所有的屬性都爲空
68.Spring事務管理的原理?
參考:http://www.codeceo.com/article/spring-transactions.html
聲明式事務管理,在Service之上或Service的方法之上,添加 @Transactional註解
@Transactional如何工作?
Spring在啓動時,會去解析生成相關的Bean,這是會查看擁有相關注解的類和方法,
並且爲這些類和方法生成代理,並根據 @Transactional的相關參數進行相關配置注入,
這樣就在代理中把相關的事務處理掉了(開啓正常提交事務,異常回滾事務)
真正的數據庫層,事務提交和回滾是通過binlog和redo log實現的
Spring事務管理機制實現原理:
參考:
http://www.jianshu.com/p/4312162b1458
http://www.cnblogs.com/duanxz/p/3750845.html
http://www.92to.com/bangong/2016/11-05/12533010.html
在調用一個需要事務的組件時,管理器首先判斷當前調用(即:當前線程)有沒有事務,如果沒有事務則啓動一個事務,並把事務與當前線程綁定,
Spring使用TransactionSynchronizationManager的bindResource方法將當前線程與一個事務綁定,採用的方式就是ThreadLocal,
參考:DataSourceTransactionManager的啓動事務用的代碼 doBegin()
通過動態代理或AOP方式,對所有需要事務管理的Bean進行加載,生成代理對象,並根據配置在invoke()方法中對當前調用的方法名進行判定,
並在method.invoke()方法前後爲其加上合適的事務管理代碼,根據method.invoke()執行結果,正常提交事務,異常回滾事務
實現了EntityManager接口的持久化上下文代理,包含3個組成部分:
1.EntityManager Proxy本身
2.事務的切面
3.事務管理器
遇到過的問題:
參考:59,爲何聲明式事務沒有生效?
69.Spring如何處理高併發?高併發下,如何保證性能?
1.單例模式 + ThreadLocal
單例模式大大節省了對象的創建和銷燬,有利於性能提高,ThreadLocal用來保證線程安全性
Spring單例模式下,用ThreadLocal來切換不同線程直接的參數,用ThreadLocal是爲了保證線程安全,實際上,ThreadLocal的key就是當前線程的Thread實例
單例模式下,Spring把每個線程可能存在線程安全問題的參數值放進了ThreadLocal,雖然是一個實例,但在不同線程下的數據是相互隔離的,
因爲運行時創建和銷燬的bean大大減少了,所以大多數場景下,這種方式對內存資源的消耗較少,並且併發越高,優勢越明顯
2.ThreadLocal
相比同步機制,ThreadLocal則從另一個角度來解決多線程的併發訪問。ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。
因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,
在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal !!!
3.Spring MVC在併發訪問時,是否會存在線程安全問題?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
http://blog.csdn.net/a236209186/article/details/61460211
Struts2是基於類的攔截(每次處理請求,都會實例化一個對象Action,不會有線程安全問題)
Spring MVC 是基於方法的攔截,粒度更細,而Spring的Controller默認是Singleton的,即:每個request請求,系統都會用同一個Controller去處理,
Spring MVC和Servlet都是方法級別的線程安全,如果單例的Controller或Servlet中存在實例變量,都是線程不安全的,而Struts2確實是線程安全的
優點:
不用每次創建Controller,減少了對象創建和銷燬
缺點:
Controller是單例的,Controller裏面的變量線程不安全
解決方案:
1.在Controller中使用ThreadLocal變量,把不安全的變量封裝進ThreadLocal,使用ThreadLocal來保存類變量,將類變量保存在線程的變量域中,讓不同的請求隔離開來
2.聲明Controller爲原型 scope="prototype",每個請求都創建新的Controller
3.Controller中不使用實例變量
Spring MVC 如何保證request對象線程安全?
參考:http://blog.csdn.net/csluanbin/article/details/50930138
InvocationHandler接口:這是springmvc保證request對象線程安全的核心。
通過實現該接口,開發者能夠在Java對象方法執行時進行干預,搭配Threadlocal就能夠實現線程安全
問題:判斷一下程序是否線程安全?
@Controller
public class UserController{
@Autowired
private HttpSession session
@RequestMapping(xxxxxxx)
public void getUser{
session.get ...
session.set...
....
}
}
結論:
該程序是線程安全的
解析:
項目啓動和運行時,Controller對象中的HttpSession並不是HttpSession實例,而是一個代理,
是org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler代理了HttpSession ,可通過這個代碼求證:System.out.println(Proxy.getInvocationHandler(session));
只要當你真正調用HttpSession中的非java.lang.Object方法時纔會真真去調用被代理的HttpSession裏面的方法
說一下session.get ...過程:首先從對象工廠從Threadlocal中取得HttpSession實例,然後通過反射調用該實例的set方法
特別注意:
1.Spring 應該是在請求進來的時候ThreadLocal.set(Session),然後在請求的生命週期中都是一個Thread ,執行完後ThreadLocal.remove(Session)
2.一個請求使用一個ThreadLocal,綁定對應的HttpSession,所以是線程安全的
3.對於 "注入" 到Controller中的單例對象, 都是由Spring統一管理的,Spring對注入Controller的對象使用了ThreadLocal + 代理機制,保證了線程安全
4.但是,對於在Controller中直接定義的實例變量,是線程不安全的!!!
eg:
@RestController
@RequestMapping("/test1")
public class ControllerTest1 {
private int i = 0;
@GetMapping("/count")
public void test1(){
System.out.println(i++);
}
//驗證Controller中的實例變量,線程不安全
@GetMapping("/t1")
public void test3(){
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i=0;i<1000;i++) {
service.execute(new Runnable() {
@Override
public void run() {
HttpUtils.sendGet("http://localhost:8080/test1/count", null);
}
});
}
}
}
調用:http://localhost:8080/test1/t1 方法,使用多線程對i進行操作,發現i的結果不是999,證明Controller中的實例變量是線程不安全的!
結論:
1.對於單例的Controller,Service中定義的實例變量,都不是線程安全的!!!
2.儘量避免在Controller和Service中定義多線程共享的實例變量
3.Spring使用ThreadLocal + InvocationHandler(動態代理)提供了高併發訪問的性能
4.對於Controller和Service中的實例變量,多線程訪問時,需要加鎖處理 或 設置 scope = "prototype"爲每個請求創一個對象
5.對於 @Autowire注入的HttpServletRequest和HttpSession,Spring進行了特殊處理,不會有線程安全問題
70.ConcurrentLinkedQueue與BlockingQueue
2類線程安全的隊列:
1.阻塞隊列 --- 阻塞算法 --- 隊列使用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式實現
2.同步隊列 --- 非阻塞算法 --- 使用循環CAS方式實現
LinkedBlockingQueue 線程安全的阻塞隊列,實現了BlockingQueue接口,BlockingQueue繼承自java.util.Queue接口,
並在接口基礎上增加了take()和put()方法, 這2個方法正式隊列操作的阻塞版本
先進先出,可以指定容量,默認最大是Integer.MAX_VALUE;其中主要用到put和take方法,put方法在隊列滿的時候會阻塞直到有隊列成員被消費,take方法在隊列空的時候會阻塞,直到有隊列成員被放進來!!!
put() 向隊列中放數據 take() 從隊列中取數據
ConcurrentLinkedQueue 是Queue的一個安全實現.Queue中元素按FIFO原則進行排序.採用CAS操作,來保證元素的一致性。
非阻塞方式實現的無界線程安全隊列 !!!
offer()添加元素, poll()獲取元素 isEmpty()判斷隊列是否爲空 (特別注意,不用size(),效率低,會遍歷隊列,儘量要避免用size而改用isEmpty())
採用CAS操作,允許多個線程併發執行,並不會因爲你加鎖而阻塞線程,使得併發性能更好!!!
使用:http://www.cnblogs.com/dmir/p/4907515.html
http://blog.csdn.net/sunxianghuang/article/details/52046150
ConcurrentLinkedQueue源碼解析:http://ifeve.com/concurrentlinkedqueue/
總結:
多數生產消費模型的首選數據結構就是隊列(先進先出)。Java提供的線程安全的Queue可以分爲阻塞隊列和非阻塞隊列,
其中阻塞隊列的典型例子是BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞隊列或者非阻塞隊列
Java中的7種阻塞隊列
阻塞隊列: --- 阻塞的是線程操作(拿和取元素)
常用於生產者和消費者場景,是生產者用來存放元素、消費者用來獲取元素的容器
put()阻塞:隊列滿時,阻塞插入元素的線程,直到隊列不滿
take()阻塞:隊列空時,阻塞獲取元素的線程,直到隊列不空
ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。 生產者和消費者直接傳遞數據,不對數據作緩存,生產者和消費者通過在隊列裏排隊的方式來阻塞和喚醒 --- 速度快
線程數少時,使用SynchronousQueue 速度更快!!!
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列
ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue的區別和使用場景?
區別:
1.三者都是線程安全的
2.2個BlockingQueue是阻塞的,ConcurrentLinkedQueue是併發的
3.2個BlockingQueue使用鎖機制實現阻塞和線程安全(通過ReentrantLock + Condition阻塞容量爲空時的取操作和容量滿時的寫操作),
ConcurrentLinkedQueue使用cas算法保證線程安全
4.ArrayBlockingQueue使用一個鎖(lock + 2個Condition),而LinkedBlockingQueue使用2個鎖(鎖分離,取用takeLock + Condition,寫用putLock+Condition),所以LinkedBlockingQueue的吞吐量大,併發性能比Array高
LinkedBlockingQueue,對頭和尾採用不同的鎖,提高了吞吐量,適合 "消費者生產者" 模式
ArrayBlockingQueue, 數組實現,使用一把全局鎖並行對queue的讀寫操作,同時使用2個Condition阻塞容量爲空時的讀操作和容量滿時的寫操作
5.正因爲LinkedBlockingQueue使用兩個獨立的鎖控制數據同步,所以可以使存取兩種操作並行執行,從而提高併發效率。
而ArrayBlockingQueue使用一把鎖,造成在存取兩種操作爭搶一把鎖,而使得性能相對低下。LinkedBlockingQueue可以不設置隊列容量,默認爲Integer.MAX_VALUE.其容易造成內存溢出,一般要設置其值
使用場景:
阻塞隊列優點:
多線程操作不需要同步,
隊列會自動平衡負載,即:生產和消費兩邊,處理快了會被阻塞,減少兩邊的處理速度差距,
自動平衡負載特性,造成它能被用於多生產者隊列,隊列滿了就要阻塞等着,直到消費者使隊列不滿才能繼續生產
ConcurrentLinkedQueue:
允許多線程共享訪問一個集合,多用於消息隊列!!!
多消費者消費同一個 用 ConcurrentLinkedQueue:
BlockingQueueue:
多線程共享時阻塞,多用於任務隊列!!!
單消費者用 BlockingQueueue:
總結: 單個消費者用LinkedBlockignQueue, 多消費者用ConcurrentLinkedQueue !!!
單生產者,單消費者 用 LinkedBlockingqueue
多生產者,單消費者 用 LinkedBlockingqueue
單生產者 ,多消費者 用 ConcurrentLinkedQueue
多生產者 ,多消費者 用 ConcurrentLinkedQueue
71.Java多線程同步機制:3種類型
volatile 變量:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性保證,不提供原子性。 --- 只保證可見性,不保證原子性,不絕對線程安全!!!
CAS 原子指令:輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性和原子化更新保證。
內部鎖(synchronized)和顯式鎖(各種Lock):重量級多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性和原子性。
參考:
非阻塞算法在併發容器中的實現:ConcurrentLinkedQueue https://www.ibm.com/developerworks/cn/java/j-lo-concurrent/index.html
72.CAS在JDK中的實現
參考:http://blog.csdn.net/canot/article/details/50759424
1.Synchronized鎖機制存在的問題:
(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險
優化:
偏向鎖、輕量級鎖、減小鎖粒度
2.鎖分類
悲觀鎖 --- 獨佔鎖 --- synchronized是獨佔鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖
樂觀鎖 --- 每次不加鎖,而是假設沒有衝突,而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止
3.CAS原理 --- 實現樂觀鎖
CAS操作:
CAS有3個操作數:
V 內存值
A 舊的預期值
B 要修改的新值
當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做!!!
非阻塞算法: 一個線程的失敗或掛起,不應該影響其他線程的失敗或掛起的算法
CAS的硬件基礎和實現原理:
現代的CPU提供了特殊指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,
而compareAndSet()就用這些代替了鎖定, compareAndSet利用JNI,藉助調用的C語言來完成CPU指令的操作
eg:
AtomicInteger 如何實現無鎖下的線程安全?
//在沒有鎖的機制下可能需要藉助volatile原語,保證線程間的數據是可見的(共享的)。這樣才獲取變量的值的時候才能直接讀取。
private volatile int value;
public final int get(){
return value;
}
//i++操作, 每次從內存中讀取數據,然後將此數據和 +1 後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功爲止
public final int incrementAndGet(){
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
39.Redis性能優化
40.MongoDB性能優化
41.MQ性能優化和對比
42.一次搞定Java多線程併發編程
參考:http://blog.csdn.net/liuguangqiang/article/details/52137188
Java併發編程:CountDownLatch、CyclicBarrier和Semaphore
http://blog.csdn.net/zheng0518/article/details/42297259
http://www.cnblogs.com/dongguacai/p/6023028.html
線程安全:
1.Synchronized 線程同步
2.Lock + ReentrantLock 線程安全
線程通信與協作:
0.wait()、notify()、notifyAll() 每個對象都有的3個方法,通道Monitor、waitSet、enterSet用來監聽鎖,存放線程隊列
1.ReentrantLock + Condition 併發控制多路複用
每個ReentrantLock可以創建多個Condition,每個Condition都可以通過控制一個對象鎖,來實現多個線程通信
Condition方法:
線程1中調用await()後,線程1將釋放鎖,等待被喚醒
線程2獲取到鎖後,執行完畢,調用signal()方法,喚醒線程1
eg:
打印1到9這9個數字,A線程打印1,2,3,然後B線程打印4,5,6,,然後再A線程打印 7,8,9
2.ReadWriteLock 讀寫鎖
3.CountDownLatch
4.CyclicBarrier
5.Semaphore
效率提升:
6.線程池 + 阻塞隊列
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.