上一篇說了類加載器、雙親委派機制、自定義類加載器java
System.out.println(new MyClassLoader().getParent()); 輸出結果:sun.misc.Launcher$AppClassLoader@18b4aac2
咱們自定義類加載器繼承了ClassLoader,new MyClassLoader()的時候會先走類加載器的構造面試
// 無參構造 調用了2個參數的構造 protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } // 這裏指定了parent parent從哪兒來 看getSystemClassLoader() private ClassLoader(Void unused, ClassLoader parent) { // 指定parent this.parent = parent; // 其餘操做 if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); package2certs = new ConcurrentHashMap<>(); domains = Collections.synchronizedSet(new HashSet<ProtectionDomain>()); assertionLock = new Object(); } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; package2certs = new Hashtable<>(); domains = new HashSet<>(); assertionLock = this; } } @CallerSensitive public static ClassLoader getSystemClassLoader() { // 返回的scl 看scl怎麼初始化的 initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; // 獲取classLoader scl = l.getClassLoader(); try { scl = AccessController.doPrivileged( new SystemClassLoaderAction(scl)); } catch (PrivilegedActionException pae) { oops = pae.getCause(); if (oops instanceof InvocationTargetException) { oops = oops.getCause(); } } if (oops != null) { if (oops instanceof Error) { throw (Error) oops; } else { // wrap the exception throw new Error(oops); } } } sclSet = true; } } // 直接返回了loader loader 是怎麼來的 public ClassLoader getClassLoader() { return this.loader; } // Launcher類初始化的時候 構造方法裏初始化了load 默認是appclassloader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
ClassLoader.getSystemClassLoader(); -> appClassLoader
// 寫一個有參的構造 傳入一個你想認的爹 而後調用super 把parent傳進去就好了 public MyClassLoader(ClassLoader parent) { super(parent); }
verification
對文件格式進行校驗小程序
preparation
給靜態變量賦默認值緩存
resolutiontomcat
將類、方法 、屬性等符號引用解析爲直接引用app
常量池中的各類符號引用解析爲指針、偏移量等內存地址的直接引用。dom
好比java.lang.Object 他是個符號引用ide
若是想找他真是的內存數據 須要根據java.lang.Object先去常量池找見這個符號,而後再根據符號找對應的類型,這個就太繞了 ,直接把符號引用解析爲直接引用的話 java.lang.Object 就變爲0x00012 內存地址 ,直接根據這個地址找類型就能夠了工具
調用初始化代碼
/** * @author 木子的晝夜 */ public class Mr { public static void main(String[] args) { System.out.println(T.count); } } class T{ // 成員變量 public static int count = 10; public static T t = new T(); // 構造 private T(){ count++; } } 結果:11
若是賦值和new 對象 換一下位置呢
/** * @author 木子的晝夜 */ public class Mr { public static void main(String[] args) { System.out.println(T.count); } } class T{ // 成員變量 public static T t = new T(); public static int count = 10; // 構造 private T(){ count++; } } 結果: 10
本身想下這個過程 想不通能夠公衆號留言 我再進行解答 應該均可以想的通 。。
/** * @author 木子的晝夜 */ public class Sig { private static T03 t03; public static T03 getInstance(){ // 先校驗是不是null if (t03 == null) { // 等鎖 synchronized (T03.class){ // 接着校驗是不是null 由於可能多我的等鎖 if (t03 == null){ t03 = new T03(); } } } return t03; } } class T03{ }
這個單例模式有什麼問題嗎 ?
面試官會瘋狂的暗示你 加volatile .
接着會問volatile的做用 : 禁止指令重排 保證可見性
這裏就是由於 咱們說的 new T03() 的時候 先分配內存 再賦初始值 再賦默認值
若是內存分配好了 另外一個線程 if(t03 == null) 就是false了
而後就返回了 若是用t03.count 那他仍是0呢
固然 機率很低 可是這是會出現的
讓咱們看一下T03 t03 = new T03();的過程
public class T03 { public int count =8; } public class Test { public static void main(String[] args) { T03 t03 = new T03(); } }
注意:這裏須要使用 idea的一個工具->jclasslib ByteCode Viewer 直接搜索安裝便可
先運行一下main方法 生成class文件
選中Test文件
view 視圖 找 Show ByteCode By jclasslib
看生成過程
0 new #2 <T03> // (1)這句話就是在內存開闢一塊空間 count = 0 3 dup 4 invokespecial #3 <T03.<init>> // (2)這句話就是初始化count值 count = 8 7 astore_1 //(3) 這句話 就是把內存空間 地址引用 賦值給t03變量 8 retur
正常狀況下 按照(1) (2) (3)的順序執行 是沒有任何問題的 可是指令可能重排
可能會出現 (1) (3)(2) 這種狀況 就是咱們上邊說的出現問題的狀況 因此要禁止指令重排 volatile
假設線程1使用cpu1 把數據 x 讀到了L0、L一、L2中的任何一個地方 這是cpu獨享的
線程2 使用cpu2 把數據x 也讀到了 cpu2的 L0 、L一、L2的任何一個地方
這時候就是一個數據 在內存中存儲着2份了 其中一份修改了 那另外一份沒改 是否是就有問題了
在cpu 讀取數據 L3-->L2 都要過總線
在cpu1讀取x的時候 給總線上一把鎖 這時候cpu2不容許讀
缺點: 總線鎖是鎖總線,也就是我cpu2不訪問x 我cpu2去訪問y 也不能訪問 這樣不是很合理吧
你們去洗腳了,你找了小麗,而後在門口上了一把鎖,憑什麼不讓我去找小蘭。。。
通常你們聊的時候 是MESI -- intel CPU 實現協議
what is MESI ? is this !
數據存儲在緩存行上 緩存行用額外兩位two bit 來標記狀態 ,這裏須要注意,若是數據誇緩存行了,那就很難用這種方式標記了,就須要使用總線鎖了,呀呼嘿嘿
這個很難表達 我試着說一會兒
1.我是cpu1, 我從主從讀取了x ,這時候只有我讀沒有其餘cpu讀,我會標記位Exclusive
至於這些狀態都是在何時變化的,這個學問就大了去了,主板上各類邏輯單元,我也不知道是什麼高科技實現的。
上邊說了 緩存行的2bit標記狀態 那什麼是緩存行呢?
cpu這個傢伙呀,在讀取數據的時候,是以緩存行爲最小單位讀取的
好比int x =666; cpu在讀取x的時候不會只讀取這四個字節,他會讀取x及x之後的N個字節
這些個字節總的就叫緩存行,通常緩存行是64字節
緩存行問題:
我是cpu1, 我讀取x的時候,會把整個緩存行讀取了
我修改了x ,我把緩存行狀態改成invalid,其實我沒有
修改y z w j 可是若是別的cpu在使用y z w j的話
就須要從新加載一遍
這個問題叫:僞共享 : 位於同一緩存行的兩個不一樣數據被兩個CPU鎖定,產生互相影響。
這裏有一個緩存行對齊的例子:
public class CacheLineTest01 { static T[] arr = new T[2]; static{ arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { final CountDownLatch cdl = new CountDownLatch(2); final long count = 1_0000_0000L; long start = System.currentTimeMillis(); // 起兩個線程 分別修改arr[0] arr[1] 對應對象T的屬性 // 這個arr很大機率上會在一個緩存行 由於就2個T對象 每一個對象就一個Long類型屬性 總共不夠64字節 new Thread(()->{ for (long i = 0; i <count; i++) { arr[0].x = i; } cdl.countDown(); }).start(); new Thread(()->{ for (int i = 0; i < count; i++) { arr[1].x = i; } cdl.countDown(); }).start(); cdl.await(); long end = System.currentTimeMillis(); System.out.println((end-start)/100); } } class T{ public volatile long x=0L; } 執行屢次輸出結果: 30、2九、2三、2六、2七、30
public class CacheLineTest02 { static T006[] arr = new T006[2]; static{ arr[0] = new T006(); arr[1] = new T006(); } public static void main(String[] args) throws InterruptedException { final CountDownLatch cdl = new CountDownLatch(2); long start = System.currentTimeMillis(); final long count = 1_0000_0000L; new Thread(()->{ for (long i = 0; i < count; i++) { arr[0].x = i; } cdl.countDown(); }).start(); new Thread(()->{ for (int i = 0; i <count; i++) { arr[1].x = i; } cdl.countDown(); }).start(); cdl.await(); long end = System.currentTimeMillis(); System.out.println((end-start)/100); } } // 加了一個對齊 也就是Padding 這樣new2個T006以後 絕對不在一個緩存行 // 因此兩個cpu修改屬性 不會相互影響 class T006 extends Padding{ public volatile long x=0L; } class Padding{ long a,b,c,d,e,f,g; } 執行屢次結果: 1四、1六、1六、1四、1七、1四、15
很明顯,第二段代碼的執行時間更快 這就是緩存行對齊對程序效率提高的做用
能夠看圖:第一段代碼 會走invalid 每次都會去內存拿數據 再進行修改 ,而第二段代碼會走Modified不須要去內存再一次拿數據
用一句話總結:cpu爲了提升執行效率,會在一條指令準備數據過程當中,執行另外一條不依賴於前一條指令的指令
能夠看一個例子:cpu在執行指令1的時候,指令1 須要去內存拿數據 ,你們知道內存讀取數據耗時至少是cpu的100倍起步,這個時間cpu等着嗎? 不能呀! 那你電腦不卡成狗了嗎。
這個時間cpu會接着去判斷下一條指令2,看指令2是否依賴指令1的執行結果,若是依賴,接着看指令3,若是不依賴就執行,依次往下執行,直到指令1拿回來數據爲止
舉個例子:
小強作飯,第一道菜是土豆燉牛腩,第二道菜是拍黃瓜
若是是你,你會怎麼作?
最容易些想到的是這樣:
準備土豆->準備牛腩->放鍋裏->看着它燉熟了->盛出來->準備黃瓜->拍黃瓜->倒醬汁->拍黃瓜作好了
可是咱們通常不會這麼作,咱們跟cpu同樣聰明:
咱們會這樣作:
準備土豆->準備牛腩->放鍋裏->判斷拍黃瓜這道菜要不要等土豆牛腩好了才能作?->不是->準備黃瓜->拍黃瓜->倒醬汁->拍黃瓜作好了->在作拍黃瓜的過程當中你確定會看着土豆牛腩,防止幹鍋,若是拍黃瓜過程當中土豆牛腩好了,你會先中止拍黃瓜,先去把牛腩撈出來(否則土豆塊成土豆湯了),而後再去拍黃瓜
合併寫的概念:
拿生活中的例子就是,小強的土豆燉牛肉好了,能夠放上桌讓別人吃了,可是他以爲,這頓飯拍黃瓜跟土豆燉牛肉一塊兒吃才能稱之爲「一頓飯」,注意這裏一頓飯在cpu中能夠對應一個數據。而後他就倆都作好了,拿一個大托盤,把2道菜合成了「一頓飯」 放上桌,你們吃的不亦樂乎。
學術上的概念大概意思就是: 多個程序對同一個數據x進行操做,cpu執行x=x+1; 準備把結果寫回L3內存,可是他「自做聰明」的發現,後邊好像還有一句 x = x+10;因此他就等着x=x+10;這句執行完以後 再把一個最終結果寫回L3內存 ,而不是寫2次。
合併寫的緩衝區WCbuffer 很小很小 只有4個字節
import java.util.concurrent.CountDownLatch; public class TestOrder { private static int a=0,b=0,x=0,y=0; public static void main(String[] args) throws InterruptedException { long count = 0; for (;;){ count++; CountDownLatch cdl = new CountDownLatch(1); CountDownLatch cdlres = new CountDownLatch(2); // 默認值 a=0;b=0;x=0;y=0; new Thread(()->{ try { cdl.await(); a = 1; x = b; } catch (InterruptedException e) { }finally { cdlres.countDown(); } }).start(); new Thread(()->{ try { cdl.await(); b = 1; y = a; } catch (InterruptedException e) { }finally { cdlres.countDown(); } }).start(); cdl.countDown(); cdlres.await(); if (x==0&&y==0){ System.out.println("存在亂序"+",一共執行:"+count+ " 次"); break; } } } }
若是不重排出現的結果應該是:
若是出現x==0 && y == 0 的狀況 說明指令重拍了
想要證實,你就拿着這個程序,跑吧, 跑一下子 ,要有耐心
看看我執行的次數:40多萬次
待續。。
歡迎關注公-衆-號: