Java經(jīng)典面試題最全匯總208道(二)
前言
短時間提升自己最快的手段就是背面試題,最近總結(jié)了Java常用的面試題,分享給大家,希望大家都能圓夢大廠,加油,我命由我不由天。
53、concurrentHashMap和HashTable有什么區(qū)別
concurrentHashMap融合了hashmap和hashtable的優(yōu)勢,hashmap是不同步的,但是單線程情況下效率高,hashtable是同步的同步情況下保證程序執(zhí)行的正確性。
但hashtable每次同步執(zhí)行的時候都要鎖住整個結(jié)構(gòu),如下圖:
concurrentHashMap鎖的方式是細(xì)粒度的。
concurrentHashMap將hash分為16個桶(默認(rèn)值),諸如get、put、remove等常用操作只鎖住當(dāng)前需要用到的桶。
concurrentHashMap的讀取并發(fā),因為讀取的大多數(shù)時候都沒有鎖定,所以讀取操作幾乎是完全的并發(fā)操作,只是在求size時才需要鎖定整個hash。
而且在迭代時,concurrentHashMap使用了不同于傳統(tǒng)集合的快速失敗迭代器的另一種迭代方式,弱一致迭代器。
在這種方式中,當(dāng)iterator被創(chuàng)建后集合再發(fā)生改變就不會拋出ConcurrentModificationException,取而代之的是在改變時new新的數(shù)據(jù)而不是影響原來的數(shù)據(jù),iterator完成后再講頭指針替代為新的數(shù)據(jù),這樣iterator時使用的是原來的數(shù)據(jù)。
54、HasmMap和HashSet的區(qū)別
(1)先了解一下HashCode
Java中的集合有兩類,一類是List,一類是Set。
List:元素有序,可以重復(fù);
Set:元素?zé)o序,不可重復(fù);
要想保證元素的不重復(fù),拿什么來判斷呢?這就是Object.equals方法了。
如果元素有很多,增加一個元素,就要判斷n次嗎?
顯然不現(xiàn)實,于是,Java采用了哈希表的原理。
哈希算法也稱為散列算法,是將數(shù)據(jù)依特定算法直接指定到一根地址上,初學(xué)者可以簡單的理解為,HashCode方法返回的就是對象存儲的物理位置(實際上并不是)。
這樣一來,當(dāng)集合添加新的元素時,先調(diào)用這個元素的hashcode()方法,就一下子能定位到他應(yīng)該放置的物理位置上。
如果這個位置上沒有元素,他就可以直接存儲在這個位置上,不用再進行任何比較了。
如果這個位置上有元素,就調(diào)用它的equals方法與新元素進行比較,想同的話就不存了,不相同就散列其它的地址。
所以這里存在一個沖突解決的問題。
這樣一來實際上調(diào)用equals方法的次數(shù)就大大降低了,幾乎只需要一兩次。
簡而言之,在集合查找時,hashcode能大大降低對象比較次數(shù),提高查找效率。
Java對象的equals方法和hashCode方法時這樣規(guī)定的:
相等的對象就必須具有相等的hashcode。
- 如果兩個對象的hashcode相同,他們并不一定相同。
- 如果兩個對象的hashcode相同,他們并不一定相同。
如果兩個Java對象A和B,A和B不相等,但是A和B的哈希碼相等,將A和B都存入HashMap時會發(fā)生哈希沖突,也就是A和B存放在HashMap內(nèi)部數(shù)組的位置索引相同
這時HashMap會在該位置建立一個鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用規(guī)則,是允許的。
當(dāng)然,哈希沖突越少越好,盡量采用好的哈希算法避免哈希沖突。
equals()相等的兩個對象,hashcode()一定相等;equals()不相等的兩個對象,卻并不能證明他們的hashcode()不相等。
(2)HashMap和HashSet的區(qū)別
55、請談?wù)?ReadWriteLock 和 StampedLock
ReadWriteLock包括兩種子鎖
(1)ReadWriteLock
ReadWriteLock 可以實現(xiàn)多個讀鎖同時進行,但是讀與寫和寫于寫互斥,只能有一個寫鎖線程在進行。
(2)StampedLock
StampedLock是Jdk在1.8提供的一種讀寫鎖,相比較ReentrantReadWriteLock性能更好
因為ReentrantReadWriteLock在讀寫之間是互斥的,使用的是一種悲觀策略,在讀線程特別多的情況下,會造成寫線程處于饑餓狀態(tài)
雖然可以在初始化的時候設(shè)置為true指定為公平,但是吞吐量又下去了,而StampedLock是提供了一種樂觀策略,更好的實現(xiàn)讀寫分離,并且吞吐量不會下降。
StampedLock包括三種鎖:
(1)寫鎖writeLock:
writeLock是一個獨占鎖寫鎖,當(dāng)一個線程獲得該鎖后,其他請求讀鎖或者寫鎖的線程阻塞, 獲取成功后,會返回一個stamp(憑據(jù))變量來表示該鎖的版本,在釋放鎖時調(diào)用unlockWrite方法傳遞stamp參數(shù)。
提供了非阻塞式獲取鎖tryWriteLock。
(2)悲觀讀鎖readLock:
readLock是一個共享讀鎖,在沒有線程獲取寫鎖情況下,多個線程可以獲取該鎖。
如果有寫鎖獲取,那么其他線程請求讀鎖會被阻塞。
悲觀讀鎖會認(rèn)為其他線程可能要對自己操作的數(shù)據(jù)進行修改,所以需要先對數(shù)據(jù)進行加鎖,這是在讀少寫多的情況下考慮的。
請求該鎖成功后會返回一個stamp值,在釋放鎖時調(diào)用unlockRead方法傳遞stamp參數(shù)。
提供了非阻塞式獲取鎖方法tryWriteLock。
(3)樂觀讀鎖tryOptimisticRead:
tryOptimisticRead相對比悲觀讀鎖,在操作數(shù)據(jù)前并沒有通過CAS設(shè)置鎖的狀態(tài),如果沒有線程獲取寫鎖,則返回一個非0的stamp變量,獲取該stamp后在操作數(shù)據(jù)前還需要調(diào)用validate方法來判斷期間是否有線程獲取了寫鎖
如果是返回值為0則有線程獲取寫鎖,如果不是0則可以使用stamp變量的鎖來操作數(shù)據(jù)。
由于tryOptimisticRead并沒有修改鎖狀態(tài),所以不需要釋放鎖。
這是讀多寫少的情況下考慮的,不涉及CAS操作,所以效率較高,在保證數(shù)據(jù)一致性上需要復(fù)制一份要操作的變量到方法棧中,并且在操作數(shù)據(jù)時可能其他寫線程已經(jīng)修改了數(shù)據(jù)
而我們操作的是方法棧里面的數(shù)據(jù),也就是一個快照,所以最多返回的不是最新的數(shù)據(jù),但是一致性得到了保證。
56、線程的run()和start()有什么區(qū)別?
每個線程都是通過某個特定Thread對象所對應(yīng)的方法run()來完成其操作的,run()方法稱為線程體。
通過調(diào)用Thread類的start()方法來啟動一個線程。
start() 方法用于啟動線程,run() 方法用于執(zhí)行線程的運行時代碼。
run() 可以重復(fù)調(diào)用,而 start() 只能調(diào)用一次。
start()方法來啟動一個線程,真正實現(xiàn)了多線程運行。
調(diào)用start()方法無需等待run方法體代碼執(zhí)行完畢,可以直接繼續(xù)執(zhí)行其他的代碼; 此時線程是處于就緒狀態(tài),并沒有運行。
然后通過此Thread類調(diào)用方法run()來完成其運行狀態(tài), run()方法運行結(jié)束, 此線程終止。然后CPU再調(diào)度其它線程。
run()方法是在本線程里的,只是線程里的一個函數(shù),而不是多線程的。
如果直接調(diào)用run(),其實就相當(dāng)于是調(diào)用了一個普通函數(shù)而已,直接待用run()方法必須等待run()方法執(zhí)行完畢才能執(zhí)行下面的代碼
所以執(zhí)行路徑還是只有一條,根本就沒有線程的特征,所以在多線程執(zhí)行時要使用start()方法而不是run()方法。
57、為什么我們調(diào)用 start() 方法時會執(zhí)行 run() 方法,為什么我們不能直接調(diào)用 run() 方法?
這是另一個非常經(jīng)典的 java 多線程面試問題,而且在面試中會經(jīng)常被問到。很簡單,但是很多人都會答不上來!
new 一個 Thread,線程進入了新建狀態(tài)。
調(diào)用 start() 方法,會啟動一個線程并使線程進入了就緒狀態(tài),當(dāng)分配到時間片后就可以開始運行了。
start() 會執(zhí)行線程的相應(yīng)準(zhǔn)備工作,然后自動執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。
而直接執(zhí)行 run() 方法,會把 run 方法當(dāng)成一個 main 線程下的普通方法去執(zhí)行,并不會在某個線程中執(zhí)行它,所以這并不是多線程工作。
總結(jié): 調(diào)用 start 方法方可啟動線程并使線程進入就緒狀態(tài),而 run 方法只是 thread 的一個普通方法調(diào)用,還是在主線程里執(zhí)行。
58、Synchronized 用過嗎,其原理是什么?
(1)可重入性
synchronized的鎖對象中有一個計數(shù)器(recursions變量)會記錄線程獲得幾次鎖;
- 可重入的好處;
- 可以避免死鎖;
- 可以讓我們更好的封裝代碼;
synchronized是可重入鎖,每部鎖對象會有一個計數(shù)器記錄線程獲取幾次鎖,在執(zhí)行完同步代碼塊時,計數(shù)器的數(shù)量會-1,直到計數(shù)器的數(shù)量為0,就釋放這個鎖。
(2)不可中斷性
- 一個線程獲得鎖后,另一個線程想要獲得鎖,必須處于阻塞或等待狀態(tài),如果第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷;
- synchronized 屬于不可被中斷;
- Lock lock方法是不可中斷的;
- Lock tryLock方法是可中斷的;
59、JVM 對 Java 的原生鎖做了哪些優(yōu)化?
(1)自旋鎖
在線程進行阻塞的時候,先讓線程自旋等待一段時間,可能這段時間其它線程已經(jīng)解鎖,這時就無需讓線程再進行阻塞操作了。
自旋默認(rèn)次數(shù)是10次。
(2)自適應(yīng)自旋鎖
自旋鎖的升級,自旋的次數(shù)不再固定,由前一次自旋次數(shù)和鎖的擁有者的狀態(tài)決定。
(3)鎖消除
在動態(tài)編譯同步代碼塊的時候,JIT編譯器借助逃逸分析技術(shù)來判斷鎖對象是否只被一個線程訪問,而沒有其他線程,這時就可以取消鎖了。
(4)鎖粗化
當(dāng)JIT編譯器發(fā)現(xiàn)一系列的操作都對同一個對象反復(fù)加鎖解鎖,甚至加鎖操作出現(xiàn)在循環(huán)中,此時會將加鎖同步的范圍粗化到整個操作系列的外部。
鎖粒度:不要鎖住一些無關(guān)的代碼。
鎖粗化:可以一次性執(zhí)行完的不要多次加鎖執(zhí)行。
60、為什么 wait(), notify()和 notifyAll()必須在同步方法或者同步塊中被調(diào)用?
Java中,任何對象都可以作為鎖,并且 wait(),notify()等方法用于等待對象的鎖或者喚醒線程,在 Java 的線程中并沒有可供任何對象使用的鎖,所以任意對象調(diào)用方法一定定義在Object類中。
wait(), notify()和 notifyAll()這些方法在同步代碼塊中調(diào)用
有的人會說,既然是線程放棄對象鎖,那也可以把wait()定義在Thread類里面啊,新定義的線程繼承于Thread類,也不需要重新定義wait()方法的實現(xiàn)。
然而,這樣做有一個非常大的問題,一個線程完全可以持有很多鎖,你一個線程放棄鎖的時候,到底要放棄哪個鎖?
當(dāng)然了,這種設(shè)計并不是不能實現(xiàn),只是管理起來更加復(fù)雜。
綜上所述,wait()、notify()和notifyAll()方法要定義在Object類中。
61、Java 如何實現(xiàn)多線程之間的通訊和協(xié)作?
可以通過中斷 和 共享變量的方式實現(xiàn)線程間的通訊和協(xié)作
比如說最經(jīng)典的生產(chǎn)者-消費者模型:當(dāng)隊列滿時,生產(chǎn)者需要等待隊列有空間才能繼續(xù)往里面放入商品,而在等待的期間內(nèi),生產(chǎn)者必須釋放對臨界資源(即隊列)的占用權(quán)。
因為生產(chǎn)者如果不釋放對臨界資源的占用權(quán),那么消費者就無法消費隊列中的商品,就不會讓隊列有空間,那么生產(chǎn)者就會一直無限等待下去。
因此,一般情況下,當(dāng)隊列滿時,會讓生產(chǎn)者交出對臨界資源的占用權(quán),并進入掛起狀態(tài)。
然后等待消費者消費了商品,然后消費者通知生產(chǎn)者隊列有空間了。
同樣地,當(dāng)隊列空時,消費者也必須等待,等待生產(chǎn)者通知它隊列中有商品了。
這種互相通信的過程就是線程間的協(xié)作。
Java中線程通信協(xié)作的最常見的兩種方式:
1、syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()
2、ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()
線程間直接的數(shù)據(jù)交換:
通過管道進行線程間通信:
1)字節(jié)流;
2)字符流
62、Thread 類中的 yield 方法有什么作用?
yield()應(yīng)該做的是讓當(dāng)前運行線程回到可運行狀態(tài),以允許具有相同優(yōu)先級的其他線程獲得運行機會。
因此,使用yield()的目的是讓相同優(yōu)先級的線程之間能適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行。
但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調(diào)度程序再次選中。
結(jié)論:yield()從未導(dǎo)致線程轉(zhuǎn)到等待/睡眠/阻塞狀態(tài)。在大多數(shù)情況下,yield()將導(dǎo)致線程從運行狀態(tài)轉(zhuǎn)到可運行狀態(tài),但有可能沒有效果。
63、為什么說 Synchronized 是非公平鎖?
當(dāng)鎖被釋放后,任何一個線程都有機會競爭得到鎖,這樣做的目的是提高效率,但缺點是可能產(chǎn)生線程饑餓現(xiàn)象。
64、請談?wù)?volatile 有什么特點,為什么它能保證變量對所有線程的可見性?
volatile只能作用于變量,保證了操作可見性和有序性,不保證原子性。
在Java的內(nèi)存模型中分為主內(nèi)存和工作內(nèi)存,Java內(nèi)存模型規(guī)定所有的變量存儲在主內(nèi)存中,每條線程都有自己的工作內(nèi)存。
主內(nèi)存和工作內(nèi)存之間的交互分為8個原子操作:
- lock
- unlock
- read
- load
- assign
- use
- store
- write
volatile修飾的變量,只有對volatile進行assign操作,才可以load,只有l(wèi)oad才可以use,,這樣就保證了在工作內(nèi)存操作volatile變量,都會同步到主內(nèi)存中。
65、為什么說 Synchronized 是一個悲觀鎖?樂觀鎖的實現(xiàn)原理又是什么?什么是 CAS,它有什么特性?
Synchronized的并發(fā)策略是悲觀的,不管是否產(chǎn)生競爭,任何數(shù)據(jù)的操作都必須加鎖。
樂觀鎖的核心是CAS,CAS包括內(nèi)存值、預(yù)期值、新值,只有當(dāng)內(nèi)存值等于預(yù)期值時,才會將內(nèi)存值修改為新值。
66、樂觀鎖一定就是好的嗎?
樂觀鎖認(rèn)為對一個對象的操作不會引發(fā)沖突,所以每次操作都不進行加鎖,只是在最后提交更改時驗證是否發(fā)生沖突,如果沖突則再試一遍,直至成功為止,這個嘗試的過程稱為自旋。
樂觀鎖沒有加鎖,但樂觀鎖引入了ABA問題,此時一般采用版本號進行控制;
也可能產(chǎn)生自旋次數(shù)過多問題,此時并不能提高效率,反而不如直接加鎖的效率高;
只能保證一個對象的原子性,可以封裝成對象,再進行CAS操作;
67、請盡可能詳盡地對比下 Synchronized 和 ReentrantLock 的異同。
(1)相似點
它們都是阻塞式的同步,也就是說一個線程獲得了對象鎖,進入代碼塊,其它訪問該同步塊的線程都必須阻塞在同步代碼塊外面等待,而進行線程阻塞和喚醒的代碼是比較高的。
(2)功能區(qū)別
Synchronized是java語言的關(guān)鍵字,是原生語法層面的互斥,需要JVM實現(xiàn);ReentrantLock 是JDK1.5之后提供的API層面的互斥鎖,需要lock和unlock()方法配合try/finally代碼塊來完成。
Synchronized使用較ReentrantLock 便利一些;
鎖的細(xì)粒度和靈活性:ReentrantLock強于Synchronized;
(3)性能區(qū)別
Synchronized引入偏向鎖,自旋鎖之后,兩者的性能差不多,在這種情況下,官方建議使用Synchronized。
① Synchronized
Synchronized會在同步塊的前后分別形成monitorenter和monitorexit兩個字節(jié)碼指令。
在執(zhí)行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個對象鎖,把鎖的計數(shù)器+1,相應(yīng)的執(zhí)行monitorexit時,計數(shù)器-1,當(dāng)計數(shù)器為0時,鎖就會被釋放。
如果獲取鎖失敗,當(dāng)前線程就要阻塞,知道對象鎖被另一個線程釋放為止。
② ReentrantLock
ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有如下三項:
等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當(dāng)于Synchronized避免出現(xiàn)死鎖的情況。
通過lock.lockInterruptibly()來實現(xiàn)這一機制;
公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖是非公平鎖;
ReentrantLock默認(rèn)也是非公平鎖,可以通過參數(shù)true設(shè)為公平鎖,但公平鎖表現(xiàn)的性能不是很好;
鎖綁定多個條件,一個ReentrantLock對象可以同時綁定多個對象。
ReentrantLock提供了一個Condition(條件)類,用來實現(xiàn)分組喚醒需要喚醒的線程們,而不是像Synchronized要么隨機喚醒一個線程,要么喚醒全部線程。
68、ReentrantLock 是如何實現(xiàn)可重入性的?
(1)什么是可重入性
一個線程持有鎖時,當(dāng)其他線程嘗試獲取該鎖時,會被阻塞;而這個線程嘗試獲取自己持有鎖時,如果成功說明該鎖是可重入的,反之則不可重入。
(2)synchronized是如何實現(xiàn)可重入性
synchronized關(guān)鍵字經(jīng)過編譯后,會在同步塊的前后分別形成monitorenter和monitorexit兩個字節(jié)碼指令。
每個鎖對象內(nèi)部維護一個計數(shù)器,該計數(shù)器初始值為0,表示任何線程都可以獲取該鎖并執(zhí)行相應(yīng)的方法。
根據(jù)虛擬機規(guī)范要求,在執(zhí)行monitorenter指令時,首先要嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了對象的鎖,把鎖的計數(shù)器+1,相應(yīng)的在執(zhí)行monitorexit指令后鎖計數(shù)器-1,當(dāng)計數(shù)器為0時,鎖就被釋放。
如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到對象鎖被另一個線程釋放為止。
(3)ReentrantLock如何實現(xiàn)可重入性
ReentrantLock使用內(nèi)部類Sync來管理鎖,所以真正的獲取鎖是由Sync的實現(xiàn)類控制的。
Sync有兩個實現(xiàn),分別為NonfairSync(非公公平鎖)和FairSync(公平鎖)。Sync通過繼承AQS實現(xiàn),在AQS中維護了一個private volatile int state來計算重入次數(shù),避免頻繁的持有釋放操作帶來的線程問題。
(4)ReentrantLock代碼實例
// Sync繼承于AQS abstract static class Sync extends AbstractQueuedSynchronizer { ... } // ReentrantLock默認(rèn)是非公平鎖 public ReentrantLock() { sync = new NonfairSync(); } // 可以通過向構(gòu)造方法中傳true來實現(xiàn)公平鎖 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
protected final boolean tryAcquire(int acquires) { // 當(dāng)前想要獲取鎖的線程 final Thread current = Thread.currentThread(); // 當(dāng)前鎖的狀態(tài) int c = getState(); // state == 0 此時此刻沒有線程持有鎖 if (c == 0) { // 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來后到, // 看看有沒有別人在隊列中等了半天了 if (!hasQueuedPredecessors() && // 如果沒有線程在等待,那就用CAS嘗試一下,成功了就獲取到鎖了, // 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個線程搶先了 =_= // 因為剛剛還沒人的,我判斷過了 compareAndSetState(0, acquires)) { // 到這里就是獲取到鎖了,標(biāo)記一下,告訴大家,現(xiàn)在是我占用了鎖 setExclusiveOwnerThread(current); return true; } } // 會進入這個else if分支,說明是重入了,需要操作:state=state+1 // 這里不存在并發(fā)問題 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)代碼分析
當(dāng)一個線程在獲取鎖過程中,先判斷state的值是否為0,如果是表示沒有線程持有鎖,就可以嘗試獲取鎖。
當(dāng)state的值不為0時,表示鎖已經(jīng)被一個線程占用了,這時會做一個判斷current==getExclusiveOwnerThread(),這個方法返回的是當(dāng)前持有鎖的線程,這個判斷是看當(dāng)前持有鎖的線程是不是自己,如果是自己,那么將state的值+1,表示重入返回即可。
69、什么是鎖消除和鎖粗化?
(1)鎖消除
所消除就是虛擬機根據(jù)一個對象是否真正存在同步情況,若不存在同步情況,則對該對象的訪問無需經(jīng)過加鎖解鎖的操作。
比如StringBuffer的append方法,因為append方法需要判斷對象是否被占用,而如果代碼不存在鎖競爭,那么這部分的性能消耗是無意義的。
于是虛擬機在即時編譯的時候就會將上面的代碼進行優(yōu)化,也就是鎖消除。
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
從源碼可以看出,append方法用了 synchronized關(guān)鍵字,它是線程安全的。
但我們可能僅在線程內(nèi)部把StringBuffer當(dāng)做局部變量使用;StringBuffer僅在方法內(nèi)作用域有效,不存在線程安全的問題,這時我們可以通過編譯器將其優(yōu)化,將鎖消除
前提是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作為方法的返回值進行返回,那么它在方法外部可能被當(dāng)作一個全局對象使用,就有可能發(fā)生線程安全問題,這時就可以說sBuf這個對象發(fā)生逃逸了,因而不應(yīng)將append操作的鎖消除,但我們上面的代碼沒有發(fā)生鎖逃逸,鎖消除就可以帶來一定的性能提升。
(2)鎖粗化
鎖的請求、同步、釋放都會消耗一定的系統(tǒng)資源,如果高頻的鎖請求反而不利于系統(tǒng)性能的優(yōu)化,鎖粗化就是把多次的鎖請求合并成一個請求,擴大鎖的范圍,降低鎖請求、同步、釋放帶來的性能損耗。
70、跟 Synchronized 相比,可重入鎖 ReentrantLock 其實現(xiàn)原理有什么不同?
(1)都是可重入鎖;
(2)ReentrantLock內(nèi)部是實現(xiàn)了Sync,Sync繼承于AQS抽象類。
Sync有兩個實現(xiàn),一個是公平鎖,一個是非公平鎖,通過構(gòu)造函數(shù)定義。
AQS中維護了一個state來計算重入次數(shù),避免頻繁的持有釋放操作帶來的線程問題。
(3)ReentrantLock只能定義代碼塊,而Synchronized可以定義方法和代碼塊;
(4)Synchronized是JVM的一個內(nèi)部關(guān)鍵字,ReentrantLock是JDK1.5之后引入的一個API層面的互斥鎖;
(5)Synchronized實現(xiàn)自動的加鎖、釋放鎖,ReentrantLock需要手動加鎖和釋放鎖,中間可以暫停;
(6)Synchronized由于引進了偏向鎖和自旋鎖,所以性能上和ReentrantLock差不多,但操作上方便很多,所以優(yōu)先使用Synchronized。
71、那么請談?wù)?AQS 框架是怎么回事兒?
(1)AQS是AbstractQueuedSynchronizer的縮寫,它提供了一個FIFO隊列,可以看成是一個實現(xiàn)同步鎖的核心組件。
AQS是一個抽象類,主要通過繼承的方式來使用,它本身沒有實現(xiàn)任何的同步接口,僅僅是定義了同步狀態(tài)的獲取和釋放的方法來提供自定義的同步組件。
(2)AQS的兩種功能:獨占鎖和共享鎖
(3)AQS的內(nèi)部實現(xiàn)
AQS的實現(xiàn)依賴內(nèi)部的同步隊列,也就是FIFO的雙向隊列,如果當(dāng)前線程競爭失敗,那么AQS會把當(dāng)前線程以及等待狀態(tài)信息構(gòu)造成一個Node加入到同步隊列中,同時再阻塞該線程。當(dāng)獲取鎖的線程釋放鎖以后,會從隊列中喚醒一個阻塞的節(jié)點(線程)。
AQS隊列內(nèi)部維護的是一個FIFO的雙向鏈表,這種結(jié)構(gòu)的特點是每個數(shù)據(jù)結(jié)構(gòu)都有兩個指針,分別指向直接的后繼節(jié)點和直接前驅(qū)節(jié)點。
所以雙向鏈表可以從任意一個節(jié)點開始很方便的范文前驅(qū)和后繼節(jié)點。每個Node其實是由線程封裝,當(dāng)線程爭搶鎖失敗后會封裝成Node加入到AQS隊列中。
72、AQS 對資源的共享方式?
AQS定義兩種資源共享方式
(1)Exclusive(獨占)
只有一個線程能執(zhí)行,如ReentrantLock。又可分為公平鎖和非公平鎖:
- 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
- 非公平鎖:當(dāng)線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
(2)Share(共享)
多個線程可同時執(zhí)行,如Semaphore/CountDownLatch。
Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在后面講到。
ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。
不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源 state 的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了。
73、如何讓 Java 的線程彼此同步?
- synchronized
- volatile
- ReenreantLock
- 使用局部變量實現(xiàn)線程同步
74、你了解過哪些同步器?請分別介紹下。
(1)Semaphore同步器
特征:
經(jīng)典的信號量,通過計數(shù)器控制對共享資源的訪問
Semaphore(int count):創(chuàng)建擁有count個許可證的信號量
acquire()/acquire(int num) : 獲取1/num個許可證
release/release(int num) : 釋放1/num個許可證
(2)CountDownLatch同步器
特征:
必須發(fā)生指定數(shù)量的事件后才可以繼續(xù)運行(比如賽跑比賽,裁判喊出3,2,1之后大家才同時跑)
CountDownLatch(int count):必須發(fā)生count個數(shù)量才可以打開鎖存器
await:等待鎖存器
countDown:觸發(fā)事件
(3)CyclicBarrier同步器
特征:
適用于只有多個線程都到達預(yù)定點時才可以繼續(xù)執(zhí)行(比如斗地主,需要等齊三個人才開始)
CyclicBarrier(int num) :等待線程的數(shù)量
CyclicBarrier(int num, Runnable action) :等待線程的數(shù)量以及所有線程到達后的操作
await() : 到達臨界點后暫停線程
(4)交換器(Exchanger)同步器
(5)Phaser同步器
75、Java 中的線程池是如何實現(xiàn)的
創(chuàng)建一個阻塞隊列來容納任務(wù),在第一次執(zhí)行任務(wù)時創(chuàng)建足夠多的線程,并處理任務(wù),之后每個工作線程自動從任務(wù)隊列中獲取線程,直到任務(wù)隊列中任務(wù)為0為止,此時線程處于等待狀態(tài),一旦有工作任務(wù)加入任務(wù)隊列中,即刻喚醒工作線程進行處理,實現(xiàn)線程的可復(fù)用性。
線程池一般包括四個基本組成部分:
(1)線程池管理器
用于創(chuàng)建線程池,銷毀線程池,添加新任務(wù)。
(2)工作線程
線程池中線程,可循環(huán)執(zhí)行任務(wù),在沒有任務(wù)時處于等待狀態(tài)。
(3)任務(wù)隊列
用于存放沒有處理的任務(wù),一種緩存機制。
(4)任務(wù)接口
每個任務(wù)必須實現(xiàn)的接口,供工作線程調(diào)度任務(wù)的執(zhí)行,主要規(guī)定了任務(wù)的開始和收尾工作,和任務(wù)的狀態(tài)。
76、創(chuàng)建線程池的幾個核心構(gòu)造參數(shù)
// Java線程池的完整構(gòu)造函數(shù) public ThreadPoolExecutor( int corePoolSize, // 線程池長期維持的最小線程數(shù),即使線程處于Idle狀態(tài),也不會回收。 int maximumPoolSize, // 線程數(shù)的上限 long keepAliveTime, // 線程最大生命周期。 TimeUnit unit, //時間單位 BlockingQueue<Runnable> workQueue, //任務(wù)隊列。當(dāng)線程池中的線程都處于運行狀態(tài),而此時任務(wù)數(shù)量繼續(xù)增加,則需要一個容器來容納這些任務(wù),這就是任務(wù)隊列。 ThreadFactory threadFactory, // 線程工廠。定義如何啟動一個線程,可以設(shè)置線程名稱,并且可以確認(rèn)是否是后臺線程等。 RejectedExecutionHandler handler // 拒絕任務(wù)處理器。由于超出線程數(shù)量和隊列容量而對繼續(xù)增加的任務(wù)進行處理的程序。 )
77、線程池中的線程是怎么創(chuàng)建的?是一開始就隨著線程池的啟動創(chuàng)建好的嗎?
線程池中的線程是在第一次提交任務(wù)submit時創(chuàng)建的
創(chuàng)建線程的方式有繼承Thread和實現(xiàn)Runnable,重寫run方法,start開始執(zhí)行,wait等待,sleep休眠,shutdown停止。
(1)newSingleThreadExecutor:單線程池。
顧名思義就是一個池中只有一個線程在運行,該線程永不超時,而且由于是一個線程,當(dāng)有多個任務(wù)需要處理時,會將它們放置到一個無界阻塞隊列中逐個處理,它的實現(xiàn)代碼如下:
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 { // 創(chuàng)建單線程執(zhí)行器 ExecutorService es = Executors.newSingleThreadExecutor(); // 執(zhí)行一個任務(wù) Future<String> future = es.submit(new Callable<String>() { @Override public String call() throws Exception { return ""; } }); // 獲得任務(wù)執(zhí)行后的返回值 System.out.println("返回值:" + future.get()); // 關(guān)閉執(zhí)行器 es.shutdown(); }
(2)newCachedThreadPool:緩沖功能的線程。
建立了一個線程池,而且線程數(shù)量是沒有限制的(當(dāng)然,不能超過Integer的最大值),新增一個任務(wù)即有一個線程處理,或者復(fù)用之前空閑的線程,或者重親啟動一個線程,但是一旦一個線程在60秒內(nèi)一直處于等待狀態(tài)時(也就是一分鐘無事可做),則會被終止,其源碼如下:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
這里需要說明的是,任務(wù)隊列使用了同步阻塞隊列,這意味著向隊列中加入一個元素,即可喚醒一個線程(新創(chuàng)建的線程或復(fù)用空閑線程來處理),這種隊列已經(jīng)沒有隊列深度的概念了。
(3)newFixedThreadPool:固定線程數(shù)量的線程池。
在初始化時已經(jīng)決定了線程的最大數(shù)量,若任務(wù)添加的能力超出了線程的處理能力,則建立阻塞隊列容納多余的任務(wù),其源碼如下:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大線程數(shù)量為nThreads。
如果任務(wù)增長的速度非???,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?
會按照ThreadPoolExecutor默認(rèn)的拒絕策略(默認(rèn)是DiscardPolicy,直接丟棄)來處理。
以上三種線程池執(zhí)行器都是ThreadPoolExecutor的簡化版,目的是幫助開發(fā)人員屏蔽過得線程細(xì)節(jié),簡化多線程開發(fā)。
當(dāng)需要運行異步任務(wù)時,可以直接通過Executors獲得一個線程池,然后運行任務(wù),不需要關(guān)注ThreadPoolExecutor的一系列參數(shù)時什么含義。
當(dāng)然,有時候這三個線程不能滿足要求,此時則可以直接操作ThreadPoolExecutor來實現(xiàn)復(fù)雜的多線程計算。
newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是線程池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操作,需要了解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。
78、volatile 關(guān)鍵字的作用
對于可見性,Java 提供了 volatile 關(guān)鍵字來保證可見性和禁止指令重排。
volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。當(dāng)一個共享變量被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當(dāng)有其他線程需要讀取時,它會去內(nèi)存中讀取新值。
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結(jié)合,保證了原子性,詳細(xì)的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
volatile 常用于多線程環(huán)境下的單次操作(單次讀或者單次寫)。
79、既然 volatile 能夠保證線程間的變量可見性,是不是就意味著基于 volatile 變量的運算就是并發(fā)安全的?
volatile修飾的變量在各個線程的工作內(nèi)存中不存在一致性的問題(在各個線程工作的內(nèi)存中,volatile修飾的變量也會存在不一致的情況,但是由于每次使用之前都會先刷新主存中的數(shù)據(jù)到工作內(nèi)存,執(zhí)行引擎看不到不一致的情況,因此可以認(rèn)為不存在不一致的問題),但是java的運算并非原子性的操作,導(dǎo)致volatile在并發(fā)下并非是線程安全的。
80、ThreadLocal 是什么?有哪些使用場景?
ThreadLocal 是一個本地線程副本變量工具類,在每個線程中都創(chuàng)建了一個 ThreadLocalMap 對象
簡單說 ThreadLocal 就是一種以空間換時間的做法,每個線程可以訪問自己內(nèi)部 ThreadLocalMap 對象內(nèi)的 value。通過這種方式,避免資源在多線程間共享。
原理:線程局部變量是局限于線程內(nèi)部的變量,屬于線程自身所有,不在多個線程間共享。
Java提供ThreadLocal類來支持線程局部變量,是一種實現(xiàn)線程安全的方式。但是在管理環(huán)境下(如 web 服務(wù)器)使用線程局部變量的時候要特別小心,在這種情況下,工作線程的生命周期比任何應(yīng)用變量的生命周期都要長。
任何線程局部變量一旦在工作完成后沒有釋放,Java 應(yīng)用就存在內(nèi)存泄露的風(fēng)險。
經(jīng)典的使用場景是為每個線程分配一個 JDBC 連接 Connection。
這樣就可以保證每個線程的都在各自的 Connection 上進行數(shù)據(jù)庫的操作,不會出現(xiàn) A 線程關(guān)了 B線程正在使用的 Connection; 還有 Session 管理 等問題。
81、請談?wù)?ThreadLocal 是怎么解決并發(fā)安全的?
在java程序中,常用的有兩種機制來解決多線程并發(fā)問題,一種是sychronized方式,通過鎖機制,一個線程執(zhí)行時,讓另一個線程等待,是以時間換空間的方式來讓多線程串行執(zhí)行。
而另外一種方式就是ThreadLocal方式,通過創(chuàng)建線程局部變量,以空間換時間的方式來讓多線程并行執(zhí)行。兩種方式各有優(yōu)劣,適用于不同的場景,要根據(jù)不同的業(yè)務(wù)場景來進行選擇。
在spring的源碼中,就使用了ThreadLocal來管理連接,在很多開源項目中,都經(jīng)常使用ThreadLocal來控制多線程并發(fā)問題,因為它足夠的簡單,我們不需要關(guān)心是否有線程安全問題,因為變量是每個線程所特有的。
82、很多人都說要慎用 ThreadLocal,談?wù)勀愕睦斫?,使?ThreadLocal 需要注意些什么?
ThreadLocal 變量解決了多線程環(huán)境下單個線程中變量的共享問題,使用名為ThreadLocalMap的哈希表進行維護(key為ThreadLocal變量名,value為ThreadLocal變量的值);
使用時需要注意以下幾點:
- 線程之間的threadLocal變量是互不影響的
- 使用private final static進行修飾,防止多實例時內(nèi)存的泄露問題
- 線程池環(huán)境下使用后將threadLocal變量remove掉或設(shè)置成一個初始值
83、為什么代碼會重排序?
在執(zhí)行程序時,為了提供性能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎么排序就怎么排序
它需要滿足以下兩個條件:
- 在單線程環(huán)境下不能改變程序運行的結(jié)果;
- 存在數(shù)據(jù)依賴關(guān)系的不允許重排序
需要注意的是:重排序不會影響單線程環(huán)境的執(zhí)行結(jié)果,但是會破壞多線程的執(zhí)行語義。
84、什么是自旋
很多 synchronized 里面的代碼只是一些很簡單的代碼,執(zhí)行時間非???,此時等待的線程都加鎖可能是一種不太值得的操作,因為線程阻塞涉及到用戶態(tài)和內(nèi)核態(tài)切換的問題。
既然 synchronized 里面的代碼執(zhí)行得非常快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界做忙循環(huán),這就是自旋。
如果做了多次循環(huán)發(fā)現(xiàn)還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
85、多線程中 synchronized 鎖升級的原理是什么?
synchronized 鎖升級原理:在鎖對象的對象頭里面有一個 threadid 字段,在第一次訪問的時候 threadid 為空,jvm 讓其持有偏向鎖,并將 threadid 設(shè)置為其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致
如果一致則可以直接使用此對象,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環(huán)一定次數(shù)來獲取鎖,執(zhí)行一定次數(shù)之后,如果還沒有正常獲取到要使用的對象
此時就會把鎖從輕量級升級為重量級鎖,此過程就構(gòu)成了 synchronized 鎖的升級。
鎖的升級的目的:鎖升級是為了減低了鎖帶來的性能消耗。
在 Java 6 之后優(yōu)化 synchronized 的實現(xiàn)方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。
86、synchronized 和 ReentrantLock 區(qū)別是什么?
synchronized 是和 if、else、for、while 一樣的關(guān)鍵字,ReentrantLock 是類,這是二者的本質(zhì)區(qū)別。
既然 ReentrantLock 是類,那么它就提供了比synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量
synchronized 早期的實現(xiàn)比較低效,對比 ReentrantLock,大多數(shù)場景性能都相差較大,但是在 Java 6 中對 synchronized 進行了非常多的改進。
相同點:兩者都是可重入鎖
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖。
比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當(dāng)其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。
同一個線程每次獲取鎖,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖。
主要區(qū)別如下:
- ReentrantLock 使用起來比較靈活,但是必須有釋放鎖的配合動作;
- ReentrantLock 必須手動獲取與釋放鎖,而 synchronized 不需要手動釋放和開啟鎖;
- ReentrantLock 只適用于代碼塊鎖,而 synchronized 可以修飾類、方法、變量等。
- 二者的鎖機制其實也是不一樣的。ReentrantLock 底層調(diào)用的是 Unsafe 的park 方法加鎖,synchronized 操作的應(yīng)該是對象頭中 mark word
Java中每一個對象都可以作為鎖,這是synchronized實現(xiàn)同步的基礎(chǔ):
- 普通同步方法,鎖是當(dāng)前實例對象
- 靜態(tài)同步方法,鎖是當(dāng)前類的class對象
- 同步方法塊,鎖是括號里面的對象
87、Java Concurrency API 中的 Lock 接口(Lock interface)是什么?對比同步它有什么優(yōu)勢?
Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操作。
他們允許更靈活的結(jié)構(gòu),可以具有完全不同的性質(zhì),并且可以支持多個相關(guān)類的條件對象。
它的優(yōu)勢有:
(1)可以使鎖更公平
(2)可以使線程在等待鎖的時候響應(yīng)中斷
(3)可以讓線程嘗試獲取鎖,并在無法獲取鎖的時候立即返回或者等待一段時間
(4)可以在不同的范圍,以不同的順序獲取和釋放鎖
整體上來說 Lock 是 synchronized 的擴展版,Lock 提供了無條件的、可輪詢的(tryLock 方法)、定時的(tryLock 帶參方法)、可中斷的(lockInterruptibly)、可多條件隊列的(newCondition 方法)鎖操作。
另外 Lock 的實現(xiàn)類基本都支持非公平鎖(默認(rèn))和公平鎖,synchronized 只支持非公平鎖,當(dāng)然,在大部分情況下,非公平鎖是高效的選擇。
88、jsp 和 servlet 有什么區(qū)別?
(1)servlet是服務(wù)器端的Java程序,它擔(dān)當(dāng)客戶端和服務(wù)端的中間層。
(2)jsp全名為Java server pages,中文名叫Java服務(wù)器頁面,其本質(zhì)是一個簡化的servlet設(shè)計。JSP是一種動態(tài)頁面設(shè)計,它的主要目的是將表示邏輯從servlet中分離出來。
(3)JVM只能識別Java代碼,不能識別JSP,JSP編譯后變成了servlet,web容器將JSP的代碼編譯成JVM能夠識別的Java類(servlet)。
(4)JSP有內(nèi)置對象、servlet沒有內(nèi)置對象。
89、jsp 有哪些內(nèi)置對象?作用分別是什么?
JSP九大內(nèi)置對象:
- pageContext,頁面上下文對象,相當(dāng)于頁面中所有功能的集合,通過它可以獲取JSP頁面的out、request、response、session、application對象。
- request
- response
- session
- application,應(yīng)用程序?qū)ο?,application實現(xiàn)了用戶間數(shù)據(jù)的共享,可存放全局變量,它開始于服務(wù)器啟動,知道服務(wù)器關(guān)閉。
- page,就是JSP本身。
- exception
- out,out用于在web瀏覽器內(nèi)輸出信息,并且管理應(yīng)用服務(wù)器上的輸出緩沖區(qū),作用域page。
- config,取得服務(wù)器的配置信息。
90、forward 和 redirect 的區(qū)別?
- forward是直接請求轉(zhuǎn)發(fā);redirect是間接請求轉(zhuǎn)發(fā),又叫重定向。
- forward,客戶端和瀏覽器執(zhí)行一次請求;redirect,客戶端和瀏覽器執(zhí)行兩次請求。
- forward,經(jīng)典的MVC模式就是forward;redirect,用于避免用戶的非正常訪問。(例如用戶非正常訪問,servlet就可以將HTTP請求重定向到登錄頁面)。
- forward,地址不變;redirect,地址改變。
- forward常用方法:RequestDispatcher類的forward()方法;redirect常用方法:HttpServletRequest類的sendRedirect()方法。
91、說一下 jsp 的 4 種作用域?
application、session、request、page
92、session 和 cookie 有什么區(qū)別?
(1)存儲位置不同
- cookie在客戶端瀏覽器;
- session在服務(wù)器;
(2)存儲容量不同
- cookie<=4K,一個站點最多保留20個cookie;
- session沒有上線,出于對服務(wù)器的保護,session內(nèi)不可存過多東西,并且要設(shè)置session刪除機制;
(3)存儲方式不同
- cookie只能保存ASCII字符串,并需要通過編碼方式存儲為Unicode字符或者二進制數(shù)據(jù);
- session中能存儲任何類型的數(shù)據(jù),包括并不局限于String、integer、list、map等;
(4)隱私策略不同
- cookie對客戶端是可見的,不安全;
- session存儲在服務(wù)器上,安全;
(5)有效期不同
- 開發(fā)可以通過設(shè)置cookie的屬性,達到使cookie長期有效的效果;
- session依賴于名為JESSIONID的cookie,而cookie JSESSIONID的過期時間默認(rèn)為-1,只需關(guān)閉窗口該session就會失效,因而session達不到長期有效的效果;
(6)跨域支持上不同
- cookie支持跨域;
- session不支持跨域;
93、如果客戶端禁止 cookie 能實現(xiàn) session 還能用嗎?
一般默認(rèn)情況下,在會話中,服務(wù)器存儲 session 的 sessionid 是通過 cookie 存到瀏覽器里。
如果瀏覽器禁用了 cookie,瀏覽器請求服務(wù)器無法攜帶 sessionid,服務(wù)器無法識別請求中的用戶身份,session失效。
但是可以通過其他方法在禁用 cookie 的情況下,可以繼續(xù)使用session。
- 通過url重寫,把 sessionid 作為參數(shù)追加的原 url 中,后續(xù)的瀏覽器與服務(wù)器交互中攜帶 sessionid 參數(shù)。
- 服務(wù)器的返回數(shù)據(jù)中包含 sessionid,瀏覽器發(fā)送請求時,攜帶 sessionid 參數(shù)。
- 通過 Http 協(xié)議其他 header 字段,服務(wù)器每次返回時設(shè)置該 header 字段信息,瀏覽器中 js 讀取該 header 字段,請求服務(wù)器時,js設(shè)置攜帶該 header 字段。
94、什么是上下文切換?
多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。
當(dāng)一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。
概括來說就是:
- 當(dāng)前任務(wù)在執(zhí)行完 CPU 時間片切換到另一個任務(wù)之前會先保存自己的狀態(tài),以便下次再切換回這個任務(wù)時,可以再加載這個任務(wù)的狀態(tài)。
- 任務(wù)從保存到再加載的過程就是一次上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當(dāng)可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。
所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統(tǒng)中時間消耗最大的操作。
Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
95、cookie、session、token
1、session機制
session是服務(wù)端存儲的一個對象,主要用來存儲所有訪問過該服務(wù)端的客戶端的用戶信息(也可以存儲其他信息),從而實現(xiàn)保持用戶會話狀態(tài)。
但是服務(wù)器重啟時,內(nèi)存會被銷毀,存儲的用戶信息也就消失了。
不同的用戶訪問服務(wù)端的時候會在session對象中存儲鍵值對,“鍵”用來存儲開啟這個用戶信息的“鑰匙”,在登錄成功后,“鑰匙”通過cookie返回給客戶端,客戶端存儲為sessionId記錄在cookie中。
當(dāng)客戶端再次訪問時,會默認(rèn)攜帶cookie中的sessionId來實現(xiàn)會話機制。
(1)session是基于cookie的。
- cookie的數(shù)據(jù)4k左右;
- cookie存儲數(shù)據(jù)的格式:字符串key=value
- cookie存儲有效期:可以自行通過expires進行具體的日期設(shè)置,如果沒設(shè)置,默認(rèn)是關(guān)閉瀏覽器時失效。
- cookie有效范圍:當(dāng)前域名下有效。所以session這種會話存儲方式方式只適用于客戶端代碼和服務(wù)端代碼運行在同一臺服務(wù)器上(前后端項目協(xié)議、域名、端口號都一致,即在一個項目下)
(2)session持久化
用于解決重啟服務(wù)器后session消失的問題。在數(shù)據(jù)庫中存儲session,而不是存儲在內(nèi)存中。通過包:express-mysql-session。
當(dāng)客戶端存儲的cookie失效后,服務(wù)端的session不會立即銷毀,會有一個延時,服務(wù)端會定期清理無效session,不會造成無效數(shù)據(jù)占用存儲空間的問題。
2、token機制
適用于前后端分離的項目(前后端代碼運行在不同的服務(wù)器下)
請求登錄時,token和sessionid原理相同,是對key和key對應(yīng)的用戶信息進行加密后的加密字符,登錄成功后,會在響應(yīng)主體中將{token:“字符串”}返回給客戶端。
客戶端通過cookie都可以進行存儲。
再次請求時不會默認(rèn)攜帶,需要在請求攔截器位置給請求頭中添加認(rèn)證字段Authorization攜帶token信息,服務(wù)器就可以通過token信息查找用戶登錄狀態(tài)。
96、說一下 session 的工作原理?
當(dāng)客戶端登錄完成后,會在服務(wù)端產(chǎn)生一個session,此時服務(wù)端會將sessionid返回給客戶端瀏覽器。
客戶端將sessionid儲存在瀏覽器的cookie中,當(dāng)用戶再次登錄時,會獲得對應(yīng)的sessionid,然后將sessionid發(fā)送到服務(wù)端請求登錄,服務(wù)端在內(nèi)存中找到對應(yīng)的sessionid,完成登錄,如果找不到,返回登錄頁面。
97、http 響應(yīng)碼 301 和 302 代表的是什么?有什么區(qū)別?
- 301和302狀態(tài)碼都表示重定向,當(dāng)瀏覽器拿到服務(wù)器返回的這個狀態(tài)碼后悔自動跳轉(zhuǎn)到一個新的URL地址。
- 301代表永久性重定向,舊地址被永久移除,客戶端向新地址發(fā)送請求。
- 302代表暫時性重定向,舊地址還在,客戶端繼續(xù)向舊地址發(fā)送請求。
- 303代表暫時性重定向,重定向到新地址時,必須使用GET方法請求新地址。
- 307代表暫時性重定向,與302的區(qū)別在于307不允許從POST改為GET。
- 307代表永久性重定向,與301的區(qū)別在于308不允許從POST改為GET。
98、簡述 tcp 和 udp的區(qū)別?
- TCP是傳輸控制協(xié)議,UDP是用戶數(shù)據(jù)表協(xié)議;
- TCP長連接,UDP無連接;
- UDP程序結(jié)構(gòu)較簡單,只需發(fā)送,無須接收;
- TCP可靠,保證數(shù)據(jù)正確性、順序性;UDP不可靠,可能丟數(shù)據(jù);
- TCP適用于少量數(shù)據(jù),UDP適用于大量數(shù)據(jù)傳輸;
- TCP速度慢,UDP速度快;
99、tcp 為什么要三次握手,兩次不行嗎?為什么?
因為客戶端和服務(wù)端都要確認(rèn)連接,
①客戶端請求連接服務(wù)端;
②針對客戶端的請求確認(rèn)應(yīng)答,并請求建立連接;
③針對服務(wù)端的請求確認(rèn)應(yīng)答,建立連接;
兩次無法確保A能收到B的數(shù)據(jù);
100、OSI 的七層模型都有哪些?
101、get 和 post 請求有哪些區(qū)別?
- get請求參數(shù)是連接在url后面的,而post請求參數(shù)是存放在requestbody內(nèi)的;
- get請求因為瀏覽器對url長度有限制,所以參數(shù)個數(shù)有限制,而post請求參數(shù)個數(shù)沒有限制;
- 因為get請求參數(shù)暴露在url上,所以安全方面post比get更加安全;
- get請求只能進行url編碼,而post請求可以支持多種編碼方式;
- get請求參數(shù)會保存在瀏覽器歷史記錄內(nèi),post請求并不會;
- get請求瀏覽器會主動cache,post并不會,除非主動設(shè)置;
- get請求產(chǎn)生1個tcp數(shù)據(jù)包,post請求產(chǎn)生2個tcp數(shù)據(jù)包;
- 在瀏覽器進行回退操作時,get請求是無害的,而post請求則會重新請求一次;
- 瀏覽器在發(fā)送get請求時會將header和data一起發(fā)送給服務(wù)器,服務(wù)器返回200狀態(tài)碼,而在發(fā)送post請求時,會先將header發(fā)送給服務(wù)器,服務(wù)器返回100,之后再將data發(fā)送給服務(wù)器,服務(wù)器返回200 OK;
102、什么是 XSS 攻擊,如何避免?
xss(Cross Site Scripting),即跨站腳本攻擊,是一種常見于web應(yīng)用程序中的計算機安全漏洞。
指的是在用戶瀏覽器上,在渲染DOM樹的時候,執(zhí)行了不可預(yù)期的JS腳本,從而發(fā)生了安全問題。
XSS就是通過在用戶端注入惡意的可運行腳本,若服務(wù)端對用戶的輸入不進行處理,直接將用戶的輸入輸出到瀏覽器,然后瀏覽器將會執(zhí)行用戶注入的腳本。
所以XSS攻擊的核心就是瀏覽器渲染DOM的時候?qū)⑽谋拘畔⒔馕龀蒍S腳本從而引發(fā)JS腳本注入,那么XSS攻擊的防御手段就是基于瀏覽器渲染這一步去做防御。
只要我們使用HTML編碼將瀏覽器需要渲染的信息編碼后,瀏覽器在渲染DOM元素的時候,會自動解碼需要渲染的信息,將上述信息解析成字符串而不是JS腳本,這就是我們防御XSS攻擊的核心想法。
預(yù)防:
1、獲取用戶的輸入,不用innerHtml,用innerText.
2、對用戶的輸入進行過濾,如對& < > " ' /等進行轉(zhuǎn)義;
103、什么是 CSRF 攻擊,如何避免?
跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通??s寫為 CSRF 或者 XSRF, 是一種挾制用戶在當(dāng)前已登錄的Web應(yīng)用程序上執(zhí)行非本意的操作的攻擊方法。
跟跨網(wǎng)站腳本(XSS)相比,XSS 利用的是用戶對指定網(wǎng)站的信任,CSRF 利用的是網(wǎng)站對用戶網(wǎng)頁瀏覽器的信任。
1、攻擊細(xì)節(jié)
跨站請求攻擊,簡單地說,是攻擊者通過一些技術(shù)手段欺騙用戶的瀏覽器去訪問一個自己曾經(jīng)認(rèn)證過的網(wǎng)站并運行一些操作(如發(fā)郵件,發(fā)消息,甚至財產(chǎn)操作如轉(zhuǎn)賬和購買商品)。
由于瀏覽器曾經(jīng)認(rèn)證過,所以被訪問的網(wǎng)站會認(rèn)為是真正的用戶操作而去運行。
這利用了web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發(fā)自某個用戶的瀏覽器,卻不能保證請求本身是用戶自愿發(fā)出的。
例子
假如一家銀行用以運行轉(zhuǎn)賬操作的URL地址如下:http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一個惡意攻擊者可以在另一個網(wǎng)站上放置如下代碼: <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
如果有賬戶名為Alice的用戶訪問了惡意站點,而她之前剛訪問過銀行不久,登錄信息尚未過期,那么她就會損失1000資金。
這種惡意的網(wǎng)址可以有很多種形式,藏身于網(wǎng)頁中的許多地方。此外,攻擊者也不需要控制放置惡意網(wǎng)址的網(wǎng)站。
例如他可以將這種地址藏在論壇,博客等任何用戶生成信息的網(wǎng)站中。這意味著如果服務(wù)端沒有合適的防御措施的話,用戶即使訪問熟悉的可信網(wǎng)站也有受攻擊的危險。
透過例子能夠看出,攻擊者并不能通過CSRF攻擊來直接獲取用戶的賬戶控制權(quán),也不能直接竊取用戶的任何信息。
他們能做到的,是欺騙用戶瀏覽器,讓其以用戶的名義運行操作。
2、防御措施
檢查Referer字段
HTTP頭中有一個Referer字段,這個字段用以標(biāo)明請求來源于哪個地址。
在處理敏感數(shù)據(jù)請求時,通常來說,Referer字段應(yīng)和請求的地址位于同一域名下。以上文銀行操作為例,Referer字段地址通常應(yīng)該是轉(zhuǎn)賬按鈕所在的網(wǎng)頁地址,應(yīng)該也位于www.examplebank.com之下。
而如果是CSRF攻擊傳來的請求,Referer字段會是包含惡意網(wǎng)址的地址,不會位于www.examplebank.com之下,這時候服務(wù)器就能識別出惡意的訪問。
這種辦法簡單易行,工作量低,僅需要在關(guān)鍵訪問處增加一步校驗。
但這種辦法也有其局限性,因其完全依賴瀏覽器發(fā)送正確的Referer字段。
雖然http協(xié)議對此字段的內(nèi)容有明確的規(guī)定,但并無法保證來訪的瀏覽器的具體實現(xiàn),亦無法保證瀏覽器沒有安全漏洞影響到此字段。
并且也存在攻擊者攻擊某些瀏覽器,篡改其Referer字段的可能。
3、添加校驗token
由于CSRF的本質(zhì)在于攻擊者欺騙用戶去訪問自己設(shè)置的地址,所以如果要求在訪問敏感數(shù)據(jù)請求時,要求用戶瀏覽器提供不保存在cookie中,并且攻擊者無法偽造的數(shù)據(jù)作為校驗,那么攻擊者就無法再運行CSRF攻擊。
這種數(shù)據(jù)通常是窗體中的一個數(shù)據(jù)項。
服務(wù)器將其生成并附加在窗體中,其內(nèi)容是一個偽隨機數(shù)
。當(dāng)客戶端通過窗體提交請求時,這個偽隨機數(shù)也一并提交上去以供校驗。正常的訪問時,客戶端瀏覽器能夠正確得到并傳回這個偽隨機數(shù),而通過CSRF傳來的欺騙性攻擊中,攻擊者無從事先得知這個偽隨機數(shù)的值,服務(wù)端就會因為校驗token的值為空或者錯誤,拒絕這個可疑請求。
104、如何實現(xiàn)跨域?說一下 JSONP 實現(xiàn)原理?
1、jsonp原理詳解——終于搞清楚jsonp是啥了
2、最流行的跨域方案cors
cors是目前主流的跨域解決方案,跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應(yīng)用被準(zhǔn)許訪問來自不同源服務(wù)器上的指定的資源。
當(dāng)一個資源從與該資源本身所在的服務(wù)器不同的域、協(xié)議或端口請求一個資源時,資源會發(fā)起一個跨域 HTTP 請求。
3、最方便的跨域方案Nginx
nginx是一款極其強大的web服務(wù)器,其優(yōu)點就是輕量級、啟動快、高并發(fā)。
現(xiàn)在的新項目中nginx幾乎是首選,我們用node或者java開發(fā)的服務(wù)通常都需要經(jīng)過nginx的反向代理。
反向代理的原理很簡單,即所有客戶端的請求都必須先經(jīng)過nginx的處理,nginx作為代理服務(wù)器再講請求轉(zhuǎn)發(fā)給node或者java服務(wù),這樣就規(guī)避了同源策略。
到此這篇關(guān)于Java經(jīng)典面試題最全匯總208道(二)的文章就介紹到這了,更多相關(guān)Java面試題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java設(shè)計模式之狀態(tài)模式State Pattern詳解
這篇文章主要介紹了Java設(shè)計模式之狀態(tài)模式State Pattern,狀態(tài)模式允許一個對象在其內(nèi)部狀態(tài)改變的時候改變其行為。這個對象看上去就像是改變了它的類一樣2022-11-11SpringBoot Session接口驗證實現(xiàn)流程詳解
這篇文章主要介紹了SpringBoot+Session實現(xiàn)接口驗證(過濾器+攔截器)文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09solr 配置中文分析器/定義業(yè)務(wù)域/配置DataImport功能方法(測試用)
下面小編就為大家?guī)硪黄猻olr 配置中文分析器/定義業(yè)務(wù)域/配置DataImport功能方法(測試用)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09