日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Java并發(fā)十二連招,你能接住嗎?

本文轉(zhuǎn)載自微信公眾號「sowhat1412」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系sowhat1412公眾號。

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供海倫網(wǎng)站建設(shè)、海倫做網(wǎng)站、海倫網(wǎng)站設(shè)計、海倫網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、海倫企業(yè)網(wǎng)站模板建站服務(wù),十年海倫做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。

1、HashMap

面試第一題必問的 HashMap,挺考驗Javaer的基礎(chǔ)功底的,別問為啥放在這,因為重要!HashMap具有如下特性:

  1. HashMap 的存取是沒有順序的。
  2. KV 均允許為 NULL。
  3. 多線程情況下該類安全,可以考慮用 HashTable。
  4. JDk8底層是數(shù)組 + 鏈表 + 紅黑樹,JDK7底層是數(shù)組 + 鏈表。
  5. 初始容量和裝載因子是決定整個類性能的關(guān)鍵點,輕易不要動。
  6. HashMap是懶漢式創(chuàng)建的,只有在你put數(shù)據(jù)時候才會 build。
  7. 單向鏈表轉(zhuǎn)換為紅黑樹的時候會先變化為雙向鏈表最終轉(zhuǎn)換為紅黑樹,切記雙向鏈表跟紅黑樹是共存的。
  8. 對于傳入的兩個key,會強制性的判別出個高低,目的是為了決定向左還是向右放置數(shù)據(jù)。
  9. 鏈表轉(zhuǎn)紅黑樹后會努力將紅黑樹的root節(jié)點和鏈表的頭節(jié)點 跟table[i]節(jié)點融合成一個。
  10. 在刪除的時候是先判斷刪除節(jié)點紅黑樹個數(shù)是否需要轉(zhuǎn)鏈表,不轉(zhuǎn)鏈表就跟RBT類似,找個合適的節(jié)點來填充已刪除的節(jié)點。
  11. 紅黑樹的root節(jié)點不一定跟table[i]也就是鏈表的頭節(jié)點是同一個,三者同步是靠MoveRootToFront實現(xiàn)的。而HashIterator.remove()會在調(diào)用removeNode的時候movable=false。

常見HashMap考點:

  1. HashMap原理,內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
  2. HashMap中的put、get、remove大致過程。
  3. HashMap中 hash函數(shù)實現(xiàn)。
  4. HashMap如何擴容。
  5. HashMap幾個重要參數(shù)為什么這樣設(shè)定。
  6. HashMap為什么線程不安全,如何替換。
  7. HashMap在JDK7跟JDK8中的區(qū)別。
  8. HashMap中鏈表跟紅黑樹切換思路。
  9. JDK7中 HashMap環(huán)產(chǎn)生原理。

2、ConcurrentHashMap

ConcurrentHashMap 是多線程模式下常用的并發(fā)容器,它的實現(xiàn)在JDK7跟JDK8區(qū)別挺大的。

2.1 JDK7

JDK7中的 ConcurrentHashMap 使用 Segment + HashEntry 分段鎖實現(xiàn)并發(fā),它的缺點是并發(fā)程度是由Segment 數(shù)組個數(shù)來決定的,并發(fā)度一旦初始化無法擴容,擴容的話只是HashEntry的擴容。

Segment 繼承自 ReentrantLock,在此扮演鎖的角色??梢岳斫鉃槲覀兊拿總€Segment都是實現(xiàn)了Lock功能的HashMap。如果我們同時有多個Segment形成了Segment數(shù)組那我們就可以實現(xiàn)并發(fā)咯。

大致的put流程如下:

1.ConcurrentHashMap底層大致實現(xiàn)?

ConcurrentHashMap允許多個修改操作并發(fā)進行,其關(guān)鍵在于使用了鎖分離技術(shù)。它使用了多個鎖來控制對hash表的不同部分進行的修改。內(nèi)部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的HashTable,只要多個修改操作發(fā)生在不同的段上就可以并發(fā)進行。

2.ConcurrentHashMap在并發(fā)下的情況下如何保證取得的元素是最新的?

用于存儲鍵值對數(shù)據(jù)的HashEntry,在設(shè)計上它的成員變量value跟next都是volatile類型的,這樣就保證別的線程對value值的修改,get方法可以馬上看到,并且get的時候是不用加鎖的。

3.ConcurrentHashMap的弱一致性體現(xiàn)在clear和get方法,原因在于沒有加鎖。

比如迭代器在遍歷數(shù)據(jù)的時候是一個Segment一個Segment去遍歷的,如果在遍歷完一個Segment時正好有一個線程在剛遍歷完的Segment上插入數(shù)據(jù),就會體現(xiàn)出不一致性。clear也是一樣。get方法和containsKey方法都是遍歷對應(yīng)索引位上所有節(jié)點,都是不加鎖來判斷的,如果是修改性質(zhì)的因為可見性的存在可以直接獲得最新值,不過如果是新添加值則無法保持一致性。

4.size 統(tǒng)計個數(shù)不準(zhǔn)確

size方法比較有趣,先無鎖的統(tǒng)計所有的數(shù)據(jù)量看下前后兩次是否數(shù)據(jù)一樣,如果一樣則返回數(shù)據(jù),如果不一樣則要把全部的segment進行加鎖,統(tǒng)計,解鎖。并且size方法只是返回一個統(tǒng)計性的數(shù)字。

2.2 JDK8

ConcurrentHashMap 在JDK8中拋棄了分段鎖,轉(zhuǎn)為用 CAS + synchronized,同時將HashEntry改為Node,還加入了紅黑樹的實現(xiàn),主要還是看put的流程(如果看了擴容這塊,絕對可以好好吹逼一番)。

ConcurrentHashMap 是如果來做到高效并發(fā)安全?

1.讀操作

get方法中根本沒有使用同步機制,也沒有使用unsafe方法,所以讀操作是支持并發(fā)操作的。

2.寫操作

基本思路跟HashMap的寫操作類似,只不過用到了CAS + syn 實現(xiàn)加鎖,同時還涉及到擴容的操作。JDK8中鎖已經(jīng)細(xì)化到 table[i] 了,數(shù)組位置不同可并發(fā),位置相同則去幫忙擴容。

3.同步處理主要是通過syn和unsafe的硬件級別原子性這兩種方式完成

當(dāng)我們對某個table[i]操作時候是用syn加鎖的。

取數(shù)據(jù)的時候用的是unsafe硬件級別指令,直接獲取指定內(nèi)存的最新數(shù)據(jù)。

3 、并發(fā)基礎(chǔ)知識

并發(fā)編程的出發(fā)點:充分利用CPU計算資源,多線程并不是一定比單線程快,要不為什么Redis6.0版本的核心操作指令仍然是單線程呢?對吧!

多線程跟單線程的性能都要具體任務(wù)具體分析,talk is cheap, show me the picture。

3.1 進程跟線程

進程:

進程是操作系統(tǒng)調(diào)用的最小單位,是系統(tǒng)進行資源分配和調(diào)度的獨立單位。

線程:

因為進程的創(chuàng)建、銷毀、切換產(chǎn)生大量的時間和空間的開銷,進程的數(shù)量不能太多,而線程是比進程更小的能獨立運行的基本單位,他是進程的一個實體,是CPU調(diào)度的最小單位。線程可以減少程序并發(fā)執(zhí)行時的時間和空間開銷,使得操作系統(tǒng)具有更好的并發(fā)性。

線程基本不擁有系統(tǒng)資源,只有一些運行時必不可少的資源,比如程序計數(shù)器、寄存器和棧,進程則占有堆、棧。線程,Java默認(rèn)有兩個線程 main 跟GC。Java是沒有權(quán)限開線程的,無法操作硬件,都是調(diào)用的 native 的 start0 方法 由 C++ 實現(xiàn)

3.2 并行跟并發(fā)

并發(fā):

concurrency : 多線程操作同一個資源,單核CPU極速的切換運行多個任務(wù)

并行:

parallelism :多個CPU同時使用,CPU多核 真正的同時執(zhí)行

3.3 線程幾個狀態(tài)

Java中線程的狀態(tài)分為6種:

1.初始(New):

新創(chuàng)建了一個線程對象,但還沒有調(diào)用start()方法。

2.可運行(Runnable):

調(diào)用線程的start()方法,此線程進入就緒狀態(tài)。就緒狀態(tài)只是說你資格運行,調(diào)度程序沒有給你CPU資源,你就永遠(yuǎn)是就緒狀態(tài)。

當(dāng)前線程sleep()方法結(jié)束,其他線程join()結(jié)束,等待用戶輸入完畢,某個線程拿到對象鎖,這些線程也將進入就緒狀態(tài)。

當(dāng)前線程時間片用完了,調(diào)用當(dāng)前線程的yield()方法,當(dāng)前線程進入就緒狀態(tài)。

鎖池里的線程拿到對象鎖后,進入就緒狀態(tài)。

3.運行中(Running)

就緒狀態(tài)的線程在獲得CPU時間片后變?yōu)檫\行中狀態(tài)(running)。這也是線程進入運行狀態(tài)的唯一的一種方式。

4.阻塞(Blocked):

阻塞狀態(tài)是線程阻塞在進入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時的狀態(tài)。

5.等待(Waiting) 跟 超時等待(Timed_Waiting):

處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間,它們要等待被顯式地喚醒(通知或中斷),否則會處于無限期等待的狀態(tài)。

處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間,不過無須無限期等待被其他線程顯示地喚醒,在達(dá)到一定時間后它們會自動喚醒。

6.終止(Terminated):

當(dāng)線程正常運行結(jié)束或者被異常中斷后就會被終止。線程一旦終止了,就不能復(fù)生。

PS:

調(diào)用 obj.wait 的線程需要先獲取 obj 的 monitor,wait會釋放 obj 的 monitor 并進入等待態(tài)。所以 wait()/notify() 都要與 synchronized 聯(lián)用。

其實線程從阻塞/等待狀態(tài) 到 可運行狀態(tài)都涉及到同步隊列跟等待隊列的,這點在 AQS 有講。

3.4. 阻塞與等待的區(qū)別

阻塞:

當(dāng)一個線程試圖獲取對象鎖(非JUC庫中的鎖,即synchronized),而該鎖被其他線程持有,則該線程進入阻塞狀態(tài)。它的特點是使用簡單,由JVM調(diào)度器來決定喚醒自己,而不需要由另一個線程來顯式喚醒自己,不響應(yīng)中斷。

等待:

當(dāng)一個線程等待另一個線程通知調(diào)度器一個條件時,該線程進入等待狀態(tài)。它的特點是需要等待另一個線程顯式地喚醒自己,實現(xiàn)靈活,語義更豐富,可響應(yīng)中斷。例如調(diào)用:Object.wait()、**Thread.join()**以及等待 Lock 或 Condition。

雖然 synchronized 和 JUC 里的 Lock 都實現(xiàn)鎖的功能,但線程進入的狀態(tài)是不一樣的。synchronized 會讓線程進入阻塞態(tài),而 JUC 里的 Lock是用park()/unpark() 來實現(xiàn)阻塞/喚醒 的,會讓線程進入等待狀態(tài)。雖然等鎖時進入的狀態(tài)不一樣,但被喚醒后又都進入Runnable狀態(tài),從行為效果來看又是一樣的。

3.5 yield 跟 sleep 區(qū)別

  1. yield 跟 sleep 都能暫停當(dāng)前線程,都不會釋放鎖資源,sleep 可以指定具體休眠的時間,而 yield 則依賴 CPU 的時間片劃分。
  2. sleep方法給其他線程運行機會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程以運行的機會。yield方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的機會。
  3. 調(diào)用 sleep 方法使線程進入等待狀態(tài),等待休眠時間達(dá)到,而調(diào)用我們的 yield方法,線程會進入就緒狀態(tài),也就是sleep需要等待設(shè)置的時間后才會進行就緒狀態(tài),而yield會立即進入就緒狀態(tài)。
  4. sleep方法聲明會拋出 InterruptedException,而 yield 方法沒有聲明任何異常
  5. yield 不能被中斷,而 sleep 則可以接受中斷。
  6. sleep方法比yield方法具有更好的移植性(跟操作系統(tǒng)CPU調(diào)度相關(guān))

3.6 wait 跟 sleep 區(qū)別

1.來源不同

wait 來自O(shè)bject,sleep 來自 Thread

2.是否釋放鎖

wait 釋放鎖,sleep 不釋放

3.使用范圍

wait 必須在同步代碼塊中,sleep 可以任意使用

4.捕捉異常

wait 不需要捕獲異常,sleep 需捕獲異常

3.7 多線程實現(xiàn)方式

  1. 繼承 Thread,實現(xiàn)run方法
  2. 實現(xiàn) Runnable接口中的run方法,然后用Thread包裝下。Thread 是線程對象,Runnable 是任務(wù),線程啟動的時候一定是對象。
  3. 實現(xiàn) Callable接口,F(xiàn)utureTask 包裝實現(xiàn)接口,Thread 包裝 FutureTask。Callable 與Runnable 的區(qū)別在于Callable的call方法有返回值,可以拋出異常,Callable有緩存。
  4. 通過線程池調(diào)用實現(xiàn)。
  5. 通過Spring的注解 @Async 實現(xiàn)。

3.8 死鎖

死鎖是指兩個或兩個以上的線程互相持有對方所需要的資源,由于某些鎖的特性,比如syn使用下,一個線程持有一個資源,或者說獲得一個鎖,在該線程釋放這個鎖之前,其它線程是獲取不到這個鎖的,而且會一直死等下去,因此這便造成了死鎖。

面試官:你給我解釋下死鎖是什么,解釋好了我就錄用你。

應(yīng)聘者:先發(fā)Offer,發(fā)了Offer我給你解釋什么是死鎖。

產(chǎn)生條件:

互斥條件:一個資源,或者說一個鎖只能被一個線程所占用,當(dāng)一個線程首先獲取到這個鎖之后,在該線程釋放這個鎖之前,其它線程均是無法獲取到這個鎖的。

占有且等待:一個線程已經(jīng)獲取到一個鎖,再獲取另一個鎖的過程中,即使獲取不到也不會釋放已經(jīng)獲得的鎖。

不可剝奪條件:任何一個線程都無法強制獲取別的線程已經(jīng)占有的鎖

循環(huán)等待條件:線程A拿著線程B的鎖,線程B拿著線程A的鎖。。

檢查:

1、jps -l 定位進程號

2、jstack 進程號找到死鎖問題

避免:

加鎖順序:線程按照相同的順序加鎖。

限時加鎖:線程獲取鎖的過程中限制一定的時間,如果給定時間內(nèi)獲取不到,就算了,這需要用到Lock的一些API。

4、JMM

4.1 JMM由來

隨著CPU、內(nèi)存、磁盤的高速發(fā)展,它們的訪問速度差別很大。為了提速就引入了L1、L2、L3三級緩存。以后程序運行獲取數(shù)據(jù)就是如下的步驟了。

這樣雖然提速了但是會導(dǎo)致緩存一致性問題跟內(nèi)存可見性問題。同時編譯器跟CPU為了加速也引入了指令重排。指令重排的大致意思就是你寫的代碼運行運算結(jié)果會按照你看到的邏輯思維去運行,但是在JVM內(nèi)部系統(tǒng)是智能化的會進行加速排序的。

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

2、指令級并行的重排序:現(xiàn)代處理器采用了指令級并行技術(shù)在不影響數(shù)據(jù)依賴性前提下重排。

3、內(nèi)存系統(tǒng)的重排序:處理器使用緩存和讀/寫緩沖區(qū) 進程重排。

指令重排這種機制會導(dǎo)致有序性問題,而在并發(fā)編程時經(jīng)常會涉及到線程之間的通信跟同步問題,一般說是可見性、原子性、有序性。這三個問題對應(yīng)的底層就是 緩存一致性、內(nèi)存可見性、有序性。

原子性:原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。在整個操作過程中不會被線程調(diào)度器中斷的操作,都可認(rèn)為是原子性。比如 a = 1。

可見性:指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。Java保證可見性可以認(rèn)為通過volatile、synchronized、final來實現(xiàn)。

有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行,Java通過volatile、synchronized來保證。

為了保證共享內(nèi)存的正確性(可見性、有序性、原子性),內(nèi)存模型定義了共享內(nèi)存模式下多線程程序讀寫操作行為的規(guī)范,既JMM模型,注意JMM只是一個約定概念,是用來保證效果一致的機制跟規(guī)范。它作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程,規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步。

在JMM中,有兩條規(guī)定:

線程對共享變量的所有操作都必須在自己的工作內(nèi)存中進行,不能直接從主內(nèi)存中讀寫。

不同線程之間無法訪問其他線程工作內(nèi)存中的變量,線程間變量值的傳遞需要通過主內(nèi)存來完成。

共享變量要實現(xiàn)可見性,必須經(jīng)過如下兩個步驟:

把本地內(nèi)存1中更新過的共享變量刷新到主內(nèi)存中。

把主內(nèi)存中最新的共享變量的值更新到本地內(nèi)存2中。

同時人們提出了內(nèi)存屏障、happen-before、af-if-serial這三種概念來保證系統(tǒng)的可見性、原子性、有序性。

4.2 內(nèi)存屏障

內(nèi)存屏障 (Memory Barrier) 是一種CPU指令,用于控制特定條件下的重排序和內(nèi)存可見性問題。Java編譯器也會根據(jù)內(nèi)存屏障的規(guī)則禁止重排序。Java編譯器在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行。具有如下功能:

保證特定操作的執(zhí)行順序。

影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。

在 volatile 中就用到了內(nèi)存屏障,volatile部分已詳細(xì)講述。

4.3 happen-before

因為有指令重排的存在會導(dǎo)致難以理解CPU內(nèi)部運行規(guī)則,JDK用 happens-before 的概念來闡述操作之間的內(nèi)存可見性。在JMM 中如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關(guān)系 。其中CPU的happens-before無需任何同步手段就可以保證的。

  • 程序順序規(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()操作成功返回。
  • 線程中斷規(guī)則:對線程interrupt方法的調(diào)用happens-before于被中斷線程的代碼檢測到中斷事件的發(fā)生。

4.4 af-if-serial

af-if-serial 的含義是不管怎么重排序(編譯器和處理器為了提高并行度),單線程環(huán)境下程序的執(zhí)行結(jié)果不能被改變且必須正確。該語義使單線程環(huán)境下程序員無需擔(dān)心重排序會干擾他們,也無需擔(dān)心內(nèi)存可見性問題。

5、volatile

volatile 關(guān)鍵字的引入可以保證變量的可見性,但是無法保證變量的原子性,比如 a++這樣的是無法保證的。這里其實涉及到JMM 的知識點,Java多線程交互是通過共享內(nèi)存的方式實現(xiàn)的。當(dāng)我們讀寫volatile變量時具有如下規(guī)則:

當(dāng)寫一個volatile變量時,JMM會把該線程對應(yīng)的本地中的共享變量值刷新到主內(nèi)存。

當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。

volatile就會用到上面說到的內(nèi)存屏障,目前有四種內(nèi)存屏障:

  1. StoreStore屏障,保證普通寫不和volatile寫發(fā)生重排序
  2. StoreLoad屏障,保證volatile寫與后面可能的volatile讀寫不發(fā)生重排序
  3. LoadLoad屏障,禁止volatile讀與后面的普通讀重排序
  4. LoadStore屏障,禁止volatile讀和后面的普通寫重排序

volatile原理:用volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令,在CPU級別的功能如下:

將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到 系統(tǒng)內(nèi)存。

這個寫回內(nèi)存的操作會告知在其他CPU你們拿到的變量是無效的下一次使用時候要重新共享內(nèi)存拿。

6、單例模式 DCL + volatile

6.1 標(biāo)準(zhǔn)單例模式

高頻考點單例模式:就是將類的構(gòu)造函數(shù)進行private化,然后只留出一個靜態(tài)的Instance 函數(shù)供外部調(diào)用者調(diào)用。單例模式一般標(biāo)準(zhǔn)寫法是 DCL + volatile:

 
 
 
 
  1. public class SingleDcl { 
  2.     private volatile static SingleDcl singleDcl; //保證可見性 
  3.     private SingleDcl(){ 
  4.     } 
  5.     public static SingleDcl getInstance(){ 
  6.         // 放置進入加鎖代碼,先判斷下是否已經(jīng)初始化好了 
  7.      if(singleDcl == null) {  
  8.      // 類鎖 可能會出現(xiàn) AB線程都在這卡著,A獲得鎖,B等待獲得鎖。 
  9.       synchronized (SingleDcl.class) {  
  10.     if(singleDcl == null) { 
  11.      // 如果A線程初始化好了,然后通過vloatile 將變量復(fù)雜給住線程。 
  12.      // 如果此時沒有singleDel === null,判斷 B進程 進來后還會再次執(zhí)行 new 語句 
  13.      singleDcl = new SingleDcl(); 
  14.     } 
  15.    } 
  16.      } 
  17.         return singleDcl; 
  18.     } 

6.2 為什么用Volatile修飾

不用Volatile則代碼運行時可能存在指令重排,會導(dǎo)致線程一在運行時執(zhí)行順序是 1-->2--> 4 就賦值給instance變量了,然后接下來再執(zhí)行構(gòu)造方法初始化。問題是如果構(gòu)造方法初始化執(zhí)行沒完成前 線程二進入發(fā)現(xiàn)instance != null,直接給線程二個半成品,加入volatile后底層會使用內(nèi)存屏障強制按照你以為的執(zhí)行。

單例模式幾乎是面試必考點,,一般有如下特性:

懶漢式:在需要用到對象時才實例化對象,正確的實現(xiàn)方式是 Double Check + Lock + volatile,解決了并發(fā)安全和性能低下問題,對內(nèi)存要求非常高,那么使用懶漢式寫法。

餓漢式:在類加載時已經(jīng)創(chuàng)建好該單例對象,在獲取單例對象時直接返回對象即可,對內(nèi)存要求不高使用餓漢式寫法,因為簡單不易出錯,且沒有任何并發(fā)安全和性能問題。

枚舉式:Effective Java 這本書也列舉了使用枚舉,其代碼精簡,沒有線程安全問題,且 Enum 類內(nèi)部防止反射和反序列化時破壞單例。

7、線程池

7.1 五分鐘了解線程池

老王是個深耕在帝都的一線碼農(nóng),辛苦一年掙了點錢,想把錢存儲到銀行卡里,拿錢去銀行辦理遇到了如下的遭遇

老王銀行門口取號后發(fā)現(xiàn)有柜臺營業(yè)ing 但是沒人辦理業(yè)務(wù)就直接辦理了。

老王取號后發(fā)現(xiàn)柜臺上都有人在辦理,等待席有空地,去坐著等辦理去了。

老王取號后發(fā)現(xiàn)柜臺都有人辦理,等待席也人坐滿了,這個時候銀行經(jīng)理看到老王是老實人本著關(guān)愛老實人的態(tài)度,新開一個臨時窗口給他辦理了。

老王取號后發(fā)現(xiàn)柜臺都滿了,等待座位席也滿了,臨時窗口也人滿了。這個時候銀行經(jīng)理給出了若干解決策略。

  • 直接告知人太多不給你辦理了。
  • 采用冷暴力模式,也不給不辦理也不讓他走。
  • 經(jīng)理讓老王取嘗試跟座位席中最前面的人聊一聊看是否可以加塞,可以就辦理,不可以還是被踢走。
  • 經(jīng)理直接跟老王說誰讓你來的你找誰去我這辦理不了。

上面的這個流程幾乎就跟JDK線程池的大致流程類似,其中7大參數(shù):

  • 營業(yè)中的3個窗口對應(yīng)核心線程池數(shù):corePoolSize
  • 銀行總的營業(yè)窗口數(shù)對應(yīng):maximumPoolSize
  • 打開的臨時窗口在多少時間內(nèi)無人辦理則關(guān)閉對應(yīng):keepAliveTime
  • 臨時窗口存貨時間單位:TimeUnit
  • 銀行里的等待座椅就是等待隊列:BlockingQueue
  • threadFactory 該參數(shù)在JDK中是 線程工廠,用來創(chuàng)建線程對象,一般不會動。
  • 無法辦理的時候銀行給出的解決方法對應(yīng):RejectedExecutionHandler

當(dāng)線程池的任務(wù)緩存隊列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略,一般有四大拒絕策略:

  • ThreadPoolExecutor.AbortPolicy :丟棄任務(wù),并拋出 RejectedExecutionException 異常。
  • ThreadPoolExecutor.CallerRunsPolicy:該任務(wù)被線程池拒絕,由調(diào)用 execute方法的線程執(zhí)行該任務(wù)。
  • ThreadPoolExecutor.DiscardOldestPolicy :拋棄隊列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)。
  • ThreadPoolExecutor.DiscardPolicy:丟棄任務(wù),也不拋出異常。

7.2 正確創(chuàng)建方式

使用Executors創(chuàng)建線程池可能會導(dǎo)致OOM。原因在于線程池中的BlockingQueue主要有兩種實現(xiàn),分別是ArrayBlockingQueue 和 LinkedBlockingQueue。

  • ArrayBlockingQueue 是一個用數(shù)組實現(xiàn)的有界阻塞隊列,必須設(shè)置容量。
  • LinkedBlockingQueue 是一個用鏈表實現(xiàn)的有界阻塞隊列,容量可以選擇進行設(shè)置,不設(shè)置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE,極易容易導(dǎo)致線程池OOM。

正確創(chuàng)建線程池的方式就是自己直接調(diào)用ThreadPoolExecutor的構(gòu)造函數(shù)來自己創(chuàng)建線程池。在創(chuàng)建的同時,給BlockQueue指定容量就可以了。

 
 
 
 
  1. private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 
  2.         60L, TimeUnit.SECONDS, 
  3.         new ArrayBlockingQueue(10)); 

7.3 常見線程池

羅列幾種常見的線程池創(chuàng)建方式。

1.Executors.newFixedThreadPool

定長的線程池,有核心線程,核心線程的即為最大的線程數(shù)量,沒有非核心線程。 使用的無界的等待隊列是LinkedBlockingQueue。使用時候小心堵滿等待隊列。

2.Executors.newSingleThreadExecutor

創(chuàng)建單個線程數(shù)的線程池,它可以保證先進先出的執(zhí)行順序

3.Executors.newCachedThreadPool

創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。

4.Executors.newScheduledThreadPool

創(chuàng)建一個定長的線程池,而且支持定時的以及周期性的任務(wù)執(zhí)行,支持定時及周期性任務(wù)執(zhí)行

5.ThreadPoolExecutor

最原始跟常見的創(chuàng)建線程池的方式,它包含了 7 個參數(shù)、4種拒絕策略 可用。

7.4 線程池核心點

線程池 在工作中常用,面試也是必考點。關(guān)于線程池的細(xì)節(jié)跟使用在以前舉例過一個 銀行排隊 辦業(yè)務(wù)的例子了。線程池一般主要也無非就是下面幾個考點了:

  • 為什么用線程池。
  • 線程池的作用。
  • 7大重要參數(shù)。
  • 4大拒絕策略。
  • 常見線程池任務(wù)隊列,如何理解有界跟無界。
  • 常用的線程池模版。
  • 如何分配線程池個數(shù),IO密集型 還是 CPU密集型。
  • 設(shè)定一個線程池優(yōu)先級隊列,Runable 類要實現(xiàn)可對比功能,任務(wù)隊列使用優(yōu)先級隊列。

8、ThreadLocal

ThreadLocal 可以簡單理解為線程本地變量,相比于 synchronized 是用空間來換時間的思想。他會在每個線程都創(chuàng)建一個副本,在線程之間通過訪問內(nèi)部副本變量的形式做到了線程之間互相隔離。這里用到了 弱引用 知識點:

如果一個對象只具有弱引用,那么GC回收器在掃描到該對象時,無論內(nèi)存充足與否,都會回收該對象的內(nèi)存。

8.1 核心點

每個Thread內(nèi)部都維護一個ThreadLocalMap字典數(shù)據(jù)結(jié)構(gòu),字典的Key值是ThreadLocal,那么當(dāng)某個ThreadLocal對象不再使用(沒有其它地方再引用)時,每個已經(jīng)關(guān)聯(lián)了此ThreadLocal的線程怎么在其內(nèi)部的ThreadLocalMap里做清除此資源呢?JDK中的ThreadLocalMap沒有繼承java.util.Map類,而是自己實現(xiàn)了一套專門用來定時清理無效資源的字典結(jié)構(gòu)。其內(nèi)部存儲實體結(jié)構(gòu)Entry

接著分析底層代碼會發(fā)現(xiàn)在調(diào)用ThreadLocal.get() 或者 ThreadLocal.set() 都會 定期回收無效的Entry 操作。

9、CAS

Compare And Swap:比較并交換,主要是通過處理器的指令來保證操作的原子性,它包含三個操作數(shù):

V:變量內(nèi)存地址

A:舊的預(yù)期值

B:準(zhǔn)備設(shè)置的新值

當(dāng)執(zhí)行CAS指令時,只有當(dāng) V 對應(yīng)的值等于 A 時才會用 B 去更新V的值,否則就不會執(zhí)行更新操作。CAS可能會帶來ABA問題、循環(huán)開銷過大問題、一個共享變量原子性操作的局限性。如何解決以前寫過,在此不再重復(fù)。

10、Synchronized

10.1 Synchronized 講解

Synchronized 是 JDK自帶的線程安全關(guān)鍵字,該關(guān)鍵字可以修飾實例方法、靜態(tài)方法、代碼塊三部分。該關(guān)鍵字可以保證互斥性、可見性、有序性(不解決重排)但保證有序性。

Syn的底層其實是C++代碼寫的,JDK6前是重量級鎖,調(diào)用的時候涉及到用戶態(tài)跟內(nèi)核態(tài)的切換,挺耗時的。JDK6之前 Doug Lea寫出了JUC包,可以方便的讓用于在用戶態(tài)實現(xiàn)鎖的使用,Syn的開發(fā)者被激發(fā)了斗志所以在JDK6后對Syn進行了各種性能升級。

10.2 Synchronized 底層

Syn里涉及到了 對象頭 包含對象頭、填充數(shù)據(jù)、實例變量。這里可以看一個美團面試題:

問題一:new Object()占多少字節(jié)

  • markword 8字節(jié) + classpointer 4字節(jié)(默認(rèn)用calssPointer壓縮) + padding 4字節(jié) = 16字節(jié)
  • 如果沒開啟classpointer壓縮:markword 8字節(jié) + classpointer 8字節(jié) = 16字節(jié)

問題二:User (int id,String name) User u = new User(1,"李四")

markword 8字節(jié) + 開啟classPointer壓縮后classpointer 4字節(jié) + instance data int 4字節(jié) + 開啟普通對象指針壓縮后String4字節(jié) + padding 4 = 24字節(jié)

10.3 Synchronized 鎖升級

synchronized 鎖在JDK6以后有四種狀態(tài),無鎖、偏向鎖、輕量級鎖、重量級鎖。這幾個狀態(tài)會隨著競爭狀態(tài)逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)。大致升級過程如下

鎖對比:

鎖狀態(tài)優(yōu)點缺點適用場景
偏向鎖加鎖解鎖無需額外消耗,跟非同步方法時間相差納秒級別如果競爭線程多,會帶來額外的鎖撤銷的消耗基本沒有其他線程競爭的同步場景
輕量級鎖競爭的線程不會阻塞而是在自旋,可提高程序響應(yīng)速度如果一直無法獲得會自旋消耗CPU少量線程競爭,持有鎖時間不長,追求響應(yīng)速度
重量級鎖線程競爭不會導(dǎo)致CPU自旋跟消耗CPU資源線程阻塞,響應(yīng)時間長很多線程競爭鎖,切鎖持有時間長,追求吞吐量時候

10.4 Synchronized 無法禁止指令重排,卻能保證有序性

指令重排是程序運行時 解釋器 跟 CPU 自帶的加速手段,可能導(dǎo)致語句執(zhí)行順序跟預(yù)想不一樣情況,但是無論如何重排 也必須遵循 as-if-serial。

避免重排的最簡單方法就是禁止處理器優(yōu)化跟指令重排,比如volatile中用內(nèi)存屏障實現(xiàn),syn是關(guān)鍵字級別的排他且可重入鎖,當(dāng)某個線程執(zhí)行到一段被syn修飾的代碼之前,會先進行加鎖,執(zhí)行完之后再進行解鎖。

當(dāng)某段代碼被syn加鎖后跟解鎖前,其他線程是無法再次獲得鎖的,只有這條加鎖線程可以重復(fù)獲得該鎖。所以代碼在執(zhí)行的時候是單線程執(zhí)行的,這就滿足了as-if-serial語義,正是因為有了as-if-serial語義保證,單線程的有序性就天然存在了。

10.5 wait 虛假喚醒

虛假喚醒定義:

當(dāng)一個條件滿足時,很多線程都被喚醒了,但只有其中部分是有用的喚醒,其它的喚醒是不對的,

比如說買賣貨物,如果商品本來沒有貨物,所有消費者線程都在wait狀態(tài)卡頓呢。這時突然生產(chǎn)者進了一件商品,喚醒了所有掛起的消費者??赡軐?dǎo)致所有的消費者都繼續(xù)執(zhí)行wait下面的代碼,出現(xiàn)錯誤調(diào)用。

虛假喚醒原因:

因為 if 只會執(zhí)行一次,執(zhí)行完會接著向下執(zhí)行 if 下面的。而 while 不會,直到條件滿足才會向下執(zhí)行 while下面的。

虛假喚醒 解決辦法:

在調(diào)用 wait 的時候要用 while 不能用 if。

10.6 notify()底層

1.為何wait跟notify必須要加synchronized鎖

synchronized 代碼塊通過 javap 生成的字節(jié)碼中包含monitorenter 和 monitorexit 指令線程,執(zhí)行 monitorenter 指令可以獲取對象的 monitor,而wait 方法通過調(diào)用 native 方法 wait(0) 實現(xiàn),該注釋說:The current thread must own this object's monitor。

2.notify 執(zhí)行后立馬喚醒線程嗎?

notify/notifyAll 調(diào)用時并不會真正釋放對象鎖,只是把等待中的線程喚醒然后放入到對象的鎖池中,但是鎖池中的所有線程都不會立馬運行,只有擁有鎖的線程運行完代碼塊釋放鎖,別的線程拿到鎖才可以運行。

 
 
 
 
  1. public void test() 
  2.     Object object = new Object(); 
  3.     synchronized (object){ 
  4.         object.notifyAll(); 
  5.         while (true){ 
  6.           // TODO 死循環(huán)會導(dǎo)致 無法釋放鎖。 
  7.         } 
  8.     } 

11、AQS

11.1 高頻考點線程交替打印

目標(biāo)是實現(xiàn)兩個線程交替打印,實現(xiàn)字母在前數(shù)字在后。你可以用信號量、Synchronized關(guān)鍵字跟Lock實現(xiàn),這里用 ReentrantLock簡單實現(xiàn):

 
 
 
 
  1. import java.util.concurrent.CountDownLatch; 
  2. import java.util.concurrent.locks.Condition; 
  3. import java.util.concurrent.locks.Lock; 
  4. import java.util.concurrent.locks.ReentrantLock; 
  5.   
  6. public class Main { 
  7.  private static Lock lock = new ReentrantLock(); 
  8.  private static Condition c1 = lock.newCondition(); 
  9.  private static Condition c2 = lock.newCondition(); 
  10.  private static CountDownLatch count = new CountDownLatch(1); 
  11.   
  12.  public static void main(String[] args) { 
  13.   String c = "ABCDEFGHI"; 
  14.   char[] ca = c.toCharArray(); 
  15.   String n = "123456789"; 
  16.   char[] na = n.toCharArray(); 
  17.   
  18.   Thread t1 = new Thread(() -> { 
  19.    try { 
  20.     lock.lock(); 
  21.     count.countDown(); 
  22.     for(char caa : ca) { 
  23.      c1.signal(); 
  24.      System.out.print(caa); 
  25.      c2.await(); 
  26.     } 
  27.     c1.signal(); 
  28.    } catch (InterruptedException e) { 
  29.     e.printStackTrace(); 
  30.    } finally { 
  31.     lock.unlock(); 
  32.    } 
  33.   }); 
  34.   
  35.   Thread t2 = new Thread(() -> { 
  36.    try { 
  37.     count.await(); 
  38.     lock.lock(); 
  39.     for(char naa : na) { 
  40.      c2.signal(); 
  41.      System.out.print(naa); 
  42.      c1.await(); 
  43.     } 
  44.     c2.signal(); 
  45.    } catch (InterruptedException e) { 
  46.     e.printStackTrace(); 
  47.    } finally { 
  48.     lock.unlock(); 
  49.    } 
  50.   }); 
  51.    
  52.   t1.start();  
  53.   t2.start(); 
  54.  } 

11.2 AQS底層

上題我們用到了ReentrantLock、Condition ,但是它們的底層是如何實現(xiàn)的呢?其實他們是基于AQS的 同步隊列 跟 等待隊列 實現(xiàn)的!

11.2.1 AQS 同步隊列

學(xué)AQS 前 CAS + 自旋 + LockSupport + 模板模式 必須會,目的是方便理解源碼,感覺比 Synchronized 簡單,因為是單純的 Java 代碼。個人理解AQS具有如下幾個特點:

  1. 在AQS 同步隊列中 -1 表示線程在睡眠狀態(tài)
  2. 當(dāng)前Node節(jié)點線程會把前一個Node.ws = -1。當(dāng)前節(jié)點把前面節(jié)點ws設(shè)置為-1,你可以理解為:你自己能知道自己睡著了嗎?只能是別人看到了發(fā)現(xiàn)你睡眠了!
  3. 持有鎖的線程永遠(yuǎn)不在隊列中。
  4. 在AQS隊列中第二個才是最先排隊的線程。
  5. 如果是交替型任務(wù)或者單線程任務(wù),即使用了Lock也不會涉及到AQS 隊列。
  6. 不到萬不得已不要輕易park線程,很耗時的!所以排隊的頭線程會自旋的嘗試幾個獲取鎖。
  7. 并不是說 CAS 一定比SYN好,如果高并發(fā)執(zhí)行時間久 ,用SYN好, 因為SYN底層用了wait() 阻塞后是不消耗CPU資源的。如果鎖競爭不激烈說明自旋不嚴(yán)重 此時用CAS。
  8. 在AQS中也要盡可能避免調(diào)用CLH隊列,因為CLH可能會調(diào)用到park,相對來耗時。

ReentrantLock底層:

11.2.2 AQS 等待隊列

當(dāng)我們調(diào)用 Condition 里的 await 跟 signal 時候底層其實是這樣走的。

12、線程思考

12.1. 變量建議使用棧封閉

所有的變量都是在方法內(nèi)部聲明的,這些變量都處于棧封閉狀態(tài)。方法調(diào)用的時候會有一個棧楨,這是一個獨立的空間。在這個獨立空間創(chuàng)建跟使用則絕對是安全的,但是注意不要返回該變量哦!

12.2. 防止線程饑餓

優(yōu)先級低的線程總是得不到執(zhí)行機會,一般要保證資源充足、公平的分配資源、防止持有鎖的線程長時間執(zhí)行。

12.3 開發(fā)步驟

多線程編程不要為了用而用,引入多線程后會引入額外的開銷。量應(yīng)用程序性能一般:服務(wù)時間、延遲時間、吞吐量、可伸縮性。做應(yīng)用的時候可以一般按照如下步驟:

  • 先確保保證程序的正確性跟健壯性,確實達(dá)不到性能要求再想如何提速。
  • 一定要以測試為基準(zhǔn)。
  • 一個程序中串行的部分永遠(yuǎn)是有的.
  • 裝逼利器:阿姆達(dá)爾定律 S=1/(1-a+a/n)

阿姆達(dá)爾定律中 a為并行計算部分所占比例,n為并行處理結(jié)點個數(shù):

  • 當(dāng)1-a=0時,(即沒有串行,只有并行)最大加速比s=n;
  • 當(dāng)a=0時(即只有串行,沒有并行),最小加速比s=1;
  • 當(dāng)n無窮大時,極限加速比s→ 1/(1-a),這就是加速比的上限。例如,若串行代碼占整個代碼的25%,則并行處理的總體性能不可能超過4。

12.4 影響性能因素

  • 縮小鎖的范圍,能鎖方法塊盡量不要鎖函數(shù)
  • 減少鎖的粒度跟鎖分段,比如ConcurrentHashMap的實現(xiàn)。
  • 讀多寫少時候用讀寫鎖,可提高十倍性能。
  • 用CAS操作來替換重型鎖。
  • 盡量用JDK自帶的常見并發(fā)容器,底層已經(jīng)足夠優(yōu)化了。

當(dāng)前名稱:Java并發(fā)十二連招,你能接住嗎?
網(wǎng)頁鏈接:http://www.dlmjj.cn/article/djegjjh.html