首頁>技術>

話不多說,乾貨走起。

1、HashMap

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

HashMap 的存取是沒有順序的。

KV 均允許為 NULL。

多執行緒情況下該類安全,可以考慮用 HashTable。

JDk8底層是陣列 + 連結串列 + 紅黑樹,JDK7底層是陣列 + 連結串列。

初始容量和裝載因子是決定整個類效能的關鍵點,輕易不要動。

HashMap是懶漢式建立的,只有在你put資料時候才會 build。

單向連結串列轉換為紅黑樹的時候會先變化為雙向連結串列最終轉換為紅黑樹,切記雙向連結串列跟紅黑樹是共存的。

對於傳入的兩個key,會強制性的判別出個高低,目的是為了決定向左還是向右放置資料。

連結串列轉紅黑樹後會努力將紅黑樹的root節點和連結串列的頭節點 跟table[i]節點融合成一個。

紅黑樹的root節點不一定跟table[i]也就是連結串列的頭節點是同一個,三者同步是靠MoveRootToFront實現的。而HashIterator.remove()會在呼叫removeNode的時候movable=false。

常見HashMap考點:

HashMap原理,內部資料結構。

HashMap中的put、get、remove大致過程。

HashMap中 hash函式實現。

HashMap如何擴容。

HashMap幾個重要引數為什麼這樣設定。

HashMap為什麼執行緒不安全,如何替換。

HashMap在JDK7跟JDK8中的區別。

HashMap中連結串列跟紅黑樹切換思路。

JDK7中 HashMap環產生原理。

2、ConcurrentHashMap

ConcurrentHashMap 是多執行緒模式下常用的併發容器,它的實現在JDK7JDK8區別挺大的。

2.1 JDK7

JDK7中的 ConcurrentHashMap 使用 Segment + HashEntry 分段鎖實現併發,它的缺點是併發程度是由Segment 陣列個數來決定的,併發度一旦初始化無法擴容,擴容的話只是HashEntry的擴容。

Segment 繼承自 ReentrantLock,在此扮演鎖的角色。可以理解為我們的每個Segment都是實現了Lock功能的HashMap。如果我們同時有多個Segment形成了Segment陣列那我們就可以實現併發咯。

大致的put流程如下:

ConcurrentHashMap底層大致實現?

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

ConcurrentHashMap在併發下的情況下如何保證取得的元素是最新的?

用於儲存鍵值對資料的HashEntry,在設計上它的成員變數valuenext都是volatile型別的,這樣就保證別的執行緒對value值的修改,get方法可以馬上看到,並且get的時候是不用加鎖的

ConcurrentHashMap弱一致性體現在clear和get方法,原因在於沒有加鎖

比如迭代器在遍歷資料的時候是一個Segment一個Segment去遍歷的,如果在遍歷完一個Segment時正好有一個執行緒在剛遍歷完的Segment上插入資料,就會體現出不一致性。clear也是一樣。get方法和containsKey方法都是遍歷對應索引位上所有節點,都是不加鎖來判斷的,如果是修改性質的因為可見性的存在可以直接獲得最新值,不過如果是新新增值則無法保持一致性

size 統計個數不準確

size方法比較有趣,先無鎖的統計所有的資料量看下前後兩次是否資料一樣,如果一樣則返回資料,如果不一樣則要把全部的segment進行加鎖,統計,解鎖。並且size方法只是返回一個統計性的數字。

2.2 JDK8

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

ConcurrentHashMap 是如果來做到高效併發安全?

讀操作

get方法中根本沒有使用同步機制,也沒有使用unsafe方法,所以讀操作是支援併發操作的。

寫操作

基本思路跟HashMap的寫操作類似,只不過用到了CAS + syn 實現加鎖,同時還涉及到擴容的操作。JDK8中鎖已經細化到 table[i] 了,陣列位置不同可併發,位置相同則去幫忙擴容。

同步處理主要是透過synunsafe的硬體級別原子性這兩種方式完成

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

取資料的時候用的是unsafe硬體級別指令,直接獲取指定記憶體的最新資料。

3 、併發基礎知識

併發程式設計的出發點:充分利用CPU計算資源,多執行緒並不是一定比單執行緒快,要不為什麼Redis6.0版本的核心操作指令仍然是單執行緒呢?對吧!

多執行緒跟單執行緒的效能都要具體任務具體分析,talk is cheap, show me the picture

3.1 程序跟執行緒

程序

程序是作業系統呼叫的最小單位,是系統進行資源分配和排程的獨立單位。

執行緒

因為程序的建立、銷燬、切換產生大量的時間和空間的開銷,程序的數量不能太多,而執行緒是比程序更小的能獨立執行的基本單位,他是程序的一個實體,是CPU排程的最小單位。執行緒可以減少程式併發執行時的時間和空間開銷,使得作業系統具有更好的併發性。

執行緒基本不擁有系統資源,只有一些執行時必不可少的資源,比如程式計數器、暫存器和棧,程序則佔有堆、棧。執行緒,Java預設有兩個執行緒 main 跟GC。Java是沒有許可權開執行緒的,無法操作硬體,都是呼叫的 nativestart0 方法 由 C++ 實現

3.2 並行跟併發

併發:

concurrency : 多執行緒操作同一個資源,單核CPU極速的切換執行多個任務

並行:

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

3.3 執行緒幾個狀態

Java中執行緒的狀態分為6種:

初始(New):

新建立了一個執行緒物件,但還沒有呼叫start()方法。

可執行(Runnable):

呼叫執行緒的start()方法,此執行緒進入就緒狀態。就緒狀態只是說你資格執行,排程程式沒有給你CPU資源,你就永遠是就緒狀態。

當前執行緒sleep()方法結束,其他執行緒join()結束,等待使用者輸入完畢,某個執行緒拿到物件鎖,這些執行緒也將進入就緒狀態。

當前執行緒時間片用完了,呼叫當前執行緒的yield()方法,當前執行緒進入就緒狀態。

鎖池裡的執行緒拿到物件鎖後,進入就緒狀態

執行中(Running)

就緒狀態的執行緒在獲得CPU時間片後變為執行中狀態(running)。這也是執行緒進入執行狀態的唯一的一種方式。

阻塞(Blocked):

阻塞狀態是執行緒阻塞在進入synchronized關鍵字修飾的方法或程式碼塊(獲取鎖)時的狀態。

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

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

處於這種狀態的執行緒不會被分配CPU執行時間,不過無須無限期等待被其他執行緒顯示地喚醒,在達到一定時間後它們會自動喚醒。

終止(Terminated):

當執行緒正常執行結束或者被異常中斷後就會被終止。執行緒一旦終止了,就不能復生。

PS:

呼叫 obj.wait 的執行緒需要先獲取 objmonitorwait會釋放 objmonitor 並進入等待態。所以 wait()/notify() 都要與 synchronized 聯用。

其實執行緒從阻塞/等待狀態 到 可執行狀態都涉及到同步佇列等待佇列的,這點在 AQS 有講。

3.4. 阻塞與等待的區別

阻塞

當一個執行緒試圖獲取物件鎖(非JUC庫中的鎖,即synchronized),而該鎖被其他執行緒持有,則該執行緒進入阻塞狀態。它的特點是使用簡單,由JVM排程器來決定喚醒自己,而不需要由另一個執行緒來顯式喚醒自己,不響應中斷。

等待

當一個執行緒等待另一個執行緒通知排程器一個條件時,該執行緒進入等待狀態。它的特點是需要等待另一個執行緒顯式地喚醒自己,實現靈活,語義更豐富,可響應中斷。例如呼叫:Object.wait()、**Thread.join()**以及等待 LockCondition

雖然 synchronized JUC 裡的 Lock 都實現鎖的功能,但執行緒進入的狀態是不一樣的。synchronized 會讓執行緒進入阻塞態,而 JUC 裡的 Lock是用park()/unpark() 來實現阻塞/喚醒 的,會讓執行緒進入等待狀態。雖然等鎖時進入的狀態不一樣,但被喚醒後又都進入Runnable狀態,從行為效果來看又是一樣的。

3.5 yield 跟 sleep 區別yieldsleep 都能暫停當前執行緒,都不會釋放鎖資源sleep 可以指定具體休眠的時間,而 yield 則依賴 CPU 的時間片劃分。sleep方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會。yield方法只會給相同優先順序或更高優先順序的執行緒以執行的機會。呼叫 sleep 方法使執行緒進入等待狀態,等待休眠時間達到,而呼叫我們的 yield方法,執行緒會進入就緒狀態,也就是sleep需要等待設定的時間後才會進行就緒狀態,而yield會立即進入就緒狀態sleep方法宣告會丟擲 InterruptedException,而 yield 方法沒有宣告任何異常yield 不能被中斷,而 sleep 則可以接受中斷。sleep方法比yield方法具有更好的移植性(跟作業系統CPU排程相關)3.6 wait 跟 sleep 區別來源不同

wait 來自Objectsleep 來自 Thread

是否釋放鎖

wait 釋放鎖,sleep 不釋放

使用範圍

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

捕捉異常

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

3.7 多執行緒實現方式繼承 Thread,實現run方法實現 Runnable介面中的run方法,然後用Thread包裝下。Thread 是執行緒物件,Runnable 是任務,執行緒啟動的時候一定是物件。實現 Callable介面,FutureTask 包裝實現介面,Thread 包裝 FutureTaskCallableRunnable 的區別在於Callablecall方法有返回值,可以丟擲異常,Callable有快取。透過執行緒池呼叫實現。透過Spring的註解 @Async 實現。3.8 死鎖

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

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

應聘者:先發Offer,發了Offer我給你解釋什麼是死鎖。

產生條件

互斥條件:一個資源,或者說一個鎖只能被一個執行緒所佔用,當一個執行緒首先獲取到這個鎖之後,在該執行緒釋放這個鎖之前,其它執行緒均是無法獲取到這個鎖的。

佔有且等待:一個執行緒已經獲取到一個鎖,在獲取另一個鎖的過程中,即使獲取不到也不會釋放已經獲得的鎖。

不可剝奪條件:任何一個執行緒都無法強制獲取別的執行緒已經佔有的鎖

迴圈等待條件:執行緒A拿著執行緒B的鎖,執行緒B拿著執行緒A的鎖。。

檢查

1、jps -l 定位程序號

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

避免

加鎖順序:執行緒按照相同的順序加鎖。

限時加鎖:執行緒獲取鎖的過程中限制一定的時間,如果給定時間內獲取不到,就算了,這需要用到Lock的一些API。

4、JMM4.1 JMM由來

隨著CPU記憶體磁碟的高速發展,它們的訪問速度差別很大。為了提速就引入了L1、L2、L3三級快取。以後程式執行獲取資料就是如下的步驟了。

這樣雖然提速了但是會導致快取一致性問題跟記憶體可見性問題。同時編譯器跟CPU為了加速也引入了指令重排。指令重排的大致意思就是你寫的程式碼執行運算結果會按照你看到的邏輯思維去執行,但是在JVM內部系統是智慧化的會進行加速排序的。

1、編譯器最佳化的重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。

2、指令級並行的重排序:現代處理器採用了指令級並行技術在不影響資料依賴性前提下重排。

3、記憶體系統的重排序:處理器使用快取和讀/寫緩衝區 程序重排。

指令重排這種機制會導致有序性問題,而在併發程式設計時經常會涉及到執行緒之間的通訊跟同步問題,一般說是可見性、原子性、有序性。這三個問題對應的底層就是 快取一致性記憶體可見性有序性

原子性:原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個執行緒來對它進行操作。在整個操作過程中不會被執行緒排程器中斷的操作,都可認為是原子性。比如 a = 1。

可見性:指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。Java保證可見性可以認為透過volatilesynchronizedfinal來實現。

有序性:程式執行的順序按照程式碼的先後順序執行,Java透過volatilesynchronized來保證。

為了保證共享記憶體的正確性(可見性、有序性、原子性),記憶體模型定義了共享記憶體模式下多執行緒程式讀寫操作行為的規範,既JMM模型,注意JMM只是一個約定概念,是用來保證效果一致的機制規範。它作用於工作記憶體和主存之間資料同步過程,規定了如何做資料同步以及什麼時候做資料同步。

在JMM中,有兩條規定:

執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫。

不同執行緒之間無法訪問其他執行緒工作記憶體中的變數,執行緒間變數值的傳遞需要透過主記憶體來完成。

共享變數要實現可見性,必須經過如下兩個步驟:

把本地記憶體1中更新過的共享變數重新整理到主記憶體中。

把主記憶體中最新的共享變數的值更新到本地記憶體2中。

同時人們提出了記憶體屏障、happen-before、af-if-serial這三種概念來保證系統的可見性、原子性、有序性。

4.2 記憶體屏障

記憶體屏障 (Memory Barrier) 是一種CPU指令,用於控制特定條件下的重排序記憶體可見性問題。Java編譯器也會根據記憶體屏障的規則禁止重排序。Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序,從而讓程式按我們預想的流程去執行。具有如下功能:

保證特定操作的執行順序。

影響某些資料(或者是某條指令的執行結果)的記憶體可見性。

在 volatile 中就用到了記憶體屏障,volatile部分已詳細講述。

4.3 happen-before

因為有指令重排的存在會導致難以理解CPU內部執行規則,JDK用 happens-before 的概念來闡述操作之間的記憶體可見性。在JMM 中如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係 。其中CPU的happens-before無需任何同步手段就可以保證的。

程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。

監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。

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

start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。

join()規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。

執行緒中斷規則:對執行緒interrupt方法的呼叫happens-before於被中斷執行緒的程式碼檢測到中斷事件的發生。

4.4 af-if-serial

af-if-serial 的含義是不管怎麼重排序(編譯器和處理器為了提高並行度),單執行緒環境下程式的執行結果不能被改變且必須正確。該語義使單執行緒環境下程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題。

5、volatile

volatile 關鍵字的引入可以保證變數的可見性,但是無法保證變數的原子性,比如 a++這樣的是無法保證的。這裡其實涉及到JMM 的知識點,Java多執行緒互動是透過共享記憶體的方式實現的。當我們讀寫volatile變數時具有如下規則:

當寫一個volatile變數時,JMM會把該執行緒對應的本地中的共享變數值重新整理到主記憶體

當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數

volatile就會用到上面說到的記憶體屏障,目前有四種記憶體屏障:

StoreStore屏障,保證普通寫不和volatile寫發生重排序

StoreLoad屏障,保證volatile寫與後面可能的volatile讀寫不發生重排序

LoadLoad屏障,禁止volatile讀與後面的普通讀重排序

LoadStore屏障,禁止volatile讀和後面的普通寫重排序

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

將當前處理器快取行的資料寫回到 系統記憶體。

這個寫回記憶體的操作會告知在其他CPU你們拿到的變數是無效的下一次使用時候要重新共享記憶體拿。

10
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 分享一個docker視覺化web介面:Portainer