<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
短時間提升自己最快的手段就是背面試題,最近總結了Java常用的面試題,分享給大家,希望大家都能圓夢大廠,加油,我命由我不由天。
concurrentHashMap融合了hashmap和hashtable的優勢,hashmap是不同步的,但是單執行緒情況下效率高,hashtable是同步的同步情況下保證程式執行的正確性。
但hashtable每次同步執行的時候都要鎖住整個結構,如下圖:
concurrentHashMap鎖的方式是細粒度的。
concurrentHashMap將hash分為16個桶(預設值),諸如get、put、remove等常用操作只鎖住當前需要用到的桶。
concurrentHashMap的讀取並行,因為讀取的大多數時候都沒有鎖定,所以讀取操作幾乎是完全的並行操作,只是在求size時才需要鎖定整個hash。
而且在迭代時,concurrentHashMap使用了不同於傳統集合的快速失敗迭代器的另一種迭代方式,弱一致迭代器。
在這種方式中,當iterator被建立後集合再發生改變就不會丟擲ConcurrentModificationException,取而代之的是在改變時new新的資料而不是影響原來的資料,iterator完成後再講頭指標替代為新的資料,這樣iterator時使用的是原來的資料。
(1)先了解一下HashCode
Java中的集合有兩類,一類是List,一類是Set。
List:元素有序,可以重複;
Set:元素無序,不可重複;
要想保證元素的不重複,拿什麼來判斷呢?這就是Object.equals方法了。
如果元素有很多,增加一個元素,就要判斷n次嗎?
顯然不現實,於是,Java採用了雜湊表的原理。
雜湊演演算法也稱為雜湊演演算法,是將資料依特定演演算法直接指定到一根地址上,初學者可以簡單的理解為,HashCode方法返回的就是物件儲存的物理位置(實際上並不是)。
這樣一來,當集合新增新的元素時,先呼叫這個元素的hashcode()方法,就一下子能定位到他應該放置的物理位置上。
如果這個位置上沒有元素,他就可以直接儲存在這個位置上,不用再進行任何比較了。
如果這個位置上有元素,就呼叫它的equals方法與新元素進行比較,想同的話就不存了,不相同就雜湊其它的地址。
所以這裡存在一個衝突解決的問題。
這樣一來實際上呼叫equals方法的次數就大大降低了,幾乎只需要一兩次。
簡而言之,在集合查詢時,hashcode能大大降低物件比較次數,提高查詢效率。
Java物件的equals方法和hashCode方法時這樣規定的:
相等的物件就必須具有相等的hashcode。
如果兩個Java物件A和B,A和B不相等,但是A和B的雜湊碼相等,將A和B都存入HashMap時會發生雜湊衝突,也就是A和B存放在HashMap內部陣列的位置索引相同
這時HashMap會在該位置建立一個連結表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用規則,是允許的。
當然,雜湊衝突越少越好,儘量採用好的雜湊演演算法避免雜湊衝突。
equals()相等的兩個物件,hashcode()一定相等;equals()不相等的兩個物件,卻並不能證明他們的hashcode()不相等。
(2)HashMap和HashSet的區別
ReadWriteLock包括兩種子鎖
(1)ReadWriteLock
ReadWriteLock 可以實現多個讀鎖同時進行,但是讀與寫和寫於寫互斥,只能有一個寫鎖執行緒在進行。
(2)StampedLock
StampedLock是Jdk在1.8提供的一種讀寫鎖,相比較ReentrantReadWriteLock效能更好
因為ReentrantReadWriteLock在讀寫之間是互斥的,使用的是一種悲觀策略,在讀執行緒特別多的情況下,會造成寫執行緒處於飢餓狀態
雖然可以在初始化的時候設定為true指定為公平,但是吞吐量又下去了,而StampedLock是提供了一種樂觀策略,更好的實現讀寫分離,並且吞吐量不會下降。
StampedLock包括三種鎖:
(1)寫鎖writeLock:
writeLock是一個獨佔鎖寫鎖,當一個執行緒獲得該鎖後,其他請求讀鎖或者寫鎖的執行緒阻塞, 獲取成功後,會返回一個stamp(憑據)變數來表示該鎖的版本,在釋放鎖時呼叫unlockWrite方法傳遞stamp引數。
提供了非阻塞式獲取鎖tryWriteLock。
(2)悲觀讀鎖readLock:
readLock是一個共用讀鎖,在沒有執行緒獲取寫鎖情況下,多個執行緒可以獲取該鎖。
如果有寫鎖獲取,那麼其他執行緒請求讀鎖會被阻塞。
悲觀讀鎖會認為其他執行緒可能要對自己操作的資料進行修改,所以需要先對資料進行加鎖,這是在讀少寫多的情況下考慮的。
請求該鎖成功後會返回一個stamp值,在釋放鎖時呼叫unlockRead方法傳遞stamp引數。
提供了非阻塞式獲取鎖方法tryWriteLock。
(3)樂觀讀鎖tryOptimisticRead:
tryOptimisticRead相對比悲觀讀鎖,在運算元據前並沒有通過CAS設定鎖的狀態,如果沒有執行緒獲取寫鎖,則返回一個非0的stamp變數,獲取該stamp後在運算元據前還需要呼叫validate方法來判斷期間是否有執行緒獲取了寫鎖
如果是返回值為0則有執行緒獲取寫鎖,如果不是0則可以使用stamp變數的鎖來運算元據。
由於tryOptimisticRead並沒有修改鎖狀態,所以不需要釋放鎖。
這是讀多寫少的情況下考慮的,不涉及CAS操作,所以效率較高,在保證資料一致性上需要複製一份要操作的變數到方法棧中,並且在運算元據時可能其他寫執行緒已經修改了資料
而我們操作的是方法棧裡面的資料,也就是一個快照,所以最多返回的不是最新的資料,但是一致性得到了保證。
每個執行緒都是通過某個特定Thread物件所對應的方法run()來完成其操作的,run()方法稱為執行緒體。
通過呼叫Thread類的start()方法來啟動一個執行緒。
start() 方法用於啟動執行緒,run() 方法用於執行執行緒的執行時程式碼。
run() 可以重複呼叫,而 start() 只能呼叫一次。
start()方法來啟動一個執行緒,真正實現了多執行緒執行。
呼叫start()方法無需等待run方法體程式碼執行完畢,可以直接繼續執行其他的程式碼; 此時執行緒是處於就緒狀態,並沒有執行。
然後通過此Thread類呼叫方法run()來完成其執行狀態, run()方法執行結束, 此執行緒終止。然後CPU再排程其它執行緒。
run()方法是在本執行緒裡的,只是執行緒裡的一個函數,而不是多執行緒的。
如果直接呼叫run(),其實就相當於是呼叫了一個普通函數而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的程式碼
所以執行路徑還是隻有一條,根本就沒有執行緒的特徵,所以在多執行緒執行時要使用start()方法而不是run()方法。
這是另一個非常經典的 java 多執行緒面試問題,而且在面試中會經常被問到。很簡單,但是很多人都會答不上來!
new 一個 Thread,執行緒進入了新建狀態。
呼叫 start() 方法,會啟動一個執行緒並使執行緒進入了就緒狀態,當分配到時間片後就可以開始執行了。
start() 會執行執行緒的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多執行緒工作。
而直接執行 run() 方法,會把 run 方法當成一個 main 執行緒下的普通方法去執行,並不會在某個執行緒中執行它,所以這並不是多執行緒工作。
總結: 呼叫 start 方法方可啟動執行緒並使執行緒進入就緒狀態,而 run 方法只是 thread 的一個普通方法呼叫,還是在主執行緒裡執行。
(1)可重入性
synchronized的鎖物件中有一個計數器(recursions變數)會記錄執行緒獲得幾次鎖;
synchronized是可重入鎖,每部鎖物件會有一個計數器記錄執行緒獲取幾次鎖,在執行完同步程式碼塊時,計數器的數量會-1,直到計數器的數量為0,就釋放這個鎖。
(2)不可中斷性
(1)自旋鎖
線上程進行阻塞的時候,先讓執行緒自旋等待一段時間,可能這段時間其它執行緒已經解鎖,這時就無需讓執行緒再進行阻塞操作了。
自旋預設次數是10次。
(2)自適應自旋鎖
自旋鎖的升級,自旋的次數不再固定,由前一次自旋次數和鎖的擁有者的狀態決定。
(3)鎖消除
在動態編譯同步程式碼塊的時候,JIT編譯器藉助逃逸分析技術來判斷鎖物件是否只被一個執行緒存取,而沒有其他執行緒,這時就可以取消鎖了。
(4)鎖粗化
當JIT編譯器發現一系列的操作都對同一個物件反覆加鎖解鎖,甚至加鎖操作出現在迴圈中,此時會將加鎖同步的範圍粗化到整個操作系列的外部。
鎖粒度:不要鎖住一些無關的程式碼。
鎖粗化:可以一次性執行完的不要多次加鎖執行。
Java中,任何物件都可以作為鎖,並且 wait(),notify()等方法用於等待物件的鎖或者喚醒執行緒,在 Java 的執行緒中並沒有可供任何物件使用的鎖,所以任意物件呼叫方法一定定義在Object類中。
wait(), notify()和 notifyAll()這些方法在同步程式碼塊中呼叫
有的人會說,既然是執行緒放棄物件鎖,那也可以把wait()定義在Thread類裡面啊,新定義的執行緒繼承於Thread類,也不需要重新定義wait()方法的實現。
然而,這樣做有一個非常大的問題,一個執行緒完全可以持有很多鎖,你一個執行緒放棄鎖的時候,到底要放棄哪個鎖?
當然了,這種設計並不是不能實現,只是管理起來更加複雜。
綜上所述,wait()、notify()和notifyAll()方法要定義在Object類中。
可以通過中斷 和 共用變數的方式實現執行緒間的通訊和共同作業
比如說最經典的生產者-消費者模型:當佇列滿時,生產者需要等待佇列有空間才能繼續往裡面放入商品,而在等待的期間內,生產者必須釋放對臨界資源(即佇列)的佔用權。
因為生產者如果不釋放對臨界資源的佔用權,那麼消費者就無法消費佇列中的商品,就不會讓佇列有空間,那麼生產者就會一直無限等待下去。
因此,一般情況下,當佇列滿時,會讓生產者交出對臨界資源的佔用權,並進入掛起狀態。
然後等待消費者消費了商品,然後消費者通知生產者佇列有空間了。
同樣地,當佇列空時,消費者也必須等待,等待生產者通知它佇列中有商品了。
這種互相通訊的過程就是執行緒間的共同作業。
Java中執行緒通訊共同作業的最常見的兩種方式:
1、syncrhoized加鎖的執行緒的Object類的wait()/notify()/notifyAll()
2、ReentrantLock類加鎖的執行緒的Condition類的await()/signal()/signalAll()
執行緒間直接的資料交換:
通過管道進行執行緒間通訊:
1)位元組流;
2)字元流
yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。
因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。
但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。
當鎖被釋放後,任何一個執行緒都有機會競爭得到鎖,這樣做的目的是提高效率,但缺點是可能產生執行緒飢餓現象。
64、請談談 volatile 有什麼特點,為什麼它能保證變數對所有執行緒的可見性?
volatile只能作用於變數,保證了操作可見性和有序性,不保證原子性。
在Java的記憶體模型中分為主記憶體和工作記憶體,Java記憶體模型規定所有的變數儲存在主記憶體中,每條執行緒都有自己的工作記憶體。
主記憶體和工作記憶體之間的互動分為8個原子操作:
volatile修飾的變數,只有對volatile進行assign操作,才可以load,只有load才可以use,,這樣就保證了在工作記憶體操作volatile變數,都會同步到主記憶體中。
Synchronized的並行策略是悲觀的,不管是否產生競爭,任何資料的操作都必須加鎖。
樂觀鎖的核心是CAS,CAS包括記憶體值、預期值、新值,只有當記憶體值等於預期值時,才會將記憶體值修改為新值。
樂觀鎖認為對一個物件的操作不會引發衝突,所以每次操作都不進行加鎖,只是在最後提交更改時驗證是否發生衝突,如果衝突則再試一遍,直至成功為止,這個嘗試的過程稱為自旋。
樂觀鎖沒有加鎖,但樂觀鎖引入了ABA問題,此時一般採用版本號進行控制;
也可能產生自旋次數過多問題,此時並不能提高效率,反而不如直接加鎖的效率高;
只能保證一個物件的原子性,可以封裝成物件,再進行CAS操作;
(1)相似點
它們都是阻塞式的同步,也就是說一個執行緒獲得了物件鎖,進入程式碼塊,其它存取該同步塊的執行緒都必須阻塞在同步程式碼塊外面等待,而進行執行緒阻塞和喚醒的程式碼是比較高的。
(2)功能區別
Synchronized是java語言的關鍵字,是原生語法層面的互斥,需要JVM實現;ReentrantLock 是JDK1.5之後提供的API層面的互斥鎖,需要lock和unlock()方法配合try/finally程式碼塊來完成。
Synchronized使用較ReentrantLock 便利一些;
鎖的細粒度和靈活性:ReentrantLock強於Synchronized;
(3)效能區別
Synchronized引入偏向鎖,自旋鎖之後,兩者的效能差不多,在這種情況下,官方建議使用Synchronized。
① Synchronized
Synchronized會在同步塊的前後分別形成monitorenter和monitorexit兩個位元組碼指令。
在執行monitorenter指令時,首先要嘗試獲取物件鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件鎖,把鎖的計數器+1,相應的執行monitorexit時,計數器-1,當計數器為0時,鎖就會被釋放。
如果獲取鎖失敗,當前執行緒就要阻塞,知道物件鎖被另一個執行緒釋放為止。
② ReentrantLock
ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高階功能,主要有如下三項:
等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於Synchronized避免出現死鎖的情況。
通過lock.lockInterruptibly()來實現這一機制;
公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖是非公平鎖;
ReentrantLock預設也是非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的效能不是很好;
鎖繫結多個條件,一個ReentrantLock物件可以同時繫結多個物件。
ReentrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的執行緒們,而不是像Synchronized要麼隨機喚醒一個執行緒,要麼喚醒全部執行緒。
(1)什麼是可重入性
一個執行緒持有鎖時,當其他執行緒嘗試獲取該鎖時,會被阻塞;而這個執行緒嘗試獲取自己持有鎖時,如果成功說明該鎖是可重入的,反之則不可重入。
(2)synchronized是如何實現可重入性
synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit兩個位元組碼指令。
每個鎖物件內部維護一個計數器,該計數器初始值為0,表示任何執行緒都可以獲取該鎖並執行相應的方法。
根據虛擬機器器規範要求,在執行monitorenter指令時,首先要嘗試獲取物件的鎖,如果這個物件沒有被鎖定,或者當前執行緒已經擁有了物件的鎖,把鎖的計數器+1,相應的在執行monitorexit指令後鎖計數器-1,當計數器為0時,鎖就被釋放。
如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到物件鎖被另一個執行緒釋放為止。
(3)ReentrantLock如何實現可重入性
ReentrantLock使用內部類Sync來管理鎖,所以真正的獲取鎖是由Sync的實現類控制的。
Sync有兩個實現,分別為NonfairSync(非公公平鎖)和FairSync(公平鎖)。Sync通過繼承AQS實現,在AQS中維護了一個private volatile int state來計算重入次數,避免頻繁的持有釋放操作帶來的執行緒問題。
(4)ReentrantLock程式碼範例
// Sync繼承於AQS abstract static class Sync extends AbstractQueuedSynchronizer { ... } // ReentrantLock預設是非公平鎖 public ReentrantLock() { sync = new NonfairSync(); } // 可以通過向構造方法中傳true來實現公平鎖 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
protected final boolean tryAcquire(int acquires) { // 當前想要獲取鎖的執行緒 final Thread current = Thread.currentThread(); // 當前鎖的狀態 int c = getState(); // state == 0 此時此刻沒有執行緒持有鎖 if (c == 0) { // 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來後到, // 看看有沒有別人在佇列中等了半天了 if (!hasQueuedPredecessors() && // 如果沒有執行緒在等待,那就用CAS嘗試一下,成功了就獲取到鎖了, // 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個執行緒搶先了 =_= // 因為剛剛還沒人的,我判斷過了 compareAndSetState(0, acquires)) { // 到這裡就是獲取到鎖了,標記一下,告訴大家,現在是我佔用了鎖 setExclusiveOwnerThread(current); return true; } } // 會進入這個else if分支,說明是重入了,需要操作:state=state+1 // 這裡不存在並行問題 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 如果到這裡,說明前面的if和else if都沒有返回true,說明沒有獲取到鎖 return false; }
(5)程式碼分析
當一個執行緒在獲取鎖過程中,先判斷state的值是否為0,如果是表示沒有執行緒持有鎖,就可以嘗試獲取鎖。
當state的值不為0時,表示鎖已經被一個執行緒佔用了,這時會做一個判斷current==getExclusiveOwnerThread(),這個方法返回的是當前持有鎖的執行緒,這個判斷是看當前持有鎖的執行緒是不是自己,如果是自己,那麼將state的值+1,表示重入返回即可。
(1)鎖消除
所消除就是虛擬機器器根據一個物件是否真正存在同步情況,若不存在同步情況,則對該物件的存取無需經過加鎖解鎖的操作。
比如StringBuffer的append方法,因為append方法需要判斷物件是否被佔用,而如果程式碼不存在鎖競爭,那麼這部分的效能消耗是無意義的。
於是虛擬機器器在即時編譯的時候就會將上面的程式碼進行優化,也就是鎖消除。
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
從原始碼可以看出,append方法用了 synchronized關鍵字,它是執行緒安全的。
但我們可能僅線上程內部把StringBuffer當做區域性變數使用;StringBuffer僅在方法內作用域有效,不存線上程安全的問題,這時我們可以通過編譯器將其優化,將鎖消除
前提是Java必須執行在server模式,同時必須開啟逃逸分析;
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 其中+DoEscapeAnalysis表示開啟逃逸分析,+EliminateLocks表示鎖消除。
public static String createStringBuffer(String str1, String str2) { StringBuffer sBuf = new StringBuffer(); sBuf.append(str1);// append方法是同步操作 sBuf.append(str2); return sBuf.toString(); }
逃逸分析:比如上面的程式碼,它要看sBuf是否可能逃出它的作用域?如果將sBuf作為方法的返回值進行返回,那麼它在方法外部可能被當作一個全域性物件使用,就有可能發生執行緒安全問題,這時就可以說sBuf這個物件發生逃逸了,因而不應將append操作的鎖消除,但我們上面的程式碼沒有發生鎖逃逸,鎖消除就可以帶來一定的效能提升。
(2)鎖粗化
鎖的請求、同步、釋放都會消耗一定的系統資源,如果高頻的鎖請求反而不利於系統效能的優化,鎖粗化就是把多次的鎖請求合併成一個請求,擴大鎖的範圍,降低鎖請求、同步、釋放帶來的效能損耗。
(1)都是可重入鎖;
(2)ReentrantLock內部是實現了Sync,Sync繼承於AQS抽象類。
Sync有兩個實現,一個是公平鎖,一個是非公平鎖,通過建構函式定義。
AQS中維護了一個state來計算重入次數,避免頻繁的持有釋放操作帶來的執行緒問題。
(3)ReentrantLock只能定義程式碼塊,而Synchronized可以定義方法和程式碼塊;
(4)Synchronized是JVM的一個內部關鍵字,ReentrantLock是JDK1.5之後引入的一個API層面的互斥鎖;
(5)Synchronized實現自動的加鎖、釋放鎖,ReentrantLock需要手動加鎖和釋放鎖,中間可以暫停;
(6)Synchronized由於引進了偏向鎖和自旋鎖,所以效能上和ReentrantLock差不多,但操作上方便很多,所以優先使用Synchronized。
(1)AQS是AbstractQueuedSynchronizer的縮寫,它提供了一個FIFO佇列,可以看成是一個實現同步鎖的核心元件。
AQS是一個抽象類,主要通過繼承的方式來使用,它本身沒有實現任何的同步介面,僅僅是定義了同步狀態的獲取和釋放的方法來提供自定義的同步元件。
(2)AQS的兩種功能:獨佔鎖和共用鎖
(3)AQS的內部實現
AQS的實現依賴內部的同步佇列,也就是FIFO的雙向佇列,如果當前執行緒競爭失敗,那麼AQS會把當前執行緒以及等待狀態資訊構造成一個Node加入到同步佇列中,同時再阻塞該執行緒。當獲取鎖的執行緒釋放鎖以後,會從佇列中喚醒一個阻塞的節點(執行緒)。
AQS佇列內部維護的是一個FIFO的雙向連結串列,這種結構的特點是每個資料結構都有兩個指標,分別指向直接的後繼節點和直接前驅節點。
所以雙向連結串列可以從任意一個節點開始很方便的範文前驅和後繼節點。每個Node其實是由執行緒封裝,當執行緒爭搶鎖失敗後會封裝成Node加入到AQS佇列中。
AQS定義兩種資源共用方式
(1)Exclusive(獨佔)
只有一個執行緒能執行,如ReentrantLock。又可分為公平鎖和非公平鎖:
(2)Share(共用)
多個執行緒可同時執行,如Semaphore/CountDownLatch。
Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。
ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個執行緒同時對某一資源進行讀。
不同的自定義同步器爭用共用資源的方式也不同。自定義同步器在實現時只需要實現共用資源 state 的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。
73、如何讓 Java 的執行緒彼此同步?
(1)Semaphore同步器
特徵:
經典的號誌,通過計數器控制對共用資源的存取
Semaphore(int count):建立擁有count個許可證的號誌
acquire()/acquire(int num) : 獲取1/num個許可證
release/release(int num) : 釋放1/num個許可證
(2)CountDownLatch同步器
特徵:
必須發生指定數量的事件後才可以繼續執行(比如賽跑比賽,裁判喊出3,2,1之後大家才同時跑)
CountDownLatch(int count):必須發生count個數量才可以開啟鎖存器
await:等待鎖存器
countDown:觸發事件
(3)CyclicBarrier同步器
特徵:
適用於只有多個執行緒都到達預定點時才可以繼續執行(比如鬥地主,需要等齊三個人才開始)
CyclicBarrier(int num) :等待執行緒的數量
CyclicBarrier(int num, Runnable action) :等待執行緒的數量以及所有執行緒到達後的操作
await() : 到達臨界點後暫停執行緒
(4)交換器(Exchanger)同步器
(5)Phaser同步器
建立一個阻塞佇列來容納任務,在第一次執行任務時建立足夠多的執行緒,並處理任務,之後每個工作執行緒自動從任務佇列中獲取執行緒,直到任務佇列中任務為0為止,此時執行緒處於等待狀態,一旦有工作任務加入任務佇列中,即刻喚醒工作執行緒進行處理,實現執行緒的可複用性。
執行緒池一般包括四個基本組成部分:
(1)執行緒池管理器
用於建立執行緒池,銷燬執行緒池,新增新任務。
(2)工作執行緒
執行緒池中執行緒,可迴圈執行任務,在沒有任務時處於等待狀態。
(3)任務佇列
用於存放沒有處理的任務,一種快取機制。
(4)任務介面
每個任務必須實現的介面,供工作執行緒排程任務的執行,主要規定了任務的開始和收尾工作,和任務的狀態。
// Java執行緒池的完整建構函式 public ThreadPoolExecutor( int corePoolSize, // 執行緒池長期維持的最小執行緒數,即使執行緒處於Idle狀態,也不會回收。 int maximumPoolSize, // 執行緒數的上限 long keepAliveTime, // 執行緒最大生命週期。 TimeUnit unit, //時間單位 BlockingQueue<Runnable> workQueue, //任務佇列。當執行緒池中的執行緒都處於執行狀態,而此時任務數量繼續增加,則需要一個容器來容納這些任務,這就是任務佇列。 ThreadFactory threadFactory, // 執行緒工廠。定義如何啟動一個執行緒,可以設定執行緒名稱,並且可以確認是否是後臺執行緒等。 RejectedExecutionHandler handler // 拒絕任務處理器。由於超出執行緒數量和佇列容量而對繼續增加的任務進行處理的程式。 )
執行緒池中的執行緒是在第一次提交任務submit時建立的
建立執行緒的方式有繼承Thread和實現Runnable,重寫run方法,start開始執行,wait等待,sleep休眠,shutdown停止。
(1)newSingleThreadExecutor:單執行緒池。
顧名思義就是一個池中只有一個執行緒在執行,該執行緒永不超時,而且由於是一個執行緒,當有多個任務需要處理時,會將它們放置到一個無界阻塞佇列中逐個處理,它的實現程式碼如下:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable())); }
它的使用方法也很簡單,下面是簡單的範例:
public static void main(String[] args) throws ExecutionException,InterruptedException { // 建立單執行緒執行器 ExecutorService es = Executors.newSingleThreadExecutor(); // 執行一個任務 Future<String> future = es.submit(new Callable<String>() { @Override public String call() throws Exception { return ""; } }); // 獲得任務執行後的返回值 System.out.println("返回值:" + future.get()); // 關閉執行器 es.shutdown(); }
(2)newCachedThreadPool:緩衝功能的執行緒。
建立了一個執行緒池,而且執行緒數量是沒有限制的(當然,不能超過Integer的最大值),新增一個任務即有一個執行緒處理,或者複用之前空閒的執行緒,或者重親啟動一個執行緒,但是一旦一個執行緒在60秒內一直處於等待狀態時(也就是一分鐘無事可做),則會被終止,其原始碼如下:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
這裡需要說明的是,任務佇列使用了同步阻塞佇列,這意味著向佇列中加入一個元素,即可喚醒一個執行緒(新建立的執行緒或複用空閒執行緒來處理),這種佇列已經沒有佇列深度的概念了。
(3)newFixedThreadPool:固定執行緒數量的執行緒池。
在初始化時已經決定了執行緒的最大數量,若任務新增的能力超出了執行緒的處理能力,則建立阻塞佇列容納多餘的任務,其原始碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大執行緒數量為nThreads。
如果任務增長的速度非常快,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?
會按照ThreadPoolExecutor預設的拒絕策略(預設是DiscardPolicy,直接丟棄)來處理。
以上三種執行緒池執行器都是ThreadPoolExecutor的簡化版,目的是幫助開發人員遮蔽過得執行緒細節,簡化多執行緒開發。
當需要執行非同步任務時,可以直接通過Executors獲得一個執行緒池,然後執行任務,不需要關注ThreadPoolExecutor的一系列引數時什麼含義。
當然,有時候這三個執行緒不能滿足要求,此時則可以直接操作ThreadPoolExecutor來實現複雜的多執行緒計算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是執行緒池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操作,需要了解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。
對於可見性,Java 提供了 volatile 關鍵字來保證可見性和禁止指令重排。
volatile 提供 happens-before 的保證,確保一個執行緒的修改能對其他執行緒是可見的。當一個共用變數被 volatile 修飾時,它會保證修改的值會立即被更新到主記憶體,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
volatile 常用於多執行緒環境下的單次操作(單次讀或者單次寫)。
volatile修飾的變數在各個執行緒的工作記憶體中不存在一致性的問題(在各個執行緒工作的記憶體中,volatile修飾的變數也會存在不一致的情況,但是由於每次使用之前都會先重新整理主記憶體中的資料到工作記憶體,執行引擎看不到不一致的情況,因此可以認為不存在不一致的問題),但是java的運算並非原子性的操作,導致volatile在並行下並非是執行緒安全的。
ThreadLocal 是一個本地執行緒副本變數工具類,在每個執行緒中都建立了一個 ThreadLocalMap 物件
簡單說 ThreadLocal 就是一種以空間換時間的做法,每個執行緒可以存取自己內部 ThreadLocalMap 物件內的 value。通過這種方式,避免資源在多執行緒間共用。
原理:執行緒區域性變數是侷限於執行緒內部的變數,屬於執行緒自身所有,不在多個執行緒間共用。
Java提供ThreadLocal類來支援執行緒區域性變數,是一種實現執行緒安全的方式。但是在管理環境下(如 web 伺服器)使用執行緒區域性變數的時候要特別小心,在這種情況下,工作執行緒的生命週期比任何應用變數的生命週期都要長。
任何執行緒區域性變數一旦在工作完成後沒有釋放,Java 應用就存在記憶體洩露的風險。
經典的使用場景是為每個執行緒分配一個 JDBC 連線 Connection。
這樣就可以保證每個執行緒的都在各自的 Connection 上進行資料庫的操作,不會出現 A 執行緒關了 B執行緒正在使用的 Connection; 還有 Session 管理 等問題。
在java程式中,常用的有兩種機制來解決多執行緒並行問題,一種是sychronized方式,通過鎖機制,一個執行緒執行時,讓另一個執行緒等待,是以時間換空間的方式來讓多執行緒序列執行。
而另外一種方式就是ThreadLocal方式,通過建立執行緒區域性變數,以空間換時間的方式來讓多執行緒並行執行。兩種方式各有優劣,適用於不同的場景,要根據不同的業務場景來進行選擇。
在spring的原始碼中,就使用了ThreadLocal來管理連線,在很多開源專案中,都經常使用ThreadLocal來控制多執行緒並行問題,因為它足夠的簡單,我們不需要關心是否有執行緒安全問題,因為變數是每個執行緒所特有的。
ThreadLocal 變數解決了多執行緒環境下單個執行緒中變數的共用問題,使用名為ThreadLocalMap的雜湊表進行維護(key為ThreadLocal變數名,value為ThreadLocal變數的值);
使用時需要注意以下幾點:
在執行程式時,為了提供效能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎麼排序就怎麼排序
它需要滿足以下兩個條件:
需要注意的是:重排序不會影響單執行緒環境的執行結果,但是會破壞多執行緒的執行語意。
很多 synchronized 裡面的程式碼只是一些很簡單的程式碼,執行時間非常快,此時等待的執行緒都加鎖可能是一種不太值得的操作,因為執行緒阻塞涉及到使用者態和核心態切換的問題。
既然 synchronized 裡面的程式碼執行得非常快,不妨讓等待鎖的執行緒不要被阻塞,而是在 synchronized 的邊界做忙迴圈,這就是自旋。
如果做了多次迴圈發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
synchronized 鎖升級原理:在鎖物件的物件頭裡面有一個 threadid 欄位,在第一次存取的時候 threadid 為空,jvm 讓其持有偏向鎖,並將 threadid 設定為其執行緒 id,再次進入的時候會先判斷 threadid 是否與其執行緒 id 一致
如果一致則可以直接使用此物件,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋迴圈一定次數來獲取鎖,執行一定次數之後,如果還沒有正常獲取到要使用的物件
此時就會把鎖從輕量級升級為重量級鎖,此過程就構成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是為了減低了鎖帶來的效能消耗。
在 Java 6 之後優化 synchronized 的實現方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的效能消耗。
synchronized 是和 if、else、for、while 一樣的關鍵字,ReentrantLock 是類,這是二者的本質區別。
既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數
synchronized 早期的實現比較低效,對比 ReentrantLock,大多數場景效能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
相同點:兩者都是可重入鎖
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。
比如一個執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。
同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
主要區別如下:
Java中每一個物件都可以作為鎖,這是synchronized實現同步的基礎:
Lock 介面比同步方法和同步塊提供了更具擴充套件性的鎖操作。
他們允許更靈活的結構,可以具有完全不同的性質,並且可以支援多個相關類的條件物件。
它的優勢有:
(1)可以使鎖更公平
(2)可以使執行緒在等待鎖的時候響應中斷
(3)可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間
(4)可以在不同的範圍,以不同的順序獲取和釋放鎖
整體上來說 Lock 是 synchronized 的擴充套件版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件佇列的(newCondition 方法)鎖操作。
另外 Lock 的實現類基本都支援非公平鎖(預設)和公平鎖,synchronized 只支援非公平鎖,當然,在大部分情況下,非公平鎖是高效的選擇。
(1)servlet是伺服器端的Java程式,它擔當使用者端和伺服器端的中間層。
(2)jsp全名為Java server pages,中文名叫Java伺服器頁面,其本質是一個簡化的servlet設計。JSP是一種動態頁面設計,它的主要目的是將表示邏輯從servlet中分離出來。
(3)JVM只能識別Java程式碼,不能識別JSP,JSP編譯後變成了servlet,web容器將JSP的程式碼編譯成JVM能夠識別的Java類(servlet)。
(4)JSP有內建物件、servlet沒有內建物件。
JSP九大內建物件:
application、session、request、page
(1)儲存位置不同
(2)儲存容量不同
(3)儲存方式不同
(4)隱私策略不同
(5)有效期不同
(6)跨域支援上不同
一般預設情況下,在對談中,伺服器儲存 session 的 sessionid 是通過 cookie 存到瀏覽器裡。
如果瀏覽器禁用了 cookie,瀏覽器請求伺服器無法攜帶 sessionid,伺服器無法識別請求中的使用者身份,session失效。
但是可以通過其他方法在禁用 cookie 的情況下,可以繼續使用session。
多執行緒程式設計中一般執行緒的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個執行緒使用,為了讓這些執行緒都能得到有效執行,CPU 採取的策略是為每個執行緒分配時間片並輪轉的形式。
當一個執行緒的時間片用完的時候就會重新處於就緒狀態讓給其他執行緒使用,這個過程就屬於一次上下文切換。
概括來說就是:
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。
所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作。
Linux 相比與其他作業系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
1、session機制
session是伺服器端儲存的一個物件,主要用來儲存所有存取過該伺服器端的使用者端的使用者資訊(也可以儲存其他資訊),從而實現保持使用者對談狀態。
但是伺服器重啟時,記憶體會被銷燬,儲存的使用者資訊也就消失了。
不同的使用者存取伺服器端的時候會在session物件中儲存鍵值對,“鍵”用來儲存開啟這個使用者資訊的“鑰匙”,在登入成功後,“鑰匙”通過cookie返回給使用者端,使用者端儲存為sessionId記錄在cookie中。
當用戶端再次存取時,會預設攜帶cookie中的sessionId來實現對談機制。
(1)session是基於cookie的。
(2)session持久化
用於解決重啟伺服器後session消失的問題。在資料庫中儲存session,而不是儲存在記憶體中。通過包:express-mysql-session。
當用戶端儲存的cookie失效後,伺服器端的session不會立即銷燬,會有一個延時,伺服器端會定期清理無效session,不會造成無效資料佔用儲存空間的問題。
2、token機制
適用於前後端分離的專案(前後端程式碼執行在不同的伺服器下)
請求登入時,token和sessionid原理相同,是對key和key對應的使用者資訊進行加密後的加密字元,登入成功後,會在響應主體中將{token:“字串”}返回給使用者端。
使用者端通過cookie都可以進行儲存。
再次請求時不會預設攜帶,需要在請求攔截器位置給請求頭中新增認證欄位Authorization攜帶token資訊,伺服器就可以通過token資訊查詢使用者登入狀態。
當用戶端登入完成後,會在伺服器端產生一個session,此時伺服器端會將sessionid返回給使用者端瀏覽器。
使用者端將sessionid儲存在瀏覽器的cookie中,當用戶再次登入時,會獲得對應的sessionid,然後將sessionid傳送到伺服器端請求登入,伺服器端在記憶體中找到對應的sessionid,完成登入,如果找不到,返回登入頁面。
98、簡述 tcp 和 udp的區別?
因為使用者端和伺服器端都要確認連線,
①使用者端請求連線伺服器端;
②針對使用者端的請求確認應答,並請求建立連線;
③針對伺服器端的請求確認應答,建立連線;
兩次無法確保A能收到B的資料;
xss(Cross Site Scripting),即跨站指令碼攻擊,是一種常見於web應用程式中的電腦保安漏洞。
指的是在使用者瀏覽器上,在渲染DOM樹的時候,執行了不可預期的JS指令碼,從而發生了安全問題。
XSS就是通過在使用者端注入惡意的可執行指令碼,若伺服器端對使用者的輸入不進行處理,直接將使用者的輸入輸出到瀏覽器,然後瀏覽器將會執行使用者注入的指令碼。
所以XSS攻擊的核心就是瀏覽器渲染DOM的時候將文字資訊解析成JS指令碼從而引發JS指令碼注入,那麼XSS攻擊的防禦手段就是基於瀏覽器渲染這一步去做防禦。
只要我們使用HTML編碼將瀏覽器需要渲染的資訊編碼後,瀏覽器在渲染DOM元素的時候,會自動解碼需要渲染的資訊,將上述資訊解析成字串而不是JS指令碼,這就是我們防禦XSS攻擊的核心想法。
預防:
1、獲取使用者的輸入,不用innerHtml,用innerText.
2、對使用者的輸入進行過濾,如對& < > " ' /等進行跳脫;
跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制使用者在當前已登入的Web應用程式上執行非本意的操作的攻擊方法。
跟跨網站指令碼(XSS)相比,XSS 利用的是使用者對指定網站的信任,CSRF 利用的是網站對使用者網頁瀏覽器的信任。
1、攻擊細節
跨站請求攻擊,簡單地說,是攻擊者通過一些技術手段欺騙使用者的瀏覽器去存取一個自己曾經認證過的網站並執行一些操作(如發郵件,發訊息,甚至財產操作如轉賬和購買商品)。
由於瀏覽器曾經認證過,所以被存取的網站會認為是真正的使用者操作而去執行。
這利用了web中使用者身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個使用者的瀏覽器,卻不能保證請求本身是使用者自願發出的。
例子
假如一家銀行用以執行轉賬操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那麼,一個惡意攻擊者可以在另一個網站上放置如下程式碼: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有賬戶名為Alice的使用者存取了惡意站點,而她之前剛存取過銀行不久,登入資訊尚未過期,那麼她就會損失1000資金。
這種惡意的網址可以有很多種形式,藏身於網頁中的許多地方。此外,攻擊者也不需要控制放置惡意網址的網站。
例如他可以將這種地址藏在論壇,部落格等任何使用者生成資訊的網站中。這意味著如果伺服器端沒有合適的防禦措施的話,使用者即使存取熟悉的可信網站也有受攻擊的危險。
透過例子能夠看出,攻擊者並不能通過CSRF攻擊來直接獲取使用者的賬戶控制權,也不能直接竊取使用者的任何資訊。
他們能做到的,是欺騙使用者瀏覽器,讓其以使用者的名義執行操作。
2、防禦措施
檢查Referer欄位
HTTP頭中有一個Referer欄位,這個欄位用以標明請求來源於哪個地址。
在處理敏感資料請求時,通常來說,Referer欄位應和請求的地址位於同一域名下。以上文銀行操作為例,Referer欄位地址通常應該是轉賬按鈕所在的網頁地址,應該也位於www.examplebank.com之下。
而如果是CSRF攻擊傳來的請求,Referer欄位會是包含惡意網址的地址,不會位於www.examplebank.com之下,這時候伺服器就能識別出惡意的存取。
這種辦法簡單易行,工作量低,僅需要在關鍵存取處增加一步校驗。
但這種辦法也有其侷限性,因其完全依賴瀏覽器傳送正確的Referer欄位。
雖然http協定對此欄位的內容有明確的規定,但並無法保證來訪的瀏覽器的具體實現,亦無法保證瀏覽器沒有安全漏洞影響到此欄位。
並且也存在攻擊者攻擊某些瀏覽器,篡改其Referer欄位的可能。
3、新增校驗token
由於CSRF的本質在於攻擊者欺騙使用者去存取自己設定的地址,所以如果要求在存取敏感資料請求時,要求使用者瀏覽器提供不儲存在cookie中,並且攻擊者無法偽造的資料作為校驗,那麼攻擊者就無法再執行CSRF攻擊。
這種資料通常是表單中的一個資料項。
伺服器將其生成並附加在表單中,其內容是一個偽亂數
。當用戶端通過表單提交請求時,這個偽亂數也一併提交上去以供校驗。正常的存取時,使用者端瀏覽器能夠正確得到並傳回這個偽亂數,而通過CSRF傳來的欺騙性攻擊中,攻擊者無從事先得知這個偽亂數的值,伺服器端就會因為校驗token的值為空或者錯誤,拒絕這個可疑請求。
1、jsonp原理詳解——終於搞清楚jsonp是啥了
2、最流行的跨域方案cors
cors是目前主流的跨域解決方案,跨域資源共用(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓執行在一個 origin (domain) 上的Web應用被准許存取來自不同源伺服器上的指定的資源。
當一個資源從與該資源本身所在的伺服器不同的域、協定或埠請求一個資源時,資源會發起一個跨域 HTTP 請求。
3、最方便的跨域方案Nginx
nginx是一款極其強大的web伺服器,其優點就是輕量級、啟動快、高並行。
現在的新專案中nginx幾乎是首選,我們用node或者java開發的服務通常都需要經過nginx的反向代理。
反向代理的原理很簡單,即所有使用者端的請求都必須先經過nginx的處理,nginx作為代理伺服器再講請求轉發給node或者java服務,這樣就規避了同源策略。
到此這篇關於Java經典面試題最全彙總208道(二)的文章就介紹到這了,更多相關Java面試題內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45