Java併發編程面試題

主題 鏈接
Java基礎知識 面試題
Java集合框架 面試題
Java併發編程 面試題
Redis 面試題

併發與並行

什麼是併發

併發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個進程快速交替的執行。

什麼是並行

並行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。所以無論從微觀還是從宏觀來看,二者都是一起執行的。

併發與並行的區別是什麼

  • 並行在多處理器系統中存在,而併發可以在單處理器和多處理器系統中都存在,併發能夠在單處理器系統中存在是因爲併發是並行的假象
  • 如果系統只有一個 CPU,則它根本不可能真正同時進行一個以上的線程,它只能把 CPU 運行時間劃分成若干個時間段,再將時間段分配給各個線程執行,在一個時間段的線程代碼運行時,其它線程處於掛起狀態.這種方式我們稱之爲併發(Concurrent)。
  • 當系統有一個以上 CPU 時,則線程的操作有可能非併發。當一個 CPU 執行一個線程時,另一個 CPU 可以執行另一個線程,兩個線程互不搶佔 CPU 資源,可以同時進行,這種方式我們稱之爲並行(Parallel)。

線程

線程的實現

實際上線程的實現與 Java 無關,由平臺所決定,Java 所做的是將 Thread 對象映射到操作系統所提供的線程上面去,對外提供統一的操作接口,向程序員隱藏了底層的細節

操作系統實現線程主要有 3 種方式:

  • 用戶級線程
  • 內核級線程
  • 用戶級線程 + 內核級線程,混合實現

線程的狀態

線程是一個動態執行的過程,它有一個從產生到死亡的過程,共五種狀態:新建(newThread)、就緒(runnable)、運行(running)、死亡(dead)、堵塞(blocked)

在這裏插入圖片描述

線程優先級

java 中的線程優先級的範圍是1~10,默認的優先級是5。10極最高。

  • 有時間片輪循機制:“高優先級線程”被分配CPU的概率高於“低優先級線程”。根據時間片輪循調度,所以能夠併發執行。無論是是級別相同還是不同,線程調用都不會絕對按照優先級執行,每次執行結果都不一樣,調度算法無規律可循,所以線程之間不能有先後依賴關係。
  • 無時間片輪循機制時:高級別的線程優先執行,如果低級別的線程正在運行時,有高級別線程可運行狀態,則會執行完低級別線程,再去執行高級別線程。如果低級別線程處於等待、睡眠、阻塞狀態,或者調用yield()函數讓當前運行線程回到可運行狀態,以允許具有相同優先級或者高級別的其他線程獲得運行機會。

線程調度

  • 搶佔式調度:搶佔式調度指的是每條線程執行的時間、線程的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條線程都分同樣的執行時間片,也可能是某些線程執行的時間片較長,甚至某些線程得不到執行的時間片。在這種機制下,一個線程的堵塞不會導致整個進程堵塞。
  • 協同式調度:協同式調度指某一線程執行完後主動通知系統切換到另一線程上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。線程的執行時間由線程本身控制,線程切換可以預知,不存在多線程同步問題,但它有一個致命弱點:如果一個線程編寫有問題,運行到一半就一直堵塞,那麼可能導致整個系統崩潰。
  • JVM規範中規定每個線程都有優先級,且優先級越高越優先執行,但優先級高並不代表能獨自佔用執行時間片,可能是優先級高得到越多的執行時間片,反之,優先級低的分到的執行時間少但不會分配不到執行時間。
  • java使用的線程調度是搶佔式調度,java中線程會按優先級分配CPU時間片運行。

創建線程的多種方式

  • 繼承Thread類實現多線程
  • 覆寫Runnable()接口實現多線程,而後同樣覆寫run()。
  • 覆寫Callable接口實現多線程(JDK1.5):核心方法叫call()方法,有返回值
  • 通過線程池啓動多線程
    FixThreadPool(int n); 固定大小的線程池
    SingleThreadPoolExecutor :單線程池
    CachedThreadPool(); 緩存線程池

繼承Thread和實現Runnable接口的區別:
a.實現Runnable接口避免多繼承侷限;
b.實現Runable接口可以實現資源共享

什麼是守護線程?

  • 只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最後一個非守護線程結束時,守護線程隨着JVM一同結束工作。
  • Daemon的作用是爲其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器)
Thread daemonTread = new Thread();
daemonThread.setDaemon(true);

線程和進程的區別是什麼?

  • 根本區別:進程是操作系統資源分配的基本單位,而線程是任務調度和執行的基本單位
  • 開銷方面:每個進程都有獨立的代碼和數據空間(程序上下文),程序之間的切換會有較大的開銷;線程可以看做輕量級的進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行棧和程序計數器(PC),線程之間切換的開銷小。
  • 所處環境:在操作系統中能同時運行多個進程(程序);而在同一個進程(程序)中有多個線程同時執行(通過CPU調度,在每個時間片中只有一個線程執行)
  • 內存分配:系統在運行的時候會爲每個進程分配不同的內存空間;而對線程而言,除了CPU外,系統不會爲線程分配內存(線程所使用的資源來自其所屬進程的資源),線程組之間只能共享資源。
  • 包含關係:沒有線程的進程可以看做是單線程的,如果一個進程內有多個線程,則執行過程不是一條線的,而是多條線(線程)共同完成的;線程是進程的一部分,所以線程也被稱爲輕權進程或者輕量級進程。

線程池

爲什麼要使用線程池?

創建線程和銷燬線程的花銷是比較大的,這些時間有可能比處理業務的時間還要長。這樣頻繁的創建線程和銷燬線程,再加上業務工作線程,消耗系統資源的時間,可能導致系統資源不足。(我們可以把創建和銷燬的線程的過程去掉)

線程池有什麼作用?

  • 提高效率:創建好一定數量的線程放在池中,等需要使用的時候就從池中拿一個,這要比需要的時候創建一個線程對象要快的多。
  • 方便管理:可以編寫線程池管理代碼對池中的線程同一進行管理,比如說啓動時有該程序創建100個線程,每當有請求的時候,就分配一個線程去工作,如果剛好併發有101個請求,那多出的這一個請求可以排隊等候,避免因無休止的創建線程導致系統崩潰。

說說幾種常見的線程池及使用場景

1、newSingleThreadExecutor
創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

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

2、newFixedThreadPool
創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

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

3、newCachedThreadPool
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。

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

4、newScheduledThreadPool
創建一個定長線程池,支持定時及週期性任務執行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

線程池都有哪幾種工作隊列

  1. ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
  2. LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
  3. SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
  4. PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。

線程池中的幾種重要的參數及流程說明

  • corePoolSize:核心池的大小,在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
  • maximumPoolSize:線程池最大線程數,它表示在線程池中最多能創建多少個線程;
  • keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0;
  • unit:參數keepAliveTime的時間單位
  • workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數有以下幾種選擇:
    ArrayBlockingQueue
    LinkedBlockingQueue
    SynchronousQueue
    PriorityBlockingQueue
  • hreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程做些更有意義的事情,比如設置daemon和優先級等
  • handler:表示當拒絕處理任務時的策略,有以下四種取值:
    1、AbortPolicy:直接拋出異常。
    2、CallerRunsPolicy:只用調用者所在線程來運行任務。
    3、DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
    4、DiscardPolicy:不處理,丟棄掉。
    5、根據應用場景需要實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化不能處理的任務

怎麼理解無界隊列和有界隊列

有界隊列

  1. 初始的poolSize < corePoolSize,提交的runnable任務,會直接做爲new一個Thread的參數,立馬執行。
  2. 當提交的任務數超過了corePoolSize,會將當前的runable提交到一個block queue中。
  3. 有界隊列滿了之後,如果poolSize < maximumPoolsize時,會嘗試new 一個Thread的進行救急處理,立馬執行對應的runnable任務。
  4. 如果3中也無法處理了,就會走到第四步執行reject操作。

無界隊列

  • 與有界隊列相比,除非系統資源耗盡,否則無界的任務隊列不存在任務入隊失敗的情況。
  • 當有新的任務到來,系統的線程數小於corePoolSize時,則新建線程執行任務。當達到corePoolSize後,就不會繼續增加,若後續仍有新的任務加入,而沒有空閒的線程資源,則任務直接進入隊列等待。
  • 若任務創建和處理的速度差異很大,無界隊列會保持快速增長,直到耗盡系統內存。
  • 當線程池的任務緩存隊列已滿並且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會採取任務拒絕策略。

如何合理配置線程池的大小

大家認爲線程池的大小經驗值應該這樣設置:(其中N爲CPU的個數)
如果是CPU密集型應用,則線程池大小設置爲N+1
如果是IO密集型應用,則線程池大小設置爲2N+1

線程等待時間所佔比例越高,需要越多線程。線程CPU時間所佔比例越高,需要越少線程。

計算密集型任務的特點是要進行大量的計算,消耗CPU資源,比如計算圓周率、對視頻進行高清解碼等等,全靠CPU的運算能力。這種計算密集型任務雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU執行任務的效率就越低,所以,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數。

第二種任務的類型是IO密集型,涉及到網絡、磁盤IO的任務都是IO密集型任務,這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因爲IO的速度遠遠低於CPU和內存的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部分任務都是IO密集型任務,比如Web應用。

submit()和execute()

JDK5往後,任務分兩類:一類是實現了Runnable接口的類,一類是實現了Callable接口的類。兩者都可以被ExecutorService執行,它們的區別是:

  • execute(Runnable x) 沒有返回值。可以執行任務,但無法判斷任務是否成功完成。——實現Runnable接口
  • submit(Runnable x) 返回一個future。可以用這個future來判斷任務是否成功完成。——實現Callable接口

線程池原理

線程池提供了兩個鉤子(beforeExecute,afterExecute)給我們,我們繼承線程池,在執行任務前後做一些事情。

線程池由兩個核心數據結構組成:
1)線程集合(workers):存放執行任務的線程,是一個HashSet;
2)任務等待隊列(workQueue):存放等待線程池調度執行的任務,是一個阻塞式隊列BlockingQueue;

任務執行流程
1)線程池中線程數量小於corePoolSize,此時任務不會進等待隊列,線程池直接創建一個線程Worker執行提交的任務;
2)線程池中線程數量不小於corePoolSize並且等待隊列未滿,任務直接添加到等待隊列,等待線程池調度執行;
3)線程池中線程數量不小於corePoolSize但是等待隊列已滿且線程數量小於maximumPoolSize,線程池會進行擴容新創建一個線程Worker執行提交的任務,新創建的Worker會被添加到線程集合workers中;
4)等待隊列已滿並且線程數量已達到maximumPoolSize,這種情況下線程池無法繼續執行任務會拒絕任務,執行一個指定的拒接策略。

JDK內置的拒絕策略主要有下面幾種:
1)調用線程執行(CallerRunsPolicy):任務被線程池拒絕後,任務會被調用線程執行;
2)終止執行(AbortPolicy):任務被拒絕時,拋出RejectedExecutionException異常報錯
3)丟棄任務(DiscardPolicy):任務被直接丟棄,不會拋異常報錯;
4)丟失老任務(DiscardOldestPolicy):把等待隊列中最老的任務刪除,刪除後重新提交當前任務。

線程安全

什麼是死鎖?

所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。 因此我們舉個例子來描述,如果此時有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖,此時產生了死鎖。

死鎖產生的4個必要條件?

  • 互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一進程所佔用。
  • 請求和保持條件:當進程因請求資源而阻塞時,對已獲得的資源保持不放。
  • 不剝奪條件:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
  • 環路等待條件:在發生死鎖時,必然存在一個進程–資源的環形鏈。

怎麼預防死鎖?

  • 資源一次性分配:一次性分配所有資源,這樣就不會再有請求了:(破壞請求條件)
  • 只要有一個資源得不到分配,也不給這個進程分配其他的資源:(破壞請保持條件)
  • 可剝奪資源:即當某進程獲得了部分資源,但得不到其它資源,則釋放已佔有的資源(破壞不可剝奪條件)
  • 資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

總結:
1、以確定的順序獲得鎖
2、超時放棄

怎麼避免死鎖?

銀行家算法:我們可以把操作系統看作是銀行家,操作系統管理的資源相當於銀行家管理的資金,進程向操作系統請求分配資源相當於用戶向銀行家貸款。爲保證資金的安全,銀行家規定:
(1) 當一個顧客對資金的最大需求量不超過銀行家現有的資金時就可接納該顧客;
(2) 顧客可以分期貸款,但貸款的總數不能超過最大需求量;
(3) 當銀行家現有的資金不能滿足顧客尚需的貸款數額時,對顧客的貸款可推遲支付,但總能使顧客在有限的時間裏得到貸款;
(4) 當顧客得到所需的全部資金後,一定能在有限的時間裏歸還所有的資金.

檢測死鎖:首先爲每個進程和每個資源指定一個唯一的號碼;然後建立資源分配表和進程等待表。

解除死鎖:當發現有進程死鎖後,便應立即把它從死鎖狀態中解脫出來,常採用的方法有:剝奪資源、撤消進程

怎麼檢測死鎖?

1、Jstack命令
jstack是java虛擬機自帶的一種堆棧跟蹤工具。 Jstack工具可以用於生成java虛擬機當前時刻的線程快照。線程快照是當前java虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。 線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在後臺做什麼事情,或者等待什麼資源。

2、JConsole工具
Jconsole是JDK自帶的監控工具,在JDK/bin目錄下可以找到。它用於連接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監控,並畫出大量的圖表,提供強大的可視化界面。而且本身佔用的服務器內存很小,甚至可以說幾乎不消耗。

在這裏插入圖片描述
Java各種鎖

CAS

  • CAS操作的就是樂觀鎖,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。
  • CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
  • CAS機制當中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。

舉例:

  1. 在內存地址V當中,存儲着值爲10的變量。
  2. 此時線程1想要把變量的值增加1。對線程1來說,舊的預期值A=10,要修改的新值B=11。
  3. 在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
  4. 線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。
  5. 線程1重新獲取內存地址V的當前值,並重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱爲自旋。
  6. 這一次比較幸運,沒有其他線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。
  7. 線程1進行SWAP,把地址V的值替換爲B,也就是12。

CAS的缺點

  • CPU開銷較大:在併發量比較高的情況下,如果許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。
  • 不能保證代碼塊的原子性:CAS機制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。

ABA問題

  • CAS可以有效的提升併發的效率,但同時也會引入ABA問題。
  • 比如線程1從內存X中取出A,這時候另一個線程2也從內存X中取出A,並且線程2進行了一些操作將內存X中的值變成了B,然後線程2又將內存X中的數據變成A,這時候線程1進行CAS操作發現內存X中仍然是A,然後線程1操作成功。雖然線程1的CAS操作成功,但是整個過程就是有問題的。
  • 比如鏈表的頭在變化了兩次後恢復了原值,但是不代表鏈表就沒有變化。
  • 所以JAVA中提供了AtomicStampedReference/AtomicMarkableReference來處理會發生ABA問題的場景,主要是在對象中額外再增加一個標記來標識對象是否有過變更。

樂觀鎖和悲觀鎖

悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時數據正確。
樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的性能大幅提升。
悲觀鎖

  • 總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
  • 傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

樂觀鎖

  • 總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號機制和CAS算法實現。
  • 樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

樂觀鎖常見的實現方式

  • 版本號機制:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
  • CAS算法:即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數:需要讀寫的內存值 V;進行比較的值 A;擬寫入的新值 B。當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。
  • 簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)

自旋鎖和適應性自旋鎖

Why:切換線程的開銷大,在許多場景中,同步資源的鎖定時間很短,爲了這一小段時間去切換線程,線程掛起和恢復現場的花費可能會讓系統得不償失。如果物理機器有多個處理器,能夠讓兩個或以上的線程同時並行執行,我們就可以讓後面那個請求鎖的線程不放棄CPU的執行時間,看看持有鎖的線程是否很快就會釋放鎖。
What:爲了讓當前線程“稍等一下”,我們需讓當前線程進行自旋,如果在自旋完成後前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷。這就是自旋鎖。

How

  • 當前線程競爭鎖失敗時,打算阻塞自己
  • 不直接阻塞自己,而是自旋一會(空等待,比如一個空的有限for循環
  • 在自旋的同時重新競爭鎖
  • 如果自旋結束前獲得了鎖,那麼鎖獲取成功;否則,自旋結束後阻塞自己
  • 如果在自旋的時間內,鎖就被舊owner釋放了,那麼當前線程就不需要阻塞自己(也不需要在未來鎖釋放時恢復),減少了一次線程切換。

自旋鎖的缺點:自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。
實現原理:自旋鎖的實現原理同樣也是CAS,AtomicInteger中調用unsafe進行自增操作的源碼中的do-while循環就是一個自旋操作,如果修改數值失敗則通過循環來執行自旋,直至修改成功。

適應性自旋鎖

  • 自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啓。JDK6中變爲默認開啓,並且引入了自適應的自旋鎖(適應性自旋鎖)。
  • 自適應意味着自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
  • 如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行中,那麼虛擬機就會認爲這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間。
  • 如果對於某個鎖,自旋很少成功獲得過,那在以後嘗試獲取這個鎖時將可能省略掉自旋過程,直接阻塞線程,避免浪費處理器資源。

無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

  • 這四種鎖是指鎖的狀態,專門針對synchronized的。偏斜鎖、輕量級鎖、重量級鎖的代碼實現,並不在核心類庫部分,而是在JVM的代碼中。
  • synchronized是悲觀鎖,在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭裏的,以Hotspot虛擬機爲例,對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。
  • 鎖一共有4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態只能升級不能降級。
  • 四種鎖狀態對應的的Mark Word內容:
    在這裏插入圖片描述

無鎖:
無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。CAS原理及應用即是無鎖的實現。

偏向鎖:

  • 偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖,降低獲取鎖的代價。
  • 當一個線程訪問同步代碼塊並獲取鎖時,會在Mark Word裏存儲鎖偏向的線程ID。在線程進入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word裏是否存儲着指向當前線程的偏向鎖。
  • 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態。撤銷偏向鎖後恢復到無鎖(標誌位爲“01”)或輕量級鎖(標誌位爲“00”)的狀態。
  • 偏向鎖在JDK 6及以後的JVM裏是默認啓用的。可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程序默認會進入輕量級鎖狀態。

輕量級鎖:

  • 是指當鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
  • 在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲“01”狀態,是否爲偏向鎖爲“0”),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,然後拷貝對象頭中的Mark Word複製到鎖記錄中。拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock Record裏的owner指針指向對象的Mark Word。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,表示此對象處於輕量級鎖定狀態。
  • 如果輕量級鎖的更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明多個線程競爭鎖。
  • 若當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級爲重量級鎖。

重量級鎖
升級爲重量級鎖時,鎖標誌的狀態值變爲“10”,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。

公平鎖和非公平鎖

  • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。公平鎖的優點是等待鎖的線程不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
  • 非公平鎖是多個線程加鎖時直接嘗試獲取鎖,獲取不到纔會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的線程先獲取鎖的場景。非公平鎖的優點是可以減少喚起線程的開銷,整體的吞吐效率高,因爲線程有機率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點是處於等待隊列中的線程可能會餓死,或者等很久纔會獲得鎖。
  • ReentrantLock裏面有一個內部類Sync,Sync繼承AQS(AbstractQueuedSynchronizer),添加鎖和釋放鎖的大部分操作實際上都是在Sync中實現的。它有公平鎖FairSync和非公平鎖NonfairSync兩個子類。ReentrantLock默認使用非公平鎖,也可以通過構造器來顯示的指定使用公平鎖。

可重入鎖和非可重入鎖

  • 可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,再進入該線程的內層方法會自動獲取鎖(前提鎖對象得是同一個對象或者class),不會因爲之前已經獲取過還沒釋放而阻塞。
  • Java中ReentrantLock和synchronized都是可重入鎖,可重入鎖的一個優點是可一定程度避免死鎖。NonReentrantLock是非可重入鎖。
public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1執行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2執行...");
    }
}
  • 類中的兩個方法都是被內置鎖synchronized修飾的,doSomething()方法中調用doOthers()方法。因爲內置鎖是可重入的,所以同一個線程在調用doOthers()時可以直接獲得當前對象的鎖,進入doOthers()進行操作。
  • 如果是一個不可重入鎖,那麼當前線程在調用doOthers()之前需要將執行doSomething()時獲取當前對象的鎖釋放掉,實際上該對象鎖已被當前線程所持有,且無法釋放。所以此時會出現死鎖。

獨享鎖和共享鎖

  • 獨享鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程T對數據A加上排它鎖後,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。JDK中的synchronized和JUC中Lock的實現類就是互斥鎖。
  • 共享鎖是指該鎖可被多個線程所持有。如果線程T對數據A加上共享鎖後,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。
  • 獨享鎖與共享鎖都是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

volatile

happens-before

Why

  • 因爲jvm會對代碼進行編譯優化,指令會出現重排序的情況,爲了避免編譯優化對併發編程安全性的影響,需要happens-before規則定義一些禁止編譯優化的場景,保證併發編程的正確性。
  • 在程序運行過程中,所有的變更會先在寄存器或本地cache中完成,然後纔會被拷貝到主存以跨越內存柵欄(本地或工作內存到主存之間的拷貝動作),此種跨越序列或順序稱爲happens-before。happens-before本質是順序,重點是跨越內存柵欄。

What?
happens-before 在Java內存模型中意味着:前一個操作的結果可以被後續的操作獲取。例如前面一個操作把變量a賦值爲1,那後面的操作肯定能知道a已經變成了1。

內存屏障

內存屏障是cpu指令

作用

  • 保證指令執行的順序,內存屏障前的指令一定先於內存屏障後的指令
  • 將write buffer的緩存行,立即刷新到內存中

編譯器指令重排和CPU指令重排

爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入的代碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中的各個語句計算的先後順序與輸入代碼中的順序一致。與處理器的亂序執行優化類似,java虛擬機的即使編譯器也有類似的指令重排優化

volatile實現原理

三個特性:

  • 可見性:即當一個線程修改了聲明爲volatile變量的值,新值對於其他要讀該變量的線程來說是立即可見的。而普通變量是不能做到這一點的,普通變量的值在線程間傳遞需要通過主內存來完成。
  • 有序性:volatile變量的所謂有序性也就是被聲明爲volatile的變量的臨界區代碼的執行是有順序的,即禁止指令重排序。
  • 受限原子性:這裏volatile變量的原子性與synchronized的原子性是不同的,synchronized的原子性是指只要聲明爲synchronized的方法或代碼塊兒在執行上就是原子操作的。而volatile是不修飾方法或代碼塊兒的,它用來修飾變量,對於單個volatile變量的讀/寫操作都具有原子性,但類似於volatile++這種複合操作不具有原子性。所以volatile的原子性是受限制的。並且在多線程環境中,volatile並不能保證原子性。
  • volatile寫-讀的內存語義:volatile寫的內存語義:當寫線程寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
  • volatile讀的內存語義:當讀線程讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效,線程接下來將從主內存讀取共享變量。

實現原理:

  • volatile可見性的實現就是藉助了CPU的lock指令,通過在寫volatile的機器指令前加上lock前綴,使寫volatile具有以下兩個原則:
    寫volatile時處理器會將緩存寫回到主內存。
    一個處理器的緩存寫回到內存會導致其他處理器的緩存失效。
  • volatile有序性的保證就是通過禁止指令重排序來實現的。

使用場景

  • 狀態量標記:使用volatile來標示flag,就能解決上面說到的可見性問題,這種對變量的讀寫操作,標記爲 volatile可以保證修改對線程立刻可見。比 synchronized, Lock有一定的效率提升。
class OrderExample{
	int a = 0;
	volatile boolean flag = false;
	public void writer(){
		a = 1;
		flag = true;
	}

	public void reader(){
		if(flag){
			int i = a + 1;
		}
	}
}
  • 實現單例模式:懶漢的單例模式,使用時才創建對象,爲了避免初始化操作的指令重排序,給 instance加上了 volatile。
class Singleton{
	private volatile static Singleton instance = null;
	private Singleton(){}
	public static Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

有了synchronized爲什麼還要volatile

區別

  • volatile本質是在告訴jvm當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。
  • volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的
  • volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量的修改可見性和原子性
  • volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化

synchronized

synchronized是如何實現的

  • synchronized是由一對兒monitorentry/monitorexit指令實現的,Monitor對象是同步的基本實現單元。
  • 在Java 6之前,Monitor的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。
  • 現代的(Oracle)JDK中,JVM對此進行了大刀闊斧地改進,提供了三種不同的Monitor實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。

synchronized和lock的關係

  • 來源: lock是一個接口,而synchronized是java的一個關鍵字,synchronized是內置的語言實現;
  • 異常是否釋放鎖: synchronized在發生異常時候會自動釋放佔有的鎖,因此不會出現死鎖;而lock發生異常時候,不會主動釋放佔有的鎖,必須手動unlock來釋放鎖,可能引起死鎖的發生。(所以最好將同步代碼塊用try catch包起來,finally中寫入unlock,避免死鎖的發生。)
  • 是否響應中斷:lock等待鎖過程中可以用interrupt來中斷等待,而synchronized只能等待鎖的釋放,不能響應中斷;
  • 是否知道獲取鎖:Lock可以通過trylock來知道有沒有獲取鎖,而synchronized不能;
  • Lock可以提高多個線程進行讀操作的效率。(可以通過readwritelock實現讀寫分離)在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。
  • synchronized使用Object對象本身的wait 、notify、notifyAll調度機制,而Lock可以使用Condition進行線程之間的調度

分佈式鎖

https://blog.csdn.net/wuzhiwei549/article/details/80692278

其它

sleep wait

  • 屬於不同的兩個類,sleep()方法是線程類(Thread)的靜態方法,wait()方法是Object類裏的方法。
  • sleep()方法不會釋放鎖,wait()方法釋放對象鎖。
  • sleep()方法可以在任何地方使用,wait()方法則只能在同步方法或同步塊中使用。
  • sleep()使線程進入阻塞狀態(線程睡眠),wait()方法使線程進入等待隊列(線程掛起),也就是阻塞類別不同。

wait notify notifAll

  • wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。
  • wait() 需要被try catch包圍,以便發生異常中斷也可以使wait等待的線程喚醒。
  • notify 和wait 的順序不能錯,如果A線程先執行notify方法,B線程再執行wait方法,那麼B線程是無法被喚醒的。
  • notify方法只喚醒一個等待線程並使該線程開始執行。notifyAll 會喚醒所有等待線程。

一般在synchronized 同步代碼塊裏使用 wait()、notify/notifyAll() 方法。

  • 由於 wait()、notify/notifyAll() 在synchronized 代碼塊執行,說明當前線程一定是獲取了鎖的。當線程執行wait()方法時候,會釋放當前的鎖,然後讓出CPU,進入等待狀態。
  • 只有當 notify/notifyAll() 被執行時候,纔會喚醒一個或多個正處於等待狀態的線程,然後繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。
  • 也就是說,notify/notifyAll() 的執行只是喚醒沉睡的線程,而不會立即釋放鎖,鎖的釋放要看代碼塊的具體執行情況。所以在編程中,儘量在使用了notify/notifyAll() 後立即退出臨界區,以喚醒其他線程讓其獲得鎖

ThreadLocal

  • ThreadLocal提供了線程內存儲變量的能力,這些變量不同之處在於每一個線程讀取的變量是對應的互相獨立的。通過get和set方法就可以得到當前線程對應的值。ThreadLocal相當於維護了一個map,key就是當前的線程,value就是需要存儲的對象。實際上是ThreadLocal的靜態內部類ThreadLocalMap爲每個Thread都維護了一個數組table,ThreadLocal確定了一個數組下標,而這個下標就是value存儲的對應位置。
  • Thread類中有一個私有的成員變量threadLocals,Thread並沒有提供成員變量threadLocals的設置與訪問的方法,ThreadLocal是線程Thread中屬性threadLocals的管理者。對於ThreadLocal的get,
    set,remove的操作結果都是針對當前線程Thread實例的threadLocals存,取,刪除操作。

ThreadLocal內部的ThreadLocalMap鍵爲弱引用,會有內存泄漏的風險,發生內存泄漏的前提條件:

  1. ThreadLocal引用被設置爲null,且後面沒有set,get,remove操作。
  2. 線程一直運行,不停止。(線程池)
  3. 觸發了垃圾回收。(Minor GC或Full GC)

避免內存泄漏:

  1. ThreadLocal申明爲private static final。
  2. ThreadLocal使用後務必調用remove方法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章