久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

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

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

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

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

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

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

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

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

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

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

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

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

      要說線程,必須得先說說進(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)行的是線程,所以也說線程是 CPU分配的基本單位。

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

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

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

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

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

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

      • 繼承Thread類,重寫run()方法,調(diào)用start()方法啟動(dòng)線程
      public class ThreadTest {      /**      * 繼承Thread類      */     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 接口,重寫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();     }}

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

      • 實(shí)現(xiàn)Callable接口,重寫call()方法,這種方式可以通過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)建出來的新線程去執(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類中有一些函數(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)方法后,沒有在指定的 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類也提供了一個(gè)方法用于等待的方法:

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

        從thread.join()返回。

      線程休眠

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

      線程中斷

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

      • void interrupt() :中斷線程,例如,當(dāng)線程A運(yùn)行時(shí),線程B可以調(diào)用錢程interrupt() 方法來設(shè)置線程的中斷標(biāo)志為true 并立即返回。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志, 線程A實(shí)際并沒有被中斷, 會(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) 說明
      NEW 初始狀態(tài):線程被創(chuàng)建,但還沒有調(diào)用start()方法
      RUNNABLE 運(yùn)行狀態(tài):Java線程將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)籠統(tǒ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來應(yīng)付多個(gè)線程。

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

      為了讓用戶感覺多個(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中的線程分為兩類,分別為 daemon 線程(守護(hù)線程)和 user 線程(用戶線程)。

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

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

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

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

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

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

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

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

      可以通過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()語句,其含義是:當(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)被附帶在線程上,也就是說一個(gè)線程可以根據(jù)一個(gè)ThreadLocal對(duì)象查詢到綁定在這個(gè)線程上的一個(gè)值。

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

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

      ThreadLocal

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

      10.ThreadLocal是什么?

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

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

      • 創(chuàng)建

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

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

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

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

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

      localVariable.get();

      11.你在工作中用到過ThreadLocal嗎?

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

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

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

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

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

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

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

      • 數(shù)據(jù)庫連接池的連接交給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類中定義了一個(gè)類型為ThreadLocal.ThreadLocalMap的成員變量threadLocals。

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

      ThreadLocalMap既然被稱為Map,那么毫無疑問它是<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)類             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(弱引用),再來看一下key怎么賦值的:

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

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

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

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

      • Thread類有一個(gè)類型為ThreadLocal.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來讓線程往ThreadLocalMap里存取值。

      13.ThreadLocal 內(nè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)存?!?/p>

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

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

      很簡(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被銷毀,此時(shí)它指向ThreadLoca的強(qiáng)引用就沒有了,但是此時(shí)key還強(qiáng)引用指向ThreadLoca,就會(huì)導(dǎo)致ThreadLocal不能被回收,這時(shí)候就發(fā)生了內(nèi)存泄漏的問題。

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

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

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

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

        一個(gè)table數(shù)組,存儲(chǔ)Entry類型的元素,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ù)字,帶來的好處就是 hash 分布非常均勻。

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

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

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

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

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

      如上圖所示,如果我們插入一個(gè)value=27的數(shù)據(jù),通過 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),就開始執(zhí)行rehash()邏輯:

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

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

      private void rehash() {     //清理過期Entry     expungeStaleEntries();      //擴(kuò)容     if (size >= threshold - threshold / 4)         resize();}//清理過期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ì)算位置,開放地址解決沖突,然后放到新的newTab,遍歷完成之后,oldTab中所有的entry數(shù)據(jù)都已經(jīng)放入到newTab中了,然后table引用指向newTab

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

      具體代碼:

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

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

      父線程能用ThreadLocal來給子線程傳值嗎?毫無疑問,不能。那該怎么辦?

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

      使用起來很簡(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類里還有另外一個(gè)變量:

      ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

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

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

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

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

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

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

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

      本地內(nèi)存是JMM的 一個(gè)抽象概念,并不真實(shí)存在。它其實(shí)涵蓋了緩存、寫緩沖區(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.說說你對(duì)原子性、可見性、有序性的理解?

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

      • 原子性:原子性指的是一個(gè)操作是不可分割、不可中斷的,要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷,要么就全不執(zhí)行。
      • 可見性:可見性指的是一個(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句是基本類型賦值,是原子性操作。
      • 第2句先讀i的值,再賦值到j(luò),兩步操作,不能保證原子性。
      • 第3和第4句其實(shí)是等效的,先讀取i的值,再+1,最后賦值到i,三步操作了,不能保證原子性。

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

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

      20.那說說什么是指令重排?

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

      1. 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
      2. 指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng) 機(jī)器指令的執(zhí)行順序。
      3. 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(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)過了編譯器的指令重排序,第二步和第三步就可能會(huì)重排序。

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

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

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

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

      happens-before的定義:

      • 如果一個(gè)操作happens-before另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
      • 兩個(gè)操作之間存在happens-before關(guān)系,并不意味著Java平臺(tái)的具體實(shí)現(xiàn)必須要按照 happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(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域的寫,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語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),單線程程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

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

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

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

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

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

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

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

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

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

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

      volatile怎么保證可見性的呢?

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

      volatile可以確保對(duì)某個(gè)變量的更新對(duì)其他線程馬上可見,一個(gè)變量被聲明為volatile 時(shí),線程在寫入變量時(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)存變量,然后再從主內(nèi)存獲取最新值。

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

      volatile怎么保證有序性的呢?

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

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

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

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

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

      24.synchronized用過嗎?怎么使用?

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

      synchronized主要有三種用法:

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

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

      synchronized void staic method() {  //業(yè)務(wù)代碼}
      • 修飾代碼塊 :指定加鎖對(duì)象,對(duì)給定對(duì)象/類加鎖。 synchronized(this|object) 表示進(jìn)?同步代碼庫前要獲得給定對(duì)象的鎖。 synchronized(類.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采用monitorenter、monitorexit兩個(gè)指令來實(shí)現(xiàn)同步,monitorenter 指令指向同步代碼塊的開始位置, 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)記符來實(shí)現(xiàn)同步,這個(gè)標(biāo)識(shí)指明了該方法是一個(gè)同步方法。

        同樣可以寫段代碼反編譯看一下。

      歸納整理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í)是一種同步工具,也可以說是一種同步機(jī)制。在Java虛擬機(jī)(HotSpot)中,Monitor是由ObjectMonitor實(shí)現(xiàn)的,可以叫做內(nèi)部鎖,或者M(jìn)onitor鎖。

      ObjectMonitor的工作原理:

      • ObjectMonitor有兩個(gè)隊(duì)列:_WaitSet、_EntryList,用來保存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 ;   }

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

      • 首先,患者在門診大廳前臺(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è)過程就和Monitor機(jī)制比較相似:

      • 門診大廳:所有待進(jìn)入的線程都必須先在入口Entry Set掛號(hào)才有資格;
      • 就診室:就診室**_Owner**里里只能有一個(gè)線程就診,就診完線程就自行離開
      • 候診室:就診室繁忙時(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可見性,有序性,可重入性怎么實(shí)現(xiàn)?

      synchronized怎么保證可見性?

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

      synchronized怎么保證有序性?

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

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

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

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

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

      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,我們來看看它的狀態(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,這種鎖被稱之為重量級(jí)鎖。從JDK6開始,HotSpot虛擬機(jī)開發(fā)團(tuán)隊(duì)對(duì)Java中的鎖進(jìn)行優(yōu)化,如增加了適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等優(yōu)化策略,提升了synchronized的性能。

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

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

      • 自旋鎖:減少不必要的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í)的過程是什么樣的?

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

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

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

      偏向鎖:

      偏向鎖的獲?。?/strong>

      1. 判斷是否為可偏向狀態(tài)–MarkWord中鎖標(biāo)志是否為‘01’,是否偏向鎖是否為‘1’
      2. 如果是可偏向狀態(tài),則查看線程ID是否為當(dāng)前線程,如果是,則進(jìn)入步驟’5’,否則進(jìn)入步驟‘3’
      3. 通過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í)行同步代碼

      偏向鎖的撤銷:

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

      輕量級(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í)過程:

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

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

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

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

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

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

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

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

      29.AQS了解多少?

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

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

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

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

      • AQS 中隊(duì)列是個(gè)雙向鏈表,也是 FIFO 先進(jìn)先出的特性
      • 通過 Head、Tail 頭尾兩個(gè)節(jié)點(diǎn)來組成隊(duì)列結(jié)構(gòu),通過 volatile 修飾保證可見性
      • 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é)可問,建議有時(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)鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個(gè)線程才能獲得鎖
      2. 公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會(huì)餓死。缺點(diǎn)是整體吞吐效率相對(duì)非公平鎖要低,等待隊(duì)列中除第一個(gè)線程以外的所有線程都會(huì)阻塞,CPU 喚醒阻塞線程的開銷比非公平鎖大

      非公平鎖 NonfairSync

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

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

      • 如果鎖當(dāng)前沒有被其它線程占用,并且當(dāng)前線程之前沒有獲取過該鎖,則當(dāng)前線程會(huì)獲取到該鎖,然后設(shè)置當(dāng)前鎖的擁有者為當(dāng)前線程,并設(shè)置 AQS 的狀態(tài)值為1 ,然后直接返回。如果當(dāng)前線程之前己經(jīng)獲取過該鎖,則這次只是簡(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)部類,只不過實(shí)現(xiàn)不同鎖語義。

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

      1. 非公平鎖在調(diào)用 lock 后,首先就會(huì)調(diào)用 CAS 進(jìn)行一次搶鎖,如果這個(gè)時(shí)候恰巧鎖沒有被占用,那么直接就獲取到鎖返回了。
      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ì)來說,非公平鎖會(huì)有更好的性能,因?yàn)樗耐掏铝勘容^大。當(dāng)然,非公平鎖讓獲取鎖的時(shí)間變得更加不確定,可能會(huì)導(dǎo)致在阻塞隊(duì)列中的線程長(zhǎng)期處于饑餓狀態(tài)。

      32.CAS呢?CAS了解多少?

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

      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 有什么問題?如何解決?

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

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

      ABA 問題

      并發(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ù)即使成功修改,也可能有問題。

      怎么解決ABA問題?

      • 加版本號(hào)

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

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

      循環(huán)性能開銷

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

      怎么解決循環(huán)性能開銷問題?

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

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

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

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

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

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

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

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

      35.原子操作類了解多少?

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

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

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

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

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

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

      • AtomicBoolean:原子更新布爾類型。

      • AtomicInteger:原子更新整型。

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

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

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

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

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

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

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

      • AtomicReference:原子更新引用類型。

      • AtomicReferenceFieldUpdater:原子更新引用類型里的字段。

      • AtomicMarkableReference:原子更新帶有標(biāo)記位的引用類型??梢栽痈乱粋€(gè)布爾類型的標(biāo)記位和引用類型。構(gòu)造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

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

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

      36.AtomicInteger 的原理?

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

      以AtomicInteger的添加方法為例:

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

      通過Unsafe類的實(shí)例來進(jìn)行添加操作,來看看具體的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來操作int類型變量。其它的原子操作類基本都是大同小異。

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

      死鎖是指兩個(gè)或兩個(gè)以上的線程在執(zhí)行過程中,因爭(zhēng)奪資源而造成的互相等待的現(xiàn)象,在無外力作用的情況下,這些線程會(huì)一直相互等待而無法繼續(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è)條件我們沒有辦法破壞,因?yàn)橛面i為的就是互斥。不過其他三個(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)資源來預(yù)防。所謂按序申請(qǐng),是指資源是有線性順序的,申請(qǐng)的時(shí)候可以先申請(qǐng)資源序號(hào)小的,再申請(qǐng)資源序號(hào)大的,這樣線性化后就不存在環(huán)路了。

      38.那死鎖問題怎么排查呢?

      可以使用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è)常見的應(yīng)用場(chǎng)景[18]:

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

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

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

      歸納整理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ù)量,各玩家通過countDownLatch.countDown()完成狀態(tài)確認(rèn),主線程通過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)子線程開始動(dòng)作:統(tǒng)一各線程動(dòng)作開始的時(shí)機(jī)

      王者游戲中也有類似的場(chǎng)景,游戲開始時(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ì)開門,所有被屏障攔截的線程才會(huì)繼續(xù)運(yùn)行。

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

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

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

      我們拿代碼模擬這一場(chǎng)景,發(fā)現(xiàn)CountDownLatch無能為力了,因?yàn)镃ountDownLatch的使用是一次性的,無法重復(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ù)由哪些線程完成無關(guān)緊要
      CyclicBarrier可以在所有的線程釋放后重新使用 CountDownLatch在計(jì)數(shù)器為0時(shí)不能再使用
      在CyclicBarrier中,如果某個(gè)線程遇到了中斷、超時(shí)等問題時(shí),則處于await的線程都會(huì)出現(xiàn)問題 在CountDownLatch中,如果某個(gè)線程出現(xiàn)問題,其他線程不受影響

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

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

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

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

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

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

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

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

      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é)作的工具類。Exchanger用于進(jìn)行線程間的數(shù)據(jù)交換。它提供一個(gè)同步點(diǎn),在這個(gè)同步點(diǎn),兩個(gè)線程可以交換彼此的數(shù)據(jù)。

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

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

      Exchanger可以用于遺傳算法,遺傳算法里需要選出兩個(gè)人作為交配對(duì)象,這時(shí)候會(huì)交換兩人的數(shù)據(jù),并使用交叉規(guī)則得出2個(gè)交配結(jié)果。Exchanger也可以用于校對(duì)工作,比如我們需要將紙制銀行流水通過人工的方式錄入成電子銀行流水,為了避免錯(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è)沒有執(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)建線程和銷毀線程的資源損耗。因?yàn)榫€程其實(shí)也是一個(gè)對(duì)象,創(chuàng)建一個(gè)對(duì)象,需要經(jīng)過類加載過程,銷毀一個(gè)對(duì)象,需要走GC垃圾回收流程,都是需要資源開銷的。
      • 提高響應(yīng)速度。 如果任務(wù)到達(dá)了,相對(duì)于從線程池拿線程,重新去創(chuàng)建一條線程執(zhí)行,速度肯定慢很多。
      • 重復(fù)利用。 線程用完,再放回池子,可以達(dá)到重復(fù)利用的效果,節(jié)省資源。

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

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

      歸納整理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 來加鎖,保證數(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)單說一下線程池的工作流程嗎?

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

      有一個(gè)營(yíng)業(yè)廳,總共有六個(gè)窗口,現(xiàn)在開放了三個(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)沒有空閑的窗口,就在排隊(duì)區(qū)排隊(duì)等。

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

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

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

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

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

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

      2. 誰叫你來辦的你找誰去

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

      4. 今天沒辦法,不行你看改一天

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

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

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

      1. 線程池剛創(chuàng)建時(shí),里面沒有一個(gè)線程。任務(wù)隊(duì)列是作為參數(shù)傳進(jìn)來的。不過,就算隊(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ù)拒絕策略來對(duì)應(yīng)處理。

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

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

      2. 當(dāng)一個(gè)線程無事可做,超過一定的時(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)注corePoolSizemaximumPoolSize、workQueuehandler這四個(gè)。

      1. corePoolSize

      此值是用來初始化線程池中核心線程數(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 ) ,非核心線程閑置下來不干活最多存活時(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í)使用的工廠,可以用來設(shè)定線程名、是否為daemon線程等等。

      1. handler

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

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

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

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

      • AbortPolicy :直接拋出異常,默認(rèn)使用此策略
      • CallerRunsPolicy:用調(diào)用者所在的線程來執(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è)無邊界的阻塞隊(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í)的無界阻塞隊(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類型的對(duì)象,通過這個(gè) future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過future的get()方法來獲取返回值
      Future<Object> future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) {      // 處理中斷異常 } catch (ExecutionException e) {      // 處理無法執(zhí)行任務(wù)異常 } finally {      // 關(guān)閉線程池 executor.shutdown();}

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

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

      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)單來說區(qū)別如下:

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

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

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

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

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

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

      1. 計(jì)算密集型一般推薦線程池不要過大,一般是CPU數(shù) + 1,+1是因?yàn)榭赡艽嬖?strong>頁缺失(就是可能存在有些數(shù)據(jù)在硬盤中需要多來一個(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í)間相差不大,拆分可以提升吞吐量,反之沒有必要。

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

      53.有哪幾種常見的線程池?

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

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

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

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

      • newSingleThreadExecutor (單線程的線程池)

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

      54.能說一下四種常見線程池的原理嗎?

      前三種線程池的構(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ì)列是無界隊(duì)列LinkedBlockingQueue,可能會(huì)導(dǎo)致OOM
      • keepAliveTime為0

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

      工作流程:

      • 提交任務(wù)
      • 線程池是否有一條線程在,如果沒有,新建線程執(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ù)大小一樣
      • 沒有所謂的非空閑時(shí)間,即keepAliveTime為0
      • 阻塞隊(duì)列為無界隊(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,即無限大,可能會(huì)因?yàn)闊o限創(chuàng)建線程,導(dǎo)致OOM
      • 阻塞隊(duì)列是SynchronousQueue
      • 非核心線程空閑存活時(shí)間為60秒

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

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

      工作流程:

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

      適用場(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)景

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

      例如newFixedThreadPool使用了無界的阻塞隊(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è)新的線程來代替異常的線程,我們可能無法感知任務(wù)出現(xiàn)了異常,因此我們需要考慮線程池異常情況。

      常見的異常處理方式:

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

      56.能說一下線程池有幾種狀態(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方法來設(shè)置線程池的參數(shù)。

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

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

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

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

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

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

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

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

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

      事中結(jié)合監(jiān)控告警機(jī)制,分析線程池的問題,或者可優(yōu)化點(diǎn),結(jié)合線程池動(dòng)態(tài)參數(shù)配置機(jī)制來調(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ù)來執(zhí)行

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

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

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

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

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

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

      并發(fā)容器和框架

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

      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的問題分解為K個(gè)規(guī)模較小的子問題,這些子問題相互獨(dú)立且與原問題性質(zhì)相同。求出子問題的解,就可得到原問題的解。

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

      工作竊取算法

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

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

      工作竊取發(fā)生的時(shí)候,它們會(huì)訪問同一個(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)