Java并發(fā)知識點有哪些

本篇內(nèi)容主要講解“Java并發(fā)知識點有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“Java并發(fā)知識點有哪些”吧!

創(chuàng)新互聯(lián)服務(wù)項目包括武夷山網(wǎng)站建設(shè)、武夷山網(wǎng)站制作、武夷山網(wǎng)頁制作以及武夷山網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢、行業(yè)經(jīng)驗、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,武夷山網(wǎng)站推廣取得了明顯的社會效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到武夷山省份的部分城市,未來相信會繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!

Java并發(fā)知識點有哪些

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

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

  • 并行就是同一時刻,兩個線程都在執(zhí)行。這就要求有兩個CPU去分別執(zhí)行兩個線程。

  • 并發(fā)就是同一時刻,只有一個執(zhí)行,但是一個時間段內(nèi),兩個線程都執(zhí)行了。并發(fā)的實現(xiàn)依賴于CPU切換線程,因為切換的時間特別短,所以基本對于用戶是無感知的。

Java并發(fā)知識點有哪些

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

Java并發(fā)知識點有哪些

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

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

  • 進(jìn)程:進(jìn)程是代碼在數(shù)據(jù)集合上的一次運(yùn)行活動,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。

  • 線程:線程是進(jìn)程的一個執(zhí)行路徑,一個進(jìn)程中至少有一個線程,進(jìn)程中的多個線程共享進(jìn)程的資源。

操作系統(tǒng)在分配資源時是把資源分配給進(jìn)程的, 但是 CPU 資源比較特殊,它是被分配到線程的,因為真正要占用CPU運(yùn)行的是線程,所以也說線程是 CPU分配的基本單位。

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

Java并發(fā)知識點有哪些

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

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

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

Java并發(fā)知識點有哪些

  • 繼承Thread類,重寫run()方法,調(diào)用start()方法啟動線程

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();
    }}
  • 實現(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é)果,該怎么辦呢?

  • 實現(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());
        //啟動線程
        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()方法時會執(zhí)行run()方法,那怎么不直接調(diào)用run()方法?

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

Java并發(fā)知識點有哪些

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

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

Java并發(fā)知識點有哪些

線程等待與通知

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

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

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

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

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

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

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

  • notify() : 一個線程A調(diào)用共享對象的 notify() 方法后,會喚醒一個在這個共享變量上調(diào)用 wait 系列方法后被掛起的線程。 一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是隨機(jī)的。

  • notifyAll() :不同于在共享變量上調(diào)用 notify() 函數(shù)會喚醒被阻塞到該共享變量上的一個線程,notifyAll()方法則會喚醒所有在該共享變量上由于調(diào)用 wait 系列方法而被掛起的線程。

Thread類也提供了一個方法用于等待的方法:

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

    從thread.join()返回。

線程休眠

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

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

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

線程中斷

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

  • void interrupt() :中斷線程,例如,當(dāng)線程A運(yùn)行時,線程B可以調(diào)用錢程interrupt() 方法來設(shè)置線程的中斷標(biāo)志為true 并立即返回。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志, 線程A實際并沒有被中斷, 會繼續(xù)往下執(zhí)行。

  • boolean isInterrupted() 方法: 檢測當(dāng)前線程是否被中斷。

  • boolean interrupted() 方法: 檢測當(dāng)前線程是否被中斷,與 isInterrupted 不同的是,該方法如果發(fā)現(xiàn)當(dāng)前線程被中斷,則會清除中斷標(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)前線程需要等待其他線程做出一些特定動作(通知或中斷)
TIME_WAITING超時等待狀態(tài):該狀態(tài)不同于 WAITIND,它是可以在指定的時間自行返回的
TERMINATED終止?fàn)顟B(tài):表示當(dāng)前線程已經(jīng)執(zhí)行完畢

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

Java并發(fā)知識點有哪些

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

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

Java并發(fā)知識點有哪些

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

Java并發(fā)知識點有哪些

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

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

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

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

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

Java并發(fā)知識點有哪些

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

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

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

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

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

  • 管道輸入/輸出流

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

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

  • 使用Thread.join()

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

  • 使用ThreadLocal

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

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

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

ThreadLocal

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

10.ThreadLocal是什么?

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

Java并發(fā)知識點有哪些

  • 創(chuàng)建

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

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

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

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

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

localVariable.get();

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

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

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

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

這時候我們就可以用到ThreadLocal,在控制層攔截請求把用戶信息存入ThreadLocal,這樣我們在任何一個地方,都可以取出ThreadLocal中存的用戶數(shù)據(jù)。

Java并發(fā)知識點有哪些

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

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

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

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

我們看一下ThreadLocal的set(T)方法,發(fā)現(xiàn)先獲取到當(dāng)前線程,再獲取ThreadLocalMap,然后把元素存到這個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實現(xiàn)的秘密都在這個ThreadLocalMap了,可以Thread類中定義了一個類型為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ì)是一個個<key,value>形式的節(jié)點組成的數(shù)組,那ThreadLocalMap的節(jié)點是什么樣的呢?

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            //節(jié)點類
            Entry(ThreadLocal<?> k, Object v) {
                //key賦值
                super(k);
                //value賦值
                value = v;
            }
        }

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

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

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

Java并發(fā)知識點有哪些

所以,怎么回答ThreadLocal原理?要答出這幾個點:

  • Thread類有一個類型為ThreadLocal.ThreadLocalMap的實例變量threadLocals,每個線程都有一個屬于自己的ThreadLocalMap。

  • ThreadLocalMap內(nèi)部維護(hù)著Entry數(shù)組,每個Entry代表一個完整的對象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。

  • 每個線程在往ThreadLocal里設(shè)置值的時候,都是往自己的ThreadLocalMap里存,讀也是以某個ThreadLocal作為引用,在自己的map里找對應(yīng)的key,從而實現(xiàn)了線程隔離。

  • ThreadLocal本身不存儲值,它只是作為一個key來讓線程往ThreadLocalMap里存取值。

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

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

所以呢,棧中存儲了ThreadLocal、Thread的引用,堆中存儲了它們的具體實例。

Java并發(fā)知識點有哪些

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

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

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

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

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

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

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

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

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

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

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

Java并發(fā)知識點有哪些

  • 元素數(shù)組

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

 private Entry[] table;
  • 散列方法

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

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

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

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

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

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

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

Java并發(fā)知識點有哪些

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

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

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()具體實現(xiàn):這里會先去清理過期的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ù)組,散列方法重新計算位置,開放地址解決沖突,然后放到新的newTab,遍歷完成之后,oldTab中所有的entry數(shù)據(jù)都已經(jīng)放入到newTab中了,然后table引用指向newTab

Java并發(fā)知識點有哪些

具體代碼:

Java并發(fā)知識點有哪些

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

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

這時候可以用到另外一個類——InheritableThreadLocal。

使用起來很簡單,在主線程的InheritableThreadLocal實例設(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();
    }}

那原理是什么呢?

原理很簡單,在Thread類里還有另外一個變量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

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

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

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

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

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

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

Java并發(fā)知識點有哪些

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

Java并發(fā)知識點有哪些

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

19.說說你對原子性、可見性、有序性的理解?

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

  • 原子性:原子性指的是一個操作是不可分割、不可中斷的,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就全不執(zhí)行。

  • 可見性:可見性指的是一個線程修改了某一個共享變量的值時,其它線程能夠立即知道這個修改。

  • 有序性:有序性指的是對于一個線程的執(zhí)行代碼,從前往后依次執(zhí)行,單線程下可以認(rèn)為程序是有序的,但是并發(fā)時有可能會發(fā)生指令重排。

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

int i = 2;int j = i;i++;i = i + 1;
  • 第1句是基本類型賦值,是原子性操作。

  • 第2句先讀i的值,再賦值到j(luò),兩步操作,不能保證原子性。

  • 第3和第4句其實是等效的,先讀取i的值,再+1,最后賦值到i,三步操作了,不能保證原子性。

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

  • 原子性:JMM只能保證基本的原子性,如果要保證一個代碼塊的原子性,需要使用synchronized。

  • 可見性:Java是利用volatile關(guān)鍵字來保證可見性的,除此之外,finalsynchronized也能保證可見性。

  • 有序性:synchronized或者volatile都可以保證多線程之間操作的有序性。

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

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

  1. 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。

  2. 指令級并行的重排序。現(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng) 機(jī)器指令的執(zhí)行順序。

  3. 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。

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

Java并發(fā)知識點有哪些

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

Java并發(fā)知識點有哪些

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

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

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

happens-before的定義:

  • 如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。

  • 兩個操作之間存在happens-before關(guān)系,并不意味著Java平臺的具體實現(xiàn)必須要按照 happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法

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

Java并發(fā)知識點有哪些

  • 程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。

  • 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。

  • volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  • start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程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語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。為了具體說明,請看下面計算圓面積的代碼示例。

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

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

Java并發(fā)知識點有哪些

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

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

Java并發(fā)知識點有哪些

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

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

volatile有兩個作用,保證可見性有序性。

volatile怎么保證可見性的呢?

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

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

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

Java并發(fā)知識點有哪些

volatile怎么保證有序性的呢?

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

Java并發(fā)知識點有哪些

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

  1. 在每個volatile寫操作的前面插入一個StoreStore屏障

  2. 在每個volatile寫操作的后面插入一個StoreLoad屏障

  3. 在每個volatile讀操作的后面插入一個LoadLoad屏障

  4. 在每個volatile讀操作的后面插入一個LoadStore屏障

Java并發(fā)知識點有哪些

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

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

synchronized主要有三種用法:

  • 修飾實例方法:作用于當(dāng)前對象實例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對象實例的鎖

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

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

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

synchronized(this) {
 //業(yè)務(wù)代碼}

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

synchronized是怎么加鎖的呢?

我們使用synchronized的時候,發(fā)現(xiàn)不用自己去lock和unlock,是因為JVM幫我們把這個事情做了。

  1. synchronized修飾代碼塊時,JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步,monitorenter 指令指向同步代碼塊的開始位置, monitorexit 指令則指向同步代碼塊的結(jié)束位置。

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

Java并發(fā)知識點有哪些

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

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

Java并發(fā)知識點有哪些

synchronized鎖住的是什么呢?

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

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

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

ObjectMonitor的工作原理:

  • ObjectMonitor有兩個隊列:_WaitSet、_EntryList,用來保存ObjectWaiter 對象列表。

  • _owner,獲取 Monitor 對象的線程進(jìn)入 _owner 區(qū)時, _count + 1。如果線程調(diào)用了 wait() 方法,此時會釋放 Monitor 對象, _owner 恢復(fù)為空, _count - 1。同時該等待線程進(jìn)入 _WaitSet 中,等待被喚醒。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄線程獲取鎖的次數(shù)
    _waiters      = 0,
    _recursions   = 0;  //鎖的重入次數(shù)
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor對象的線程
    _WaitSet      = NULL;  // 處于wait狀態(tài)的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 處于等待鎖block狀態(tài)的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

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

  • 首先,患者在門診大廳前臺或自助掛號機(jī)進(jìn)行掛號;

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

    • 診室每次只能有一個患者就診;

    • 如果此時診室空閑,直接進(jìn)入就診;

    • 如果此時診室內(nèi)有其它患者就診,那么當(dāng)前患者進(jìn)入候診室,等待叫號;

  • 就診結(jié)束后,走出就診室,候診室的下一位候診患者進(jìn)入就診室。

Java并發(fā)知識點有哪些

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

  • 門診大廳:所有待進(jìn)入的線程都必須先在入口Entry Set掛號才有資格;

  • 就診室:就診室**_Owner**里里只能有一個線程就診,就診完線程就自行離開

  • 候診室:就診室繁忙時,進(jìn)入等待區(qū)(Wait Set),就診室空閑的時候就從**等待區(qū)(Wait Set)**叫新的線程

Java并發(fā)知識點有哪些

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

  • monitorenter,在判斷擁有同步標(biāo)識 ACC_SYNCHRONIZED 搶先進(jìn)入此方法的線程會優(yōu)先擁有 Monitor 的 owner ,此時計數(shù)器 +1。

  • monitorexit,當(dāng)執(zhí)行完退出后,計數(shù)器 -1,歸 0 后被其他進(jìn)入的線程獲得。

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

synchronized怎么保證可見性?

  • 線程加鎖前,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時需要從主內(nèi)存中重新讀取最新的值。

  • 線程加鎖后,其它線程無法獲取主內(nèi)存中的共享變量。

  • 線程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。

synchronized怎么保證有序性?

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

因為as-if-serial語義的存在,單線程的程序能保證最終結(jié)果是有序的,但是不保證不會指令重排。

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

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

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

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

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

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

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

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

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

Java并發(fā)知識點有哪些

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

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

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

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

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

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

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

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

鎖升級的過程是什么樣的?

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

Java并發(fā)知識點有哪些

我們看一下升級的過程:

偏向鎖:

偏向鎖的獲?。?/strong>

  1. 判斷是否為可偏向狀態(tài)–MarkWord中鎖標(biāo)志是否為‘01’,是否偏向鎖是否為‘1’

  2. 如果是可偏向狀態(tài),則查看線程ID是否為當(dāng)前線程,如果是,則進(jìn)入步驟’5’,否則進(jìn)入步驟‘3’

  3. 通過CAS操作競爭鎖,如果競爭成功,則將MarkWord中線程ID設(shè)置為當(dāng)前線程ID,然后執(zhí)行‘5’;競爭失敗,則執(zhí)行‘4’

  4. CAS獲取偏向鎖失敗表示有競爭。當(dāng)達(dá)到safepoint時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼塊

  5. 執(zhí)行同步代碼

偏向鎖的撤銷:

  1. 偏向鎖不會主動釋放(撤銷),只有遇到其他線程競爭時才會執(zhí)行撤銷,由于撤銷需要知道當(dāng)前持有該偏向鎖的線程棧狀態(tài),因此要等到safepoint時執(zhí)行,此時持有該偏向鎖的線程(T)有‘2’,‘3’兩種情況;

  2. 撤銷----T線程已經(jīng)退出同步代碼塊,或者已經(jīng)不再存活,則直接撤銷偏向鎖,變成無鎖狀態(tài)----該狀態(tài)達(dá)到閾值20則執(zhí)行批量重偏向

  3. 升級----T線程還在同步代碼塊中,則將T線程的偏向鎖升級為輕量級鎖,當(dāng)前線程執(zhí)行輕量級鎖狀態(tài)下的鎖獲取步驟----該狀態(tài)達(dá)到閾值40則執(zhí)行批量撤銷

輕量級鎖:

輕量級鎖的獲?。?/strong>

  1. 進(jìn)行加鎖操作時,jvm會判斷是否已經(jīng)時重量級鎖,如果不是,則會在當(dāng)前線程棧幀中劃出一塊空間,作為該鎖的鎖記錄,并且將鎖對象MarkWord復(fù)制到該鎖記錄中

  2. 復(fù)制成功之后,jvm使用CAS操作將對象頭MarkWord更新為指向鎖記錄的指針,并將鎖記錄里的owner指針指向?qū)ο箢^的MarkWord。如果成功,則執(zhí)行‘3’,否則執(zhí)行‘4’

  3. 更新成功,則當(dāng)前線程持有該對象鎖,并且對象MarkWord鎖標(biāo)志設(shè)置為‘00’,即表示此對象處于輕量級鎖狀態(tài)

  4. 更新失敗,jvm先檢查對象MarkWord是否指向當(dāng)前線程棧幀中的鎖記錄,如果是則執(zhí)行‘5’,否則執(zhí)行‘4’

  5. 表示鎖重入;然后當(dāng)前線程棧幀中增加一個鎖記錄第一部分(Displaced Mark Word)為null,并指向Mark Word的鎖對象,起到一個重入計數(shù)器的作用。

  6. 表示該鎖對象已經(jīng)被其他線程搶占,則進(jìn)行自旋等待(默認(rèn)10次),等待次數(shù)達(dá)到閾值仍未獲取到鎖,則升級為重量級鎖

大體上省簡的升級過程:

Java并發(fā)知識點有哪些

完整的升級過程:

Java并發(fā)知識點有哪些

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

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

  • 鎖的實現(xiàn):synchronized是Java語言的關(guān)鍵字,基于JVM實現(xiàn)。而ReentrantLock是基于JDK的API層面實現(xiàn)的(一般是lock()和unlock()方法配合try/finally 語句塊來完成。)

  • 性能:在JDK1.6鎖優(yōu)化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6開始,增加了適應(yīng)性自旋、鎖消除等,兩者性能就差不多了。

  • 功能特點:ReentrantLock 比 synchronized 增加了一些高級功能,如等待可中斷、可實現(xiàn)公平鎖、可實現(xiàn)選擇性通知。

    • ReentrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制,通過lock.lockInterruptibly()來實現(xiàn)這個機(jī)制

    • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。

    • synchronized與wait()和notify()/notifyAll()方法結(jié)合實現(xiàn)等待/通知機(jī)制,ReentrantLock類借助Condition接口與newCondition()方法實現(xiàn)。

    • ReentrantLock需要手工聲明來加鎖和釋放鎖,一般跟finally配合釋放鎖。而synchronized不用手動釋放鎖。

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

Java并發(fā)知識點有哪些

29.AQS了解多少?

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

  • AQS是基于一個FIFO的雙向隊列,其內(nèi)部定義了一個節(jié)點類Node,Node 節(jié)點內(nèi)部的 SHARED 用來標(biāo)記該線程是獲取共享資源時被阻掛起后放入AQS 隊列的, EXCLUSIVE 用來標(biāo)記線程是 取獨(dú)占資源時被掛起后放入AQS 隊列

  • AQS 使用一個 volatile 修飾的 int 類型的成員變量 state 來表示同步狀態(tài),修改同步狀態(tài)成功即為獲得鎖,volatile 保證了變量在多線程之間的可見性,修改 State 值時通過 CAS 機(jī)制來保證修改的原子性

  • 獲取state的方式分為兩種,獨(dú)占方式和共享方式,一個線程使用獨(dú)占方式獲取了資源,其它線程就會在獲取失敗后被阻塞。一個線程使用共享方式獲取了資源,另外一個線程還可以通過CAS的方式進(jìn)行獲取。

  • 如果共享資源被占用,需要一定的阻塞等待喚醒機(jī)制來保證鎖的分配,AQS 中會將競爭共享資源失敗的線程添加到一個變體的 CLH 隊列中。

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

Java并發(fā)知識點有哪些

AQS 中的隊列是 CLH 變體的虛擬雙向隊列,通過將每條請求共享資源的線程封裝成一個節(jié)點來實現(xiàn)鎖的分配:

Java并發(fā)知識點有哪些

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

  • AQS 中隊列是個雙向鏈表,也是 FIFO 先進(jìn)先出的特性

  • 通過 Head、Tail 頭尾兩個節(jié)點來組成隊列結(jié)構(gòu),通過 volatile 修飾保證可見性

  • Head 指向節(jié)點為已獲得鎖的節(jié)點,是一個虛擬節(jié)點,節(jié)點本身不持有具體線程

  • 獲取不到同步狀態(tài),會將節(jié)點進(jìn)行自旋獲取鎖,自旋一定次數(shù)失敗后會將線程阻塞,相對于 CLH 隊列性能較好

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

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

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

看看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. 公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進(jìn)入隊列中排隊,隊列中的第一個線程才能獲得鎖

  2. 公平鎖的優(yōu)點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU 喚醒阻塞線程的開銷比非公平鎖大

非公平鎖 NonfairSync

  • 非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那么這個線程可以無需阻塞直接獲取到鎖

  • 非公平鎖的優(yōu)點是可以減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU 不必喚醒所有線程。缺點是處于等待隊列中的線程可能會餓死,或者等很久才會獲得鎖

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