深入AQS源碼閱讀與強軟弱虛4種引用以及ThreadLocal原理與源碼 var是什么意思

前言今天咱們繼續講AQS的源碼,在上節課我教大家怎么閱讀AQS源碼,跑不起來的不讀、解決問題就好 —目的性、一條線索到底、無關細節略過,讀源碼的時候應該先讀骨架,比如拿AQS來說 , 你需要了解AQS是這么一個數據 結構 , 你讀源碼的時候讀起來就會好很多,在這里需要插一句,從第一章到本章,章章的內容都是環環相扣的,沒學習前邊 , 建議先去補習一下前面的章節 。

深入AQS源碼閱讀與強軟弱虛4種引用以及ThreadLocal原理與源碼 var是什么意思

文章插圖
通過ReentrantLock來解讀AQS源碼AQS大家還記得嗎?最核心的是它的一個共享的int類型值叫做state,這個state用來干什么,其實主要是看他的子類是怎么實現的,比如ReentrantLock這個state是用來干什么的?拿這個state來記錄這個線程到底重入了多少次 , 比如說有一個線程拿到state這個把鎖了,state的值就從0變成了1,這個線程又重入了一次,state就變成2了,又重入一次就變成3等等,什么時候釋放了呢?從3變成2變成1變成0就釋放了,這個就是AQS核心的東西,一個數,這個數代表了什么要看子類怎么去實現它,那么在這個state核心上還會有一堆的線程節點,當然這個節點是node,每個node里面包含一個線程 , 我們稱為線程節點 , 這么多的線程節點去爭用這個state,誰拿到了state,就表示誰得到了這把鎖 , AQS得核心就是一個共享的數據 , 一堆互相搶奪競爭的線程 , 這個就是AQS 。
我們接著上節課來講,首先給lock()方法處打斷點,然后debug運行程序,
//JDK源碼public class TestReentrantLock {private static volatile int i = 0;public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();lock.lock();//synchronized (TestReentrantLock.class) {i++;//}lock.unlock();//synchronized 程序員的麗春院 JUC}}在lock()方法里里面,我們可以讀到它調用了sync.acquire(1),
//JDK源碼public class ReentrantLock implements Lock, java.io.Serializable {public void lock(){sync.acquire(1);}}再跟進到acquire(1)里,可以看到acquire(1)里又調用了我們自己定義自己寫的那個tryAcquire(arg)
//JDK源碼public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg){if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}}跟進到tryAcquire(arg)里又調用了nonfairTrytAcquire(acquires)
//JDK源碼public class ReentrantLock implements Lock, java.io.Serializable {public void lock(){sync.acquire(1);}static final NonfairSync extends Sync{protected final boolean tryAcquire(int acquire){return nonfairTrytAcquire(acquires);}}}nonfairTrytAcquire(acquires)我們讀進去會發現它的里面就調用到了state這個值,到這里我們就接上了上一章講的,nonfairTrytAcquire(acquires)里是這樣的,首先拿到當前線程,拿到state的值,然后進行if判斷,如果state的值為0,說明沒人上鎖,沒人上鎖怎么辦呢?就給自己上鎖,當前線程就拿到這把鎖,拿到這個把鎖的操作用到了CAS(compareAndSetState)的操作,從0讓他變成1,state的值設置為1以后,設置當前線程是獨一無二的擁有這把鎖的線程,否則如果當前線程已經占有這把鎖了,怎么辦?很簡單我們在原來的基礎上加1就可以了,這樣就能拿到這把鎖了,就重入,前者是加鎖后者是重入
//JDK源碼public class ReentrantLock implements Lock, java.io.Serializable {public void lock(){sync.acquire(1);}static final NonfairSync extends Sync{protected final boolean tryAcquire(int acquire){return nonfairTrytAcquire(acquires);}}final boolean nonfairTrytAcquire(int acquire){//獲取當前線程final Thread current = Thread.currentThread();//拿到AQS核心數值stateint c getState();//如果數值為0說明沒人上鎖if(c == 0){//給當線程上鎖if(compareAndSetState(0,acquires)){//設置當前線程為獨一無二擁有這把鎖的線程setExclusiveOwnerThread(current);return true}}//判斷當前線程是否擁有這個把鎖else if(current == getExclusiveOwnerThread){//設置重入int nextc = c + acquires;if(nextc < 0)throw new Error("Maximum lock count wxceeded");setState(nextc);return true;}return false;}}我們跟進到tryAcquire(arg)是拿到了這把鎖以后的操作,如果拿不到呢?如果拿不到它實際上是調用了acquireQueued()方法,acquireQueued()方法里又調用了addWaiter(Node.EXCLUSIVE)然后后面寫一個arg(數值1),方法結構是這樣的acquireQueued(addWaiter(Node.EXCLUSIVE),arg)通過acquireQueued這個方法名字你猜一下這是干什么的 , 你想如果是我得到這把鎖了,想一下后面的acquireQueued是不用運行的,如果沒有得到這把鎖,后面的acquireQueued()才需要運行,那么想一下沒有得到這把鎖的時候它會運行什么呢?他會運行acquireQueued,Queued隊列 , acquire獲得 , 跑到隊列里去獲得,那意思是什么?排隊去,那排隊的時候需要傳遞兩個參數,第一個參數是某個方法的返回值addWaiter(Node.EXCLUSIVE),來看這個方法的名字addWaiter,Waiter等待者,addWaiter添加一個等待者 , 用什么樣的方式呢?Node.EXCLUSIVE排他形式,意思就是把當線程作為排他形式扔到隊列里邊 。
//JDK源碼public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg){//判斷是否得到鎖if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}}我們來說一下這個addWaiter()方法,這個方法意思是說你添加等待者的時候,使用的是什么類型,如果這個線程是Node.EXCLUSIVE那么就是排他鎖,Node.SHARED就是共享鎖,首先是獲得當前要加進等待者隊列的線程的節點 , 然后是一個死循環,這意思就是說我不干成這件事我誓不罷休,那它干了一件什么事呢?
//JDK源碼public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg){//判斷是否得到鎖if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}private Node addWaiter(Node mode){//獲取當前要加進來的線程的node(節點)Node node = new Node(mode);for(;;){//回想一下AQS數據結構圖Node oldTail = tail;if(oldTail != null){//把我們這個新節點的前置節點設置在等待隊列的末端node.setPrevRelaved(oldTail);//CAS操作,把我們這個新節點設置為tail末端if(compareAndAetTail(oldTail,node)){oldTail.next = node;return node;}}else{initializeSuncQueue();}}}}你想想想看,我們回想一下AQS數據結構圖,就是他有一個int類型的數叫state,然后在state下面排了一個隊列,這個隊列是個雙向的鏈表有一個head和一個tail,現在你要往這個隊列中加一個節點上來,要排隊嘛 , 我們仔細想一下加節點的話,應該得加到這個隊列的末端是不是?它是怎么做到的呢?首先把tail記錄在oldTail里,oldTail指向這個tail了,如果oldTail不等于空,它會把我們這個新節點的前置節點設置在這個隊列的末端,接下來再次用到CAS操作,把我們這個新的節點設置為tail,整段代碼看似繁瑣 , 其實很簡單 , 就是要把當前要加進等待者隊列的線程的節點加到等待隊列的末端,這里提一點,加到末端為什么要用CAS操作呢?因為CAS效率高,這個問題關系到AQS的核心操作,理解了這一點,你就理解了AQS為什么效率高,我們接著講源碼,這個增加線程節點操作,如果沒有成功,那么就會不斷的試 , 一直試到我們的這個node節點被加到線程隊列末端為止 , 意思就是說,其它的節點也加到線程隊列末端了,我無非就是等著你其它的線程都加到末端了,我加最后一個,不管怎么樣我都要加到線程末端去為止 。
源碼讀這里我們可以總結得出,AQS(AbstractQueuedSynchronizer)的核心就是用CAS(compareAndSet)去操作head和tail,就是說用CAS操作代替了鎖整條雙向鏈表的操作
通過AQS是如何設置鏈表尾巴的來理解AQS為什么效率這么高
我們的思路是什么呢?假如你要往一個鏈表上添加尾巴,尤其是好多線程都要往鏈表上添加尾巴,我們仔細想想看用普通的方法怎么做?第一點要加鎖這一點是肯定的 , 因為多線程,你要保證線程安全,一般的情況下 , 我們會鎖定整個鏈表(Sync),我們的新線程來了以后,要加到尾巴上,這樣很正常,但是我們鎖定整個鏈表的話,鎖的太多太大了,現在呢它用的并不是鎖定整個鏈表的方法 , 而是只觀測tail這一個節點就可以了 , 怎么做到的呢?compareAndAetTail(oldTail,node) , 中oldTail是它的預期值,假如說我們想把當前線程設置為整個鏈表尾巴的過程中,另外一個線程來了,它插入了一個節點,那么仔細想一下Node oldTail = tail;的整個oldTail還等于整個新的Tail嗎?不等于了吧,那么既然不等于了,說明中間有線程被其它線程打斷了,那如果說卻是還是等于原來的oldTail,這個時候就說明沒有線程被打斷,那我們就接著設置尾巴,只要設置成功了OK , compareAndAetTail(oldTail,node)方法中的參數node就做為新的Tail了,所以用了CAS操作就不需要把原來的整個鏈表上鎖,這也是AQS在效率上比較高的核心 。
為什么是雙向鏈表?
其實你要添加一個線程節點的時候,需要看一下前面這個節點的狀態,如果前面的節點是持有線程的過程中 , 這個時候你就得在后面等著,如果說前面這個節點已經取消掉了,那你就應該越過這個節點,不去考慮它的狀態,所以你需要看前面節點狀態的時候 , 就必須是雙向的 。
接下來我們來解讀acquireQueued()這個方法,這個方法的意思是,在隊列里嘗試去獲得鎖 , 在隊列里排隊獲得鎖,那么它是怎么做到的呢?我們先大致走一遍這個方法,首先在for循環里獲得了Node節點的前置節點 , 然后判斷如果前置節點是頭節點,并且調用tryAcquire(arg)方法嘗試一下去得到這把鎖,獲得了頭節點以后 , 你設置的節點就是第二個 , 你這個節點要去和前置節點爭這把鎖,這個時候前置節點釋放了,如果你設置的節點拿到了這把鎖 , 拿到以后你設置的節點也就是當前節點就被設置為前置節點,如果沒有拿到這把鎖,當前節點就會阻塞等著 , 等著什么?等著前置節點叫醒你,所以它上來之后是競爭,怎么競爭呢?如果你是最后節點,你就下別說了,你就老老實實等著,如果你的前面已經是頭節點了,說明什么?說明快輪到我了,那我就跑一下,試試看能不能拿到這把鎖,說不定前置節點這會兒已經釋放這把鎖了,如果拿不著阻塞 , 阻塞以后干什么?等著前置節點釋放這把鎖以后,叫醒隊列里的線程,我想執行過程已經很明了了,打個比方 , 有一個人,他后面又有幾個人在后面排隊,這時候第一個人是獲得了這把鎖,永遠都是第一個人獲得鎖 , 那么后邊來的人干什么呢?站在隊伍后面排隊,然后他會探頭看他前面這個人是不是往前走了一步,如果走了,他也走一步,當后來的這個人排到了隊伍的第二個位置的時候 , 發現前面就是第一個人了,等這第一個人走了就輪到他了,他會看第一個人是不if(shouldParkAfterFailedAcquire(p,node))interrupted |= parkAndCheckInterrupt();}catch (Throwable t){cancelAcquire(node);if(interrupted)selfInterrupt();throw t;}}}}到這里AQS還有其它的一些細節我建議大家讀一下,比如AQS是怎么釋放鎖的,釋放完以后是怎么通知后置節點的,這個就比較簡單了,本章不再一一贅述了,那么在你掌握了讀源碼的技巧,以及在前面教你了AQS大體的結構,還教了你怎么去記住這個隊列,那么怎么去unlock這件事,就由大家自己去探索了 。
VarHandle
我們再來講一個細節,我們看addWaiter()這個方法里邊有一個node.setPrevRelaved(oldTail),這個方法的意思是把當前節點的前置節點寫成tail,進入這個方法你會看到PREV.set(this,p) , 那這個PREV是什么東西呢?當你真正去讀這個代碼,讀的特別細的時候你會發現 , PREV有這么一個東西叫VarHandle,這個VarHandle是什么呢?這個東西實在JDK1.9之后才有的,我們說一下這個VarHandle,Var叫變量(variable),Handle叫句柄,打個比方,比如我們寫了一句話叫Object o= new Object(),我們new了一個Object,這個時候內存里有一個小的引用“O”,指向一段大的內存這個內存里是new的那個Object對象,那么這個VarHandle指什么呢?指的是這個“引用”,我們思考一下,如果VarHandle代表“引用”,那么VarHandle所代表的這個值PREV是不是也這個“引用”呢?當然是了 。這個時候我們會生出一個疑問 , 本來已經有一個“O”指向這個Object對象了,為什么還要用另外一個引用也指向這個對象,這是為什么?
//JDK源碼public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg){//判斷是否得到鎖if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}private Node addWaiter(Node mode){//獲取當前要加進來的線程的node(節點)Node node = new Node(mode);for(;;){//回想一下AQS數據結構圖Node oldTail = tail;if(oldTail != null){//把我們這個新節點的前置節點設置在等待隊列的末端node.setPrevRelaved(oldTail);//CAS操作,把我們這個新節點設置為tail末端if(compareAndAetTail(oldTail,node)){oldTail.next = node;return node;}}else{是完事了,完事了他就變成頭節點了,就是這么個意思 。//JDK源碼public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg){//判斷是否得到鎖if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}final boolean acquireQueud(final Node node,int arg){boolean interrupted = false;try{for(;;){final Node p = node.predecessor();if(p == head && tryAcquire(arg)){setHead(node);p.next = null;return interrupted;}initializeSuncQueue();}}}final void setPrevRelaved(Node p){PREV.set(this,p);}private static final VarHandle PREV;static {try{MethodHandles.Lookup l = MethodHandles.lookup():PREV = l.findVarHandle(Node.class,"prev",Node.class);}catch(ReflectiveOperationException e){throw new ExceptionInInitializerError(e);}}}我們來看一個小程序,用這個小程序來理解這個VarHandle是什么意思,在這個類,我們定義了一個int類型的變量x,然后定義了一個VarHandle類型的變量handle,在靜態代碼塊里設置了handle指向T01_HelloVarHandle類里的x變量的引用,換句話說就是通過這個handle也能找到這個x,這么說比較精確,通過這個x能找到這個x , 里面裝了個8 , 通過handle也能找到這個x,這樣我們就可以通過這個handle來操作這個x的值,我們看main方法里 , 我們創建了T01_HelloVarHandle對象叫t,這個t對象里有一個x,里面還有個handle,這個handle也指向這個x,既然handle指向x,我當然可以(int)handle.get(t)拿到這個x的值不就是8嗎?我還可以通過handle.set(t,9)來設置這個t對象的x值為9,讀寫操作很容易理解,因為handle指向了這個變量,但是最關鍵的是通過這個handle可以做什么事呢?handle.compareAndSet(t,9,10),做原子性的修改值 , 我通過handle.compareAndSet(t,9,10)把9改成10改成100,這是原子性的操作,你通過x=100  , 它會是原子性的嗎?當然int類型是原子性的,但是long類型呢?就是說long類型連x=100都不是原子性的,所以通過這個handle可以做一些compareAndSet操作(原子操作),還可以handle.getAndAdd()操作這也是原子操作 , 比如說你原來寫x=x+10,這肯定不是原子操作,因為當你寫這句話的時候 , 你是需要加鎖的,要做到線程安全的話是需要加鎖的 , 但是如果通過handle是不需要的,所以這就是為什么會有VarHandle,VarHandle除了可以完成普通屬性的原子操作,還可以完成原子性的線程安全的操作,這也是VarHandle的含義 。
在JDK1.9之前要操作類里邊的成員變量的屬性,只能通過反射完成,用反射和用VarHandle的區別在于,VarHandle的效率要高的多,反射每次用之前要檢查 , VarHandle不需要,VarHandle可以理解為直接操縱二進制碼,所以VarHandle反射高的多
//小程序public class T01_HelloVarHandle {int x = 8;private static VarHandle handle;static {try {handle =MethodHandles.lookup().findVarHandle(T01_HelloVarHandle.class, "x", int.class);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}public static void main(String[] args) {T01_HelloVarHandle t = new T01_HelloVarHandle();//plain read / writeSystem.out.println((int)handle.get(t));handle.set(t,9);System.out.println(t.x);handle.compareAndSet(t, 9, 10);System.out.println(t.x);handle.getAndAdd(t, 10);System.out.println(t.x);}}ThreadLocal
首先我們來說一下ThreadLocal的含義,Thread線程,Local本地 , 線程本地到底是什么意思呢?我們來看下面這個小程序 , 我們可以看到這個小程序里定義了一個類,這個類叫Person , 類里面定義了一個String類型的變量name,name的值為“zhangsan”,在ThreadLocal1這個類里,我們實例化了這個Person類 , 然后在main方法里我們創建了兩個線程,第一個線程打印了p.name,第二個線程把p.name的值改為了“lisi”,兩個線程訪問了同一個對象
public class ThreadLocal1 {volatile static Person p = new Person();public static void main(String[] args) {new Thread(()->{try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(p.name);}).start();new Thread(()->{try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}p.name = "lisi";}).start();}}class Person {String name = "zhangsan";}這個小程序想想也知道 , 最后的結果肯定是打印出了“lisi”而不是“zhangsan”,因為原來的值雖然是“zhangsan” , 但是有一個線程1秒終之后把它變成“lisi”了,另一個線程兩秒鐘之后才打印出來,那它一定是變成“lisi”了,所以這件事很正常,但是有的時候我們想讓這個對象每個線程里都做到自己獨有的一份,我在訪問這個對象的時候,我一個線程要修改內容的時候要聯想另外一個線程,怎么做呢?我們來看這個小程序,這個小程序中,我們用到了ThreadLocal,我們看main方法中第二個線程,這個線程在1秒終之后往tl對象中設置了一個Person對象,雖然我們訪問的仍然是這個tl對象,第一個線程在兩秒鐘之后回去get獲取tl對象里面的值 , 第二個線程是1秒鐘之后往tl對象里set了一個值,從多線程普通的角度來講 , 既然我一個線程往里邊set了一個值,另外一個線程去get這個值的時候應該是能get到才對 , 但是很不幸的是,來看代碼,我們1秒終的時候set了一個值 , 兩秒鐘的時候去拿這個值是拿不到的,這個小程序證明了這一點,這是為什么呢?原因是如果我們用ThreadLocal的時候,里邊設置的這個值是線程獨有的,線程獨有的是什么意思呢?就是說這個線程里用到這個ThreadLocal的時候 , 只有自己去往里設置,設置的是只有自己線程里才能訪問到的Person,而另外一個線程要訪問的時候,設置也是自己線程才能訪問到的Person,這就是ThreadLocal的含義
public class ThreadLocal2 {//volatile static Person p = new Person();static ThreadLocal<Person> tl = new ThreadLocal<>();public static void main(String[] args) {new Thread(()->{try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(tl.get());}).start();new Thread(()->{try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}tl.set(new Person());}).start();}static class Person {String name = "zhangsan";}講到這里,有沒有想過我就是往tl對象里設置了一個Person,但是設置好了以后,另一個線程為什么就是讀取不到呢?這到底是怎么做到的呢?要想理解怎么做到的,得去讀一下ThreadLocal的源碼,我們嘗試一下讀ThreadLocal的源碼
ThreadLocal源碼
我們先來看一個ThreadLocal源碼的set方法,ThreadLocal往里邊設置值的時候是怎么設置的呢?首先拿到當前線程,這是你會發現,這個set方法里多了一個容器ThreadLocalMap,這個容器是一個map,是一個key/value對 , 然后再往下讀你會發現,其實這個值是設置到了map里面,而且這個map是什么樣的 , key設置的是this,value設置的是我們想要的那個值,這個this就是當前對象ThreadLocal,value就是Person類,這么理解就行了,如果map不等于空的情況下就設置進去就行了,如果等于空呢?就創建一個map
//ThraedLocal源碼public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}}我們回過頭來看這個map,ThreadLocalMap map=getMap(t),我們來看看這個map到底在哪里 , 我們點擊到了getMap這個方法看到,它的返回值是t.threadLocals
//ThreadLocal源碼public class ThreadLocal<T> {public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t){return t.threadLocals;}}我們進入這個t.threadLocals,你會發現ThreadLocalMap這個東西在哪里呢?居然是在Thread這個類里 , 所以說這個map是在Thred類里的
【深入AQS源碼閱讀與強軟弱虛4種引用以及ThreadLocal原理與源碼 var是什么意思】public class Thread implements Runnable{ThreadLocal.ThreadLocalMap threadLocals = null;}這個時候我們應該明白,map的set方法其實就是設置當前線程里面的map:
·set
- Thread.currentThread.map(ThreadLocal,person)
所以這個時候你會發現,原來Person類被set到了,當前線程里的某一個map里面去了,這個時候,我們是不是就能想明白了,我set了一個值以后,為什么其他線程訪問不到?我們注重“當前線程”這個段話 , 所以個t1線程set了一個Person對象到自己的map里,t2線程去訪問的也是自己的屬于t2線程的map,所以是讀不到值的,因此你使用ThreadLocal的時候 , 你用set和get就完全的把他隔離開了,就是我自己線程里面所特有的,其它的線程是沒有的,以前我們的理解是都在一個map , 然而并不是,所以你得讀源碼,讀源碼你就明白了
為什么要用ThreadLocal?
我們根據Spirng的聲明式事務來解析,為什么要用ThreadLocal,聲明式事務一般來講我們是要通過數據庫的,但是我們知道Spring結合Mybatis,我們是可以把整個事務寫在配置文件中的,而這個配置文件里的事務,它實際上是管理了一系列的方法,方法1、方法2、方法3....,而這些方法里面可能寫了,比方說第1個方法寫了去配置文件里拿到數據庫連接Connection , 第2個、第3個都是一樣去拿數據庫連接,然后聲明式事務可以把這幾個方法合在一起,視為一個完整的事務 , 如果說在這些方法里,每一個方法拿的連接,它拿的不是同一個對象,你覺的這個東西能形成一個完整的事務嗎?Connection會放到一個連接池里邊 , 如果第1個方法拿的是第1個Connection,第2個拿的是第2個,第3個拿的是第3個,這東西能形成一個完整的事務嗎?百分之一萬的不可能,沒聽說過不同的Connection還能形成一個完整的事務的,那么怎么保證這么多Connection之間保證是同一個Connection呢?把這個Connection放到這個線程的本地對象里ThreadLocal里面,以后再拿的時候,實際上我是從ThreadLocal里拿的,第1個方法拿的時候就把Connection放到ThreadLocal里面,后面的方法要拿的時候,從ThreadLocal里直接拿,不從線程池拿 。
java的四種引用:強軟弱虛
其實java有4種引用 , 4種可分為強、軟、弱、虛
gc:java的垃圾回收機制
首先明白什么是一個引用?
Object o = new Object()這就是一個引用了,一個變量指向new出來的對象,這就叫以個引用,引用這個東西,在java里面分4種 , 普通的引用比如Object o = new Object(),這個就叫強引用,強引用有什么特點呢?我們來看下面的小程序 。
強引用
首先看到我們有一個類叫M , 在這個類里我重寫了一個方法叫fifinalize() , 我們可以看到這個方法是已經被廢棄的方法,為什么要重寫他呢?主要想說明一下在垃圾回收的過程中,各種引用它不同的表現,垃圾回收的時候,它是會調用fifinalize()這個方法的,什么意思?當我們new出來一個象,在java語言里是不需要手動回收的,C和C++是需要的 , 在這種情況下,java的垃圾回收機制會自動的幫你回收這個對象,但是它回收對象的時候它會調用fifinalize()這個方法,我們重寫這個方法之后我們能觀察出來,它什么時候被垃圾回收了,什么時候被調用了,我在這里重寫這個方法的含義是為了以后面試的時候方便你們造火箭,讓你們觀察結果用的,并不說以后在什么情況下需要重寫這個方法,這個方法永遠都不需要
重寫,而且也不應該被重寫 。
public class M {@Overrideprotected void finalize() throws Throwable {System.out.println("finalize");}}我們來解釋一下普通的引用NormalReference , 普通的引用也就是默認的引用,默認的引用就是說,只要有一個應用指向這個對象,那么垃圾回收器一定不會回收它 , 這就是普通的引用 , 也就是強引用 , 為什么不會回收?因為有引用指向,所以不會回收,只有沒有引用指向的時候才會回收,指向誰?指向你創建的那個對象 。
我們來看下面這個小程序,我new了一個m出來,然后調用了System.gc(),顯式的來調用一下垃圾回收,讓垃圾回收嘗試一下 , 看能不能回收這個m,需要注意的是,要在最后阻塞住當前線程,為什么?
因為System.gc()是跑在別的線程里邊的,如果main線程直接退出了,那整個程序就退出了,那gc不gc就沒有什么意義了,所以你要阻塞當前線程,在這里調用了System.in.read()阻塞方法 , 它沒有什么含義 , 只是阻塞當前線程的意思 。
阻塞當前線程就是讓當前整個程序不會停止 , 程序運行起來你會發現 , 程序永遠不會輸出,為什么呢?
我們想一下,這個M是有一個小引用m指向它的 , 那有引用指向它,它肯定不是垃圾,不是垃圾的話一定不會被回收 。
public class T01_NormalReference {public static void main(String[] args) throws IOException {M m = new M();System.gc(); //DisableExplicitGCSystem.in.read();}}那你想讓它顯示回收 , 怎么做呢?我們讓m=null,m=nul的意思就是不會再有引用指向這個M對象了,也就是說把m和new M()之間的引用給打斷了,不再有關聯了 , 這個時候再運行程序,你會發現,輸出了:fifinalize,說明什么?說明M對象被回收了,綜上所述這個就是強引用
public class T01_NormalReference {public static void main(String[] args) throws IOException {M m = new M();m = null;System.gc(); //DisableExplicitGCSystem.in.read();}}我們來看一下什么是軟引用,要聲明一個軟引用,要在內存里面體現一個軟引用,怎么做呢?我們來看下面這個小程序SoftReference叫軟引用,Soft是軟的意思 。
我們來分析SoftReference<byte[]> m = new SoftReference<>(new byte[1024