2020年的Java程式員面試三件套:演算法+多執行緒+微服務,對于那些想面試高級 Java 崗位的同學來說,演算法+多執行緒+微服務是繞不過的坎!剩下針對實際作業的題目就屬于真正的本事了,熱門技術的細節和難點成為了面試時主要考察的內容,
這里總結了演算法+多執行緒+微服務相關面試題,有的很基礎,有的很細節,大家可以評估一下自己掌握的情況,
這里把重要的知識點都寫出來了,不管是核心知識點也好還是面試題也好,讓大家對知識框架有個基本輪廓
需要的朋友可以,點擊這里領取!!!,暗號是:CSDN
演算法大全
一. 最小生成樹演算法
連通圖:在無向圖G中,若從頂點i到頂點j有路徑,則稱頂點i和頂點j是連通的,若圖G中任意兩個頂點都連通,則稱G為連通圖,
生成樹:一個連通圖的生成樹是該連通圖的一個極小連通子圖,它含有全部頂點,但只有構成一個數的(n-1)條邊,
最小生成樹:對于一個帶權連通無向圖G中的不同生成樹,各樹的邊上的 權值之和最小,構造最小生成樹的準則有三條:
必須只使用該圖中的邊來構造最小生成樹,
必須使用且僅使用(n-1)條邊來連接圖中的n個頂點,
不能使用產生回路的邊,
- Prim演算法
假設G=(V,E)是一個具有n個頂點的帶權連通無向圖,T(U,TE)是G的最小生成樹,其中U是T的頂點集,TE是T的邊集,則由G構造從起始頂點v出發的最小生成樹T的步驟為:
初始化U={v},以v到其他頂點的所有邊為候選邊(U中所有點到其他頂點的邊),
重復以下步驟(n-1)次,使得其他(n-1)個頂點被加入到U中,
從候選邊中挑選權值最小的邊加入TE,設該邊在V-U(這里是集合減)中的頂點是k,將k加入U中,
考察當前V-U中的所有頂點j,修改候選邊,若邊(k,j)的權值小于原來和頂點j關聯的候選邊,則用(k,j)取代后者作為候選邊,

- Kruskal演算法
假設G=(V,E)是一個具有n個頂點的帶權連通無向圖,T(U,TE)是G的最小生成樹,其中U是T的頂點集,TE是T的邊集,則由G構造從起始頂點v出發的最小生成樹T的步驟為:
置U的初始值等于V(即包含G中的全部頂點),TE的初始值為空
將圖G中的邊按權值從小到大的順序依次選取,若選取的邊未使生成樹T形成回路,則加入TE,否則放棄,知道TE中包含(n-1)條邊為止,
二. 最短路徑演算法
- Dijkstra —— 貪心演算法
從一個頂點到其余頂點的最短路徑
設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分成兩組,第1組為已求出最短路徑的頂點(用S表示,初始時S只有一個源點,以后每求得一條最短路徑v,…k,就將k加到集合S中,直到全部頂點都加入S),第2組為其余未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序把第2組的頂點加入S中,
步驟:
初始時,S只包含源點,即S={v},頂點v到自己的距離為0,U包含除v外的其他頂點,v到U中頂點i的距離為邊上的權,
從U中選取一個頂點u,頂點v到u的距離最小,然后把頂點u加入S中,
以頂點u為新考慮的中間點,修改v到U中各個點的距離,
重復以上步驟知道S包含所有頂點,
2. Floyd —— 動態規劃
Floyd 演算法是解決任意兩點間的最短路徑的一種演算法,可以正確處理有向圖或負權(但不可存在負權回路)的最短路徑問題,該演算法的時間復雜度為
O
(
N
3
)
O(N^{3})
O(N3),空間復雜度為
O
(
N
2
)
O(N^{2})
O(N2)
設 D i , j , k D_{i,j,k} Di,j,k?為從 i i i到 j j j的只以 ( 1.. k ) (1..k) (1..k)集合中的節點為中間節點的最短路徑的長度,
D i , j , k = { D i , j , k ? 1 最 短 路 徑 不 經 過 k D i , k , k ? 1 + D k , j , k ? 1 最 短 路 徑 經 過 k D{i,j,k}=\begin{cases} D{i,j,k-1} &最短路徑不經過k D{i,k,k-1}+D{k,j,k-1} &最短路徑經過k \end{cases} Di,j,k={Di,j,k?1?最短路徑不經過kDi,k,k?1+Dk,j,k?1?最短路徑經過k?
因此, D i , j , k = m i n ( D i , k , k ? 1 + D k , j , k ? 1 , D i , j , k ? 1 ) D{i,j,k}=min(D{i,k,k-1}+D{k,j,k-1},D{i,j,k-1}) Di,j,k=min(Di,k,k?1+Dk,j,k?1,Di,j,k?1),偽代碼描述如下:
// let dist be a |V| × |V| array of minimum distances initialized to ∞ (infinity)
for each vertex v
dist[v][v] ← 0
for each edge (u,v)
dist[u][v] ← w(u,v) // the weight of the edge (u,v)
for k from 1 to |V|
for i from 1 to |V|
for j from 1 to |V|
if dist[i][j] > dist[i][k] + dist[k][j]
dist[i][j] ← dist[i][k] + dist[k][j]
end if
三. KMP演算法
KMP演算法解決的問題是字符匹配,這個演算法把字符匹配的時間復雜度縮小到O(m+n),而空間復雜度也只有O(m),n是target的長度,m是pattern的長度,
部分匹配表(Next陣列):表的作用是 讓演算法無需多次匹配S中的任何字符,能夠實作線性時間搜索的關鍵是 在不錯過任何潛在匹配的情況下,我們”預搜索”這個模式串本身并將其譯成一個包含所有可能失配的位置對應可以繞過最多無效字符的串列,
Next陣列(前綴和前綴的比較):t為模式串,j為下標
Next[0] = -1
Next[j] = MAX{ k | 0 < k < j | " t0 t1 … tk " = “t ( j-k ) t ( j-k+1 ) … t( j-1 )” }
|i| 0| 1| 2| 3| 4| 5 |6| |–| | t[i]| A| B| C| D| A| B| D| |next[i]| -1| 0 |0 |0 |0 |1 |2|
NextVal陣列:是一種優化后的Next陣列,是為了解決類似aaaab這種模式串的匹配,減少重復的比較, 如果t[next[j]]=t[j]:nextval[j]=nextval[next[j]],否則nextval[j]=next[j],
|i| 0| 1| 2| 3| 4| 5 |6| |–| | t | a| b| c| a| b| a |a| |next[j] | -1| 0 |0 |0 |1 |2 |1| |nextval[j] | -1| 0 |0 |-1 |0 |2 |1|
在上面的表格中,t[next[4]]=t[4]=b,所以nextval[4]=nextval[next[4]]=0
四. 查找演算法
-
ASL
由于查找演算法的主要運算是關鍵字的比較,所以通常把查找程序中對關鍵字的平均比較次數(平均查找長度)作為衡量一個查找演算法效率的標準,ASL= ∑(n,i=1) Pi*Ci,其中n為元素個數,Pi是查找第i個元素的概率,一般為Pi=1/n,Ci是找到第i個元素所需比較的次數, -
順序查找
原理是讓關鍵字與佇列中的數從最后一個開始逐個比較,直到找出與給定關鍵字相同的數為止,它的缺點是效率低下,時間復雜度o(n), -
折半查找
折半查找要求線性表是有序表,搜索程序從陣列的中間元素開始,如果中間元素正好是要查找的元素,則搜索程序結束;如果某一特定元素大于或者小于中間元素,則在陣列大于或小于中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較,如果在某一步驟陣列為空,則代表找不到,這種搜索演算法每一次比較都使搜索范圍縮小一半,折半搜索每次把搜索區域減少一半,時間復雜度為O(log n),
可以借助二叉判定樹求得折半查找的平均查找長度:log2(n+1)-1,
折半查找在失敗時所需比較的關鍵字個數不超過判定樹的深度,n個元素的判定樹的深度和n個元素的完全二叉樹的深度相同log2(n)+1,
public int binarySearchStandard(int[] num, int target){
int start = 0;
int end = num.length - 1;
while(start <= end){
//注意1
int mid = start + ((end - start) >> 1);
if(num[mid] == target)
return mid; else if(num[mid] > target){
end = mid - 1;
//注意2
} else{
start = mid + 1;
//注意3
}
}
return -1;
}
如果是start < end,那么當target等于num[num.length-1]時,會找不到該值,
因為num[mid] > target, 所以如果有num[index] == target, index一定小于mid,能不能寫成end = mid呢?舉例來說:num = {1, 2, 5, 7, 9}; 如果寫成end = mid,當回圈到start = 0, end = 0時(即num[start] = 1, num[end] = 1時),mid將永遠等于0,此時end也將永遠等于0,陷入死回圈,也就是說尋找target = -2時,程式將死回圈,
因為num[mid] < target, 所以如果有num[index] == target, index一定大于mid,能不能寫成start = mid呢?舉例來說:num = {1, 2, 5, 7, 9}; 如果寫成start = mid,當回圈到start = 3, end = 4時(即num[start] = 7, num[end] = 9時),mid將永遠等于3,此時start也將永遠等于3,陷入死回圈,也就是說尋找target = 9時,程式將死回圈,
4. 分塊查找
分塊查找又稱索引順序查找,它是一種性能介于順序查找和折半查找之間的查找方法,分塊查找由于只要求索引表是有序的,對塊內節點沒有排序要求,因此特別適合于節點動態變化的情況,
五. 排序演算法
- 常見排序演算法
穩定排序:
冒泡排序 — O(n2)
插入排序 — O(n2)
桶排序 — O(n); 需要 O(k) 額外空間
歸并排序 — O(nlogn); 需要 O(n) 額外空間
二叉排序樹排序 — O(n log n) 期望時間; O(n2)最壞時間; 需要 O(n) 額外空間
基數排序 — O(n·k); 需要 O(n) 額外空間
不穩定排序:
選擇排序 — O(n2)
希爾排序 — O(nlogn)
堆排序 — O(nlogn)
快速排序 — O(nlogn) 期望時間, O(n2) 最壞情況; 對于大的、亂數串行一般相信是最快的已知排序
2. 交換排序
冒泡排序
它重復地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來,走訪數列的作業是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成,冒泡排序總的平均時間復雜度為O(n^2),冒泡排序是一種穩定排序演算法, - 比較相鄰的元素,如果第一個比第二個大,就交換他們兩個, - 對每一對相鄰元素作同樣的作業,從開始第一對到結尾的最后一對,在這一點,最后的元素應該會是最大的數, - 針對所有的元素重復以上的步驟,除了最后一個, - 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較,
void bubble_sort(int a[], int n)
{
int i, j, temp;
for (j = 0; j < n - 1; j++)
for (i = 0; i < n - 1 - j; i++)
{
if(a[i] > a[i + 1])
{
temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
}
}
}
快速排序
快速排序是一種 不穩定 的排序演算法,平均時間復雜度為 O(nlogn),快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists), 步驟為:
從數列中挑出一個元素,稱為”基準”(pivot),
重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的后面(相同的數可以到任一邊),在這個磁區結束之后,該基準就處于數列的中間位置,這個稱為磁區(partition)操作,
遞回地(recursive)把小于基準值元素的子數列和大于基準值元素的子數列排序,
快排的時間花費主要在劃分上,所以 - 最壞情況:時間復雜度為O(n^2),因為最壞情況發生在每次劃分程序產生的兩個區間分別包含n-1個元素和1個元素的時候, - 最好情況:每次劃分選取的基準都是當前無序區的中值,如果每次劃分程序產生的區間大小都為n/2,則快速排序法運行就快得多了,
public void sort(int[] arr, int low, int high) {
int l = low;
int h = high;
int povit = arr[low];
while (l < h) {
while (l < h && arr[h] >= povit)
h--;
if (l < h) {
arr[l] = arr[h];
l++;
}
while (l < h && arr[l] <= povit)
l++;
if (l < h) {
arr[h] = arr[l];
h--;
}
}
arr[l] = povit;
System.out.print("l=" + (l + 1) + ";h=" + (h + 1) + ";povit=" + povit + "n");
System.out.println(Arrays.toString(arr));
if (l - 1 > low) sort(arr, low, l - 1);
if (h + 1 < high) sort(arr, h + 1, high);
}
快排的優化
當待排序序列的長度分割到一定大小后,使用插入排序,
快排函式在函式尾部有兩次遞回操作,我們可以對其使用尾遞回優化,優化后,可以縮減堆疊深度,由原來的O(n)縮減為O(logn),將會提高性能,
從左、中、右三個數中取中間值,
多執行緒面試題
1、多執行緒有什么用?
(1)發揮多核CPU的優勢
隨著工業的進步,現在的筆記本、臺式機乃至商用的應用服務器至少也都是雙核的,4核、8核甚至16核的也都不少見,如果是單執行緒的程式,那么在雙核CPU上就浪費了50%,在4核CPU上就浪費了75%,單核CPU上所謂的"多執行緒"那是假的多執行緒,同一時間處理器只會處理一段邏輯,只不過執行緒之間切換得比較快,看著像多個執行緒"同時"運行罷了,多核CPU上的多執行緒才是真正的多執行緒,它能讓你的多段邏輯同時作業,多執行緒,可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的,
(2)防止阻塞
從程式運行效率的角度來看,單核CPU不但不會發揮出多執行緒的優勢,反而會因為在單核CPU上運行多執行緒導致執行緒背景關系的切換,而降低程式整體的效率,但是單核CPU我們還是要應用多執行緒,就是為了防止阻塞,試想,如果單核CPU使用單執行緒,那么只要這個執行緒阻塞了,比方說遠程讀取某個資料吧,對端遲遲未回傳又沒有設定超時時間,那么你的整個程式在資料回傳回來之前就停止運行了,多執行緒可以防止這個問題,多條執行緒同時運行,哪怕一條執行緒的代碼執行讀取資料阻塞,也不會影響其它任務的執行,
(3)便于建模
這是另外一個沒有這么明顯的優點了,假設有一個大的任務A,單執行緒編程,那么就要考慮很多,建立整個程式模型比較麻煩,但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程式模型,并通過多執行緒分別運行這幾個任務,那就簡單很多了,
2、創建執行緒的方式
比較常見的一個問題了,一般就是兩種:
(1)繼承Thread類
(2)實作Runnable介面
至于哪個好,不用說肯定是后者好,因為實作介面的方式比繼承類的方式更靈活,也能減少程式之間的耦合度,面向介面編程也是設計模式6大原則的核心,
3、start()方法和run()方法的區別
只有呼叫了start()方法,才會表現出多執行緒的特性,不同執行緒的run()方法里面的代碼交替執行,如果只是呼叫run()方法,那么代碼還是同步執行的,必須等待一個執行緒的run()方法里面的代碼全部執行完畢之后,另外一個執行緒才可以執行其run()方法里面的代碼,
4、Runnable介面和Callable介面的區別
有點深的問題了,也看出一個Java程式員學習知識的廣度,
Runnable介面中的run()方法的回傳值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;Callable介面中的call()方法是有回傳值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果,
這其實是很有用的一個特性,因為多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已,而Callable+Future/FutureTask卻可以獲取多執行緒運行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務,真的是非常有用,
5、CyclicBarrier和CountDownLatch的區別
兩個看上去有點像的類,都在java.util.concurrent下,都可以用來表示代碼運行到某個點上,二者的區別在于:
(1)CyclicBarrier的某個執行緒運行到某個點上之后,該執行緒即停止運行,直到所有的執行緒都到達了這個點,所有執行緒才重新運行;CountDownLatch則不是,某執行緒運行到某個點上之后,只是給某個數值-1而已,該執行緒繼續運行
(2)CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務
(3)CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了
6、volatile關鍵字的作用
一個非常重要的問題,是每個學習、應用多執行緒的Java程式員都必須掌握的,理解volatile關鍵字的作用的前提是要理解Java記憶體模型,這里就不講Java記憶體模型了,可以參見第31點,volatile關鍵字的作用主要有兩個:
(1)多執行緒主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變數,保證了其在多執行緒之間的可見性,即每次讀取到volatile變數,一定是最新的資料
(2)代碼底層執行不像我們看到的高級語言----Java程式這么簡單,它的執行是Java代碼–>位元組碼–>根據位元組碼執行對應的C/C++代碼–>C/C++代碼被編譯成匯編語言–>和硬體電路互動,現實中,為了獲取更好的性能JVM可能會對指令進行重排序,多執行緒下可能會出現一些意想不到的問題,使用volatile則會對禁止語意重排序,當然這也一定程度上降低了代碼執行效率
從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger,
7、什么是執行緒安全
又是一個理論的問題,各式各樣的答案有很多,我給出一個個人認為解釋地最好的:如果你的代碼在多執行緒下執行和在單執行緒下執行永遠都能獲得一樣的結果,那么你的代碼就是執行緒安全的,
這個問題有值得一提的地方,就是執行緒安全也是有幾個級別的:
(1)不可變
像String、Integer、Long這些,都是final型別的類,任何一個執行緒都改變不了它們的值,要改變除非新創建一個,因此這些不可變物件不需要任何同步手段就可以直接在多執行緒環境下使用
(2)絕對執行緒安全
不管運行時環境如何,呼叫者都不需要額外的同步措施,要做到這一點通常需要付出許多額外的代價,Java中標注自己是執行緒安全的類,實際上絕大多數都不是執行緒安全的,不過絕對執行緒安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相對執行緒安全
相對執行緒安全也就是我們通常意義上所說的執行緒安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限于此,如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制,
(4)執行緒非安全
這個就沒什么好說的了,ArrayList、LinkedList、HashMap等都是執行緒非安全的類
8、Java中如何獲取到執行緒dump檔案
死回圈、死鎖、阻塞、頁面打開慢等問題,打執行緒dump是最好的解決問題的途徑,所謂執行緒dump也就是執行緒堆疊,獲取到執行緒堆疊有兩步:
(1)獲取到執行緒的pid,可以通過使用jps命令,在Linux環境下還可以使用ps -ef | grep java
(2)列印執行緒堆疊,可以通過使用jstack pid命令,在Linux環境下還可以使用kill -3 pid
另外提一點,Thread類提供了一個getStackTrace()方法也可以用于獲取執行緒堆疊,這是一個實體方法,因此此方法是和具體執行緒實體系結的,每次獲取獲取到的是具體某個執行緒當前運行的堆疊,
9、一個執行緒如果出現了運行時例外會怎么樣
如果這個例外沒有被捕獲的話,這個執行緒就停止執行了,另外重要的一點是:如果這個執行緒持有某個某個物件的監視器,那么這個物件監視器會被立即釋放
10、如何在兩個執行緒之間共享資料
通過在執行緒之間共享物件就可以了,然后通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞佇列BlockingQueue就是為執行緒之間共享資料而設計的
11、sleep方法和wait方法有什么區別
這個問題常問,sleep方法和wait方法都可以用來放棄CPU一定的時間,不同點在于如果執行緒持有某個物件的監視器,sleep方法不會放棄這個物件的監視器,wait方法會放棄這個物件的監視器
12、生產者消費者模型的作用是什么
這個問題很理論,但是很重要:
(1)通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的運行效率,這是生產者消費者模型最重要的作用
(2)解耦,這是生產者消費者模型附帶的作用,解耦意味著生產者和消費者之間的聯系少,聯系越少越可以獨自發展而不需要收到相互的制約
13、ThreadLocal有什么用
簡單說ThreadLocal就是一種以空間換時間的做法,在每個Thread里面維護了一個以開地址法實作的ThreadLocal.ThreadLocalMap,把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了
14、為什么wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫
這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖
15、wait()方法和notify()/notifyAll()方法在放棄物件監視器時有什么區別
wait()方法和notify()/notifyAll()方法在放棄物件監視器的時候的區別在于:wait()方法立即釋放物件監視器,notify()/notifyAll()方法則會等待執行緒剩余代碼執行完畢才會放棄物件監視器,
16、為什么要使用執行緒池
避免頻繁地創建和銷毀執行緒,達到執行緒物件的重用,另外,使用執行緒池還可以根據專案靈活地控制并發的數目,
17、怎么檢測一個執行緒是否持有物件監視器
我也是在網上看到一道多執行緒面試題才知道有方法可以判斷某個執行緒是否持有物件監視器:Thread類提供了一個holdsLock(Object obj)方法,當且僅當物件obj的監視器被某條執行緒持有的時候才會回傳true,注意這是一個static方法,這意味著"某條執行緒"指的是當前執行緒,
18、synchronized和ReentrantLock的區別
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別,既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴展性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設定,可以讓等待鎖的執行緒回應中斷,這樣就避免了死鎖,使用synchronized只會讓等待的執行緒一直等待下去,不能回應中斷
(2)ReentrantLock可以獲取各種鎖的資訊
(3)ReentrantLock可以靈活地實作多路通知,提高多個執行緒進行讀操作的效率
(4)synchronized發生例外時,會自動釋放執行緒占用的鎖,故不會發生死鎖現象,Lock發生例外,若沒有主動釋放,極有可能造成死鎖,故需要在finally中呼叫unLock方法釋放鎖;
(5)Lock是一個介面,synchronized是Java中的關鍵字,synchronized是內置的語言實作
19、ConcurrentHashMap的并發度是什么
ConcurrentHashMap的并發度就是segment的大小,默認為16,這意味著最多同時可以有16條執行緒操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優勢,任何情況下,Hashtable能同時有兩條執行緒獲取Hashtable中的資料嗎?
20、ReadWriteLock是什么
首先明確一下,不是說ReentrantLock不好,只是ReentrantLock某些時候有局限,如果使用ReentrantLock,可能本身是為了防止執行緒A在寫資料、執行緒B在讀資料造成的資料不一致,但這樣,如果執行緒C在讀資料、執行緒D也在讀資料,讀資料是不會改變資料的,沒有必要加鎖,但是還是加鎖了,降低了程式的性能,
因為這個,才誕生了讀寫鎖ReadWriteLock,ReadWriteLock是一個讀寫鎖介面,ReentrantReadWriteLock是ReadWriteLock介面的一個具體實作,實作了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能,
21、FutureTask是什么
FutureTask表示一個異步運算的任務,FutureTask里面可以傳入一個Callable的具體實作類,可以對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作,當然,由于FutureTask也是Runnable介面的實作類,所以FutureTask也可以放入執行緒池中,
22、Linux環境下如何查找哪個執行緒使用CPU最長
(1)獲取專案的pid,jps或者ps -ef | grep java
(2)top -H -p pid,順序不能改變
這樣就可以列印出當前的專案,每條執行緒占用CPU時間的百分比,注意這里打出的是LWP,也就是作業系統原生執行緒的執行緒號,
使用"top -H -p pid"+"jps pid"可以很容易地找到某條占用CPU高的執行緒的執行緒堆疊,從而定位占用CPU高的原因,一般是因為不當的代碼操作導致了死回圈,
最后提一點,"top -H -p pid"打出來的LWP是十進制的,"jps pid"打出來的本地執行緒號是十六進制的,轉換一下,就能定位到占用CPU高的執行緒的當前執行緒堆疊了,
23、Java編程寫一個會導致死鎖的程式
很多人都知道死鎖是怎么一回事兒:執行緒A和執行緒B相互等待對方持有的鎖導致程式無限死回圈下去,
真正理解什么是死鎖,這個問題其實不難,幾個步驟:
(1)兩個執行緒里面分別持有兩個Object物件:lock1和lock2,這兩個lock作為同步代碼塊的鎖;
(2)執行緒1的run()方法中同步代碼塊先獲取lock1的物件鎖,Thread.sleep(xxx),時間不需要太多,50毫秒差不多了,然后接著獲取lock2的物件鎖,這么做主要是為了防止執行緒1啟動一下子就連續獲得了lock1和lock2兩個物件的物件鎖
(3)執行緒2的run)(方法中同步代碼塊先獲取lock2的物件鎖,接著獲取lock1的物件鎖,當然這時lock1的物件鎖已經被執行緒1鎖持有,執行緒2肯定是要等待執行緒1釋放lock1的物件鎖的
24、什么是CAS
CAS,全稱為Compare and Swap,即比較-替換,假設有三個運算元:記憶體值V、舊的預期值A、要修改的值B,當且僅當預期值A和記憶體值V相同時,才會將記憶體值修改為B并回傳true,否則什么都不做并回傳false,當然CAS一定要volatile變數配合,這樣才能保證每次拿到的變數是主記憶體中最新的那個值,否則舊的預期值A對某條執行緒來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功,
25、什么是樂觀鎖和悲觀鎖
(1)樂觀鎖:就像它的名字一樣,對于并發間操作產生的執行緒安全問題持樂觀狀態,樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生沖突,那么就應該有相應的重試邏輯,
(2)悲觀鎖:還是像它的名字一樣,對于并發間操作產生的執行緒安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了,
26、什么是AQS
簡單說一下AQS,AQS全稱為AbstractQueuedSychronizer,翻譯過來應該是抽象佇列同步器,
如果說java.util.concurrent的基礎是CAS的話,那么AQS就是整個Java并發包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它,AQS實際上以雙向佇列的形式連接所有的Entry,比方說ReentrantLock,所有等待的執行緒都被放在一個Entry中并連成雙向佇列,前面一個執行緒使用ReentrantLock好了,則雙向佇列實際上的第一個Entry開始運行,
AQS定義了對雙向佇列所有的操作,而只開放了tryLock和tryRelease方法給開發者使用,開發者可以根據自己的實作重寫tryLock和tryRelease方法,以實作自己的并發功能,
27、單例模式的執行緒安全性
首先要說的是單例模式的執行緒安全意味著:某個類的實體在多執行緒環境下只會被創建一次出來,單例模式有很多種的寫法,我總結一下:
(1)餓漢式單例模式的寫法:執行緒安全
(2)懶漢式單例模式的寫法:非執行緒安全
(3)雙檢鎖單例模式的寫法:執行緒安全
28、Semaphore有什么作用
Semaphore就是一個信號量,它的作用是限制某段代碼塊的并發數,Semaphore有一個建構式,可以傳入一個int型整數n,表示某段代碼最多只有n個執行緒可以訪問,如果超出了n,那么請等待,等到某個執行緒執行完畢這段代碼塊,下一個執行緒再進入,由此可以看出如果Semaphore建構式中傳入的int型整數n=1,相當于變成了一個synchronized了,
29、Hashtable的size()方法中明明只有一條陳述句"return count",為什么還要做同步?
這是我之前的一個困惑,不知道大家有沒有想過這個問題,某個方法中如果有多條陳述句,并且都在操作同一個類變數,那么在多執行緒環境下不加鎖,勢必會引發執行緒安全問題,這很好理解,但是size()方法明明只有一條陳述句,為什么還要加鎖?
關于這個問題,在慢慢地作業、學習中,有了理解,主要原因有兩點:
(1)同一時間只能有一條執行緒執行固定類的同步方法,但是對于類的非同步方法,可以多條執行緒同時訪問,所以,這樣就有問題了,可能執行緒A在執行Hashtable的put方法添加資料,執行緒B則可以正常呼叫size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能執行緒A添加了完了資料,但是沒有對size++,執行緒B就已經讀取size了,那么對于執行緒B來說讀取到的size一定是不準確的,而給size()方法加了同步之后,意味著執行緒B呼叫size()方法只有在執行緒A呼叫put方法完畢之后才可以呼叫,這樣就保證了執行緒安全性
(2)CPU執行代碼,執行的不是Java代碼,這點很關鍵,一定得記住,Java代碼最終是被翻譯成機器碼執行的,機器碼才是真正可以和硬體電路互動的代碼,即使你看到Java代碼只有一行,甚至你看到Java代碼編譯之后生成的位元組碼也只有一行,也不意味著對于底層來說這句陳述句的操作只有一個,一句"return count"假設被翻譯成了三句匯編陳述句執行,一句匯編陳述句和其機器碼做對應,完全可能執行完第一句,執行緒就切換了,
30、執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
這是一個非常刁鉆和狡猾的問題,請記住:執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法里面的代碼才是被執行緒自身所呼叫的,
如果說上面的說法讓你感到困惑,那么我舉個例子,假設Thread2中new了Thread1,main函式中new了Thread2,那么:
(1)Thread2的構造方法、靜態塊是main執行緒呼叫的,Thread2的run()方法是Thread2自己呼叫的
(2)Thread1的構造方法、靜態塊是Thread2呼叫的,Thread1的run()方法是Thread1自己呼叫的
31、同步方法和同步塊,哪個是更好的選擇
同步塊,這意味著同步塊之外的代碼是異步執行的,這比同步整個方法更提升代碼的效率,請知道一條原則:同步的范圍越小越好,
借著這一條,我額外提一點,雖說同步的范圍越少越好,但是在Java虛擬機中還是存在著一種叫做鎖粗化的優化方法,這種方法就是把同步范圍變大,這是有用的,比方說StringBuffer,它是一個執行緒安全的類,自然最常用的append()方法是一個同步方法,我們寫代碼的時候會反復append字串,這意味著要進行反復的加鎖->解鎖,這對性能不利,因為這意味著Java虛擬機在這條執行緒上要反復地在內核態和用戶態之間進行切換,因此Java虛擬機會將多次append方法呼叫的代碼進行一個鎖粗化的操作,將多次的append的操作擴展到append方法的頭尾,變成一個大的同步塊,這樣就減少了加鎖–>解鎖的次數,有效地提升了代碼執行的效率,
32、高并發、任務執行時間短的業務怎樣使用執行緒池?并發不高、任務執行時間長的業務怎樣使用執行緒池?并發高、業務執行時間長的業務怎樣使用執行緒池?
這是我在并發編程網上看到的一個問題,把這個問題放在最后一個,希望每個人都能看到并且思考一下,因為這個問題非常好、非常實際、非常專業,關于這個問題,個人看法是:
(1)高并發、任務執行時間短的業務,執行緒池執行緒數可以設定為CPU核數+1,減少執行緒背景關系的切換
(2)并發不高、任務執行時間長的業務要區分開看:
a)假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大執行緒池中的執行緒數目,讓CPU處理更多的業務
b)假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)一樣吧,執行緒池中的執行緒數設定得少一些,減少執行緒背景關系的切換
(3)并發高、業務執行時間長,解決這種型別任務的關鍵不在于執行緒池而在于整體架構的設計,看看這些業務里面某些資料是否能做快取是第一步,增加服務器是第二步,至于執行緒池的設定,設定參考(2),最后,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦
微服務面試題
Spring Cloud
1、什么是 Spring Cloud?
Spring cloud 流應用程式啟動器是基于 Spring Boot 的 Spring 集成應用程式,提供與外部系統的集成,Spring cloud Task,一個生命周期短暫的微服務框架,用于快速構建執行有限資料處理的應用程式,
2、使用 Spring Cloud 有什么優勢?
使用 Spring Boot 開發分布式微服務時,我們面臨以下問題
(1)與分布式系統相關的復雜性-這種開銷包括網路問題,延遲開銷,帶寬問題,安全問題,
(2)服務發現-服務發現工具管理群集中的流程和服務如何查找和互相交談,它涉及一個服務目錄,在該目錄中注冊服務,然后能夠查找并連接到該目錄中的服務,
(3)冗余-分布式系統中的冗余問題,
(4)負載平衡 --負載平衡改善跨多個計算資源的作業負荷,諸如計算機,計算機集群,網路鏈路,中央處理單元,或磁盤驅動器的分布,
(5)性能-問題 由于各種運營開銷導致的性能問題,
(6)部署復雜性-Devops 技能的要求,
3、服務注冊和發現是什么意思?Spring Cloud 如何實作?
當我們開始一個專案時,我們通常在屬性檔案中進行所有的配置,隨著越來越多的服務開發和部署,添加和修改這些屬性變得更加復雜,有些服務可能會下降,而某些位置可能會發生變化,手動更改屬性可能會產生問題, Eureka 服務注冊和發現可以在這種情況下提供幫助,由于所有服務都在 Eureka 服務器上注冊并通過呼叫 Eureka 服務器完成查找,因此無需處理服務地點的任何更改和處理,
4、Spring Cloud 和dubbo區別?
(1)服務呼叫方式 dubbo是RPC springcloud Rest Api
(2)注冊中心,dubbo 是zookeeper springcloud是eureka,也可以是zookeeper
(3)服務網關,dubbo本身沒有實作,只能通過其他第三方技術整合,springcloud有Zuul路由網關,作為路由服務器,進行消費者的請求分發,springcloud支持斷路器,與git完美集成組態檔支持版本控制,事物總線實作組態檔的更新與服務自動裝配等等一系列的微服務架構要素,
5、SpringBoot和SpringCloud的區別?
SpringBoot專注于快速方便的開發單個個體微服務,
SpringCloud是關注全域的微服務協調整理治理框架,它將SpringBoot開發的一個個單體微服務整合并管理起來,
為各個微服務之間提供,配置管理、服務發現、斷路器、路由、微代理、事件總線、全域鎖、決策競選、分布式會話等等集成服務
SpringBoot可以離開SpringCloud獨立使用開發專案, 但是SpringCloud離不開SpringBoot ,屬于依賴的關系.
SpringBoot專注于快速、方便的開發單個微服務個體,SpringCloud關注全域的服務治理框架,
6、負載平衡的意義什么?
在計算中,負載平衡可以改善跨計算機,計算機集群,網路鏈接,中央處理單元或磁盤驅動器等多種計算資源的作業負載分布,負載平衡旨在優化資源使用,最大化吞吐量,最小化回應時間并避免任何單一資源的過載,使用多個組件進行負載平衡而不是單個組件可能會通過冗余來提高可靠性和可用性,負載平衡通常涉及專用軟體或硬體,例如多層交換機或域名系統服務器行程,
Spring Boot
1、什么是 Spring Boot?
多年來,隨著新功能的增加,spring 變得越來越復雜,訪問spring官網頁面,我們就會看到可以在我們的應用程式中使用的所有 Spring 專案的不同功能,如果必須啟動一個新的 Spring 專案,我們必須添加構建路徑或添加 Maven 依賴關系,配置應用程式服務器,添加 spring 配置,因此,開始一個新的 spring 專案需要很多努力,因為我們現在必須從頭開始做所有事情,
Spring Boot 是解決這個問題的方法,Spring Boot 已經建立在現有 spring 框架之上,使用 spring 啟動,我們避免了之前我們必須做的所有樣板代碼和配置,因此,Spring Boot 可以幫助我們以最少的作業量,更加健壯地使用現有的 Spring功能,
2、Spring Boot 有哪些優點?
Spring Boot 的優點有:
1、減少開發,測驗時間和努力,
2、使用 JavaConfig 有助于避免使用 XML,
3、避免大量的 Maven 匯入和各種版本沖突,
4、提供意見發展方法,
5、通過提供默認值快速開始開發,
6、沒有單獨的 Web 服務器需要,這意味著你不再需要啟動 Tomcat,Glassfish或其他任何東西,
7、需要更少的配置 因為沒有 web.xml 檔案,只需添加用@ Configuration 注釋的類,然后添加用@Bean 注釋的方法,Spring 將自動加載物件并像以前一樣對其進行管理,您甚至可以將@Autowired 添加到 bean 方法中,以使 Spring 自動裝入需要的依賴關系中,
8、基于環境的配置 使用這些屬性,您可以將您正在使用的環境傳遞到應用程式:-Dspring.profiles.active = {enviornment},在加載主應用程式屬性檔案后,Spring 將在(application{environment} .properties)中加載后續的應用程式屬性檔案,
3、什么是 JavaConfig?
Spring JavaConfig 是 Spring 社區的產品,它提供了配置 Spring IoC 容器的純Java 方法,因此它有助于避免使用 XML 配置,使用 JavaConfig 的優點在于:
(1)面向物件的配置,由于配置被定義為 JavaConfig 中的類,因此用戶可以充分利用 Java 中的面向物件功能,一個配置類可以繼承另一個,重寫它的@Bean 方法等,
(2)減少或消除 XML 配置,基于依賴注入原則的外化配置的好處已被證明,但是,許多開發人員不希望在 XML 和 Java 之間來回切換,JavaConfig 為開發人員提供了一種純 Java 方法來配置與 XML 配置概念相似的 Spring 容器,從技術角度來講,只使用 JavaConfig 配置類來配置容器是可行的,但實際上很多人認為將JavaConfig 與 XML 混合匹配是理想的,
(3)型別安全和重構友好,JavaConfig 提供了一種型別安全的方法來配置 Spring容器,由于 Java 5.0 對泛型的支持,現在可以按型別而不是按名稱檢索 bean,不需要任何強制轉換或基于字串的查找,
4、如何重新加載 Spring Boot 上的更改,而無需重新啟動服務器?
這可以使用 DEV 工具來實作,通過這種依賴關系,您可以節省任何更改,嵌入式tomcat 將重新啟動,Spring Boot 有一個開發工具(DevTools)模塊,它有助于提高開發人員的生產力,Java 開發人員面臨的一個主要挑戰是將檔案更改自動部署到服務器并自動重啟服務器,開發人員可以重新加載 Spring Boot 上的更改,而無需重新啟動服務器,這將消除每次手動部署更改的需要,Spring Boot 在發布它的第一個版本時沒有這個功能,這是開發人員最需要的功能,DevTools 模塊完全滿足開發人員的需求,該模塊將在生產環境中被禁用,它還提供 H2 資料庫控制臺以更好地測驗應用程式,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
5、Spring Boot 中的監視器是什么?
Spring boot actuator 是 spring 啟動框架中的重要功能之一,Spring boot 監視器可幫助您訪問生產環境中正在運行的應用程式的當前狀態,有幾個指標必須在生產環境中進行檢查和監控,即使一些外部應用程式可能正在使用這些服務來向相關人員觸發警報訊息,監視器模塊公開了一組可直接作為 HTTP URL 訪問的REST 端點來檢查狀態,
6、如何在 Spring Boot 中禁用 Actuator 端點安全性?
默認情況下,所有敏感的 HTTP 端點都是安全的,只有具有 ACTUATOR 角色的用戶才能訪問它們,安全性是使用標準的 HttpServletRequest.isUserInRole 方法實施的, 我們可以使用來禁用安全性,只有在執行機構端點在防火墻后訪問時,才建議禁用安全性,
7、如何在自定義埠上運行 Spring Boot 應用程式?
為了在自定義埠上運行 Spring Boot 應用程式,您可以在application.properties 中指定埠,server.port = 8090
8、什么是 YAML?
YAML 是一種人類可讀的資料序列化語言,它通常用于組態檔,與屬性檔案相比,如果我們想要在組態檔中添加復雜的屬性,YAML 檔案就更加結構化,而且更少混淆,可以看出 YAML 具有分層配置資料,
9、如何實作 Spring Boot 應用程式的安全性?
為了實作 Spring Boot 的安全性,我們使用 spring-boot-starter-security 依賴項,并且必須添加安全配置,它只需要很少的代碼,配置類將必須擴展WebSecurityConfigurerAdapter 并覆寫其方法,
10、如何集成 Spring Boot 和 ActiveMQ?
對于集成 Spring Boot 和 ActiveMQ,我們使用依賴關系, 它只需要很少的配置,并且不需要樣板代碼,
11、如何使用 Spring Boot 實作分頁和排序?
使用 Spring Boot 實作分頁非常簡單,使用 Spring Data-JPA 可以實作將可分頁的傳遞給存盤庫方法,
12、什么是 Swagger?你用 Spring Boot 實作了它嗎?
Swagger 廣泛用于可視化 API,使用 Swagger UI 為前端開發人員提供在線沙箱,Swagger 是用于生成 RESTful Web 服務的可視化表示的工具,規范和完整框架實作,它使檔案能夠以與服務器相同的速度更新,當通過 Swagger 正確定義時,消費者可以使用最少量的實作邏輯來理解遠程服務并與其進行互動,因此,Swagger消除了呼叫服務時的猜測,
13、什么是 Spring Profiles?
Spring Profiles 允許用戶根據組態檔(dev,test,prod 等)來注冊 bean,因此,當應用程式在開發中運行時,只有某些 bean 可以加載,而在 PRODUCTION中,某些其他 bean 可以加載,假設我們的要求是 Swagger 檔案僅適用于 QA 環境,并且禁用所有其他檔案,這可以使用組態檔來完成,Spring Boot 使得使用組態檔非常簡單,
14、什么是 Spring Batch?
Spring Boot Batch 提供可重用的函式,這些函式在處理大量記錄時非常重要,包括日志/跟蹤,事務管理,作業處理統計資訊,作業重新啟動,跳過和資源管理,它還提供了更先進的技術服務和功能,通過優化和磁區技術,可以實作極高批量和高性能批處理作業,簡單以及復雜的大批量批處理作業可以高度可擴展的方式利用框架處理重要大量的資訊,
15、什么是 FreeMarker 模板?
FreeMarker 是一個基于 Java 的模板引擎,最初專注于使用 MVC 軟體架構進行動態網頁生成,使用 Freemarker 的主要優點是表示層和業務層的完全分離,程式員可以處理應用程式代碼,而設計人員可以處理 html 頁面設計,最后使用freemarker 可以將這些結合起來,給出最終的輸出頁面,
16、如何使用 Spring Boot 實作例外處理?
Spring 提供了一種使用 ControllerAdvice 處理例外的非常有用的方法, 我們通過實作一個 ControlerAdvice 類,來處理控制器類拋出的所有例外,
17、您使用了哪些 starter maven 依賴項?
使用了下面的一些依賴項
spring-boot-starter-activemq
spring-boot-starter-security
這有助于增加更少的依賴關系,并減少版本的沖突,
18、什么是 CSRF 攻擊?
CSRF 代表跨站請求偽造,這是一種攻擊,迫使最終用戶在當前通過身份驗證的Web 應用程式上執行不需要的操作,CSRF 攻擊專門針對狀態改變請求,而不是資料竊取,因為攻擊者無法查看對偽造請求的回應,
19、什么是 WebSockets?
WebSocket 是一種計算機通信協議,通過單個 TCP 連接提供全雙工通信信道,
1、WebSocket 是雙向的 -使用 WebSocket 客戶端或服務器可以發起訊息發送,
2、WebSocket 是全雙工的 -客戶端和服務器通信是相互獨立的,
3、單個 TCP 連接 -初始連接使用 HTTP,然后將此連接升級到基于套接字的連接,然后這個單一連接用于所有未來的通信
4、Light -與 http 相比,WebSocket 訊息資料交換要輕得多,
20、什么是 AOP?
在軟體開發程序中,跨越應用程式多個點的功能稱為交叉問題,這些交叉問題與應用程式的主要業務邏輯不同,因此,將這些橫切關注與業務邏輯分開是面向方面編程(AOP)的地方,
21、什么是 Apache Kafka?
Apache Kafka 是一個分布式發布 - 訂閱訊息系統,它是一個可擴展的,容錯的發布 - 訂閱訊息系統,它使我們能夠構建分布式應用程式,這是一個 Apache 頂級專案,Kafka 適合離線和在線訊息消費,
22、我們如何監視所有 Spring Boot 微服務?
Spring Boot 提供監視器端點以監控各個微服務的度量,這些端點對于獲取有關應用程式的資訊(如它們是否已啟動)以及它們的組件(如資料庫等)是否正常運行很有幫助,但是,使用監視器的一個主要缺點或困難是,我們必須單獨打開應用程式的知識點以了解其狀態或健康狀況,想象一下涉及 50 個應用程式的微服務,管理員將不得不擊中所有 50 個應用程式的執行終端,為了幫助我們處理這種情況,我們將使用位于的開源專案, 它建立在 Spring Boot Actuator 之上,它提供了一個 Web UI,使我們能夠可視化多個應用程式的度量,
最后
看完有識訓?那么希望老鐵別吝嗇你的三連擊哦
點贊,收藏 、轉發可以讓更多的人看到這篇文章
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/125820.html
標籤:其他
下一篇:HTML總結
