站長(zhǎng)資訊網(wǎng)
        最全最豐富的資訊網(wǎng)站

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        本篇文章給大家?guī)?lái)了關(guān)于java的相關(guān)知識(shí),其中主要介紹了java并發(fā)的相關(guān)問(wèn)題,總結(jié)了一些問(wèn)題,大家來(lái)看一下會(huì)多少,希望對(duì)大家有幫助。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        推薦學(xué)習(xí):《java教程》

        1.并行跟并發(fā)有什么區(qū)別?

        從操作系統(tǒng)的角度來(lái)看,線程是CPU分配的最小單位。

        • 并行就是同一時(shí)刻,兩個(gè)線程都在執(zhí)行。這就要求有兩個(gè)CPU去分別執(zhí)行兩個(gè)線程。
        • 并發(fā)就是同一時(shí)刻,只有一個(gè)執(zhí)行,但是一個(gè)時(shí)間段內(nèi),兩個(gè)線程都執(zhí)行了。并發(fā)的實(shí)現(xiàn)依賴(lài)于CPU切換線程,因?yàn)榍袚Q的時(shí)間特別短,所以基本對(duì)于用戶是無(wú)感知的。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        就好像我們?nèi)ナ程么蝻垼⑿芯褪俏覀冊(cè)诙鄠€(gè)窗口排隊(duì),幾個(gè)阿姨同時(shí)打菜;并發(fā)就是我們擠在一個(gè)窗口,阿姨給這個(gè)打一勺,又手忙腳亂地給那個(gè)打一勺。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        2.說(shuō)說(shuō)什么是進(jìn)程和線程?

        要說(shuō)線程,必須得先說(shuō)說(shuō)進(jìn)程。

        • 進(jìn)程:進(jìn)程是代碼在數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
        • 線程:線程是進(jìn)程的一個(gè)執(zhí)行路徑,一個(gè)進(jìn)程中至少有一個(gè)線程,進(jìn)程中的多個(gè)線程共享進(jìn)程的資源。

        操作系統(tǒng)在分配資源時(shí)是把資源分配給進(jìn)程的, 但是 CPU 資源比較特殊,它是被分配到線程的,因?yàn)檎嬲加肅PU運(yùn)行的是線程,所以也說(shuō)線程是 CPU分配的基本單位。

        比如在Java中,當(dāng)我們啟動(dòng) main 函數(shù)其實(shí)就啟動(dòng)了一個(gè)JVM進(jìn)程,而 main 函數(shù)在的線程就是這個(gè)進(jìn)程中的一個(gè)線程,也稱(chēng)主線程。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        一個(gè)進(jìn)程中有多個(gè)線程,多個(gè)線程共用進(jìn)程的堆和方法區(qū)資源,但是每個(gè)線程有自己的程序計(jì)數(shù)器和棧。

        3.說(shuō)說(shuō)線程有幾種創(chuàng)建方式?

        Java中創(chuàng)建線程主要有三種方式,分別為繼承Thread類(lèi)、實(shí)現(xiàn)Runnable接口、實(shí)現(xiàn)Callable接口。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 繼承Thread類(lèi),重寫(xiě)run()方法,調(diào)用start()方法啟動(dòng)線程
        public class ThreadTest {      /**      * 繼承Thread類(lèi)      */     public static class MyThread extends Thread {         @Override         public void run() {             System.out.println("This is child thread");         }     }      public static void main(String[] args) {         MyThread thread = new MyThread();         thread.start();     }}
        • 實(shí)現(xiàn) Runnable 接口,重寫(xiě)run()方法
        public class RunnableTask implements Runnable {     public void run() {         System.out.println("Runnable!");     }      public static void main(String[] args) {         RunnableTask task = new RunnableTask();         new Thread(task).start();     }}

        上面兩種都是沒(méi)有返回值的,但是如果我們需要獲取線程的執(zhí)行結(jié)果,該怎么辦呢?

        • 實(shí)現(xiàn)Callable接口,重寫(xiě)call()方法,這種方式可以通過(guò)FutureTask獲取任務(wù)執(zhí)行的返回值
        public class CallerTask implements Callable<String> {     public String call() throws Exception {         return "Hello,i am running!";     }      public static void main(String[] args) {         //創(chuàng)建異步任務(wù)         FutureTask<String> task=new FutureTask<String>(new CallerTask());         //啟動(dòng)線程         new Thread(task).start();         try {             //等待執(zhí)行完成,并獲取返回結(jié)果             String result=task.get();             System.out.println(result);         } catch (InterruptedException e) {             e.printStackTrace();         } catch (ExecutionException e) {             e.printStackTrace();         }     }}

        4.為什么調(diào)用start()方法時(shí)會(huì)執(zhí)行run()方法,那怎么不直接調(diào)用run()方法?

        JVM執(zhí)行start方法,會(huì)先創(chuàng)建一條線程,由創(chuàng)建出來(lái)的新線程去執(zhí)行thread的run方法,這才起到多線程的效果。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        **為什么我們不能直接調(diào)用run()方法?**也很清楚, 如果直接調(diào)用Thread的run()方法,那么run方法還是運(yùn)行在主線程中,相當(dāng)于順序執(zhí)行,就起不到多線程的效果。

        5.線程有哪些常用的調(diào)度方法?

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        線程等待與通知

        在Object類(lèi)中有一些函數(shù)可以用于線程的等待與通知。

        • wait():當(dāng)一個(gè)線程A調(diào)用一個(gè)共享變量的 wait()方法時(shí), 線程A會(huì)被阻塞掛起, 發(fā)生下面幾種情況才會(huì)返回 :

          • (1) 線程A調(diào)用了共享對(duì)象 notify()或者 notifyAll()方法;

          • (2)其他線程調(diào)用了線程A的 interrupt() 方法,線程A拋出InterruptedException異常返回。

        • wait(long timeout) :這個(gè)方法相比 wait() 方法多了一個(gè)超時(shí)參數(shù),它的不同之處在于,如果線程A調(diào)用共享對(duì)象的wait(long timeout)方法后,沒(méi)有在指定的 timeout ms時(shí)間內(nèi)被其它線程喚醒,那么這個(gè)方法還是會(huì)因?yàn)槌瑫r(shí)而返回。

        • wait(long timeout, int nanos),其內(nèi)部調(diào)用的是 wait(long timout)函數(shù)。

        上面是線程等待的方法,而喚醒線程主要是下面兩個(gè)方法:

        • notify() : 一個(gè)線程A調(diào)用共享對(duì)象的 notify() 方法后,會(huì)喚醒一個(gè)在這個(gè)共享變量上調(diào)用 wait 系列方法后被掛起的線程。 一個(gè)共享變量上可能會(huì)有多個(gè)線程在等待,具體喚醒哪個(gè)等待的線程是隨機(jī)的。
        • notifyAll() :不同于在共享變量上調(diào)用 notify() 函數(shù)會(huì)喚醒被阻塞到該共享變量上的一個(gè)線程,notifyAll()方法則會(huì)喚醒所有在該共享變量上由于調(diào)用 wait 系列方法而被掛起的線程。

        Thread類(lèi)也提供了一個(gè)方法用于等待的方法:

        • join():如果一個(gè)線程A執(zhí)行了thread.join()語(yǔ)句,其含義是:當(dāng)前線程A等待thread線程終止之后才

          從thread.join()返回。

        線程休眠

        • sleep(long millis) :Thread類(lèi)中的靜態(tài)方法,當(dāng)一個(gè)執(zhí)行中的線程A調(diào)用了Thread 的sleep方法后,線程A會(huì)暫時(shí)讓出指定時(shí)間的執(zhí)行權(quán),但是線程A所擁有的監(jiān)視器資源,比如鎖還是持有不讓出的。指定的睡眠時(shí)間到了后該函數(shù)會(huì)正常返回,接著參與 CPU 的調(diào)度,獲取到 CPU 資源后就可以繼續(xù)運(yùn)行。

        讓出優(yōu)先權(quán)

        • yield() :Thread類(lèi)中的靜態(tài)方法,當(dāng)一個(gè)線程調(diào)用 yield 方法時(shí),實(shí)際就是在暗示線程調(diào)度器當(dāng)前線程請(qǐng)求讓出自己的CPU ,但是線程調(diào)度器可以無(wú)條件忽略這個(gè)暗示。

        線程中斷

        Java 中的線程中斷是一種線程間的協(xié)作模式,通過(guò)設(shè)置線程的中斷標(biāo)志并不能直接終止該線程的執(zhí)行,而是被中斷的線程根據(jù)中斷狀態(tài)自行處理。

        • void interrupt() :中斷線程,例如,當(dāng)線程A運(yùn)行時(shí),線程B可以調(diào)用錢(qián)程interrupt() 方法來(lái)設(shè)置線程的中斷標(biāo)志為true 并立即返回。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志, 線程A實(shí)際并沒(méi)有被中斷, 會(huì)繼續(xù)往下執(zhí)行。
        • boolean isInterrupted() 方法: 檢測(cè)當(dāng)前線程是否被中斷。
        • boolean interrupted() 方法: 檢測(cè)當(dāng)前線程是否被中斷,與 isInterrupted 不同的是,該方法如果發(fā)現(xiàn)當(dāng)前線程被中斷,則會(huì)清除中斷標(biāo)志。

        6.線程有幾種狀態(tài)?

        在Java中,線程共有六種狀態(tài):

        狀態(tài) 說(shuō)明
        NEW 初始狀態(tài):線程被創(chuàng)建,但還沒(méi)有調(diào)用start()方法
        RUNNABLE 運(yùn)行狀態(tài):Java線程將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)籠統(tǒng)的稱(chēng)作“運(yùn)行”
        BLOCKED 阻塞狀態(tài):表示線程阻塞于鎖
        WAITING 等待狀態(tài):表示線程進(jìn)入等待狀態(tài),進(jìn)入該狀態(tài)表示當(dāng)前線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷)
        TIME_WAITING 超時(shí)等待狀態(tài):該狀態(tài)不同于 WAITIND,它是可以在指定的時(shí)間自行返回的
        TERMINATED 終止?fàn)顟B(tài):表示當(dāng)前線程已經(jīng)執(zhí)行完畢

        線程在自身的生命周期中, 并不是固定地處于某個(gè)狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換,Java線程狀態(tài)變化如圖示:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        7.什么是線程上下文切換?

        使用多線程的目的是為了充分利用CPU,但是我們知道,并發(fā)其實(shí)是一個(gè)CPU來(lái)應(yīng)付多個(gè)線程。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        為了讓用戶感覺(jué)多個(gè)線程是在同時(shí)執(zhí)行的, CPU 資源的分配采用了時(shí)間片輪轉(zhuǎn)也就是給每個(gè)線程分配一個(gè)時(shí)間片,線程在時(shí)間片內(nèi)占用 CPU 執(zhí)行任務(wù)。當(dāng)線程使用完時(shí)間片后,就會(huì)處于就緒狀態(tài)并讓出 CPU 讓其他線程占用,這就是上下文切換。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        8.守護(hù)線程了解嗎?

        Java中的線程分為兩類(lèi),分別為 daemon 線程(守護(hù)線程)和 user 線程(用戶線程)。

        在JVM 啟動(dòng)時(shí)會(huì)調(diào)用 main 函數(shù),main函數(shù)所在的錢(qián)程就是一個(gè)用戶線程。其實(shí)在 JVM 內(nèi)部同時(shí)還啟動(dòng)了很多守護(hù)線程, 比如垃圾回收線程。

        那么守護(hù)線程和用戶線程有什么區(qū)別呢?區(qū)別之一是當(dāng)最后一個(gè)非守護(hù)線程束時(shí), JVM會(huì)正常退出,而不管當(dāng)前是否存在守護(hù)線程,也就是說(shuō)守護(hù)線程是否結(jié)束并不影響 JVM退出。換而言之,只要有一個(gè)用戶線程還沒(méi)結(jié)束,正常情況下JVM就不會(huì)退出。

        9.線程間有哪些通信方式?

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • volatile和synchronized關(guān)鍵字

        關(guān)鍵字volatile可以用來(lái)修飾字段(成員變量),就是告知程序任何對(duì)該變量的訪問(wèn)均需要從共享內(nèi)存中獲取,而對(duì)它的改變必須同步刷新回共享內(nèi)存,它能保證所有線程對(duì)變量訪問(wèn)的可見(jiàn)性。

        關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來(lái)進(jìn)行使用,它主要確保多個(gè)線程在同一個(gè)時(shí)刻,只能有一個(gè)線程處于方法或者同步塊中,它保證了線程對(duì)變量訪問(wèn)的可見(jiàn)性和排他性。

        • 等待/通知機(jī)制

        可以通過(guò)Java內(nèi)置的等待/通知機(jī)制(wait()/notify())實(shí)現(xiàn)一個(gè)線程修改一個(gè)對(duì)象的值,而另一個(gè)線程感知到了變化,然后進(jìn)行相應(yīng)的操作。

        • 管道輸入/輸出流

        管道輸入/輸出流和普通的文件輸入/輸出流或者網(wǎng)絡(luò)輸入/輸出流不同之處在于,它主要用于線程之間的數(shù)據(jù)傳輸,而傳輸?shù)拿浇闉閮?nèi)存。

        管道輸入/輸出流主要包括了如下4種具體實(shí)現(xiàn):PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向字節(jié),而后兩種面向字符。

        • 使用Thread.join()

        如果一個(gè)線程A執(zhí)行了thread.join()語(yǔ)句,其含義是:當(dāng)前線程A等待thread線程終止之后才從thread.join()返回。。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個(gè)具備超時(shí)特性的方法。

        • 使用ThreadLocal

        ThreadLocal,即線程變量,是一個(gè)以ThreadLocal對(duì)象為鍵、任意對(duì)象為值的存儲(chǔ)結(jié)構(gòu)。這個(gè)結(jié)構(gòu)被附帶在線程上,也就是說(shuō)一個(gè)線程可以根據(jù)一個(gè)ThreadLocal對(duì)象查詢到綁定在這個(gè)線程上的一個(gè)值。

        可以通過(guò)set(T)方法來(lái)設(shè)置一個(gè)值,在當(dāng)前線程下再通過(guò)get()方法獲取到原先設(shè)置的值。

        關(guān)于多線程,其實(shí)很大概率還會(huì)出一些筆試題,比如交替打印、銀行轉(zhuǎn)賬、生產(chǎn)消費(fèi)模型等等,后面老三會(huì)單獨(dú)出一期來(lái)盤(pán)點(diǎn)一下常見(jiàn)的多線程筆試題。

        ThreadLocal

        ThreadLocal其實(shí)應(yīng)用場(chǎng)景不是很多,但卻是被炸了千百遍的面試?yán)嫌蜅l,涉及到多線程、數(shù)據(jù)結(jié)構(gòu)、JVM,可問(wèn)的點(diǎn)比較多,一定要拿下。

        10.ThreadLocal是什么?

        ThreadLocal,也就是線程本地變量。如果你創(chuàng)建了一個(gè)ThreadLocal變量,那么訪問(wèn)這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的一個(gè)本地拷貝,多個(gè)線程操作這個(gè)變量的時(shí)候,實(shí)際是操作自己本地內(nèi)存里面的變量,從而起到線程隔離的作用,避免了線程安全問(wèn)題。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 創(chuàng)建

        創(chuàng)建了一個(gè)ThreadLoca變量localVariable,任何一個(gè)線程都能并發(fā)訪問(wèn)localVariable。

        //創(chuàng)建一個(gè)ThreadLocal變量public static ThreadLocal<String> localVariable = new ThreadLocal<>();
        • 寫(xiě)入

        線程可以在任何地方使用localVariable,寫(xiě)入變量。

        localVariable.set("鄙人三某”);
        • 讀取

        線程在任何地方讀取的都是它寫(xiě)入的變量。

        localVariable.get();

        11.你在工作中用到過(guò)ThreadLocal嗎?

        有用到過(guò)的,用來(lái)做用戶信息上下文的存儲(chǔ)。

        我們的系統(tǒng)應(yīng)用是一個(gè)典型的MVC架構(gòu),登錄后的用戶每次訪問(wèn)接口,都會(huì)在請(qǐng)求頭中攜帶一個(gè)token,在控制層可以根據(jù)這個(gè)token,解析出用戶的基本信息。那么問(wèn)題來(lái)了,假如在服務(wù)層和持久層都要用到用戶信息,比如rpc調(diào)用、更新用戶獲取等等,那應(yīng)該怎么辦呢?

        一種辦法是顯式定義用戶相關(guān)的參數(shù),比如賬號(hào)、用戶名……這樣一來(lái),我們可能需要大面積地修改代碼,多少有點(diǎn)瓜皮,那該怎么辦呢?

        這時(shí)候我們就可以用到ThreadLocal,在控制層攔截請(qǐng)求把用戶信息存入ThreadLocal,這樣我們?cè)谌魏我粋€(gè)地方,都可以取出ThreadLocal中存的用戶數(shù)據(jù)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        很多其它場(chǎng)景的cookie、session等等數(shù)據(jù)隔離也都可以通過(guò)ThreadLocal去實(shí)現(xiàn)。

        我們常用的數(shù)據(jù)庫(kù)連接池也用到了ThreadLocal:

        • 數(shù)據(jù)庫(kù)連接池的連接交給ThreadLoca進(jìn)行管理,保證當(dāng)前線程的操作都是同一個(gè)Connnection。

        12.ThreadLocal怎么實(shí)現(xiàn)的呢?

        我們看一下ThreadLocal的set(T)方法,發(fā)現(xiàn)先獲取到當(dāng)前線程,再獲取ThreadLocalMap,然后把元素存到這個(gè)map中。

            public void set(T value) {         //獲取當(dāng)前線程         Thread t = Thread.currentThread();         //獲取ThreadLocalMap         ThreadLocalMap map = getMap(t);         //講當(dāng)前元素存入map         if (map != null)             map.set(this, value);         else             createMap(t, value);     }

        ThreadLocal實(shí)現(xiàn)的秘密都在這個(gè)ThreadLocalMap了,可以Thread類(lèi)中定義了一個(gè)類(lèi)型為ThreadLocal.ThreadLocalMap的成員變量threadLocals

        public class Thread implements Runnable {    //ThreadLocal.ThreadLocalMap是Thread的屬性    ThreadLocal.ThreadLocalMap threadLocals = null;}

        ThreadLocalMap既然被稱(chēng)為Map,那么毫無(wú)疑問(wèn)它是<key,value>型的數(shù)據(jù)結(jié)構(gòu)。我們都知道m(xù)ap的本質(zhì)是一個(gè)個(gè)<key,value>形式的節(jié)點(diǎn)組成的數(shù)組,那ThreadLocalMap的節(jié)點(diǎn)是什么樣的呢?

                static class Entry extends WeakReference<ThreadLocal<?>> {             /** The value associated with this ThreadLocal. */             Object value;              //節(jié)點(diǎn)類(lèi)             Entry(ThreadLocal<?> k, Object v) {                 //key賦值                 super(k);                 //value賦值                 value = v;             }         }

        這里的節(jié)點(diǎn),key可以簡(jiǎn)單低視作ThreadLocal,value為代碼中放入的值,當(dāng)然實(shí)際上key并不是ThreadLocal本身,而是它的一個(gè)弱引用,可以看到Entry的key繼承了 WeakReference(弱引用),再來(lái)看一下key怎么賦值的:

            public WeakReference(T referent) {         super(referent);     }

        key的賦值,使用的是WeakReference的賦值。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        所以,怎么回答ThreadLocal原理?要答出這幾個(gè)點(diǎn):

        • Thread類(lèi)有一個(gè)類(lèi)型為T(mén)hreadLocal.ThreadLocalMap的實(shí)例變量threadLocals,每個(gè)線程都有一個(gè)屬于自己的ThreadLocalMap。
        • ThreadLocalMap內(nèi)部維護(hù)著Entry數(shù)組,每個(gè)Entry代表一個(gè)完整的對(duì)象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
        • 每個(gè)線程在往ThreadLocal里設(shè)置值的時(shí)候,都是往自己的ThreadLocalMap里存,讀也是以某個(gè)ThreadLocal作為引用,在自己的map里找對(duì)應(yīng)的key,從而實(shí)現(xiàn)了線程隔離。
        • ThreadLocal本身不存儲(chǔ)值,它只是作為一個(gè)key來(lái)讓線程往ThreadLocalMap里存取值。

        13.ThreadLocal 內(nèi)存泄露是怎么回事?

        我們先來(lái)分析一下使用ThreadLocal時(shí)的內(nèi)存,我們都知道,在JVM中,棧內(nèi)存線程私有,存儲(chǔ)了對(duì)象的引用,堆內(nèi)存線程共享,存儲(chǔ)了對(duì)象實(shí)例。

        所以呢,棧中存儲(chǔ)了ThreadLocal、Thread的引用,堆中存儲(chǔ)了它們的具體實(shí)例。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        ThreadLocalMap中使用的 key 為 ThreadLocal 的弱引用。

        “弱引用:只要垃圾回收機(jī)制一運(yùn)行,不管JVM的內(nèi)存空間是否充足,都會(huì)回收該對(duì)象占用的內(nèi)存。”

        那么現(xiàn)在問(wèn)題就來(lái)了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一樣的,它這時(shí)候如果不被回收,就會(huì)出現(xiàn)這種情況:ThreadLocalMap的key沒(méi)了,value還在,這就會(huì)造成了內(nèi)存泄漏問(wèn)題

        那怎么解決內(nèi)存泄漏問(wèn)題呢?

        很簡(jiǎn)單,使用完ThreadLocal后,及時(shí)調(diào)用remove()方法釋放內(nèi)存空間。

        ThreadLocal<String> localVariable = new ThreadLocal();try {     localVariable.set("鄙人三某”);     ……} finally {     localVariable.remove();}

        那為什么key還要設(shè)計(jì)成弱引用?

        key設(shè)計(jì)成弱引用同樣是為了防止內(nèi)存泄漏。

        假如key被設(shè)計(jì)成強(qiáng)引用,如果ThreadLocal Reference被銷(xiāo)毀,此時(shí)它指向ThreadLoca的強(qiáng)引用就沒(méi)有了,但是此時(shí)key還強(qiáng)引用指向ThreadLoca,就會(huì)導(dǎo)致ThreadLocal不能被回收,這時(shí)候就發(fā)生了內(nèi)存泄漏的問(wèn)題。

        14.ThreadLocalMap的結(jié)構(gòu)了解嗎?

        ThreadLocalMap雖然被叫做Map,其實(shí)它是沒(méi)有實(shí)現(xiàn)Map接口的,但是結(jié)構(gòu)還是和HashMap比較類(lèi)似的,主要關(guān)注的是兩個(gè)要素:元素?cái)?shù)組散列方法

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 元素?cái)?shù)組

          一個(gè)table數(shù)組,存儲(chǔ)Entry類(lèi)型的元素,Entry是ThreaLocal弱引用作為key,Object作為value的結(jié)構(gòu)。

         private Entry[] table;
        • 散列方法

          散列方法就是怎么把對(duì)應(yīng)的key映射到table數(shù)組的相應(yīng)下標(biāo),ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table數(shù)組長(zhǎng)度減一&運(yùn)算(相當(dāng)于取余)。

        int i = key.threadLocalHashCode & (table.length - 1);

        這里的threadLocalHashCode計(jì)算有點(diǎn)東西,每創(chuàng)建一個(gè)ThreadLocal對(duì)象,它就會(huì)新增0x61c88647,這個(gè)值很特殊,它是斐波那契數(shù) 也叫 黃金分割數(shù)hash增量為 這個(gè)數(shù)字,帶來(lái)的好處就是 hash 分布非常均勻

            private static final int HASH_INCREMENT = 0x61c88647;          private static int nextHashCode() {         return nextHashCode.getAndAdd(HASH_INCREMENT);     }

        15.ThreadLocalMap怎么解決Hash沖突的?

        我們可能都知道HashMap使用了鏈表來(lái)解決沖突,也就是所謂的鏈地址法。

        ThreadLocalMap沒(méi)有使用鏈表,自然也不是用鏈地址法來(lái)解決沖突了,它用的是另外一種方式——開(kāi)放定址法。開(kāi)放定址法是什么意思呢?簡(jiǎn)單來(lái)說(shuō),就是這個(gè)坑被人占了,那就接著去找空著的坑。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        如上圖所示,如果我們插入一個(gè)value=27的數(shù)據(jù),通過(guò) hash計(jì)算后應(yīng)該落入第 4 個(gè)槽位中,而槽位 4 已經(jīng)有了 Entry數(shù)據(jù),而且Entry數(shù)據(jù)的key和當(dāng)前不相等。此時(shí)就會(huì)線性向后查找,一直找到 Entry為 null的槽位才會(huì)停止查找,把元素放到空的槽中。

        在get的時(shí)候,也會(huì)根據(jù)ThreadLocal對(duì)象的hash值,定位到table中的位置,然后判斷該槽位Entry對(duì)象中的key是否和get的key一致,如果不一致,就判斷下一個(gè)位置。

        16.ThreadLocalMap擴(kuò)容機(jī)制了解嗎?

        在ThreadLocalMap.set()方法的最后,如果執(zhí)行完啟發(fā)式清理工作后,未清理到任何數(shù)據(jù),且當(dāng)前散列數(shù)組中Entry的數(shù)量已經(jīng)達(dá)到了列表的擴(kuò)容閾值(len*2/3),就開(kāi)始執(zhí)行rehash()邏輯:

        if (!cleanSomeSlots(i, sz) && sz >= threshold)     rehash();

        再著看rehash()具體實(shí)現(xiàn):這里會(huì)先去清理過(guò)期的Entry,然后還要根據(jù)條件判斷size >= threshold - threshold / 4 也就是size >= threshold* 3/4來(lái)決定是否需要擴(kuò)容。

        private void rehash() {     //清理過(guò)期Entry     expungeStaleEntries();      //擴(kuò)容     if (size >= threshold - threshold / 4)         resize();}//清理過(guò)期Entryprivate void expungeStaleEntries() {     Entry[] tab = table;     int len = tab.length;     for (int j = 0; j < len; j++) {         Entry e = tab[j];         if (e != null && e.get() == null)             expungeStaleEntry(j);     }}

        接著看看具體的resize()方法,擴(kuò)容后的newTab的大小為老數(shù)組的兩倍,然后遍歷老的table數(shù)組,散列方法重新計(jì)算位置,開(kāi)放地址解決沖突,然后放到新的newTab,遍歷完成之后,oldTab中所有的entry數(shù)據(jù)都已經(jīng)放入到newTab中了,然后table引用指向newTab

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        具體代碼:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        17.父子線程怎么共享數(shù)據(jù)?

        父線程能用ThreadLocal來(lái)給子線程傳值嗎?毫無(wú)疑問(wèn),不能。那該怎么辦?

        這時(shí)候可以用到另外一個(gè)類(lèi)——InheritableThreadLocal

        使用起來(lái)很簡(jiǎn)單,在主線程的InheritableThreadLocal實(shí)例設(shè)置值,在子線程中就可以拿到了。

        public class InheritableThreadLocalTest {          public static void main(String[] args) {         final ThreadLocal threadLocal = new InheritableThreadLocal();         // 主線程         threadLocal.set("不擅技術(shù)");         //子線程         Thread t = new Thread() {             @Override             public void run() {                 super.run();                 System.out.println("鄙人三某 ," + threadLocal.get());             }         };         t.start();     }}

        那原理是什么呢?

        原理很簡(jiǎn)單,在Thread類(lèi)里還有另外一個(gè)變量:

        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

        在Thread.init的時(shí)候,如果父線程的inheritableThreadLocals不為空,就把它賦給當(dāng)前線程(子線程)的inheritableThreadLocals

                if (inheritThreadLocals && parent.inheritableThreadLocals != null)             this.inheritableThreadLocals =                 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

        18.說(shuō)一下你對(duì)Java內(nèi)存模型(JMM)的理解?

        Java內(nèi)存模型(Java Memory Model,JMM),是一種抽象的模型,被定義出來(lái)屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異。

        JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本。

        Java內(nèi)存模型的抽象圖:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        本地內(nèi)存是JMM的 一個(gè)抽象概念,并不真實(shí)存在。它其實(shí)涵蓋了緩存、寫(xiě)緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        圖里面的是一個(gè)雙核 CPU 系統(tǒng)架構(gòu) ,每個(gè)核有自己的控制器和運(yùn)算器,其中控制器包含一組寄存器和操作控制器,運(yùn)算器執(zhí)行算術(shù)邏輔運(yùn)算。每個(gè)核都有自己的一級(jí)緩存,在有些架構(gòu)里面還有一個(gè)所有 CPU 共享的二級(jí)緩存。 那么 Java 內(nèi)存模型里面的工作內(nèi)存,就對(duì)應(yīng)這里的 Ll 緩存或者 L2 緩存或者 CPU 寄存器。

        19.說(shuō)說(shuō)你對(duì)原子性、可見(jiàn)性、有序性的理解?

        原子性、有序性、可見(jiàn)性是并發(fā)編程中非常重要的基礎(chǔ)概念,JMM的很多技術(shù)都是圍繞著這三大特性展開(kāi)。

        • 原子性:原子性指的是一個(gè)操作是不可分割、不可中斷的,要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷,要么就全不執(zhí)行。
        • 可見(jiàn)性:可見(jiàn)性指的是一個(gè)線程修改了某一個(gè)共享變量的值時(shí),其它線程能夠立即知道這個(gè)修改。
        • 有序性:有序性指的是對(duì)于一個(gè)線程的執(zhí)行代碼,從前往后依次執(zhí)行,單線程下可以認(rèn)為程序是有序的,但是并發(fā)時(shí)有可能會(huì)發(fā)生指令重排。

        分析下面幾行代碼的原子性?

        int i = 2;int j = i;i++;i = i + 1;
        • 第1句是基本類(lèi)型賦值,是原子性操作。
        • 第2句先讀i的值,再賦值到j(luò),兩步操作,不能保證原子性。
        • 第3和第4句其實(shí)是等效的,先讀取i的值,再+1,最后賦值到i,三步操作了,不能保證原子性。

        原子性、可見(jiàn)性、有序性都應(yīng)該怎么保證呢?

        • 原子性:JMM只能保證基本的原子性,如果要保證一個(gè)代碼塊的原子性,需要使用synchronized
        • 可見(jiàn)性:Java是利用volatile關(guān)鍵字來(lái)保證可見(jiàn)性的,除此之外,finalsynchronized也能保證可見(jiàn)性。
        • 有序性:synchronized或者volatile都可以保證多線程之間操作的有序性。

        20.那說(shuō)說(shuō)什么是指令重排?

        在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分3種類(lèi)型。

        1. 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
        2. 指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴(lài)性,處理器可以改變語(yǔ)句對(duì)應(yīng) 機(jī)器指令的執(zhí)行順序。
        3. 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫(xiě)緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

        從Java源代碼到最終實(shí)際執(zhí)行的指令序列,會(huì)分別經(jīng)歷下面3種重排序,如圖:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們比較熟悉的雙重校驗(yàn)單例模式就是一個(gè)經(jīng)典的指令重排的例子,Singleton instance=new Singleton();對(duì)應(yīng)的JVM指令分為三步:分配內(nèi)存空間–>初始化對(duì)象—>對(duì)象指向分配的內(nèi)存空間,但是經(jīng)過(guò)了編譯器的指令重排序,第二步和第三步就可能會(huì)重排序。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上,通過(guò)禁止特定類(lèi)型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。

        21.指令重排有限制嗎?happens-before了解嗎?

        指令重排也是有一些限制的,有兩個(gè)規(guī)則happens-beforeas-if-serial來(lái)約束。

        happens-before的定義:

        • 如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見(jiàn),而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
        • 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照 happens-before關(guān)系指定的順序來(lái)執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么這種重排序并不非法

        happens-before和我們息息相關(guān)的有六大規(guī)則:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
        • 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。
        • volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě),happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。
        • 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
        • start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的 ThreadB.start()操作happens-before于線程B中的任意操作。
        • join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作 happens-before于線程A從ThreadB.join()操作成功返回。

        22.as-if-serial又是什么?單線程的程序一定是順序的嗎?

        as-if-serial語(yǔ)義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),單線程程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。

        為了遵守as-if-serial語(yǔ)義,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴(lài)關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴(lài)關(guān)系,這些操作就可能被編譯器和處理器重排序。為了具體說(shuō)明,請(qǐng)看下面計(jì)算圓面積的代碼示例。

        double pi = 3.14;   // Adouble r = 1.0;   // B double area = pi * r * r;   // C

        上面3個(gè)操作的數(shù)據(jù)依賴(lài)關(guān)系:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        A和C之間存在數(shù)據(jù)依賴(lài)關(guān)系,同時(shí)B和C之間也存在數(shù)據(jù)依賴(lài)關(guān)系。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會(huì)被改變)。但A和B之間沒(méi)有數(shù)據(jù)依賴(lài)關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。

        所以最終,程序可能會(huì)有兩種執(zhí)行順序:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        as-if-serial語(yǔ)義把單線程程序保護(hù)了起來(lái),遵守as-if-serial語(yǔ)義的編譯器、runtime和處理器共同編織了這么一個(gè)“楚門(mén)的世界”:?jiǎn)尉€程程序是按程序的“順序”來(lái)執(zhí)行的。as- if-serial語(yǔ)義使單線程情況下,我們不需要擔(dān)心重排序的問(wèn)題,可見(jiàn)性的問(wèn)題。

        23.volatile實(shí)現(xiàn)原理了解嗎?

        volatile有兩個(gè)作用,保證可見(jiàn)性有序性

        volatile怎么保證可見(jiàn)性的呢?

        相比synchronized的加鎖方式來(lái)解決共享變量的內(nèi)存可見(jiàn)性問(wèn)題,volatile就是更輕量的選擇,它沒(méi)有上下文切換的額外開(kāi)銷(xiāo)成本。

        volatile可以確保對(duì)某個(gè)變量的更新對(duì)其他線程馬上可見(jiàn),一個(gè)變量被聲明為volatile 時(shí),線程在寫(xiě)入變量時(shí)不會(huì)把值緩存在寄存器或者其他地方,而是會(huì)把值刷新回主內(nèi)存 當(dāng)其它線程讀取該共享變量 ,會(huì)從主內(nèi)存重新獲取最新值,而不是使用當(dāng)前線程的本地內(nèi)存中的值。

        例如,我們聲明一個(gè) volatile 變量 volatile int x = 0,線程A修改x=1,修改完之后就會(huì)把新的值刷新回主內(nèi)存,線程B讀取x的時(shí)候,就會(huì)清空本地內(nèi)存變量,然后再?gòu)闹鲀?nèi)存獲取最新值。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        volatile怎么保證有序性的呢?

        重排序可以分為編譯器重排序和處理器重排序,valatile保證有序性,就是通過(guò)分別限制這兩種類(lèi)型的重排序。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型的處理器重排序。

        1. 在每個(gè)volatile寫(xiě)操作的前面插入一個(gè)StoreStore屏障
        2. 在每個(gè)volatile寫(xiě)操作的后面插入一個(gè)StoreLoad屏障
        3. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
        4. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        24.synchronized用過(guò)嗎?怎么使用?

        synchronized經(jīng)常用的,用來(lái)保證代碼的原子性。

        synchronized主要有三種用法:

        • 修飾實(shí)例方法: 作用于當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對(duì)象實(shí)例的鎖
        synchronized void method() {   //業(yè)務(wù)代碼}
        • 修飾靜態(tài)方法:也就是給當(dāng)前類(lèi)加鎖,會(huì)作?于類(lèi)的所有對(duì)象實(shí)例 ,進(jìn)?同步代碼前要獲得當(dāng)前 class 的鎖。因?yàn)殪o態(tài)成員不屬于任何?個(gè)實(shí)例對(duì)象,是類(lèi)成員( static 表明這是該類(lèi)的?個(gè)靜態(tài)資源,不管 new 了多少個(gè)對(duì)象,只有?份)。

          如果?個(gè)線程 A 調(diào)??個(gè)實(shí)例對(duì)象的?靜態(tài) synchronized ?法,?線程 B 需要調(diào)?這個(gè)實(shí)例對(duì)象所屬類(lèi)的靜態(tài) synchronized ?法,是允許的,不會(huì)發(fā)?互斥現(xiàn)象,因?yàn)樵L問(wèn)靜態(tài) synchronized ?法占?的鎖是當(dāng)前類(lèi)的鎖,?訪問(wèn)?靜態(tài) synchronized ?法占?的鎖是當(dāng)前實(shí)例對(duì)象鎖。

        synchronized void staic method() {  //業(yè)務(wù)代碼}
        • 修飾代碼塊 :指定加鎖對(duì)象,對(duì)給定對(duì)象/類(lèi)加鎖。 synchronized(this|object) 表示進(jìn)?同步代碼庫(kù)前要獲得給定對(duì)象的鎖。 synchronized(類(lèi).class) 表示進(jìn)?同步代碼前要獲得 當(dāng)前 class 的鎖
        synchronized(this) {  //業(yè)務(wù)代碼}

        25.synchronized的實(shí)現(xiàn)原理?

        synchronized是怎么加鎖的呢?

        我們使用synchronized的時(shí)候,發(fā)現(xiàn)不用自己去lock和unlock,是因?yàn)镴VM幫我們把這個(gè)事情做了。

        1. synchronized修飾代碼塊時(shí),JVM采用monitorentermonitorexit兩個(gè)指令來(lái)實(shí)現(xiàn)同步,monitorenter 指令指向同步代碼塊的開(kāi)始位置, monitorexit 指令則指向同步代碼塊的結(jié)束位置。

          反編譯一段synchronized修飾代碼塊代碼,javap -c -s -v -l SynchronizedDemo.class,可以看到相應(yīng)的字節(jié)碼指令。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. synchronized修飾同步方法時(shí),JVM采用ACC_SYNCHRONIZED標(biāo)記符來(lái)實(shí)現(xiàn)同步,這個(gè)標(biāo)識(shí)指明了該方法是一個(gè)同步方法。

          同樣可以寫(xiě)段代碼反編譯看一下。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        synchronized鎖住的是什么呢?

        monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor實(shí)現(xiàn)的。

        實(shí)例對(duì)象結(jié)構(gòu)里有對(duì)象頭,對(duì)象頭里面有一塊結(jié)構(gòu)叫Mark Word,Mark Word指針指向了monitor

        所謂的Monitor其實(shí)是一種同步工具,也可以說(shuō)是一種同步機(jī)制。在Java虛擬機(jī)(HotSpot)中,Monitor是由ObjectMonitor實(shí)現(xiàn)的,可以叫做內(nèi)部鎖,或者M(jìn)onitor鎖。

        ObjectMonitor的工作原理:

        • ObjectMonitor有兩個(gè)隊(duì)列:_WaitSet、_EntryList,用來(lái)保存ObjectWaiter 對(duì)象列表。
        • _owner,獲取 Monitor 對(duì)象的線程進(jìn)入 _owner 區(qū)時(shí), _count + 1。如果線程調(diào)用了 wait() 方法,此時(shí)會(huì)釋放 Monitor 對(duì)象, _owner 恢復(fù)為空, _count – 1。同時(shí)該等待線程進(jìn)入 _WaitSet 中,等待被喚醒。
        ObjectMonitor() {     _header       = NULL;     _count        = 0; // 記錄線程獲取鎖的次數(shù)     _waiters      = 0,     _recursions   = 0;  //鎖的重入次數(shù)     _object       = NULL;     _owner        = NULL;  // 指向持有ObjectMonitor對(duì)象的線程     _WaitSet      = NULL;  // 處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet     _WaitSetLock  = 0 ;     _Responsible  = NULL ;     _succ         = NULL ;     _cxq          = NULL ;     FreeNext      = NULL ;     _EntryList    = NULL ;  // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表     _SpinFreq     = 0 ;     _SpinClock    = 0 ;     OwnerIsThread = 0 ;   }

        可以類(lèi)比一個(gè)去醫(yī)院就診的例子[18]:

        • 首先,患者在門(mén)診大廳前臺(tái)或自助掛號(hào)機(jī)進(jìn)行掛號(hào)

        • 隨后,掛號(hào)結(jié)束后患者找到對(duì)應(yīng)的診室就診

          • 診室每次只能有一個(gè)患者就診;
          • 如果此時(shí)診室空閑,直接進(jìn)入就診;
          • 如果此時(shí)診室內(nèi)有其它患者就診,那么當(dāng)前患者進(jìn)入候診室,等待叫號(hào);
        • 就診結(jié)束后,走出就診室,候診室的下一位候診患者進(jìn)入就診室。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        這個(gè)過(guò)程就和Monitor機(jī)制比較相似:

        • 門(mén)診大廳:所有待進(jìn)入的線程都必須先在入口Entry Set掛號(hào)才有資格;
        • 就診室:就診室**_Owner**里里只能有一個(gè)線程就診,就診完線程就自行離開(kāi)
        • 候診室:就診室繁忙時(shí),進(jìn)入等待區(qū)(Wait Set),就診室空閑的時(shí)候就從**等待區(qū)(Wait Set)**叫新的線程

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        所以我們就知道了,同步是鎖住的什么東西:

        • monitorenter,在判斷擁有同步標(biāo)識(shí) ACC_SYNCHRONIZED 搶先進(jìn)入此方法的線程會(huì)優(yōu)先擁有 Monitor 的 owner ,此時(shí)計(jì)數(shù)器 +1。
        • monitorexit,當(dāng)執(zhí)行完退出后,計(jì)數(shù)器 -1,歸 0 后被其他進(jìn)入的線程獲得。

        26.除了原子性,synchronized可見(jiàn)性,有序性,可重入性怎么實(shí)現(xiàn)?

        synchronized怎么保證可見(jiàn)性?

        • 線程加鎖前,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值。
        • 線程加鎖后,其它線程無(wú)法獲取主內(nèi)存中的共享變量。
        • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。

        synchronized怎么保證有序性?

        synchronized同步的代碼塊,具有排他性,一次只能被一個(gè)線程擁有,所以synchronized保證同一時(shí)刻,代碼是單線程執(zhí)行的。

        因?yàn)閍s-if-serial語(yǔ)義的存在,單線程的程序能保證最終結(jié)果是有序的,但是不保證不會(huì)指令重排。

        所以synchronized保證的有序是執(zhí)行結(jié)果的有序性,而不是防止指令重排的有序性。

        synchronized怎么實(shí)現(xiàn)可重入的呢?

        synchronized 是可重入鎖,也就是說(shuō),允許一個(gè)線程二次請(qǐng)求自己持有對(duì)象鎖的臨界資源,這種情況稱(chēng)為可重入鎖。

        synchronized 鎖對(duì)象的時(shí)候有個(gè)計(jì)數(shù)器,他會(huì)記錄下線程獲取鎖的次數(shù),在執(zhí)行完對(duì)應(yīng)的代碼塊之后,計(jì)數(shù)器就會(huì)-1,直到計(jì)數(shù)器清零,就釋放鎖了。

        之所以,是可重入的。是因?yàn)?synchronized 鎖對(duì)象有個(gè)計(jì)數(shù)器,會(huì)隨著線程獲取鎖后 +1 計(jì)數(shù),當(dāng)線程執(zhí)行完畢后 -1,直到清零釋放鎖。

        27.鎖升級(jí)?synchronized優(yōu)化了解嗎?

        了解鎖升級(jí),得先知道,不同鎖的狀態(tài)是什么樣的。這個(gè)狀態(tài)指的是什么呢?

        Java對(duì)象頭里,有一塊結(jié)構(gòu),叫Mark Word標(biāo)記字段,這塊結(jié)構(gòu)會(huì)隨著鎖的狀態(tài)變化而變化。

        64 位虛擬機(jī) Mark Word 是 64bit,我們來(lái)看看它的狀態(tài)變化:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        Mark Word存儲(chǔ)對(duì)象自身的運(yùn)行數(shù)據(jù),如哈希碼、GC分代年齡、鎖狀態(tài)標(biāo)志、偏向時(shí)間戳(Epoch) 等。

        synchronized做了哪些優(yōu)化?

        在JDK1.6之前,synchronized的實(shí)現(xiàn)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱(chēng)之為重量級(jí)鎖。從JDK6開(kāi)始,HotSpot虛擬機(jī)開(kāi)發(fā)團(tuán)隊(duì)對(duì)Java中的鎖進(jìn)行優(yōu)化,如增加了適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等優(yōu)化策略,提升了synchronized的性能。

        • 偏向鎖:在無(wú)競(jìng)爭(zhēng)的情況下,只是在Mark Word里存儲(chǔ)當(dāng)前線程指針,CAS操作都不做。

        • 輕量級(jí)鎖:在沒(méi)有多線程競(jìng)爭(zhēng)時(shí),相對(duì)重量級(jí)鎖,減少操作系統(tǒng)互斥量帶來(lái)的性能消耗。但是,如果存在鎖競(jìng)爭(zhēng),除了互斥量本身開(kāi)銷(xiāo),還額外有CAS操作的開(kāi)銷(xiāo)。

        • 自旋鎖:減少不必要的CPU上下文切換。在輕量級(jí)鎖升級(jí)為重量級(jí)鎖時(shí),就使用了自旋加鎖的方式

        • 鎖粗化:將多個(gè)連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個(gè)范圍更大的鎖。

        • 鎖消除:虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。

        鎖升級(jí)的過(guò)程是什么樣的?

        鎖升級(jí)方向:無(wú)鎖–>偏向鎖—> 輕量級(jí)鎖—->重量級(jí)鎖,這個(gè)方向基本上是不可逆的。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們看一下升級(jí)的過(guò)程:

        偏向鎖:

        偏向鎖的獲取:

        1. 判斷是否為可偏向狀態(tài)–MarkWord中鎖標(biāo)志是否為‘01’,是否偏向鎖是否為‘1’
        2. 如果是可偏向狀態(tài),則查看線程ID是否為當(dāng)前線程,如果是,則進(jìn)入步驟’5’,否則進(jìn)入步驟‘3’
        3. 通過(guò)CAS操作競(jìng)爭(zhēng)鎖,如果競(jìng)爭(zhēng)成功,則將MarkWord中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行‘5’;競(jìng)爭(zhēng)失敗,則執(zhí)行‘4’
        4. CAS獲取偏向鎖失敗表示有競(jìng)爭(zhēng)。當(dāng)達(dá)到safepoint時(shí)獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊
        5. 執(zhí)行同步代碼

        偏向鎖的撤銷(xiāo):

        1. 偏向鎖不會(huì)主動(dòng)釋放(撤銷(xiāo)),只有遇到其他線程競(jìng)爭(zhēng)時(shí)才會(huì)執(zhí)行撤銷(xiāo),由于撤銷(xiāo)需要知道當(dāng)前持有該偏向鎖的線程棧狀態(tài),因此要等到safepoint時(shí)執(zhí)行,此時(shí)持有該偏向鎖的線程(T)有‘2’,‘3’兩種情況;
        2. 撤銷(xiāo)—-T線程已經(jīng)退出同步代碼塊,或者已經(jīng)不再存活,則直接撤銷(xiāo)偏向鎖,變成無(wú)鎖狀態(tài)—-該狀態(tài)達(dá)到閾值20則執(zhí)行批量重偏向
        3. 升級(jí)—-T線程還在同步代碼塊中,則將T線程的偏向鎖升級(jí)為輕量級(jí)鎖,當(dāng)前線程執(zhí)行輕量級(jí)鎖狀態(tài)下的鎖獲取步驟—-該狀態(tài)達(dá)到閾值40則執(zhí)行批量撤銷(xiāo)

        輕量級(jí)鎖:

        輕量級(jí)鎖的獲取:

        1. 進(jìn)行加鎖操作時(shí),jvm會(huì)判斷是否已經(jīng)時(shí)重量級(jí)鎖,如果不是,則會(huì)在當(dāng)前線程棧幀中劃出一塊空間,作為該鎖的鎖記錄,并且將鎖對(duì)象MarkWord復(fù)制到該鎖記錄中
        2. 復(fù)制成功之后,jvm使用CAS操作將對(duì)象頭MarkWord更新為指向鎖記錄的指針,并將鎖記錄里的owner指針指向?qū)ο箢^的MarkWord。如果成功,則執(zhí)行‘3’,否則執(zhí)行‘4’
        3. 更新成功,則當(dāng)前線程持有該對(duì)象鎖,并且對(duì)象MarkWord鎖標(biāo)志設(shè)置為‘00’,即表示此對(duì)象處于輕量級(jí)鎖狀態(tài)
        4. 更新失敗,jvm先檢查對(duì)象MarkWord是否指向當(dāng)前線程棧幀中的鎖記錄,如果是則執(zhí)行‘5’,否則執(zhí)行‘4’
        5. 表示鎖重入;然后當(dāng)前線程棧幀中增加一個(gè)鎖記錄第一部分(Displaced Mark Word)為null,并指向Mark Word的鎖對(duì)象,起到一個(gè)重入計(jì)數(shù)器的作用。
        6. 表示該鎖對(duì)象已經(jīng)被其他線程搶占,則進(jìn)行自旋等待(默認(rèn)10次),等待次數(shù)達(dá)到閾值仍未獲取到鎖,則升級(jí)為重量級(jí)鎖

        大體上省簡(jiǎn)的升級(jí)過(guò)程:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        完整的升級(jí)過(guò)程:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        28.說(shuō)說(shuō)synchronized和ReentrantLock的區(qū)別?

        可以從鎖的實(shí)現(xiàn)、功能特點(diǎn)、性能等幾個(gè)維度去回答這個(gè)問(wèn)題:

        • 鎖的實(shí)現(xiàn): synchronized是Java語(yǔ)言的關(guān)鍵字,基于JVM實(shí)現(xiàn)。而ReentrantLock是基于JDK的API層面實(shí)現(xiàn)的(一般是lock()和unlock()方法配合try/finally 語(yǔ)句塊來(lái)完成。)
        • 性能: 在JDK1.6鎖優(yōu)化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6開(kāi)始,增加了適應(yīng)性自旋、鎖消除等,兩者性能就差不多了。
        • 功能特點(diǎn): ReentrantLock 比 synchronized 增加了一些高級(jí)功能,如等待可中斷、可實(shí)現(xiàn)公平鎖、可實(shí)現(xiàn)選擇性通知。
          • ReentrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過(guò)lock.lockInterruptibly()來(lái)實(shí)現(xiàn)這個(gè)機(jī)制
          • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
          • synchronized與wait()和notify()/notifyAll()方法結(jié)合實(shí)現(xiàn)等待/通知機(jī)制,ReentrantLock類(lèi)借助Condition接口與newCondition()方法實(shí)現(xiàn)。
          • ReentrantLock需要手工聲明來(lái)加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動(dòng)釋放鎖。

        下面的表格列出出了兩種鎖之間的區(qū)別:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        29.AQS了解多少?

        AbstractQueuedSynchronizer 抽象同步隊(duì)列,簡(jiǎn)稱(chēng) AQS ,它是Java并發(fā)包的根基,并發(fā)包中的鎖就是基于AQS實(shí)現(xiàn)的。

        • AQS是基于一個(gè)FIFO的雙向隊(duì)列,其內(nèi)部定義了一個(gè)節(jié)點(diǎn)類(lèi)Node,Node 節(jié)點(diǎn)內(nèi)部的 SHARED 用來(lái)標(biāo)記該線程是獲取共享資源時(shí)被阻掛起后放入AQS 隊(duì)列的, EXCLUSIVE 用來(lái)標(biāo)記線程是 取獨(dú)占資源時(shí)被掛起后放入AQS 隊(duì)列
        • AQS 使用一個(gè) volatile 修飾的 int 類(lèi)型的成員變量 state 來(lái)表示同步狀態(tài),修改同步狀態(tài)成功即為獲得鎖,volatile 保證了變量在多線程之間的可見(jiàn)性,修改 State 值時(shí)通過(guò) CAS 機(jī)制來(lái)保證修改的原子性
        • 獲取state的方式分為兩種,獨(dú)占方式和共享方式,一個(gè)線程使用獨(dú)占方式獲取了資源,其它線程就會(huì)在獲取失敗后被阻塞。一個(gè)線程使用共享方式獲取了資源,另外一個(gè)線程還可以通過(guò)CAS的方式進(jìn)行獲取。
        • 如果共享資源被占用,需要一定的阻塞等待喚醒機(jī)制來(lái)保證鎖的分配,AQS 中會(huì)將競(jìng)爭(zhēng)共享資源失敗的線程添加到一個(gè)變體的 CLH 隊(duì)列中。

        歸納整理Java并發(fā)知識(shí)點(diǎn)先簡(jiǎn)單了解一下CLH:Craig、Landin and Hagersten 隊(duì)列,是 單向鏈表實(shí)現(xiàn)的隊(duì)列。申請(qǐng)線程只在本地變量上自旋,它不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn) 前驅(qū)節(jié)點(diǎn)釋放了鎖就結(jié)束自旋

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        AQS 中的隊(duì)列是 CLH 變體的虛擬雙向隊(duì)列,通過(guò)將每條請(qǐng)求共享資源的線程封裝成一個(gè)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖的分配:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        AQS 中的 CLH 變體等待隊(duì)列擁有以下特性:

        • AQS 中隊(duì)列是個(gè)雙向鏈表,也是 FIFO 先進(jìn)先出的特性
        • 通過(guò) Head、Tail 頭尾兩個(gè)節(jié)點(diǎn)來(lái)組成隊(duì)列結(jié)構(gòu),通過(guò) volatile 修飾保證可見(jiàn)性
        • Head 指向節(jié)點(diǎn)為已獲得鎖的節(jié)點(diǎn),是一個(gè)虛擬節(jié)點(diǎn),節(jié)點(diǎn)本身不持有具體線程
        • 獲取不到同步狀態(tài),會(huì)將節(jié)點(diǎn)進(jìn)行自旋獲取鎖,自旋一定次數(shù)失敗后會(huì)將線程阻塞,相對(duì)于 CLH 隊(duì)列性能較好

        ps:AQS源碼里面有很多細(xì)節(jié)可問(wèn),建議有時(shí)間好好看看AQS源碼。

        30.ReentrantLock實(shí)現(xiàn)原理?

        ReentrantLock 是可重入的獨(dú)占鎖,只能有一個(gè)線程可以獲取該鎖,其它獲取該鎖的線程會(huì)被阻塞而被放入該鎖的阻塞隊(duì)列里面。

        看看ReentrantLock的加鎖操作:

            // 創(chuàng)建非公平鎖     ReentrantLock lock = new ReentrantLock();     // 獲取鎖操作     lock.lock();     try {         // 執(zhí)行代碼邏輯     } catch (Exception ex) {         // ...     } finally {         // 解鎖操作         lock.unlock();     }

        new ReentrantLock()構(gòu)造函數(shù)默認(rèn)創(chuàng)建的是非公平鎖 NonfairSync。

        公平鎖 FairSync

        1. 公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖
        2. 公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞,CPU 喚醒阻塞線程的開(kāi)銷(xiāo)比非公平鎖大

        非公平鎖 NonfairSync

        • 非公平鎖是多個(gè)線程加鎖時(shí)直接嘗試獲取鎖,獲取不到才會(huì)到等待隊(duì)列的隊(duì)尾等待。但如果此時(shí)鎖剛好可用,那么這個(gè)線程可以無(wú)需阻塞直接獲取到鎖
        • 非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開(kāi)銷(xiāo),整體的吞吐效率高,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU 不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會(huì)餓死,或者等很久才會(huì)獲得鎖

        默認(rèn)創(chuàng)建的對(duì)象lock()的時(shí)候:

        • 如果鎖當(dāng)前沒(méi)有被其它線程占用,并且當(dāng)前線程之前沒(méi)有獲取過(guò)該鎖,則當(dāng)前線程會(huì)獲取到該鎖,然后設(shè)置當(dāng)前鎖的擁有者為當(dāng)前線程,并設(shè)置 AQS 的狀態(tài)值為1 ,然后直接返回。如果當(dāng)前線程之前己經(jīng)獲取過(guò)該鎖,則這次只是簡(jiǎn)單地把 AQS 的狀態(tài)值加1后返回。
        • 如果該鎖己經(jīng)被其他線程持有,非公平鎖會(huì)嘗試去獲取鎖,獲取失敗的話,則調(diào)用該方法線程會(huì)被放入 AQS 隊(duì)列阻塞掛起。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        31.ReentrantLock怎么實(shí)現(xiàn)公平鎖的?

        new ReentrantLock()構(gòu)造函數(shù)默認(rèn)創(chuàng)建的是非公平鎖 NonfairSync

        public ReentrantLock() {     sync = new NonfairSync();}

        同時(shí)也可以在創(chuàng)建鎖構(gòu)造函數(shù)中傳入具體參數(shù)創(chuàng)建公平鎖 FairSync

        ReentrantLock lock = new ReentrantLock(true);--- ReentrantLock// true 代表公平鎖,false 代表非公平鎖public ReentrantLock(boolean fair) {     sync = fair ? new FairSync() : new NonfairSync();}

        FairSync、NonfairSync 代表公平鎖和非公平鎖,兩者都是 ReentrantLock 靜態(tài)內(nèi)部類(lèi),只不過(guò)實(shí)現(xiàn)不同鎖語(yǔ)義。

        非公平鎖和公平鎖的兩處不同:

        1. 非公平鎖在調(diào)用 lock 后,首先就會(huì)調(diào)用 CAS 進(jìn)行一次搶鎖,如果這個(gè)時(shí)候恰巧鎖沒(méi)有被占用,那么直接就獲取到鎖返回了。
        2. 非公平鎖在 CAS 失敗后,和公平鎖一樣都會(huì)進(jìn)入到 tryAcquire 方法,在 tryAcquire 方法中,如果發(fā)現(xiàn)鎖這個(gè)時(shí)候被釋放了(state == 0),非公平鎖會(huì)直接 CAS 搶鎖,但是公平鎖會(huì)判斷等待隊(duì)列是否有線程處于等待狀態(tài),如果有則不去搶鎖,乖乖排到后面。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        相對(duì)來(lái)說(shuō),非公平鎖會(huì)有更好的性能,因?yàn)樗耐掏铝勘容^大。當(dāng)然,非公平鎖讓獲取鎖的時(shí)間變得更加不確定,可能會(huì)導(dǎo)致在阻塞隊(duì)列中的線程長(zhǎng)期處于饑餓狀態(tài)。

        32.CAS呢?CAS了解多少?

        CAS叫做CompareAndSwap,?較并交換,主要是通過(guò)處理器的指令來(lái)保證操作的原?性的。

        CAS 指令包含 3 個(gè)參數(shù):共享變量的內(nèi)存地址 A、預(yù)期的值 B 和共享變量的新值 C。

        只有當(dāng)內(nèi)存中地址 A 處的值等于 B 時(shí),才能將內(nèi)存中地址 A 處的值更新為新值 C。作為一條 CPU 指令,CAS 指令本身是能夠保證原子性的 。

        33.CAS 有什么問(wèn)題?如何解決?

        CAS的經(jīng)典三大問(wèn)題:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        ABA 問(wèn)題

        并發(fā)環(huán)境下,假設(shè)初始條件是A,去修改數(shù)據(jù)時(shí),發(fā)現(xiàn)是A就會(huì)執(zhí)行修改。但是看到的雖然是A,中間可能發(fā)生了A變B,B又變回A的情況。此時(shí)A已經(jīng)非彼A,數(shù)據(jù)即使成功修改,也可能有問(wèn)題。

        怎么解決ABA問(wèn)題?

        • 加版本號(hào)

        每次修改變量,都在這個(gè)變量的版本號(hào)上加1,這樣,剛剛A->B->A,雖然A的值沒(méi)變,但是它的版本號(hào)已經(jīng)變了,再判斷版本號(hào)就會(huì)發(fā)現(xiàn)此時(shí)的A已經(jīng)被改過(guò)了。參考樂(lè)觀鎖的版本號(hào),這種做法可以給數(shù)據(jù)帶上了一種實(shí)效性的檢驗(yàn)。

        Java提供了AtomicStampReference類(lèi),它的compareAndSet方法首先檢查當(dāng)前的對(duì)象引用值是否等于預(yù)期引用,并且當(dāng)前印戳(Stamp)標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將引用值和印戳標(biāo)志的值更新為給定的更新值。

        循環(huán)性能開(kāi)銷(xiāo)

        自旋CAS,如果一直循環(huán)執(zhí)行,一直不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷(xiāo)。

        怎么解決循環(huán)性能開(kāi)銷(xiāo)問(wèn)題?

        在Java中,很多使用自旋CAS的地方,會(huì)有一個(gè)自旋次數(shù)的限制,超過(guò)一定次數(shù),就停止自旋。

        只能保證一個(gè)變量的原子操作

        CAS 保證的是對(duì)一個(gè)變量執(zhí)行操作的原子性,如果對(duì)多個(gè)變量操作時(shí),CAS 目前無(wú)法直接保證操作的原子性的。

        怎么解決只能保證一個(gè)變量的原子操作問(wèn)題?

        • 可以考慮改用鎖來(lái)保證操作的原子性
        • 可以考慮合并多個(gè)變量,將多個(gè)變量封裝成一個(gè)對(duì)象,通過(guò)AtomicReference來(lái)保證原子性。

        34.Java有哪些保證原子性的方法?如何保證多線程下i++ 結(jié)果正確?

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 使用循環(huán)原子類(lèi),例如AtomicInteger,實(shí)現(xiàn)i++原子操作
        • 使用juc包下的鎖,如ReentrantLock ,對(duì)i++操作加鎖lock.lock()來(lái)實(shí)現(xiàn)原子性
        • 使用synchronized,對(duì)i++操作加鎖

        35.原子操作類(lèi)了解多少?

        當(dāng)程序更新一個(gè)變量時(shí),如果多線程同時(shí)更新這個(gè)變量,可能得到期望之外的值,比如變量i=1,A線程更新i+1,B線程也更新i+1,經(jīng)過(guò)兩個(gè)線程操作之后可能i不等于3,而是等于2。因?yàn)锳和B線程在更新變量i的時(shí)候拿到的i都是1,這就是線程不安全的更新操作,一般我們會(huì)使用synchronized來(lái)解決這個(gè)問(wèn)題,synchronized會(huì)保證多線程不會(huì)同時(shí)更新變量i。

        其實(shí)除此之外,還有更輕量級(jí)的選擇,Java從JDK 1.5開(kāi)始提供了java.util.concurrent.atomic包,這個(gè)包中的原子操作類(lèi)提供了一種用法簡(jiǎn)單、性能高效、線程安全地更新一個(gè)變量的方式。

        因?yàn)樽兞康念?lèi)型有很多種,所以在Atomic包里一共提供了13個(gè)類(lèi),屬于4種類(lèi)型的原子更新方式,分別是原子更新基本類(lèi)型、原子更新數(shù)組、原子更新引用和原子更新屬性(字段)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        Atomic包里的類(lèi)基本都是使用Unsafe實(shí)現(xiàn)的包裝類(lèi)。

        使用原子的方式更新基本類(lèi)型,Atomic包提供了以下3個(gè)類(lèi):

        • AtomicBoolean:原子更新布爾類(lèi)型。

        • AtomicInteger:原子更新整型。

        • AtomicLong:原子更新長(zhǎng)整型。

        通過(guò)原子的方式更新數(shù)組里的某個(gè)元素,Atomic包提供了以下4個(gè)類(lèi):

        • AtomicIntegerArray:原子更新整型數(shù)組里的元素。

        • AtomicLongArray:原子更新長(zhǎng)整型數(shù)組里的元素。

        • AtomicReferenceArray:原子更新引用類(lèi)型數(shù)組里的元素。

        • AtomicIntegerArray類(lèi)主要是提供原子的方式更新數(shù)組里的整型

        原子更新基本類(lèi)型的AtomicInteger,只能更新一個(gè)變量,如果要原子更新多個(gè)變量,就需要使用這個(gè)原子更新引用類(lèi)型提供的類(lèi)。Atomic包提供了以下3個(gè)類(lèi):

        • AtomicReference:原子更新引用類(lèi)型。

        • AtomicReferenceFieldUpdater:原子更新引用類(lèi)型里的字段。

        • AtomicMarkableReference:原子更新帶有標(biāo)記位的引用類(lèi)型。可以原子更新一個(gè)布爾類(lèi)型的標(biāo)記位和引用類(lèi)型。構(gòu)造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

        如果需原子地更新某個(gè)類(lèi)里的某個(gè)字段時(shí),就需要使用原子更新字段類(lèi),Atomic包提供了以下3個(gè)類(lèi)進(jìn)行原子字段更新:

        • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
        • AtomicLongFieldUpdater:原子更新長(zhǎng)整型字段的更新器。
        • AtomicStampedReference:原子更新帶有版本號(hào)的引用類(lèi)型。該類(lèi)將整數(shù)值與引用關(guān)聯(lián)起來(lái),可用于原子的更新數(shù)據(jù)和數(shù)據(jù)的版本號(hào),可以解決使用CAS進(jìn)行原子更新時(shí)可能出現(xiàn)的 ABA問(wèn)題。

        36.AtomicInteger 的原理?

        一句話概括:使用CAS實(shí)現(xiàn)

        以AtomicInteger的添加方法為例:

            public final int getAndIncrement() {         return unsafe.getAndAddInt(this, valueOffset, 1);     }

        通過(guò)Unsafe類(lèi)的實(shí)例來(lái)進(jìn)行添加操作,來(lái)看看具體的CAS操作:

            public final int getAndAddInt(Object var1, long var2, int var4) {         int var5;         do {             var5 = this.getIntVolatile(var1, var2);         } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));          return var5;     }

        compareAndSwapInt 是一個(gè)native方法,基于CAS來(lái)操作int類(lèi)型變量。其它的原子操作類(lèi)基本都是大同小異。

        37.線程死鎖了解嗎?該如何避免?

        死鎖是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的互相等待的現(xiàn)象,在無(wú)外力作用的情況下,這些線程會(huì)一直相互等待而無(wú)法繼續(xù)運(yùn)行下去。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        那么為什么會(huì)產(chǎn)生死鎖呢? 死鎖的產(chǎn)生必須具備以下四個(gè)條件:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 互斥條件:指線程對(duì)己經(jīng)獲取到的資源進(jìn)行它性使用,即該資源同時(shí)只由一個(gè)線程占用。如果此時(shí)還有其它線程請(qǐng)求獲取獲取該資源,則請(qǐng)求者只能等待,直至占有資源的線程釋放該資源。
        • 請(qǐng)求并持有條件:指一個(gè) 線程己經(jīng)持有了至少一個(gè)資源,但又提出了新的資源請(qǐng)求,而新資源己被其它線程占有,所以當(dāng)前線程會(huì)被阻塞,但阻塞 的同時(shí)并不釋放自己已經(jīng)獲取的資源。
        • 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其它線程搶占,只有在自己使用完畢后才由自己釋放該資源。
        • 環(huán)路等待條件:指在發(fā)生死鎖時(shí),必然存在一個(gè)線程——資源的環(huán)形鏈,即線程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的資源,Tl1正在等待 T2用的資源,…… Tn 在等待己被 T0占用的資源。

        該如何避免死鎖呢?答案是至少破壞死鎖發(fā)生的一個(gè)條件

        • 其中,互斥這個(gè)條件我們沒(méi)有辦法破壞,因?yàn)橛面i為的就是互斥。不過(guò)其他三個(gè)條件都是有辦法破壞掉的,到底如何做呢?

        • 對(duì)于“請(qǐng)求并持有”這個(gè)條件,可以一次性請(qǐng)求所有的資源。

        • 對(duì)于“不可剝奪”這個(gè)條件,占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到,可以主動(dòng)釋放它占有的資源,這樣不可搶占這個(gè)條件就破壞掉了。

        • 對(duì)于“環(huán)路等待”這個(gè)條件,可以靠按序申請(qǐng)資源來(lái)預(yù)防。所謂按序申請(qǐng),是指資源是有線性順序的,申請(qǐng)的時(shí)候可以先申請(qǐng)資源序號(hào)小的,再申請(qǐng)資源序號(hào)大的,這樣線性化后就不存在環(huán)路了。

        38.那死鎖問(wèn)題怎么排查呢?

        可以使用jdk自帶的命令行工具排查:

        1. 使用jps查找運(yùn)行的Java進(jìn)程:jps -l
        2. 使用jstack查看線程堆棧信息:jstack -l 進(jìn)程id

        基本就可以看到死鎖的信息。

        還可以利用圖形化工具,比如JConsole。出現(xiàn)線程死鎖以后,點(diǎn)擊JConsole線程面板的檢測(cè)到死鎖按鈕,將會(huì)看到線程的死鎖信息。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        39.CountDownLatch(倒計(jì)數(shù)器)了解嗎?

        CountDownLatch,倒計(jì)數(shù)器,有兩個(gè)常見(jiàn)的應(yīng)用場(chǎng)景[18]:

        場(chǎng)景1:協(xié)調(diào)子線程結(jié)束動(dòng)作:等待所有子線程運(yùn)行結(jié)束

        CountDownLatch允許一個(gè)或多個(gè)線程等待其他線程完成操作。

        例如,我們很多人喜歡玩的王者榮耀,開(kāi)黑的時(shí)候,得等所有人都上線之后,才能開(kāi)打。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        CountDownLatch模仿這個(gè)場(chǎng)景(參考[18]):

        創(chuàng)建大喬、蘭陵王、安其拉、哪吒和鎧等五個(gè)玩家,主線程必須在他們都完成確認(rèn)后,才可以繼續(xù)運(yùn)行。

        在這段代碼中,new CountDownLatch(5)用戶創(chuàng)建初始的latch數(shù)量,各玩家通過(guò)countDownLatch.countDown()完成狀態(tài)確認(rèn),主線程通過(guò)countDownLatch.await()等待。

            public static void main(String[] args) throws InterruptedException {         CountDownLatch countDownLatch = new CountDownLatch(5);          Thread 大喬 = new Thread(countDownLatch::countDown);         Thread 蘭陵王 = new Thread(countDownLatch::countDown);         Thread 安其拉 = new Thread(countDownLatch::countDown);         Thread 哪吒 = new Thread(countDownLatch::countDown);         Thread 鎧 = new Thread(() -> {             try {                 // 稍等,上個(gè)衛(wèi)生間,馬上到...                 Thread.sleep(1500);                 countDownLatch.countDown();             } catch (InterruptedException ignored) {}         });          大喬.start();         蘭陵王.start();         安其拉.start();         哪吒.start();         鎧.start();         countDownLatch.await();         System.out.println("所有玩家已經(jīng)就位!");     }

        場(chǎng)景2. 協(xié)調(diào)子線程開(kāi)始動(dòng)作:統(tǒng)一各線程動(dòng)作開(kāi)始的時(shí)機(jī)

        王者游戲中也有類(lèi)似的場(chǎng)景,游戲開(kāi)始時(shí),各玩家的初始狀態(tài)必須一致。不能有的玩家都出完裝了,有的才降生。

        所以大家得一塊出生,在

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        在這個(gè)場(chǎng)景中,仍然用五個(gè)線程代表大喬、蘭陵王、安其拉、哪吒和鎧等五個(gè)玩家。需要注意的是,各玩家雖然都調(diào)用了start()線程,但是它們?cè)谶\(yùn)行時(shí)都在等待countDownLatch的信號(hào),在信號(hào)未收到前,它們不會(huì)往下執(zhí)行。

            public static void main(String[] args) throws InterruptedException {         CountDownLatch countDownLatch = new CountDownLatch(1);          Thread 大喬 = new Thread(() -> waitToFight(countDownLatch));         Thread 蘭陵王 = new Thread(() -> waitToFight(countDownLatch));         Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));         Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));         Thread 鎧 = new Thread(() -> waitToFight(countDownLatch));          大喬.start();         蘭陵王.start();         安其拉.start();         哪吒.start();         鎧.start();         Thread.sleep(1000);         countDownLatch.countDown();         System.out.println("敵方還有5秒達(dá)到戰(zhàn)場(chǎng),全軍出擊!");     }      private static void waitToFight(CountDownLatch countDownLatch) {         try {             countDownLatch.await(); // 在此等待信號(hào)再繼續(xù)             System.out.println("收到,發(fā)起進(jìn)攻!");         } catch (InterruptedException e) {             e.printStackTrace();         }     }

        CountDownLatch的核心方法也不多:

        • await():等待latch降為0;
        • boolean await(long timeout, TimeUnit unit):等待latch降為0,但是可以設(shè)置超時(shí)時(shí)間。比如有玩家超時(shí)未確認(rèn),那就重新匹配,總不能為了某個(gè)玩家等到天荒地老。
        • countDown():latch數(shù)量減1;
        • getCount():獲取當(dāng)前的latch數(shù)量。

        40.CyclicBarrier(同步屏障)了解嗎?

        CyclicBarrier的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一 組線程到達(dá)一個(gè)屏障(也可以叫同步點(diǎn))時(shí)被阻塞,直到最后一個(gè)線程到達(dá)屏障時(shí),屏障才會(huì)開(kāi)門(mén),所有被屏障攔截的線程才會(huì)繼續(xù)運(yùn)行。

        它和CountDownLatch類(lèi)似,都可以協(xié)調(diào)多線程的結(jié)束動(dòng)作,在它們結(jié)束后都可以執(zhí)行特定動(dòng)作,但是為什么要有CyclicBarrier,自然是它有和CountDownLatch不同的地方。

        不知道你聽(tīng)沒(méi)聽(tīng)過(guò)一個(gè)新人UP主小約翰可汗,小約翰生平有兩大恨——“想結(jié)衣結(jié)衣不依,迷愛(ài)理愛(ài)理不理。”我們來(lái)還原一下事情的經(jīng)過(guò):小約翰在親政后認(rèn)識(shí)了新垣結(jié)衣,于是決定第一次選妃,向結(jié)衣表白,等待回應(yīng)。然而新垣結(jié)衣回應(yīng)嫁給了星野源,小約翰傷心欲絕,發(fā)誓生平不娶,突然發(fā)現(xiàn)了鈴木愛(ài)理,于是小約翰決定第二次選妃,求愛(ài)理搭理,等待回應(yīng)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們拿代碼模擬這一場(chǎng)景,發(fā)現(xiàn)CountDownLatch無(wú)能為力了,因?yàn)镃ountDownLatch的使用是一次性的,無(wú)法重復(fù)利用,而這里等待了兩次。此時(shí),我們用CyclicBarrier就可以實(shí)現(xiàn),因?yàn)樗梢灾貜?fù)利用。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        運(yùn)行結(jié)果:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        CyclicBarrier最最核心的方法,仍然是await():

        • 如果當(dāng)前線程不是第一個(gè)到達(dá)屏障的話,它將會(huì)進(jìn)入等待,直到其他線程都到達(dá),除非發(fā)生被中斷屏障被拆除屏障被重設(shè)等情況;

        上面的例子抽象一下,本質(zhì)上它的流程就是這樣就是這樣:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        41.CyclicBarrier和CountDownLatch有什么區(qū)別?

        兩者最核心的區(qū)別[18]:

        • CountDownLatch是一次性的,而CyclicBarrier則可以多次設(shè)置屏障,實(shí)現(xiàn)重復(fù)利用;
        • CountDownLatch中的各個(gè)子線程不可以等待其他線程,只能完成自己的任務(wù);而CyclicBarrier中的各個(gè)線程可以等待其他線程

        它們區(qū)別用一個(gè)表格整理:

        CyclicBarrier CountDownLatch
        CyclicBarrier是可重用的,其中的線程會(huì)等待所有的線程完成任務(wù)。屆時(shí),屏障將被拆除,并可以選擇性地做一些特定的動(dòng)作。 CountDownLatch是一次性的,不同的線程在同一個(gè)計(jì)數(shù)器上工作,直到計(jì)數(shù)器為0.
        CyclicBarrier面向的是線程數(shù) CountDownLatch面向的是任務(wù)數(shù)
        在使用CyclicBarrier時(shí),你必須在構(gòu)造中指定參與協(xié)作的線程數(shù),這些線程必須調(diào)用await()方法 使用CountDownLatch時(shí),則必須要指定任務(wù)數(shù),至于這些任務(wù)由哪些線程完成無(wú)關(guān)緊要
        CyclicBarrier可以在所有的線程釋放后重新使用 CountDownLatch在計(jì)數(shù)器為0時(shí)不能再使用
        在CyclicBarrier中,如果某個(gè)線程遇到了中斷、超時(shí)等問(wèn)題時(shí),則處于await的線程都會(huì)出現(xiàn)問(wèn)題 在CountDownLatch中,如果某個(gè)線程出現(xiàn)問(wèn)題,其他線程不受影響

        42.Semaphore(信號(hào)量)了解嗎?

        Semaphore(信號(hào)量)是用來(lái)控制同時(shí)訪問(wèn)特定資源的線程數(shù)量,它通過(guò)協(xié)調(diào)各個(gè)線程,以保證合理的使用公共資源。

        聽(tīng)起來(lái)似乎很抽象,現(xiàn)在汽車(chē)多了,開(kāi)車(chē)出門(mén)在外的一個(gè)老大難問(wèn)題就是停車(chē) 。停車(chē)場(chǎng)的車(chē)位是有限的,只能允許若干車(chē)輛停泊,如果停車(chē)場(chǎng)還有空位,那么顯示牌顯示的就是綠燈和剩余的車(chē)位,車(chē)輛就可以駛?cè)耄蝗绻\?chē)場(chǎng)沒(méi)位了,那么顯示牌顯示的就是綠燈和數(shù)字0,車(chē)輛就得等待。如果滿了的停車(chē)場(chǎng)有車(chē)離開(kāi),那么顯示牌就又變綠,顯示空車(chē)位數(shù)量,等待的車(chē)輛就能進(jìn)停車(chē)場(chǎng)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們把這個(gè)例子類(lèi)比一下,車(chē)輛就是線程,進(jìn)入停車(chē)場(chǎng)就是線程在執(zhí)行,離開(kāi)停車(chē)場(chǎng)就是線程執(zhí)行完畢,看見(jiàn)紅燈就表示線程被阻塞,不能執(zhí)行,Semaphore的本質(zhì)就是協(xié)調(diào)多個(gè)線程對(duì)共享資源的獲取

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們?cè)賮?lái)看一個(gè)Semaphore的用途:它可以用于做流量控制,特別是公用資源有限的應(yīng)用場(chǎng)景,比如數(shù)據(jù)庫(kù)連接。

        假如有一個(gè)需求,要讀取幾萬(wàn)個(gè)文件的數(shù)據(jù),因?yàn)槎际荌O密集型任務(wù),我們可以啟動(dòng)幾十個(gè)線程并發(fā)地讀取,但是如果讀到內(nèi)存后,還需要存儲(chǔ)到數(shù)據(jù)庫(kù)中,而數(shù)據(jù)庫(kù)的連接數(shù)只有10個(gè),這時(shí)我們必須控制只有10個(gè)線程同時(shí)獲取數(shù)據(jù)庫(kù)連接保存數(shù)據(jù),否則會(huì)報(bào)錯(cuò)無(wú)法獲取數(shù)據(jù)庫(kù)連接。這個(gè)時(shí)候,就可以使用Semaphore來(lái)做流量控制,如下:

        public class SemaphoreTest {     private static final int THREAD_COUNT = 30;     private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);     private static Semaphore s = new Semaphore(10);      public static void main(String[] args) {         for (int i = 0; i < THREAD_COUNT; i++) {             threadPool.execute(new Runnable() {                 @Override                 public void run() {                     try {                         s.acquire();                         System.out.println("save data");                         s.release();                     } catch (InterruptedException e) {                     }                 }             });         }         threadPool.shutdown();     }}

        在代碼中,雖然有30個(gè)線程在執(zhí)行,但是只允許10個(gè)并發(fā)執(zhí)行。Semaphore的構(gòu)造方法Semaphore(int permits)接受一個(gè)整型的數(shù)字,表示可用的許可證數(shù)量。Semaphore(10)表示允許10個(gè)線程獲取許可證,也就是最大并發(fā)數(shù)是10。Semaphore的用法也很簡(jiǎn)單,首先線程使用 Semaphore的acquire()方法獲取一個(gè)許可證,使用完之后調(diào)用release()方法歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證。

        43.Exchanger 了解嗎?

        Exchanger(交換者)是一個(gè)用于線程間協(xié)作的工具類(lèi)。Exchanger用于進(jìn)行線程間的數(shù)據(jù)交換。它提供一個(gè)同步點(diǎn),在這個(gè)同步點(diǎn),兩個(gè)線程可以交換彼此的數(shù)據(jù)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        這兩個(gè)線程通過(guò) exchange方法交換數(shù)據(jù),如果第一個(gè)線程先執(zhí)行exchange()方法,它會(huì)一直等待第二個(gè)線程也執(zhí)行exchange方法,當(dāng)兩個(gè)線程都到達(dá)同步點(diǎn)時(shí),這兩個(gè)線程就可以交換數(shù)據(jù),將本線程生產(chǎn)出來(lái)的數(shù)據(jù)傳遞給對(duì)方。

        Exchanger可以用于遺傳算法,遺傳算法里需要選出兩個(gè)人作為交配對(duì)象,這時(shí)候會(huì)交換兩人的數(shù)據(jù),并使用交叉規(guī)則得出2個(gè)交配結(jié)果。Exchanger也可以用于校對(duì)工作,比如我們需要將紙制銀行流水通過(guò)人工的方式錄入成電子銀行流水,為了避免錯(cuò)誤,采用AB崗兩人進(jìn)行錄入,錄入到Excel之后,系統(tǒng)需要加載這兩個(gè)Excel,并對(duì)兩個(gè)Excel數(shù)據(jù)進(jìn)行校對(duì),看看是否錄入一致。

        public class ExchangerTest {     private static final Exchanger<String> exgr = new Exchanger<String>();     private static ExecutorService threadPool = Executors.newFixedThreadPool(2);      public static void main(String[] args) {         threadPool.execute(new Runnable() {             @Override             public void run() {                 try {                     String A = "銀行流水A"; // A錄入銀行流水?dāng)?shù)據(jù)                      exgr.exchange(A);                 } catch (InterruptedException e) {                 }             }         });         threadPool.execute(new Runnable() {             @Override             public void run() {                 try {                     String B = "銀行流水B"; // B錄入銀行流水?dāng)?shù)據(jù)                      String A = exgr.exchange("B");                     System.out.println("A和B數(shù)據(jù)是否一致:" + A.equals(B) + ",A錄入的是:"                             + A + ",B錄入是:" + B);                 } catch (InterruptedException e) {                 }             }         });         threadPool.shutdown();     }}

        假如兩個(gè)線程有一個(gè)沒(méi)有執(zhí)行exchange()方法,則會(huì)一直等待,如果擔(dān)心有特殊情況發(fā)生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit)設(shè)置最大等待時(shí)長(zhǎng)

        44.什么是線程池?

        線程池: 簡(jiǎn)單理解,它就是一個(gè)管理線程的池子。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 它幫我們管理線程,避免增加創(chuàng)建線程和銷(xiāo)毀線程的資源損耗。因?yàn)榫€程其實(shí)也是一個(gè)對(duì)象,創(chuàng)建一個(gè)對(duì)象,需要經(jīng)過(guò)類(lèi)加載過(guò)程,銷(xiāo)毀一個(gè)對(duì)象,需要走GC垃圾回收流程,都是需要資源開(kāi)銷(xiāo)的。
        • 提高響應(yīng)速度。 如果任務(wù)到達(dá)了,相對(duì)于從線程池拿線程,重新去創(chuàng)建一條線程執(zhí)行,速度肯定慢很多。
        • 重復(fù)利用。 線程用完,再放回池子,可以達(dá)到重復(fù)利用的效果,節(jié)省資源。

        45.能說(shuō)說(shuō)工作中線程池的應(yīng)用嗎?

        之前我們有一個(gè)和第三方對(duì)接的需求,需要向第三方推送數(shù)據(jù),引入了多線程來(lái)提升數(shù)據(jù)推送的效率,其中用到了線程池來(lái)管理線程。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        主要代碼如下:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        完整可運(yùn)行代碼地址:https://gitee.com/fighter3/thread-demo.git

        線程池的參數(shù)如下:

        • corePoolSize:線程核心參數(shù)選擇了CPU數(shù)×2

        • maximumPoolSize:最大線程數(shù)選擇了和核心線程數(shù)相同

        • keepAliveTime:非核心閑置線程存活時(shí)間直接置為0

        • unit:非核心線程保持存活的時(shí)間選擇了 TimeUnit.SECONDS 秒

        • workQueue:線程池等待隊(duì)列,使用 LinkedBlockingQueue阻塞隊(duì)列

        同時(shí)還用了synchronized 來(lái)加鎖,保證數(shù)據(jù)不會(huì)被重復(fù)推送:

          synchronized (PushProcessServiceImpl.class) {}

        ps:這個(gè)例子只是簡(jiǎn)單地進(jìn)行了數(shù)據(jù)推送,實(shí)際上還可以結(jié)合其他的業(yè)務(wù),像什么數(shù)據(jù)清洗啊、數(shù)據(jù)統(tǒng)計(jì)啊,都可以套用。

        46.能簡(jiǎn)單說(shuō)一下線程池的工作流程嗎?

        用一個(gè)通俗的比喻:

        有一個(gè)營(yíng)業(yè)廳,總共有六個(gè)窗口,現(xiàn)在開(kāi)放了三個(gè)窗口,現(xiàn)在有三個(gè)窗口坐著三個(gè)營(yíng)業(yè)員小姐姐在營(yíng)業(yè)。

        老三去辦業(yè)務(wù),可能會(huì)遇到什么情況呢?

        1. 老三發(fā)現(xiàn)有空間的在營(yíng)業(yè)的窗口,直接去找小姐姐辦理業(yè)務(wù)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. 老三發(fā)現(xiàn)沒(méi)有空閑的窗口,就在排隊(duì)區(qū)排隊(duì)等。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. 老三發(fā)現(xiàn)沒(méi)有空閑的窗口,等待區(qū)也滿了,蚌埠住了,經(jīng)理一看,就讓休息的小姐姐趕緊回來(lái)上班,等待區(qū)號(hào)靠前的趕緊去新窗口辦,老三去排隊(duì)區(qū)排隊(duì)。小姐姐比較辛苦,假如一段時(shí)間發(fā)現(xiàn)他們可以不用接著營(yíng)業(yè),經(jīng)理就讓她們接著休息。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. 老三一看,六個(gè)窗口都滿了,等待區(qū)也沒(méi)位置了。老三急了,要鬧,經(jīng)理趕緊出來(lái)了,經(jīng)理該怎么辦呢?

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. 我們銀行系統(tǒng)已經(jīng)癱瘓

        2. 誰(shuí)叫你來(lái)辦的你找誰(shuí)去

        3. 看你比較急,去隊(duì)里加個(gè)塞

        4. 今天沒(méi)辦法,不行你看改一天

        上面的這個(gè)流程幾乎就跟 JDK 線程池的大致流程類(lèi)似,

        1. 營(yíng)業(yè)中的 3個(gè)窗口對(duì)應(yīng)核心線程池?cái)?shù):corePoolSize
        2. 總的營(yíng)業(yè)窗口數(shù)6對(duì)應(yīng):maximumPoolSize
        3. 打開(kāi)的臨時(shí)窗口在多少時(shí)間內(nèi)無(wú)人辦理則關(guān)閉對(duì)應(yīng):unit
        4. 排隊(duì)區(qū)就是等待隊(duì)列:workQueue
        5. 無(wú)法辦理的時(shí)候銀行給出的解決方法對(duì)應(yīng):RejectedExecutionHandler
        6. threadFactory 該參數(shù)在 JDK 中是 線程工廠,用來(lái)創(chuàng)建線程對(duì)象,一般不會(huì)動(dòng)。

        所以我們線程池的工作流程也比較好理解了:

        1. 線程池剛創(chuàng)建時(shí),里面沒(méi)有一個(gè)線程。任務(wù)隊(duì)列是作為參數(shù)傳進(jìn)來(lái)的。不過(guò),就算隊(duì)列里面有任務(wù),線程池也不會(huì)馬上執(zhí)行它們。
        2. 當(dāng)調(diào)用 execute() 方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判斷:
        • 如果正在運(yùn)行的線程數(shù)量小于 corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);
        • 如果正在運(yùn)行的線程數(shù)量大于或等于 corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列;
        • 如果這時(shí)候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于 maximumPoolSize,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行這個(gè)任務(wù);
        • 如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于 maximumPoolSize,那么線程池會(huì)根據(jù)拒絕策略來(lái)對(duì)應(yīng)處理。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        1. 當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來(lái)執(zhí)行。

        2. 當(dāng)一個(gè)線程無(wú)事可做,超過(guò)一定的時(shí)間(keepAliveTime)時(shí),線程池會(huì)判斷,如果當(dāng)前運(yùn)行的線程數(shù)大于 corePoolSize,那么這個(gè)線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會(huì)收縮到 corePoolSize 的大小。

        47.線程池主要參數(shù)有哪些?

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        線程池有七大參數(shù),需要重點(diǎn)關(guān)注corePoolSizemaximumPoolSizeworkQueuehandler這四個(gè)。

        1. corePoolSize

        此值是用來(lái)初始化線程池中核心線程數(shù),當(dāng)線程池中線程池?cái)?shù)< corePoolSize時(shí),系統(tǒng)默認(rèn)是添加一個(gè)任務(wù)才創(chuàng)建一個(gè)線程池。當(dāng)線程數(shù) = corePoolSize時(shí),新任務(wù)會(huì)追加到workQueue中。

        1. maximumPoolSize

        maximumPoolSize表示允許的最大線程數(shù) = (非核心線程數(shù)+核心線程數(shù)),當(dāng)BlockingQueue也滿了,但線程池中總線程數(shù) < maximumPoolSize時(shí)候就會(huì)再次創(chuàng)建新的線程。

        1. keepAliveTime

        非核心線程 =(maximumPoolSize – corePoolSize ) ,非核心線程閑置下來(lái)不干活最多存活時(shí)間。

        1. unit

        線程池中非核心線程保持存活的時(shí)間的單位

        • TimeUnit.DAYS; 天
        • TimeUnit.HOURS; 小時(shí)
        • TimeUnit.MINUTES; 分鐘
        • TimeUnit.SECONDS; 秒
        • TimeUnit.MILLISECONDS; 毫秒
        • TimeUnit.MICROSECONDS; 微秒
        • TimeUnit.NANOSECONDS; 納秒
        1. workQueue

        線程池等待隊(duì)列,維護(hù)著等待執(zhí)行的Runnable對(duì)象。當(dāng)運(yùn)行當(dāng)線程數(shù)= corePoolSize時(shí),新的任務(wù)會(huì)被添加到workQueue中,如果workQueue也滿了則嘗試用非核心線程執(zhí)行任務(wù),等待隊(duì)列應(yīng)該盡量用有界的。

        1. threadFactory

        創(chuàng)建一個(gè)新線程時(shí)使用的工廠,可以用來(lái)設(shè)定線程名、是否為daemon線程等等。

        1. handler

        corePoolSizeworkQueuemaximumPoolSize都不可用的時(shí)候執(zhí)行的飽和策略。

        48.線程池的拒絕策略有哪些?

        類(lèi)比前面的例子,無(wú)法辦理業(yè)務(wù)時(shí)的處理方式,幫助記憶:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • AbortPolicy :直接拋出異常,默認(rèn)使用此策略
        • CallerRunsPolicy:用調(diào)用者所在的線程來(lái)執(zhí)行任務(wù)
        • DiscardOldestPolicy:丟棄阻塞隊(duì)列里最老的任務(wù),也就是隊(duì)列里靠前的任務(wù)
        • DiscardPolicy :當(dāng)前任務(wù)直接丟棄

        想實(shí)現(xiàn)自己的拒絕策略,實(shí)現(xiàn)RejectedExecutionHandler接口即可。

        49.線程池有哪幾種工作隊(duì)列?

        常用的阻塞隊(duì)列主要有以下幾種:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • ArrayBlockingQueue:ArrayBlockingQueue(有界隊(duì)列)是一個(gè)用數(shù)組實(shí)現(xiàn)的有界阻塞隊(duì)列,按FIFO排序量。
        • LinkedBlockingQueue:LinkedBlockingQueue(可設(shè)置容量隊(duì)列)是基于鏈表結(jié)構(gòu)的阻塞隊(duì)列,按FIFO排序任務(wù),容量可以選擇進(jìn)行設(shè)置,不設(shè)置的話,將是一個(gè)無(wú)邊界的阻塞隊(duì)列,最大長(zhǎng)度為Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool線程池使用了這個(gè)隊(duì)列
        • DelayQueue:DelayQueue(延遲隊(duì)列)是一個(gè)任務(wù)定時(shí)周期的延遲執(zhí)行的隊(duì)列。根據(jù)指定的執(zhí)行時(shí)間從小到大排序,否則根據(jù)插入到隊(duì)列的先后排序。newScheduledThreadPool線程池使用了這個(gè)隊(duì)列。
        • PriorityBlockingQueue:PriorityBlockingQueue(優(yōu)先級(jí)隊(duì)列)是具有優(yōu)先級(jí)的無(wú)界阻塞隊(duì)列
        • SynchronousQueue:SynchronousQueue(同步隊(duì)列)是一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列,每個(gè)插入操作必須等到另一個(gè)線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool線程池使用了這個(gè)隊(duì)列。

        50.線程池提交execute和submit有什么區(qū)別?

        1. execute 用于提交不需要返回值的任務(wù)
        threadsPool.execute(new Runnable() {      @Override public void run() {          // TODO Auto-generated method stub }      });
        1. submit()方法用于提交需要返回值的任務(wù)。線程池會(huì)返回一個(gè)future類(lèi)型的對(duì)象,通過(guò)這個(gè) future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過(guò)future的get()方法來(lái)獲取返回值
        Future<Object> future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) {      // 處理中斷異常 } catch (ExecutionException e) {      // 處理無(wú)法執(zhí)行任務(wù)異常 } finally {      // 關(guān)閉線程池 executor.shutdown();}

        51.線程池怎么關(guān)閉知道嗎?

        可以通過(guò)調(diào)用線程池的shutdownshutdownNow方法來(lái)關(guān)閉線程池。它們的原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的interrupt方法來(lái)中斷線程,所以無(wú)法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無(wú)法終止。

        shutdown() 將線程池狀態(tài)置為shutdown,并不會(huì)立即停止

        1. 停止接收外部submit的任務(wù)
        2. 內(nèi)部正在跑的任務(wù)和隊(duì)列里等待的任務(wù),會(huì)執(zhí)行完
        3. 等到第二步完成后,才真正停止

        shutdownNow() 將線程池狀態(tài)置為stop。一般會(huì)立即停止,事實(shí)上不一定

        1. 和shutdown()一樣,先停止接收外部提交的任務(wù)
        2. 忽略隊(duì)列里等待的任務(wù)
        3. 嘗試將正在跑的任務(wù)interrupt中斷
        4. 返回未執(zhí)行的任務(wù)列表

        shutdown 和shutdownnow簡(jiǎn)單來(lái)說(shuō)區(qū)別如下:

        • shutdownNow()能立即停止線程池,正在跑的和正在等待的任務(wù)都停下了。這樣做立即生效,但是風(fēng)險(xiǎn)也比較大。
        • shutdown()只是關(guān)閉了提交通道,用submit()是無(wú)效的;而內(nèi)部的任務(wù)該怎么跑還是怎么跑,跑完再?gòu)氐淄V咕€程池。

        52.線程池的線程數(shù)應(yīng)該怎么配置?

        線程在Java中屬于稀缺資源,線程池不是越大越好也不是越小越好。任務(wù)分為計(jì)算密集型、IO密集型、混合型。

        1. 計(jì)算密集型:大部分都在用CPU跟內(nèi)存,加密,邏輯操作業(yè)務(wù)處理等。
        2. IO密集型:數(shù)據(jù)庫(kù)鏈接,網(wǎng)絡(luò)通訊傳輸?shù)取?/li>

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        一般的經(jīng)驗(yàn),不同類(lèi)型線程池的參數(shù)配置:

        1. 計(jì)算密集型一般推薦線程池不要過(guò)大,一般是CPU數(shù) + 1,+1是因?yàn)榭赡艽嬖?strong>頁(yè)缺失(就是可能存在有些數(shù)據(jù)在硬盤(pán)中需要多來(lái)一個(gè)線程將數(shù)據(jù)讀入內(nèi)存)。如果線程池?cái)?shù)太大,可能會(huì)頻繁的 進(jìn)行線程上下文切換跟任務(wù)調(diào)度。獲得當(dāng)前CPU核心數(shù)代碼如下:
        Runtime.getRuntime().availableProcessors();
        1. IO密集型:線程數(shù)適當(dāng)大一點(diǎn),機(jī)器的Cpu核心數(shù)*2。
        2. 混合型:可以考慮根絕情況將它拆分成CPU密集型和IO密集型任務(wù),如果執(zhí)行時(shí)間相差不大,拆分可以提升吞吐量,反之沒(méi)有必要。

        當(dāng)然,實(shí)際應(yīng)用中沒(méi)有固定的公式,需要結(jié)合測(cè)試和監(jiān)控來(lái)進(jìn)行調(diào)整。

        53.有哪幾種常見(jiàn)的線程池?

        面試常問(wèn),主要有四種,都是通過(guò)工具類(lèi)Excutors創(chuàng)建出來(lái)的,需要注意,阿里巴巴《Java開(kāi)發(fā)手冊(cè)》里禁止使用這種方式來(lái)創(chuàng)建線程池。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • newFixedThreadPool (固定數(shù)目線程的線程池)

        • newCachedThreadPool (可緩存線程的線程池)

        • newSingleThreadExecutor (單線程的線程池)

        • newScheduledThreadPool (定時(shí)及周期執(zhí)行的線程池)

        54.能說(shuō)一下四種常見(jiàn)線程池的原理嗎?

        前三種線程池的構(gòu)造直接調(diào)用ThreadPoolExecutor的構(gòu)造方法。

        newSingleThreadExecutor

          public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {         return new FinalizableDelegatedExecutorService             (new ThreadPoolExecutor(1, 1,                                     0L, TimeUnit.MILLISECONDS,                                     new LinkedBlockingQueue<Runnable>(),                                     threadFactory));     }

        線程池特點(diǎn)

        • 核心線程數(shù)為1
        • 最大線程數(shù)也為1
        • 阻塞隊(duì)列是無(wú)界隊(duì)列LinkedBlockingQueue,可能會(huì)導(dǎo)致OOM
        • keepAliveTime為0

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        工作流程:

        • 提交任務(wù)
        • 線程池是否有一條線程在,如果沒(méi)有,新建線程執(zhí)行任務(wù)
        • 如果有,將任務(wù)加到阻塞隊(duì)列
        • 當(dāng)前的唯一線程,從隊(duì)列取任務(wù),執(zhí)行完一個(gè),再繼續(xù)取,一個(gè)線程執(zhí)行任務(wù)。

        適用場(chǎng)景

        適用于串行執(zhí)行任務(wù)的場(chǎng)景,一個(gè)任務(wù)一個(gè)任務(wù)地執(zhí)行。

        newFixedThreadPool

          public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {         return new ThreadPoolExecutor(nThreads, nThreads,                                       0L, TimeUnit.MILLISECONDS,                                       new LinkedBlockingQueue<Runnable>(),                                       threadFactory);     }

        線程池特點(diǎn):

        • 核心線程數(shù)和最大線程數(shù)大小一樣
        • 沒(méi)有所謂的非空閑時(shí)間,即keepAliveTime為0
        • 阻塞隊(duì)列為無(wú)界隊(duì)列LinkedBlockingQueue,可能會(huì)導(dǎo)致OOM

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        工作流程:

        • 提交任務(wù)
        • 如果線程數(shù)少于核心線程,創(chuàng)建核心線程執(zhí)行任務(wù)
        • 如果線程數(shù)等于核心線程,把任務(wù)添加到LinkedBlockingQueue阻塞隊(duì)列
        • 如果線程執(zhí)行完任務(wù),去阻塞隊(duì)列取任務(wù),繼續(xù)執(zhí)行。

        使用場(chǎng)景

        FixedThreadPool 適用于處理CPU密集型的任務(wù),確保CPU在長(zhǎng)期被工作線程使用的情況下,盡可能的少的分配線程,即適用執(zhí)行長(zhǎng)期的任務(wù)。

        newCachedThreadPool

           public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                       60L, TimeUnit.SECONDS,                                       new SynchronousQueue<Runnable>(),                                       threadFactory);     }

        線程池特點(diǎn):

        • 核心線程數(shù)為0
        • 最大線程數(shù)為Integer.MAX_VALUE,即無(wú)限大,可能會(huì)因?yàn)闊o(wú)限創(chuàng)建線程,導(dǎo)致OOM
        • 阻塞隊(duì)列是SynchronousQueue
        • 非核心線程空閑存活時(shí)間為60秒

        當(dāng)提交任務(wù)的速度大于處理任務(wù)的速度時(shí),每次提交一個(gè)任務(wù),就必然會(huì)創(chuàng)建一個(gè)線程。極端情況下會(huì)創(chuàng)建過(guò)多的線程,耗盡 CPU 和內(nèi)存資源。由于空閑 60 秒的線程會(huì)被終止,長(zhǎng)時(shí)間保持空閑的 CachedThreadPool 不會(huì)占用任何資源。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        工作流程:

        • 提交任務(wù)
        • 因?yàn)闆](méi)有核心線程,所以任務(wù)直接加到SynchronousQueue隊(duì)列。
        • 判斷是否有空閑線程,如果有,就去取出任務(wù)執(zhí)行。
        • 如果沒(méi)有空閑線程,就新建一個(gè)線程執(zhí)行。
        • 執(zhí)行完任務(wù)的線程,還可以存活60秒,如果在這期間,接到任務(wù),可以繼續(xù)活下去;否則,被銷(xiāo)毀。

        適用場(chǎng)景

        用于并發(fā)執(zhí)行大量短期的小任務(wù)。

        newScheduledThreadPool

            public ScheduledThreadPoolExecutor(int corePoolSize) {         super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,               new DelayedWorkQueue());     }

        線程池特點(diǎn)

        • 最大線程數(shù)為Integer.MAX_VALUE,也有OOM的風(fēng)險(xiǎn)
        • 阻塞隊(duì)列是DelayedWorkQueue
        • keepAliveTime為0
        • scheduleAtFixedRate() :按某種速率周期執(zhí)行
        • scheduleWithFixedDelay():在某個(gè)延遲后執(zhí)行

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        工作機(jī)制

        • 線程從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務(wù)是指ScheduledFutureTask的time大于等于當(dāng)前時(shí)間。
        • 線程執(zhí)行這個(gè)ScheduledFutureTask。
        • 線程修改ScheduledFutureTask的time變量為下次將要被執(zhí)行的時(shí)間。
        • 線程把這個(gè)修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        使用場(chǎng)景

        周期性執(zhí)行任務(wù)的場(chǎng)景,需要限制線程數(shù)量的場(chǎng)景

        使用無(wú)界隊(duì)列的線程池會(huì)導(dǎo)致什么問(wèn)題嗎?

        例如newFixedThreadPool使用了無(wú)界的阻塞隊(duì)列LinkedBlockingQueue,如果線程獲取一個(gè)任務(wù)后,任務(wù)的執(zhí)行時(shí)間比較長(zhǎng),會(huì)導(dǎo)致隊(duì)列的任務(wù)越積越多,導(dǎo)致機(jī)器內(nèi)存使用不停飆升,最終導(dǎo)致OOM。

        55.線程池異常怎么處理知道嗎?

        在使用線程池處理任務(wù)的時(shí)候,任務(wù)代碼可能拋出RuntimeException,拋出異常后,線程池可能捕獲它,也可能創(chuàng)建一個(gè)新的線程來(lái)代替異常的線程,我們可能無(wú)法感知任務(wù)出現(xiàn)了異常,因此我們需要考慮線程池異常情況。

        常見(jiàn)的異常處理方式:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        56.能說(shuō)一下線程池有幾種狀態(tài)嗎?

        線程池有這幾個(gè)狀態(tài):RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。

           //線程池狀態(tài)    private static final int RUNNING    = -1 << COUNT_BITS;    private static final int SHUTDOWN   =  0 << COUNT_BITS;    private static final int STOP       =  1 << COUNT_BITS;    private static final int TIDYING    =  2 << COUNT_BITS;    private static final int TERMINATED =  3 << COUNT_BITS;

        線程池各個(gè)狀態(tài)切換圖:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        RUNNING

        • 該狀態(tài)的線程池會(huì)接收新任務(wù),并處理阻塞隊(duì)列中的任務(wù);
        • 調(diào)用線程池的shutdown()方法,可以切換到SHUTDOWN狀態(tài);
        • 調(diào)用線程池的shutdownNow()方法,可以切換到STOP狀態(tài);

        SHUTDOWN

        • 該狀態(tài)的線程池不會(huì)接收新任務(wù),但會(huì)處理阻塞隊(duì)列中的任務(wù);
        • 隊(duì)列為空,并且線程池中執(zhí)行的任務(wù)也為空,進(jìn)入TIDYING狀態(tài);

        STOP

        • 該狀態(tài)的線程不會(huì)接收新任務(wù),也不會(huì)處理阻塞隊(duì)列中的任務(wù),而且會(huì)中斷正在運(yùn)行的任務(wù);
        • 線程池中執(zhí)行的任務(wù)為空,進(jìn)入TIDYING狀態(tài);

        TIDYING

        • 該狀態(tài)表明所有的任務(wù)已經(jīng)運(yùn)行終止,記錄的任務(wù)數(shù)量為0。
        • terminated()執(zhí)行完畢,進(jìn)入TERMINATED狀態(tài)

        TERMINATED

        • 該狀態(tài)表示線程池徹底終止

        57.線程池如何實(shí)現(xiàn)參數(shù)的動(dòng)態(tài)修改?

        線程池提供了幾個(gè) setter方法來(lái)設(shè)置線程池的參數(shù)。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        這里主要有兩個(gè)思路:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        • 在我們微服務(wù)的架構(gòu)下,可以利用配置中心如Nacos、Apollo等等,也可以自己開(kāi)發(fā)配置中心。業(yè)務(wù)服務(wù)讀取線程池配置,獲取相應(yīng)的線程池實(shí)例來(lái)修改線程池的參數(shù)。

        • 如果限制了配置中心的使用,也可以自己去擴(kuò)展ThreadPoolExecutor,重寫(xiě)方法,監(jiān)聽(tīng)線程池參數(shù)變化,來(lái)動(dòng)態(tài)修改線程池參數(shù)。

        線程池調(diào)優(yōu)了解嗎?

        線程池配置沒(méi)有固定的公式,通常事前會(huì)對(duì)線程池進(jìn)行一定評(píng)估,常見(jiàn)的評(píng)估方案如下:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        上線之前也要進(jìn)行充分的測(cè)試,上線之后要建立完善的線程池監(jiān)控機(jī)制。

        事中結(jié)合監(jiān)控告警機(jī)制,分析線程池的問(wèn)題,或者可優(yōu)化點(diǎn),結(jié)合線程池動(dòng)態(tài)參數(shù)配置機(jī)制來(lái)調(diào)整配置。

        事后要注意仔細(xì)觀察,隨時(shí)調(diào)整。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        具體的調(diào)優(yōu)案例可以查看參考[7]美團(tuán)技術(shù)博客。

        58.你能設(shè)計(jì)實(shí)現(xiàn)一個(gè)線程池嗎?

        這道題在阿里的面試中出現(xiàn)頻率比較高

        線程池實(shí)現(xiàn)原理可以查看 要是以前有人這么講線程池,我早就該明白了! ,當(dāng)然,我們自己實(shí)現(xiàn), 只需要抓住線程池的核心流程-參考[6]:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        我們自己的實(shí)現(xiàn)就是完成這個(gè)核心流程:

        • 線程池中有N個(gè)工作線程
        • 把任務(wù)提交給線程池運(yùn)行
        • 如果線程池已滿,把任務(wù)放入隊(duì)列
        • 最后當(dāng)有空閑時(shí),獲取隊(duì)列中任務(wù)來(lái)執(zhí)行

        實(shí)現(xiàn)代碼[6]:

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        這樣,一個(gè)實(shí)現(xiàn)了線程池主要流程的類(lèi)就完成了。

        59.單機(jī)線程池執(zhí)行斷電了應(yīng)該怎么處理?

        我們可以對(duì)正在處理和阻塞隊(duì)列的任務(wù)做事務(wù)管理或者對(duì)阻塞隊(duì)列中的任務(wù)持久化處理,并且當(dāng)斷電或者系統(tǒng)崩潰,操作無(wú)法繼續(xù)下去的時(shí)候,可以通過(guò)回溯日志的方式來(lái)撤銷(xiāo)正在處理的已經(jīng)執(zhí)行成功的操作。然后重新執(zhí)行整個(gè)阻塞隊(duì)列。

        也就是說(shuō),對(duì)阻塞隊(duì)列持久化;正在處理任務(wù)事務(wù)控制;斷電之后正在處理任務(wù)的回滾,通過(guò)日志恢復(fù)該次操作;服務(wù)器重啟后阻塞隊(duì)列中的數(shù)據(jù)再加載。

        并發(fā)容器和框架

        關(guān)于一些并發(fā)容器,可以去看看 面渣逆襲:Java集合連環(huán)三十問(wèn) ,里面有CopyOnWriteListConcurrentHashMap這兩種線程安全容器類(lèi)的問(wèn)答。。

        60.Fork/Join框架了解嗎?

        Fork/Join框架是Java7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。

        要想掌握Fork/Join框架,首先需要理解兩個(gè)點(diǎn),分而治之工作竊取算法

        分而治之

        Fork/Join框架的定義,其實(shí)就體現(xiàn)了分治思想:將一個(gè)規(guī)模為N的問(wèn)題分解為K個(gè)規(guī)模較小的子問(wèn)題,這些子問(wèn)題相互獨(dú)立且與原問(wèn)題性質(zhì)相同。求出子問(wèn)題的解,就可得到原問(wèn)題的解。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        工作竊取算法

        大任務(wù)拆成了若干個(gè)小任務(wù),把這些小任務(wù)放到不同的隊(duì)列里,各自創(chuàng)建單獨(dú)線程來(lái)執(zhí)行隊(duì)列里的任務(wù)。

        那么問(wèn)題來(lái)了,有的線程干活塊,有的線程干活慢。干完活的線程不能讓它空下來(lái),得讓它去幫沒(méi)干完活的線程干活。它去其它線程的隊(duì)列里竊取一個(gè)任務(wù)來(lái)執(zhí)行,這就是所謂的工作竊取

        工作竊取發(fā)生的時(shí)候,它們會(huì)訪問(wèn)同一個(gè)隊(duì)列,為了減少竊取任務(wù)線程和被竊取任務(wù)線程之間的競(jìng)爭(zhēng),通常任務(wù)會(huì)使用雙端隊(duì)列,被竊取任務(wù)線程永遠(yuǎn)從雙端隊(duì)列的頭部拿,而竊取任務(wù)的線程永遠(yuǎn)從雙端隊(duì)列的尾部拿任務(wù)執(zhí)行。

        歸納整理Java并發(fā)知識(shí)點(diǎn)

        看一個(gè)Fork/Join框架應(yīng)用的例子,計(jì)算1~n之間的和:1+2+3+…+n

        • 設(shè)置一個(gè)分割閾值,任務(wù)大于閾值就拆分任務(wù)
        • 任務(wù)有結(jié)果,所以需要繼承RecursiveTask
        public class CountTask extends RecursiveTask<Integer> {     private static final int THRESHOLD = 16; // 閾值     private int start;     private int end;      public CountTask(int start, int end) {         this.start = start;         this.end = end;     }      @Override     protected Integer compute() {         int sum = 0;         // 如果任務(wù)足夠小就計(jì)算任務(wù)         boolean canCompute = (end - start) <= THRESHOLD;         if (canCompute) {             for (int i = start; i <= end; i++) {                 sum += i;             }         } else {             // 如果任務(wù)大于閾值,就分裂成兩個(gè)子任務(wù)計(jì)算             int middle = (start + end) / 2;             CountTask leftTask = new CountTask(start, middle);             CountTask rightTask = new CountTask(middle + 1, end);             // 執(zhí)行子任務(wù)             leftTask.fork();             rightTask.fork(); // 等待子任務(wù)執(zhí)行完,并得到其結(jié)果             int leftResult = leftTask.join();             int rightResult = rightTask.join(); // 合并子任務(wù)             sum = leftResult + rightResult;         }         return sum;     }      public static void main(String[] args) {         ForkJoinPool forkJoinPool = new ForkJoinPool(); // 生成一個(gè)計(jì)算任務(wù),負(fù)責(zé)計(jì)算1+2+3+4         CountTask task = new CountTask(1, 100); // 執(zhí)行一個(gè)任務(wù)         Future<Integer> result = forkJoinPool.submit(task);         try {             System.out.println(result.get());         } catch (InterruptedException e) {         } catch (ExecutionException e) {         }     }     }

        ForkJoinTask與一般Task的主要區(qū)別在于它需要實(shí)現(xiàn)compute方法,在這個(gè)方法里,首先需要判斷任務(wù)是否足夠小,如果足夠小就直接執(zhí)行任務(wù)。如果比較大,就必須分割成兩個(gè)子任務(wù),每個(gè)子任務(wù)在調(diào)用fork方法時(shí),又會(huì)進(jìn)compute方法,看看當(dāng)前子任務(wù)是否需要繼續(xù)分割成子任務(wù),如果不需要繼續(xù)分割,則執(zhí)行當(dāng)前子任務(wù)并返回結(jié)果。使用join方法會(huì)等待子任務(wù)執(zhí)行完并得到其結(jié)果。

        推薦學(xué)習(xí):《java教程》

        贊(0)
        分享到: 更多 (0)
        網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)
        主站蜘蛛池模板: 亚洲国产成人久久精品动漫| 久久亚洲美女精品国产精品| 亚洲国产另类久久久精品| 青青草精品视频| 国产精品丝袜黑色高跟鞋| 无码人妻精品一区二区蜜桃AV| 亚洲av无码国产精品色午夜字幕 | 久久精品无码一区二区三区日韩| 一级成人精品h| 亚洲av无码成人精品区| 91自慰精品亚洲| 欧美视频精品一区二区三区| 国产精品自在拍一区二区不卡| 99国产欧美久久久精品蜜芽| 无码人妻精品一区二区| 日本精品一区二区久久久| 国产精品亚洲专区无码WEB| 98香蕉草草视频在线精品看| 国产精品免费网站| 99国产精品一区二区| 国产女人精品视频国产灰线| 无码精品一区二区三区在线| 伊人久久精品无码二区麻豆| 亚洲精品国产成人影院| 人人妻人人澡人人爽精品欧美| 国产精品麻豆入口| 国产VA免费精品高清在线| 99久久精品免费观看国产| 久久免费国产精品一区二区| 99久久成人国产精品免费| 99re6在线视频精品免费| 2048亚洲精品国产| 高清免费久久午夜精品| 91精品国产乱码久久久久久| 国产精品福利一区二区| 国产高清在线精品二区一| 国产精品嫩草视频永久网址| 四虎永久在线精品884aa下载| 青青草国产精品久久| 911亚洲精品国内自产| 国产精品秘入口福利姬网站|