<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入AQS源碼閱讀與強軟弱虛4種引用以及ThreadLocal原理與源碼

          共 19541字,需瀏覽 40分鐘

           ·

          2022-06-08 15:44

          前言

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


          通過ReentrantLock來解讀AQS源碼

          AQS大家還記得嗎?最核心的是它的一個共享的int類型值叫做state,這個state用來干什么,其實主要是看他的子類是怎么實現(xiàn)的,比如ReentrantLock這個state是用來干什么的?拿這個state來記錄這個線程到底重入了多少次,比如說有一個線程拿到state這個把鎖了,state的值就從0變成了1,這個線程又重入了一次,state就變成2了,又重入一次就變成3等等,什么時候釋放了呢?從3變成2變成1變成0就釋放了,這個就是AQS核心的東西,一個數(shù),這個數(shù)代表了什么要看子類怎么去實現(xiàn)它,那么在這個state核心上還會有一堆的線程節(jié)點,當然這個節(jié)點是node,每個node里面包含一個線程,我們稱為線程節(jié)點,這么多的線程節(jié)點去爭用這個state,誰拿到了state,就表示誰得到了這把鎖,AQS得核心就是一個共享的數(shù)據(jù),一堆互相搶奪競爭的線程,這個就是AQS。

          我們接著上節(jié)課來講,首先給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()方法里里面,我們可以讀到它調(diào)用了sync.acquire(1),

          //JDK源碼public class ReentrantLock implements Lock, java.io.Serializable {public void lock(){
          sync.acquire(1);
          }
          }

          再跟進到acquire(1)里,可以看到acquire(1)里又調(diào)用了我們自己定義自己寫的那個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)里又調(diào)用了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)我們讀進去會發(fā)現(xiàn)它的里面就調(diào)用到了state這個值,到這里我們就接上了上一章講的,nonfairTrytAcquire(acquires)里是這樣的,首先拿到當前線程,拿到state的值,然后進行if判斷,如果state的值為0,說明沒人上鎖,沒人上鎖怎么辦呢?就給自己上鎖,當前線程就拿到這把鎖,拿到這個把鎖的操作用到了CAS(compareAndSetState)的操作,從0讓他變成1,state的值設(shè)置為1以后,設(shè)置當前線程是獨一無二的擁有這把鎖的線程,否則如果當前線程已經(jīng)占有這把鎖了,怎么辦?很簡單我們在原來的基礎(chǔ)上加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核心數(shù)值stateint c getState();//如果數(shù)值為0說明沒人上鎖if(c == 0){//給當線程上鎖if(compareAndSetState(0,acquires)){//設(shè)置當前線程為獨一無二擁有這把鎖的線程setExclusiveOwnerThread(current);return true}
          }//判斷當前線程是否擁有這個把鎖else if(current == getExclusiveOwnerThread){//設(shè)置重入int nextc = c + acquires;if(nextc < 0)throw new Error("Maximum lock count wxceeded");
          setState(nextc);return true;
          }return false;
          }
          }

          我們跟進到tryAcquire(arg)是拿到了這把鎖以后的操作,如果拿不到呢?如果拿不到它實際上是調(diào)用了acquireQueued()方法,acquireQueued()方法里又調(diào)用了addWaiter(Node.EXCLUSIVE)然后后面寫一個arg(數(shù)值1),方法結(jié)構(gòu)是這樣的acquireQueued(addWaiter(Node.EXCLUSIVE),arg)通過acquireQueued這個方法名字你猜一下這是干什么的,你想如果是我得到這把鎖了,想一下后面的acquireQueued是不用運行的,如果沒有得到這把鎖,后面的acquireQueued()才需要運行,那么想一下沒有得到這把鎖的時候它會運行什么呢?他會運行acquireQueued,Queued隊列,acquire獲得,跑到隊列里去獲得,那意思是什么?排隊去,那排隊的時候需要傳遞兩個參數(shù),第一個參數(shù)是某個方法的返回值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就是共享鎖,首先是獲得當前要加進等待者隊列的線程的節(jié)點,然后是一個死循環(huán),這意思就是說我不干成這件事我誓不罷休,那它干了一件什么事呢?

          //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(節(jié)點)Node node = new Node(mode);for(;;){//回想一下AQS數(shù)據(jù)結(jié)構(gòu)圖Node oldTail = tail;if(oldTail != null){//把我們這個新節(jié)點的前置節(jié)點設(shè)置在等待隊列的末端node.setPrevRelaved(oldTail);//CAS操作,把我們這個新節(jié)點設(shè)置為tail末端if(compareAndAetTail(oldTail,node)){
          oldTail.next = node;return node;
          }
          }else{
          initializeSuncQueue();
          }
          }
          }
          }

          你想想想看,我們回想一下AQS數(shù)據(jù)結(jié)構(gòu)圖,就是他有一個int類型的數(shù)叫state,然后在state下面排了一個隊列,這個隊列是個雙向的鏈表有一個head和一個tail,現(xiàn)在你要往這個隊列中加一個節(jié)點上來,要排隊嘛,我們仔細想一下加節(jié)點的話,應(yīng)該得加到這個隊列的末端是不是?它是怎么做到的呢?首先把tail記錄在oldTail里,oldTail指向這個tail了,如果oldTail不等于空,它會把我們這個新節(jié)點的前置節(jié)點設(shè)置在這個隊列的末端,接下來再次用到CAS操作,把我們這個新的節(jié)點設(shè)置為tail,整段代碼看似繁瑣,其實很簡單,就是要把當前要加進等待者隊列的線程的節(jié)點加到等待隊列的末端,這里提一點,加到末端為什么要用CAS操作呢?因為CAS效率高,這個問題關(guān)系到AQS的核心操作,理解了這一點,你就理解了AQS為什么效率高,我們接著講源碼,這個增加線程節(jié)點操作,如果沒有成功,那么就會不斷的試,一直試到我們的這個node節(jié)點被加到線程隊列末端為止,意思就是說,其它的節(jié)點也加到線程隊列末端了,我無非就是等著你其它的線程都加到末端了,我加最后一個,不管怎么樣我都要加到線程末端去為止。

          源碼讀這里我們可以總結(jié)得出,AQS(
          AbstractQueuedSynchronizer)的核心就是用CAS(compareAndSet)去操作head和tail,就是說用CAS操作代替了鎖整條雙向鏈表的操作

          通過AQS是如何設(shè)置鏈表尾巴的來理解AQS為什么效率這么高

          我們的思路是什么呢?假如你要往一個鏈表上添加尾巴,尤其是好多線程都要往鏈表上添加尾巴,我們仔細想想看用普通的方法怎么做?第一點要加鎖這一點是肯定的,因為多線程,你要保證線程安全,一般的情況下,我們會鎖定整個鏈表(Sync),我們的新線程來了以后,要加到尾巴上,這樣很正常,但是我們鎖定整個鏈表的話,鎖的太多太大了,現(xiàn)在呢它用的并不是鎖定整個鏈表的方法,而是只觀測tail這一個節(jié)點就可以了,怎么做到的呢?compareAndAetTail(oldTail,node),中oldTail是它的預(yù)期值,假如說我們想把當前線程設(shè)置為整個鏈表尾巴的過程中,另外一個線程來了,它插入了一個節(jié)點,那么仔細想一下Node oldTail = tail;的整個oldTail還等于整個新的Tail嗎?不等于了吧,那么既然不等于了,說明中間有線程被其它線程打斷了,那如果說卻是還是等于原來的oldTail,這個時候就說明沒有線程被打斷,那我們就接著設(shè)置尾巴,只要設(shè)置成功了OK,compareAndAetTail(oldTail,node)方法中的參數(shù)node就做為新的Tail了,所以用了CAS操作就不需要把原來的整個鏈表上鎖,這也是AQS在效率上比較高的核心。

          為什么是雙向鏈表?

          其實你要添加一個線程節(jié)點的時候,需要看一下前面這個節(jié)點的狀態(tài),如果前面的節(jié)點是持有線程的過程中,這個時候你就得在后面等著,如果說前面這個節(jié)點已經(jīng)取消掉了,那你就應(yīng)該越過這個節(jié)點,不去考慮它的狀態(tài),所以你需要看前面節(jié)點狀態(tài)的時候,就必須是雙向的。

          接下來我們來解讀acquireQueued()這個方法,這個方法的意思是,在隊列里嘗試去獲得鎖,在隊列里排隊獲得鎖,那么它是怎么做到的呢?我們先大致走一遍這個方法,首先在for循環(huán)里獲得了Node節(jié)點的前置節(jié)點,然后判斷如果前置節(jié)點是頭節(jié)點,并且調(diào)用tryAcquire(arg)方法嘗試一下去得到這把鎖,獲得了頭節(jié)點以后,你設(shè)置的節(jié)點就是第二個,你這個節(jié)點要去和前置節(jié)點爭這把鎖,這個時候前置節(jié)點釋放了,如果你設(shè)置的節(jié)點拿到了這把鎖,拿到以后你設(shè)置的節(jié)點也就是當前節(jié)點就被設(shè)置為前置節(jié)點,如果沒有拿到這把鎖,當前節(jié)點就會阻塞等著,等著什么?等著前置節(jié)點叫醒你,所以它上來之后是競爭,怎么競爭呢?如果你是最后節(jié)點,你就下別說了,你就老老實實等著,如果你的前面已經(jīng)是頭節(jié)點了,說明什么?說明快輪到我了,那我就跑一下,試試看能不能拿到這把鎖,說不定前置節(jié)點這會兒已經(jīng)釋放這把鎖了,如果拿不著阻塞,阻塞以后干什么?等著前置節(jié)點釋放這把鎖以后,叫醒隊列里的線程,我想執(zhí)行過程已經(jīng)很明了了,打個比方,有一個人,他后面又有幾個人在后面排隊,這時候第一個人是獲得了這把鎖,永遠都是第一個人獲得鎖,那么后邊來的人干什么呢?站在隊伍后面排隊,然后他會探頭看他前面這個人是不是往前走了一步,如果走了,他也走一步,當后來的這個人排到了隊伍的第二個位置的時候,發(fā)現(xiàn)前面就是第一個人了,等這第一個人走了就輪到他了,他會看第一個人是不if(shouldParkAfterFailedAcquire(p,node))
          interrupted |= parkAndCheckInterrupt();
          }catch (Throwable t){
          cancelAcquire(node);if(interrupted)
          selfInterrupt();throw t;
          }
          }
          }
          }

          到這里AQS還有其它的一些細節(jié)我建議大家讀一下,比如AQS是怎么釋放鎖的,釋放完以后是怎么通知后置節(jié)點的,這個就比較簡單了,本章不再一一贅述了,那么在你掌握了讀源碼的技巧,以及在前面教你了AQS大體的結(jié)構(gòu),還教了你怎么去記住這個隊列,那么怎么去unlock這件事,就由大家自己去探索了。

          VarHandle

          我們再來講一個細節(jié),我們看addWaiter()這個方法里邊有一個node.setPrevRelaved(oldTail),這個方法的意思是把當前節(jié)點的前置節(jié)點寫成tail,進入這個方法你會看到PREV.set(this,p),那這個PREV是什么東西呢?當你真正去讀這個代碼,讀的特別細的時候你會發(fā)現(xiàn),PREV有這么一個東西叫VarHandle,這個VarHandle是什么呢?這個東西實在JDK1.9之后才有的,我們說一下這個VarHandle,Var叫變量(variable),Handle叫句柄,打個比方,比如我們寫了一句話叫Object o= new Object(),我們new了一個Object,這個時候內(nèi)存里有一個小的引用“O”,指向一段大的內(nèi)存這個內(nèi)存里是new的那個Object對象,那么這個VarHandle指什么呢?指的是這個“引用”,我們思考一下,如果VarHandle代表“引用”,那么VarHandle所代表的這個值PREV是不是也這個“引用”呢?當然是了。這個時候我們會生出一個疑問,本來已經(jīng)有一個“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(節(jié)點)Node node = new Node(mode);for(;;){//回想一下AQS數(shù)據(jù)結(jié)構(gòu)圖Node oldTail = tail;if(oldTail != null){//把我們這個新節(jié)點的前置節(jié)點設(shè)置在等待隊列的末端node.setPrevRelaved(oldTail);//CAS操作,把我們這個新節(jié)點設(shè)置為tail末端if(compareAndAetTail(oldTail,node)){
          oldTail.next = node;return node;
          }
          }else{是完事了,完事了他就變成頭節(jié)點了,就是這么個意思。//JDK源碼public abstract class AbstractQueuedSynchronizer
          extends AbstractOwnableSynchronizer
          implements 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,在靜態(tài)代碼塊里設(shè)置了handle指向T01_HelloVarHandle類里的x變量的引用,換句話說就是通過這個handle也能找到這個x,這么說比較精確,通過這個x能找到這個x,里面裝了個8,通過handle也能找到這個x,這樣我們就可以通過這個handle來操作這個x的值,我們看main方法里,我們創(chuàng)建了T01_HelloVarHandle對象叫t,這個t對象里有一個x,里面還有個handle,這個handle也指向這個x,既然handle指向x,我當然可以(int)handle.get(t)拿到這個x的值不就是8嗎?我還可以通過handle.set(t,9)來設(shè)置這個t對象的x值為9,讀寫操作很容易理解,因為handle指向了這個變量,但是最關(guān)鍵的是通過這個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的區(qū)別在于,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方法里我們創(chuàng)建了兩個線程,第一個線程打印了p.name,第二個線程把p.name的值改為了“l(fā)isi”,兩個線程訪問了同一個對象

          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";
          }

          這個小程序想想也知道,最后的結(jié)果肯定是打印出了“l(fā)isi”而不是“zhangsan”,因為原來的值雖然是“zhangsan”,但是有一個線程1秒終之后把它變成“l(fā)isi”了,另一個線程兩秒鐘之后才打印出來,那它一定是變成“l(fā)isi”了,所以這件事很正常,但是有的時候我們想讓這個對象每個線程里都做到自己獨有的一份,我在訪問這個對象的時候,我一個線程要修改內(nèi)容的時候要聯(lián)想另外一個線程,怎么做呢?我們來看這個小程序,這個小程序中,我們用到了ThreadLocal,我們看main方法中第二個線程,這個線程在1秒終之后往tl對象中設(shè)置了一個Person對象,雖然我們訪問的仍然是這個tl對象,第一個線程在兩秒鐘之后回去get獲取tl對象里面的值,第二個線程是1秒鐘之后往tl對象里set了一個值,從多線程普通的角度來講,既然我一個線程往里邊set了一個值,另外一個線程去get這個值的時候應(yīng)該是能get到才對,但是很不幸的是,來看代碼,我們1秒終的時候set了一個值,兩秒鐘的時候去拿這個值是拿不到的,這個小程序證明了這一點,這是為什么呢?原因是如果我們用ThreadLocal的時候,里邊設(shè)置的這個值是線程獨有的,線程獨有的是什么意思呢?就是說這個線程里用到這個ThreadLocal的時候,只有自己去往里設(shè)置,設(shè)置的是只有自己線程里才能訪問到的Person,而另外一個線程要訪問的時候,設(shè)置也是自己線程才能訪問到的Person,這就是ThreadLocal的含義

          public class ThreadLocal2 {//volatile static Person p = new Person();static ThreadLocal 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對象里設(shè)置了一個Person,但是設(shè)置好了以后,另一個線程為什么就是讀取不到呢?這到底是怎么做到的呢?要想理解怎么做到的,得去讀一下ThreadLocal的源碼,我們嘗試一下讀ThreadLocal的源碼

          ThreadLocal源碼

          我們先來看一個ThreadLocal源碼的set方法,ThreadLocal往里邊設(shè)置值的時候是怎么設(shè)置的呢?首先拿到當前線程,這是你會發(fā)現(xiàn),這個set方法里多了一個容器ThreadLocalMap,這個容器是一個map,是一個key/value對,然后再往下讀你會發(fā)現(xiàn),其實這個值是設(shè)置到了map里面,而且這個map是什么樣的,key設(shè)置的是this,value設(shè)置的是我們想要的那個值,這個this就是當前對象ThreadLocal,value就是Person類,這么理解就行了,如果map不等于空的情況下就設(shè)置進去就行了,如果等于空呢?就創(chuàng)建一個map

          //ThraedLocal源碼public class ThreadLocal {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 {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,你會發(fā)現(xiàn)ThreadLocalMap這個東西在哪里呢?居然是在Thread這個類里,所以說這個map是在Thred類里的

          public class Thread implements Runnable{
          ThreadLocal.ThreadLocalMap threadLocals = null;
          }

          這個時候我們應(yīng)該明白,map的set方法其實就是設(shè)置當前線程里面的map:

          ·set

          - Thread.currentThread.map(ThreadLocal,person)

          所以這個時候你會發(fā)現(xiàn),原來Person類被set到了,當前線程里的某一個map里面去了,這個時候,我們是不是就能想明白了,我set了一個值以后,為什么其他線程訪問不到?我們注重“當前線程”這個段話,所以個t1線程set了一個Person對象到自己的map里,t2線程去訪問的也是自己的屬于t2線程的map,所以是讀不到值的,因此你使用ThreadLocal的時候,你用set和get就完全的把他隔離開了,就是我自己線程里面所特有的,其它的線程是沒有的,以前我們的理解是都在一個map,然而并不是,所以你得讀源碼,讀源碼你就明白了

          為什么要用ThreadLocal?

          我們根據(jù)Spirng的聲明式事務(wù)來解析,為什么要用ThreadLocal,聲明式事務(wù)一般來講我們是要通過數(shù)據(jù)庫的,但是我們知道Spring結(jié)合Mybatis,我們是可以把整個事務(wù)寫在配置文件中的,而這個配置文件里的事務(wù),它實際上是管理了一系列的方法,方法1、方法2、方法3....,而這些方法里面可能寫了,比方說第1個方法寫了去配置文件里拿到數(shù)據(jù)庫連接Connection,第2個、第3個都是一樣去拿數(shù)據(jù)庫連接,然后聲明式事務(wù)可以把這幾個方法合在一起,視為一個完整的事務(wù),如果說在這些方法里,每一個方法拿的連接,它拿的不是同一個對象,你覺的這個東西能形成一個完整的事務(wù)嗎?Connection會放到一個連接池里邊,如果第1個方法拿的是第1個Connection,第2個拿的是第2個,第3個拿的是第3個,這東西能形成一個完整的事務(wù)嗎?百分之一萬的不可能,沒聽說過不同的Connection還能形成一個完整的事務(wù)的,那么怎么保證這么多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(),我們可以看到這個方法是已經(jīng)被廢棄的方法,為什么要重寫他呢?主要想說明一下在垃圾回收的過程中,各種引用它不同的表現(xiàn),垃圾回收的時候,它是會調(diào)用fifinalize()這個方法的,什么意思?當我們new出來一個象,在java語言里是不需要手動回收的,C和C++是需要的,在這種情況下,java的垃圾回收機制會自動的幫你回收這個對象,但是它回收對象的時候它會調(diào)用fifinalize()這個方法,我們重寫這個方法之后我們能觀察出來,它什么時候被垃圾回收了,什么時候被調(diào)用了,我在這里重寫這個方法的含義是為了以后面試的時候方便你們造火箭,讓你們觀察結(jié)果用的,并不說以后在什么情況下需要重寫這個方法,這個方法永遠都不需要

          重寫,而且也不應(yīng)該被重寫。

          public class M {@Overrideprotected void finalize() throws Throwable {
          System.out.println("finalize");
          }
          }

          我們來解釋一下普通的引用NormalReference,普通的引用也就是默認的引用,默認的引用就是說,只要有一個應(yīng)用指向這個對象,那么垃圾回收器一定不會回收它,這就是普通的引用,也就是強引用,為什么不會回收?因為有引用指向,所以不會回收,只有沒有引用指向的時候才會回收,指向誰?指向你創(chuàng)建的那個對象。

          我們來看下面這個小程序,我new了一個m出來,然后調(diào)用了System.gc(),顯式的來調(diào)用一下垃圾回收,讓垃圾回收嘗試一下,看能不能回收這個m,需要注意的是,要在最后阻塞住當前線程,為什么?

          因為System.gc()是跑在別的線程里邊的,如果main線程直接退出了,那整個程序就退出了,那gc不gc就沒有什么意義了,所以你要阻塞當前線程,在這里調(diào)用了System.in.read()阻塞方法,它沒有什么含義,只是阻塞當前線程的意思。

          阻塞當前線程就是讓當前整個程序不會停止,程序運行起來你會發(fā)現(xiàn),程序永遠不會輸出,為什么呢?

          我們想一下,這個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()之間的引用給打斷了,不再有關(guān)聯(lián)了,這個時候再運行程序,你會發(fā)現(xiàn),輸出了: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();
          }
          }

          我們來看一下什么是軟引用,要聲明一個軟引用,要在內(nèi)存里面體現(xiàn)一個軟引用,怎么做呢?我們來看下面這個小程序SoftReference叫軟引用,Soft是軟的意思。

          我們來分析SoftReference m = new SoftReference<>(new byte[1024102410]),首先棧內(nèi)存里有一個m,指向堆內(nèi)存里的SoftReference軟引用對象,注意這個軟引用對象里邊又有一個對象,可以想象一下軟引用里邊一個引用指向了一個10MB大小的字節(jié)數(shù)組,然后通過m.get()拿到這個字節(jié)數(shù)組然后輸出,它會輸出HashCode值,然后調(diào)用System.gc(),讓垃圾回收去運行,那么如果說,這個時候如果gc運行完以后,字節(jié)數(shù)組被回收了,你再次打印m.get()的時候,它應(yīng)該是個null值了,然后在后面又分配了一個15MB大小的數(shù)組,最后再打印m.get(),注意還是第一個m的get,如果這個時候被回收了它應(yīng)該打印null值,沒有被回收的話,應(yīng)該打印一個HashCode值

          軟引用

          我們來說一下軟引用的含義,當有一個對象(字節(jié)數(shù)組)被一個軟引用所指向的時候,只有系統(tǒng)內(nèi)存不夠用的時候,才會回收它(字節(jié)數(shù)組)

          我們來跑一下這個程序,在程序運行的時候,我們來設(shè)置一下堆內(nèi)存最大為20MB,就是說我堆內(nèi)存直接給你分配20MB,你要設(shè)置一下堆內(nèi)存,如果不設(shè)置它永遠不會回收的,這個時候我們運行程序你會發(fā)現(xiàn),第三次調(diào)用m.get()輸出的時候,輸出的值為null,我們來分析一下,第一次我們的堆內(nèi)存這個時候最多只能放20MB,第一次創(chuàng)建字節(jié)數(shù)組的時候分配了10MB,這個時候堆內(nèi)存是能分配下的,這個時候我調(diào)用了gc來做回收是無法回收的,因為堆內(nèi)存夠用,第二次創(chuàng)建字節(jié)數(shù)組的時候分配了15MB,這個時候?qū)?nèi)存的內(nèi)存還夠15MB嗎?肯定是不夠的,不夠了怎么辦?清理,清理的時候既然內(nèi)存不夠用,就會把你這個軟引用給干掉,然后15MB內(nèi)存分配進去,所以這個時候你再去get第一個字節(jié)數(shù)組的時候它是一個null值,這是就是軟引用的含義,用大腿想一想這個軟引用的使用場景:做緩存用,這個東西主要做緩存用

          //軟引用非常適合緩存使用
          public class T02_SoftReference {public static void main(String[] args) {
          SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);//m = null;System.out.println(m.get());
          System.gc();try {
          Thread.sleep(500);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          System.out.println(m.get());//再分配一個數(shù)組,heap將裝不下,這時候系統(tǒng)會垃圾回收,先回收一次,如果不夠,會//把軟引用干掉byte[] b = new byte[1024*1024*15];
          System.out.println(m.get());
          }
          }

          舉個例子你從內(nèi)存里邊讀一個大圖片,特別的圖片出來,然你用完了之后就沒什么用了,你可以放在內(nèi)存里邊緩存在那里,要用的時候直接從內(nèi)存里邊拿,但是由于這個大圖片占的空間比較大,如果不用的話,那別人也要用這塊空間,那就把它干掉,這個時候就用到了軟引用

          再舉個例子,從數(shù)據(jù)庫里讀一大堆的數(shù)據(jù)出來,這個數(shù)據(jù)有可能比如說你按一下back,我還可以訪問到這些數(shù)據(jù),如果內(nèi)存里邊有的話,我就不用從數(shù)據(jù)庫里拿了,這個時候我也可以用軟應(yīng)用,需要新的空間你可以把我干掉,沒問題我下次去數(shù)據(jù)庫取就行了,但是新空間還夠用的時候,我下次就不用從數(shù)據(jù)庫取,直接從內(nèi)存里拿就行了

          弱引用

          接下來我們來說一下弱引用,弱引用的意思是,只要遭遇到gc就會回收,剛才我們說到軟引用的概念是,垃圾回收不一定回收它,只有空間不夠了才會回收它,所以軟引用的生存周期還是比較長的,我們接著說弱應(yīng)用,弱引用就是說,只要垃圾回收看到這個引用是一個特別弱的引用指向的時候,就直接把它給干掉

          我們來看這個小程序,WeakReference m = new WeakReference<>(new M()),這里我們new了一個對象這是第一點,這m指向的是一個弱引用,這個弱引用里邊有一個引用,是弱弱的指向了new出來的另外一個M對象,然后通過m.get()來打印這個M對象,接下來gc調(diào)用垃圾回收,如果他它沒有被回收,你接下來get還能拿到,反之則不能

          public class T03_WeakReference {public static void main(String[] args) {
          WeakReference m = new WeakReference<>(new M());
          System.out.println(m.get());
          System.gc();
          System.out.println(m.get());
          ThreadLocal tl = new ThreadLocal<>();
          tl.set(new M());
          tl.remove();
          }
          }

          運行程序以后我們看到,第一次打印出來了,第二次打印之前調(diào)用了gc,所以第二次打印出了null值,那我們想這東西本來指向一個弱引用對象,小m指向這個弱引用對象,這個弱引用對象里邊有一個弱弱的引用指向了另外一個大M對象,但這個大M對象垃圾回收一來就把它干掉了,那么把它創(chuàng)建出來有什么用呢?這個東西作用就在于,如果有另外一個強引用指向了這個弱引用之后,只要這個強引用消失掉,這個弱引用就應(yīng)該去被回收,我就不用管了,只要這個強引用消失掉,我就不用管這個弱引用了,這個弱引用也一定是被回收了,這個東西用在什么地方呢?一般用在容器里

          我來講一個弱引用最典型的一個應(yīng)用ThreadLocal,我們來看下面的代碼,注意看我們創(chuàng)建了一個對象叫tl,這個tl對象的引用指向ThreadLocal對象,ThreadLocal對象里又指向了一個M對象,這是我們最直觀的想法!

          [05_01](C:\Users\Admin\Desktop\images\05_01.png)public class T03_WeakReference{public static void main(String[] args) {
          WeakReference m = new WeakReference<>(new M());
          System.out.println(m.get());
          System.gc();
          System.out.println(m.get());
          ThreadLocal tl = new ThreadLocal<>();
          tl.set(new M());
          tl.remove();
          }
          }


          我們的想法是這么個想法,但是它里面到底執(zhí)行了一個什么樣的操作呢?我們來看上面的圖,從左往右看,首先我們來說當前肯定是有一個線程的,任何一個方法肯定是要運行在某個線程里的,這個線程是我的主線程,在這個線程里有一個線程的局部變量叫tl,tl它new出來了一個ThreadLoal對象,這是一個強引用沒問題,然后我又往ThreadLocal里放了一個對象,可是你們是不是還記得,往ThreadLocal里放對象的話,實際上是放到了當前線程的一個threadLocals變量里面,這個threadLocals變量指向的是一個Map,也就是我們把這個M對象給放到了這Map里面,它的key是我們的ThreadLocal對象,value是我們的M對象,我們來回想一下,往ThreadLocal里面set的時候,先拿到當前線程,然后拿到當前線程里面的那個Map,然后通過這個Map把ThreadLocal對象給set進去,這個map.set(this, value)方法中的this是誰?是ThreadLocal對象,set進去的時候往里面放了這么一個東西叫Entry,這個Entry又是什么呢?注意看代碼,這個Entry是從弱引用WeakReference繼承出來的

          現(xiàn)在也就是說有一個Entry,它的父類是一個WeakReference,這個WeakReference里面裝的是什么?

          是ThreadLocal對象,也就是說這個Entry一個key一個value,而這個Entry的key的類型是ThreadLocal,這個value當然就是我們的那個M的值或者其它什么值這個不重要,這個key是ThreadLocal,而由于這個Entry是從ThreadLocal繼承的,在Entry構(gòu)造的時候調(diào)用了super(k),這個k指的就是ThreadLocal對象,我們想一下WeakReference不就相當于new WeakReference key嗎?

          //ThreadLocal源碼public class ThreadLocal {public void set(T value) {//獲取當前線程Thread t = Thread.currentThread();//獲取當前線程的MapThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
          }ThreadLocalMap getMap(Thread t){return t.threadLocals;
          }private void set(ThreadLocal key, Object value) {
          Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
          ThreadLocal k = e.geif (k == key) {
          e.value = value;return;if (k == null) {
          replaceStaleEntry(key, value, i);return;
          }
          tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
          }static class Entry extends WeakReference> {/** The value associated with this ThreadLocal. */Object value;
          Entry(ThreadLocal k, Object v) {
          super(k);
          value = v;
          }
          }
          }

          我們來看下面圖中從左開始看,這時候我們應(yīng)該明白了,這里tl是一個強引用指向這個ThreadLocal對象,而Map里的key是通過一個弱引用指向了一個ThreadLocal對象,我們假設(shè)這是個強引用,當tl指向這個ThreadLocal對象消失的時候,tl這個東西是個局部變量,方法已結(jié)束它就消失了,當tl消失了,如果這個ThreadLocal對象還被一個強引用的key指向的時候,這個ThreadLocal對象能被回收嗎?肯定不行,而且由于這個線程有很多線程是長期存在的,比如這個是一個服務(wù)器線程,7*24小時一年365天不間斷運行,那么不間斷運行的時候,這個tl會長期存在,這個Map會長期存在,這個Map的key也會長期存在,這個key長期存在的話,這個ThreadLocal對象永遠不會被消失,所以這里是不是就會有內(nèi)存泄漏,但是如果這個key是弱引用的話還會存在這個問題嗎?當這個強引用消失的時候這個弱引用是不是自動就會回收了,這也是為什么用WeakReference的原因


          關(guān)于ThreadLocal還有一個問題,當我們tl這個強引用消失了,key的指向也被回收了,可是很不幸的是這個key指向了一個null值,但是這個threadLocals的Map是永遠存在的,相當于說key/value對,你這個key是null的,你這個value指向的東西,你的這個10MB的字節(jié)碼,你還能訪問到嗎?訪問不到了,如果這個Map越積攢越多,越來越多,它還是會內(nèi)存泄漏,怎么辦呢?所以必須記住這一點,使用ThreadLocal里面的對象不用了,務(wù)必要remove掉,不然還會有內(nèi)存泄漏

          ThradLocalM> tl = new ThreadLocal<>();
          tl.set(new M());
          tl.remove();

          虛引用

          對于虛引用它就干一件事,它就是管理堆外內(nèi)存的,首先第一點,這個虛引用的構(gòu)造方法至少都是兩個參數(shù)的,第二個參數(shù)還必須是一個隊列,這個虛引用基本沒用,就是說不是給你用的,那么它是給誰用的呢?是給寫JVM(虛擬機)的人用的

          我們來看下面的小程序,在小程序里創(chuàng)建了一個List集合用于模擬內(nèi)存溢出,還創(chuàng)建了一個

          ReferenceQueue(引用隊列),在main方法里創(chuàng)建一個虛引用對象PhantomReference,這個虛引用對象指向的這個內(nèi)存里是什么樣子的呢?有一個phantomReference對象指向了一個new出來的

          PhantomReference對象,這個對像里面可以訪問兩個內(nèi)容,第一個內(nèi)容是它又通過一個特別虛的引用指向了我們new出來的一個M對象,第二個內(nèi)容它關(guān)聯(lián)了一個Queue(隊列),這個時候一但虛引用被回收,這個虛引用會裝到這個隊列里,也就是說這個隊列是干什么的呢?就是垃圾回收的時候,一但把這個虛引用給回收的時候,會裝到這個隊列里,讓你接收到一個通知,什么時候你檢測到這個隊列里面如果有一個引用存在了,那說明什么呢?說明這個虛引用被回收了,這個虛引用叫特別虛的引用,指向的任何一個對象,垃圾回收二話不說,上來就把這個M對象給干掉這是肯定的,只要有垃圾回收, 而且虛引用最關(guān)鍵的是當M對象被干掉的時候,你會收到一個通知,通知你的方式是什么呢?通知你的方式就是往這個Queue(隊列)里放進一個值

          那么我們這個小程序是什么意思呢?在小程序啟動前先設(shè)置好了堆內(nèi)存的最大值,然后看第一個線程啟動以后,它會不停的往List集合里分配對象,什么時候內(nèi)存占滿了,觸發(fā)垃圾回收的時候,另外一個線程就不斷的監(jiān)測這個隊列里邊的變動,如果有就說明這個虛引用被放進去了,就說明被回收了在第一個線程啟動后我們會看到,無論我們怎么get這個phantomReference里面的值,它輸出的都是空值,虛引用和弱引用的區(qū)別就在于,弱引用里邊有值你get的時候還是get的到的,但是虛引用你get里邊的值你是get不到的

          public class T04_PhantomReference {private static final List LIST = new LinkedList<>();private static final ReferenceQueue QUEUE = new ReferenceQueue<>();public static void main(String[] args) {
          PhantomReference phantomReference = new PhantomReference<>(new M(),
          QUEUE);new Thread(() -> {while (true) {
          LIST.add(new byte[1024 * 1024]);try {
          Thread.sleep(1000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          Thread.currentThread().interrupt();
          }
          System.out.println(phantomReference.get());
          }
          }).start();new Thread(() -> {while (true) {
          Reference poll = QUEUE.poll();if (poll != null) {
          System.out.println("--- 虛引用對象被jvm回收了 ---- " + poll);
          }
          }
          }).start();try {
          Thread.sleep(500);
          } catch (InterruptedException e) {
          e.printSackTrace();
          }
          }
          }

          那么我們想一下你拿不到這里邊的值我用它來干什么呢?這里再強調(diào)一遍,只是為了給你一個通知,通知的時候放到隊列里,這虛引用干什么用?就是寫JVM的人拿來用,寫JVM的人用的時候怎么用呢?他會當Queue這個值,檢測到隊列里邊有虛引用指向這個東西被回收的時候做出相應(yīng)的處理,什么時候出現(xiàn)相應(yīng)的處理呢?

          經(jīng)常會有一種情況,NIO里邊有一個比較新的新的Buffffer叫DirectByteBuffffer(直接內(nèi)存),直接內(nèi)存是不被JVM(虛擬機)直接管理的內(nèi)存,被誰管理?被操作系統(tǒng)管理,又叫做堆外內(nèi)存,這個DirectByteBuffffer是可以指向堆外內(nèi)存的,那我們想一下,如果這個DirectByteBuffffer設(shè)為null,垃圾回收器能回收DirectByteBuffffer嗎?它指向內(nèi)存都沒在堆里,你怎么回收它,所以沒有辦法回收,那么寫虛擬機的人怎么回收DirectByteBuffffer呢?如果有一天你也用到堆外內(nèi)存的時候,當這個DirectByteBuffffer被設(shè)為null的時候,你怎么回收堆外這個內(nèi)存呢?你可以用虛引用,當我們檢測到這個虛引用被垃圾回收器回收的時候,你做出相應(yīng)處理去回收堆外內(nèi)存


          說不定將來的某一天,你寫了一個Netty,然后你再Netty里邊分配內(nèi)存的時候,用的是堆外內(nèi)存,那么堆外內(nèi)存你又想做到自動的垃圾回收,你不能讓人家用你API的人,讓人家自己去回收對不對?所以你這個時候怎么做到自動回收呢?你可以檢測虛引用里的Queue,什么時候Queue檢測到DirectByteBuffffer(直接內(nèi)存)被回收了,這個時候你就去清理堆外內(nèi)存,堆外內(nèi)存怎么回收呢?你如果是C和C++語言寫的虛擬機的話,當然是del和free這個兩個函數(shù),它們也是C和C++提供的,java里面現(xiàn)在也提供了,堆外內(nèi)存回收,這個回收的類叫Unsafe,這個類在JDK1.8的時候可以用java的反射機制來用它,但是JDK1.9以后它被加到包里了,普通人是用不了的,但JUC的一些底層有很多都用到了這個類,這個Unsafe類里面有兩個方法,allocateMemory方法直接分配內(nèi)存也就是分配堆外內(nèi)存,freeMemory方法回收內(nèi)存也就是手動回收內(nèi)存,這和C/C++里邊一樣你直接分配內(nèi)存,必須得手動回收,

          今天講的是AQS源碼閱讀與強軟弱虛4種引用以及ThreadLocal原理與源碼的內(nèi)容,喜歡的朋友可以轉(zhuǎn)發(fā)關(guān)注一下小編!

          字節(jié)大佬,技術(shù)文章都沒得的推薦吶~

          本文就是愿天堂沒有BUG給大家分享的內(nèi)容,大家有收獲的話可以分享下,想學(xué)習(xí)更多的話可以到微信公眾號里找我,我等你哦。

          瀏覽 79
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    亚洲色婷婷天天看 | 无码人妻aV一区二区三区色欲 | 日日夜夜抽麻豆 | 色婷婷综合在线观看 | 日韩啪啪啪啪 |