分布式和微服务面试
文章目錄
- 一、Spring Boot常見面試題
- 1、Spring、Spring Boot和Spring Cloud的關(guān)系
- 2、Spring Boot如何配置多環(huán)境?
- 3、實(shí)際工作中,如何全局處理異常?
- 二、線程常見面試題
- 1、線程如何啟動(dòng)?
- 2、實(shí)現(xiàn)多線程的方法有幾種?
- 3、創(chuàng)建線程的原理是什么?
- 4、線程有哪幾種狀態(tài)? 生命周期是什么?
- 三、分布式的面試題
- 1、什么是分布式?
- 2、分布式和單體結(jié)構(gòu)哪個(gè)更好?
- 3、CAP理論是什么?
- 4、CAP怎么選?
- 四、Docker相關(guān)面試題
- 1、為什么需要Docker ?
- 2、Docker的架構(gòu)是什么樣的?
- 3、Docker的網(wǎng)絡(luò)模式有哪些?
- 五、Nginx和Zookeeper相關(guān)面試題
- 1、Nginx的適用場(chǎng)景有哪些?
- 2、Nginx常用命令有哪些?
- 3、Zookeeper有哪些節(jié)點(diǎn)類型?
- 六、RabbitMQ相關(guān)面試題
- 1、為什么要用消息隊(duì)列?什么場(chǎng)景用?
- 2、RabbitMQ核心概念
- 3、交換機(jī)工作模式有哪4種?
- 七、微服務(wù)相關(guān)
- 1、微服務(wù)有哪兩大門派?
- 2、Spring Cloud核心組件有哪些?
- 3、能畫一下Eureka架構(gòu)嗎?
- 4、負(fù)載均衡的兩種類型是什么?
- 5、為什么需要斷路器?
- 6、為什么需要網(wǎng)關(guān)?
- 7、Dubbo的工作流程是什么?
- 八、鎖分類、死鎖
- 1、Lock簡(jiǎn)介、地位、作用
- 2、Lock主要方法介紹
- 3、synchronized和Lock有什么異同?
- 4、你知道有幾種鎖?
- 5、對(duì)比公平和非公平的優(yōu)缺點(diǎn)
- 6、什么是樂(lè)觀鎖和悲觀鎖?
- 7、自旋鎖和阻塞鎖
- 8、可重入的性質(zhì)
- 9、中斷鎖和不可中斷鎖
- 10、什么是死鎖?
- 九、HashMap和final
- 1、Hashmap為什么不安全?
- 2、final的作用是什么?有哪些用法?
- 十、單例模式的八種寫法
- 1、什么是單例模式?
- 2、為什么需要單例模式?
- 3、應(yīng)用場(chǎng)景
- 4、單例模式的八種寫法
- 1)、餓漢式(靜態(tài)常量)(可用)
- 2)、餓漢式(靜態(tài)代碼塊)(可用)
- 3)、懶漢式(線程不安全)
- 4)、懶漢式(線程安全,同步方法)(不推薦)
- 5)、假如我們升級(jí)一下(同步的范圍盡量縮小),上面的代碼
- 6)、雙重檢查(推薦)
- 7)、靜態(tài)內(nèi)部類寫法(推薦用)
- 8)、枚舉單例模式
一、Spring Boot常見面試題
1、Spring、Spring Boot和Spring Cloud的關(guān)系
- Spring最初利用IOC和AOP解耦
- 按照這種模式搞了MVC框架
- 寫很多樣板代碼很麻煩,就有了Spring Boot
- Spring Cloud是在Spring Boot基礎(chǔ)上誕生的
你知不知道spring、spring boot和spring cloud的關(guān)系呢?
????????這是一道常見的面試題,并且有可能面試官會(huì)從這道題出發(fā)來(lái)逐步的去考察你對(duì)于spring spring boot以及spring cloud的了解,有可能呢,有的候選人啊,他不知道spring boot,也有的候選人呢不知道spring cloud,所以通過(guò)這道題呢,其實(shí)可以挖掘出候選人的很多信息,那大部分同學(xué)啊至少都是知道spring的,所以首先呢,我們要從spring出發(fā),去講一下spring他的最大的特點(diǎn)。
????????對(duì)于spring而言,它最大的兩個(gè)核心特點(diǎn)呢就是IOC和AOP。這是spring的兩大核心功能,并且呢,spring在這兩大核心功能的基礎(chǔ)上逐漸發(fā)展出來(lái)了,像spring事務(wù)、spring mvc這樣的框架,而且這些框架呢,其實(shí)也都是非常強(qiáng)大非常偉大的,最終呢也就此成就了spring帝國(guó),隨著spring逐漸完善,它幾乎可以解決企業(yè)開發(fā)中遇到的所有的問(wèn)題,不過(guò)也正是因?yàn)樗鼉?nèi)容的豐富以及功能不斷完善,不斷強(qiáng)大,導(dǎo)致了它的體積也越來(lái)越大,而且也越來(lái)越笨重,這個(gè)笨重主要就體現(xiàn)在我們即便是開發(fā)一個(gè)簡(jiǎn)單的程序,都需要對(duì)它進(jìn)行很繁瑣的配置,而且呢有很多配置都是大同小異的,不同的項(xiàng)目之間,他們配置起來(lái)的內(nèi)容呢,幾乎是一模一樣的,但是你不配置呢又不行,所以就寫了很多的樣板代碼,也正是因?yàn)檫@樣的樣板代碼很麻煩。才誕生了spring boot,這也是spring boot誕生的初衷。
????????最開始呢想開發(fā)spring boot的最核心的原因就是希望能解決掉,開發(fā)人員開發(fā)一個(gè)程序,這種配置太繁瑣的這個(gè)問(wèn)題,而且啊,這個(gè)開發(fā)spring的配套公司,也把spring boot定位為能夠幫助程序員快速開發(fā)的一個(gè)快速框架,而且呢,這也是企業(yè)中所夢(mèng)寐以求的,開發(fā)一個(gè)程序速度越快對(duì)于業(yè)務(wù)就越有利,也有利于搶占市場(chǎng)的先機(jī),他們之間的關(guān)系,所以說(shuō)啊,這里的第1層關(guān)系就出來(lái)了,spring和spring boot是什么關(guān)系呢?
????????其實(shí)是這樣子的,spring boot他是在強(qiáng)大的spring帝國(guó)生態(tài)上面發(fā)展而來(lái)的,而且發(fā)明spring boot是為了讓人們更容易的去使用spring,所以說(shuō)如果最開始沒(méi)有spring的強(qiáng)大的功能和生態(tài)的話,那就更不可能會(huì)有后期的spring boot的火熱,那spring boot呢,它的理念是約定優(yōu)于配置,所以正是在這樣理念的驅(qū)動(dòng)下,很多配置我們都不需要再去額外的進(jìn)行配置了,直接按照約定來(lái)就可以了,這也讓我們的spring煥發(fā)了生機(jī),讓他的生命力更加強(qiáng)大了,那現(xiàn)在啊我們就說(shuō)到了spring cloud,spring boot和spring cloud又是什么關(guān)系呢?
????????其實(shí)spring cloud和spring boot的關(guān)系就類似于spring boot和spring的關(guān)系,也就是說(shuō)啊,spring cloud他是在我們spring boot的基礎(chǔ)上誕生的,spring cloud并沒(méi)有去重復(fù)的造輪子,他只是將各家公司開發(fā)的,比較成熟的,經(jīng)得起考驗(yàn)的服務(wù)框架呢,給組合了起來(lái),并且啊同樣的去利用spring boot這樣的風(fēng)格屏蔽掉復(fù)雜的配置和實(shí)現(xiàn)原理,給開發(fā)者呢,留出了一套簡(jiǎn)單易懂的、容易部署、容易維護(hù)的微服務(wù)框架,它是一系列框架的有序集合,比如說(shuō)就包含服務(wù)注冊(cè)與發(fā)現(xiàn)、登錄器、網(wǎng)關(guān)等等,而且每一個(gè)模塊啊,它其實(shí)都是具有spring boot風(fēng)格的,比如說(shuō)呢,可以做到一鍵的啟動(dòng),綜上啊,我們就理解了,我們來(lái)總結(jié)一下,正是由于spring和spring這兩個(gè)強(qiáng)大的功能才有了spring,而spring生態(tài)不斷發(fā)展蓬勃壯大之后,由于它的配置繁瑣,所以因此呢,就誕生了spring boot,spring boot讓spring更加有生命力,而spring cloud呢正是基于spring boot的一套微服務(wù)框架,所以啊,從中也可以看出 spring,spring boot,spring cloud之間也是具有層層遞進(jìn)逐步演化這樣的關(guān)系的,這也符合軟件發(fā)展的歷程,軟件發(fā)展的通常也不是一蹴而就的,也是不斷迭代不斷升級(jí)的。
2、Spring Boot如何配置多環(huán)境?
????????面試官有的時(shí)候?yàn)榱丝疾炷愕膶?shí)戰(zhàn)經(jīng)歷,他可能會(huì)問(wèn)你這樣的問(wèn)題,比如說(shuō)你在開發(fā)的時(shí)候是不是只在本地開發(fā)?有沒(méi)有去針對(duì)不同的環(huán)境做不同的區(qū)分呢?那么如果我們被問(wèn)到這道題,首先我們可以這樣回答面試官。
????????你好,我這邊對(duì)于多環(huán)境的知識(shí)是了解的,我平時(shí)會(huì)使用多套環(huán)境,比如說(shuō)開發(fā)環(huán)境、測(cè)試環(huán)境、預(yù)發(fā)環(huán)境和線上環(huán)境。然后你還可以介紹一下這四個(gè)環(huán)境的用途。開發(fā)環(huán)境通常可以在本地它所連接的數(shù)據(jù)庫(kù)也是專門用于開發(fā)的。里面的數(shù)據(jù)一定程度上也是我們?cè)斐鰜?lái)的,因?yàn)椴⒉恍枰陂_發(fā)環(huán)境就保證數(shù)據(jù)的完全準(zhǔn)確。為了開發(fā)效率的提高,我們通常會(huì)造一些模擬的數(shù)據(jù)。那通常我們開發(fā)完之后需要把這個(gè)程序部署到測(cè)試環(huán)境。為什么需要測(cè)試環(huán)境呢?因?yàn)闇y(cè)試環(huán)境通常是公司所提供的一個(gè)服務(wù)器,而開發(fā)環(huán)境很有可能是我們本機(jī)。那對(duì)于本機(jī)而言,如果你電腦關(guān)閉了,或者你本機(jī)的程序停止了,那別人就無(wú)法訪問(wèn)了。但是測(cè)試的同學(xué)它和你的工作時(shí)間不可能是完全的一致。這樣的話一旦你把你的程序關(guān)掉了,它就沒(méi)有辦法進(jìn)行測(cè)試了,這樣是不行的。所以我們需要給測(cè)試的同學(xué)提供一套穩(wěn)定的環(huán)境去測(cè)試。而且有的時(shí)候我們會(huì)同時(shí)開發(fā)多種功能,那么有可能我前一個(gè)功能開發(fā)完了需要去測(cè)試,那這個(gè)時(shí)候后面我又要去開發(fā)新的功能了。所以你本地的代碼其實(shí)已經(jīng)發(fā)生了變化。也說(shuō)如果我們把開發(fā)環(huán)境當(dāng)作測(cè)試環(huán)境,這兩個(gè)環(huán)境不獨(dú)立的話會(huì)導(dǎo)致的問(wèn)題,就是他實(shí)際測(cè)試的可能和他想要測(cè)試的并不是同一套代碼,這樣的話也會(huì)有很大的問(wèn)題。正是基于這樣的原因,通常情況下測(cè)試環(huán)境是必不可少的,我們用一臺(tái)穩(wěn)定的服務(wù)器去把我們開發(fā)好的需要被測(cè)試的代碼給部署上去。這樣的話無(wú)論你的電腦是不是關(guān)機(jī),都不會(huì)影響到測(cè)試同學(xué)的進(jìn)度,這是測(cè)試環(huán)境所主要做的工作。但是在測(cè)試環(huán)境的數(shù)據(jù)庫(kù)配置往往可以和開發(fā)環(huán)境保持一致。也就是說(shuō)可以允許他們共用同一個(gè)數(shù)據(jù)庫(kù),畢竟里面的數(shù)據(jù)都是模擬出來(lái)的,所以不需要做嚴(yán)格的區(qū)分。下一個(gè)環(huán)境是預(yù)發(fā)環(huán)境,為什么需要一個(gè)預(yù)發(fā)環(huán)境預(yù)發(fā)這兩個(gè)字,顧名思義就是預(yù)備發(fā)布,準(zhǔn)備發(fā)布。也就是說(shuō)其實(shí)它和真正的線上環(huán)境是高度統(tǒng)一的。那么它和測(cè)試環(huán)境的差異點(diǎn)在哪兒呢?第一就是網(wǎng)絡(luò)的隔離。通常為了保證線上服務(wù)的穩(wěn)定,我們會(huì)做環(huán)境的隔離。環(huán)境隔離指的說(shuō)我們?cè)诒镜鼗蛘呤菧y(cè)試環(huán)境是沒(méi)有辦法訪問(wèn)到線上的環(huán)境的機(jī)器的。也說(shuō)通常情況下我們是不能在測(cè)試環(huán)境直接訪問(wèn)到生產(chǎn)環(huán)境的數(shù)據(jù)庫(kù),包括它的容器的。而且在預(yù)發(fā)環(huán)境通常我們會(huì)采用真實(shí)的數(shù)據(jù)去進(jìn)行測(cè)試。有的時(shí)候正是因?yàn)檫@一點(diǎn)細(xì)微的差別,可能在測(cè)試環(huán)境并不能把所有的問(wèn)題都測(cè)出來(lái)。所以正是因?yàn)檫@些區(qū)別,有的時(shí)候我們?cè)跍y(cè)試環(huán)境無(wú)法測(cè)試出來(lái)的問(wèn)題,在預(yù)發(fā)環(huán)境就可以暴露出來(lái)了。比如說(shuō)我們舉一個(gè)數(shù)據(jù)的例子,有的時(shí)候我們?cè)跍y(cè)試環(huán)境自己模擬出來(lái)的數(shù)據(jù)不是特別的準(zhǔn)確,和真實(shí)數(shù)據(jù)有一定的差別。比如說(shuō)我們?nèi)ツM一個(gè)商品詳情,可能我們只造了 50 個(gè)字,但是后來(lái)你發(fā)現(xiàn)真實(shí)的需求有好幾千個(gè)字。那么這個(gè)時(shí)候只有用了真實(shí)的數(shù)據(jù),你才能發(fā)現(xiàn)我的數(shù)據(jù)庫(kù)所設(shè)置的大小不夠。或者有的時(shí)候你在測(cè)試環(huán)境模擬數(shù)據(jù)的時(shí)候,模擬的都是一些整數(shù),但是你發(fā)現(xiàn)到了真實(shí)的數(shù)據(jù)里面,它其實(shí)是小數(shù),所以這個(gè)時(shí)候又能幫助你去發(fā)現(xiàn)問(wèn)題,你也同樣的需要做一定的調(diào)整。那這就是預(yù)發(fā)環(huán)境的作用,最主要是起到隔離以及數(shù)據(jù)驗(yàn)真的作用。最后一個(gè)環(huán)境就是生產(chǎn)環(huán)境了,生產(chǎn)環(huán)境也稱為線上環(huán)境,就是我們真實(shí)對(duì)外所提供服務(wù)的。這里所采用的數(shù)據(jù)那肯定是真實(shí)數(shù)據(jù)了,并且也會(huì)有很多的流量進(jìn)來(lái)。生產(chǎn)環(huán)境和預(yù)發(fā)環(huán)境最大的不同就在于流量的多少,通常的預(yù)發(fā)環(huán)境不會(huì)對(duì)外暴露,但是生產(chǎn)環(huán)境是直接面向所有用戶的,所以也會(huì)存在一些并發(fā)訪問(wèn)的問(wèn)題。那么一旦我們發(fā)布到生產(chǎn)環(huán)境,就要盡量的去確保這個(gè)程序是穩(wěn)定的,是沒(méi)有問(wèn)題的。以上我們就介紹了這四種最常見的環(huán)境。那我們?nèi)绾卫?spring 去配置多環(huán)境呢?我們最不可取的做法就是在發(fā)布到某一個(gè)環(huán)境之前,把它的配置文件全部的去刪除替換,這樣的話不但費(fèi)時(shí)費(fèi)力,而且很有可能由于你漏了替換,導(dǎo)致發(fā)布了錯(cuò)誤的配置。比如說(shuō)一旦你發(fā)布到生產(chǎn)環(huán)境時(shí),所使用的配置文件是測(cè)試環(huán)境的數(shù)據(jù)庫(kù),那么就有可能造成對(duì)外暴露的是測(cè)試數(shù)據(jù)的情況,這實(shí)際上就是很嚴(yán)重的事故了。所以我們需要通過(guò)更加優(yōu)雅的方法去解決這個(gè)問(wèn)題。在 spring boot 中,我們可以通過(guò)改變配置中的 profile.active 這個(gè)值來(lái)加載對(duì)應(yīng)的環(huán)境,只要做小小的修改,就能把整個(gè)配置文件進(jìn)行替換。
3、實(shí)際工作中,如何全局處理異常?
????????面試官可能會(huì)問(wèn)你,你在實(shí)際工作中如何去處理這種異常?你是全局處理的嗎?還是逐個(gè)處理的?還是說(shuō)就不進(jìn)行任何的處理?
????????那在這里,面試官其實(shí)并不是說(shuō)想聽你回答。我是全局處理的,這個(gè)答案過(guò)于簡(jiǎn)單了,其實(shí)他真正想聽到的是你背后的思考,也就是說(shuō)他想讓你主動(dòng)的去回答這個(gè)問(wèn)題。為什么異常需要全局處理,不處理行不行?那么剛才那個(gè)問(wèn)題的答案,正確答案當(dāng)然是應(yīng)該全局處理,你不處理是不行的。但重點(diǎn)在于這個(gè)理由我們可以跟面試官這樣回答。
????????首先如果我們不進(jìn)行處理的話,那么很有可能這個(gè)異常會(huì)把整個(gè)的堆棧都拋出去,這也是默認(rèn)的情況。也說(shuō)我們?nèi)绻贿M(jìn)行處理,一旦發(fā)生異常用戶或者是別有用心的黑客,他們就可以看到詳細(xì)的異常發(fā)生的情況,包含你的詳細(xì)的錯(cuò)誤信息,甚至是你代碼的行數(shù)。那么在這種情況下,對(duì)方可以利用簡(jiǎn)單的一個(gè)漏洞不停地去嘗試,而且他們還可以順藤摸瓜分析出你更多的潛在的風(fēng)險(xiǎn),最終把系統(tǒng)給攻破。所以我們異常是必須處理的。
????????那么處理的時(shí)候?yàn)槭裁葱枰痔幚砟?#xff1f;這個(gè)時(shí)候我們需要舉一個(gè)我們?cè)趯戨娚痰臅r(shí)候的例子,我們來(lái)看一看當(dāng)時(shí)我們是怎么寫的,好切換到我們的電商項(xiàng)目。在這里會(huì)有一個(gè)和異常相關(guān)的包叫做 exception 而這里面最重要的就是這個(gè) global exceptionhandler 這也是我們?nèi)痔幚碇凶钪匾囊粋€(gè)類。我們可以跟面試官說(shuō)我們使用了這樣的一個(gè) handler 去處理。具體處理的方式首先它會(huì)去加上 controller advice 注解,并且在這里面有多個(gè)方法,這多個(gè)方法的區(qū)別在于它們處理的異常不同。比如說(shuō)第一個(gè)它處理的異常是 exception 鎮(zhèn)電 plus 也就是所有的異常的父類。他處理的辦法是首先打出一個(gè)日志,然后把這個(gè)系統(tǒng)錯(cuò)誤,這個(gè) system error 也就是 2 萬(wàn)這個(gè)錯(cuò)誤碼進(jìn)行返回。而假設(shè)我們拋出的異常是我們自己定義的 exception 這個(gè)時(shí)候他就會(huì)使用到這個(gè)方法 handler exception 那么他處理的時(shí)候會(huì)更加的優(yōu)雅,它會(huì)根據(jù)我們具體的異常,也就是這里這個(gè) E 的類型去把它的狀態(tài)碼和它的信息給取出來(lái)。比如說(shuō)這里面的這些異常的枚舉都是有可能會(huì)拋出來(lái)的,比如說(shuō)用戶名不能為空,密碼不能為空等等。好我們回去。那這是對(duì)于 imkmorexception 下面我們還有一個(gè)我們?cè)隍?yàn)證參數(shù)的時(shí)候,如果它的參數(shù)不符合我們的規(guī)定,比如說(shuō)參數(shù)不能為空或者參數(shù)的長(zhǎng)度超出限制。那么它的異常的類型是 method argument notvalid exception 如果系統(tǒng)識(shí)別到現(xiàn)在遇到了這個(gè)異常,它就會(huì)調(diào)用這個(gè)處理器。那么那調(diào)用的時(shí)候也會(huì)友好的提示給用戶,說(shuō)你現(xiàn)在參數(shù)不符合我們的規(guī)定。所以通過(guò)以上這個(gè)代碼我們就知道了,在處理異常的時(shí)候,我們?nèi)绻麑懥诉@樣的全局異常處理器,也就是 global exception handler 那么就可以非常輕松地去針對(duì)不同類型的異常去做出定制化的解決方案,不但增加了安全性,而且對(duì)用戶也是非常友好的。用戶可以通過(guò)你的錯(cuò)誤信息知道他應(yīng)該怎么去調(diào)整,并且不會(huì)從中去暴露關(guān)鍵的敏感信息,這就是實(shí)際工作中正確的處理異常的方式。那我們?cè)谟龅竭@個(gè)問(wèn)題的時(shí)候,可以參考這樣的回答思路去跟面試官進(jìn)行交流。
二、線程常見面試題
1、線程如何啟動(dòng)?
????????面試官在面試的時(shí)候通常有一個(gè)循序漸進(jìn)的過(guò)程,比如說(shuō)他會(huì)首先問(wèn)你線程如何啟動(dòng)。線程啟動(dòng)可以使用 thread.start 方法來(lái)進(jìn)行啟動(dòng)。但是 start 方法最終它其實(shí)背后所要執(zhí)行的也是 run 方法。那在你回答完這個(gè) start 方法啟動(dòng)之后,它可能會(huì)繼續(xù)問(wèn)你這個(gè)問(wèn)題, 既然 start 方法會(huì)調(diào)用 run 方法,那為什么我們要多此一舉?為什么我們要多走一步去調(diào)用 star 的方法,而不是直接的去調(diào)用 run 方法呢?這樣做又有什么好處呢?
????????你可以這樣回答,如果你選擇直接去調(diào)用 run 方法。那么其實(shí)它就是一個(gè)普通的 Java 方法,就跟你去調(diào)用一個(gè)自己寫的普通的方法沒(méi)有任何的區(qū)別。那最重要的缺點(diǎn)在于它不會(huì)真正的去啟動(dòng)一個(gè)線程,你調(diào)用了一次 run 方法之后,它就執(zhí)行一次,而且是在主線程中執(zhí)行的,那就沒(méi)有起到任何的創(chuàng)建線程的效果了。如果我們選擇 star 的方法,它會(huì)在后臺(tái)進(jìn)行很多的工作,比如說(shuō)去申請(qǐng)一個(gè)新的線程,比如說(shuō)去讓這個(gè)子線程執(zhí)行 run 方法里面的內(nèi)容,而且還包括在執(zhí)行完畢之后的對(duì)于線程狀態(tài)的調(diào)整。所以我們?cè)趩?dòng)線程的時(shí)候,雖然表面上看起來(lái)你使用 star 的方法和 run 方法都是去執(zhí)行一段代碼,但是其背后是有很大不同的。
????????那這個(gè)時(shí)候面試官可能還會(huì)去問(wèn)好,現(xiàn)在你說(shuō)的對(duì)應(yīng)該用 star 的方法來(lái)啟動(dòng)線程,那我如果啟動(dòng)兩次會(huì)怎么樣呢?也就是說(shuō)如果我兩次調(diào)用 start 方法會(huì)出現(xiàn)什么情況呢?
????????我曾測(cè)試過(guò),他這個(gè)說(shuō)的是拋出了一個(gè)異常,并且這個(gè)異常叫做 illegal thread state exception 含義就是說(shuō)線程的狀態(tài)不對(duì),去看一下 star 的方法里面究竟是怎么執(zhí)行的,為什么會(huì)拋出這個(gè)異常呢?
源碼是這樣的如果 thread state 不等于0,它就會(huì)拋出這個(gè)異常。源碼上面的這個(gè)注釋。A zero status value corresponds to state new 也就是說(shuō) 0 代表這個(gè)狀態(tài)是 new 那我們知道,如果這個(gè)線程一旦被 start 之后,它的線程狀態(tài)會(huì)從 new 變成 runnable 所以它的狀態(tài)肯定就不是 new 了,所以它這個(gè)值也不是0。所以第二次你去執(zhí)行 start 的時(shí)候,自然就會(huì)拋出這樣的一場(chǎng)。
????????那這道題我們就可以這樣回答了: 兩次調(diào)用 start 方法會(huì)拋出異常,這個(gè)異常的類型叫做 illegal threadstateexception 之所以會(huì)拋出這個(gè)異常,是因?yàn)樵?start 的時(shí)候會(huì)首先進(jìn)行線程狀態(tài)的檢測(cè),只有是 new 的時(shí)候才能去正常的啟動(dòng),不允許啟動(dòng)兩次。
2、實(shí)現(xiàn)多線程的方法有幾種?
實(shí)現(xiàn)多線程主要有這兩種方法。第一種方法是實(shí)現(xiàn) runnable 接口,而第二種方法是繼承 thread 類。
兩種方法進(jìn)行一下對(duì)比。哪種方式它會(huì)更好?
答案還是比較明確的,runnable 接口的這種方式會(huì)更好。
????????第一個(gè)角度是從代碼架構(gòu)的角度去考慮的,代碼架構(gòu)的角度是這樣分析的。事實(shí)上,之所以 Java 在設(shè)計(jì)的時(shí)候會(huì)有 runnable 接口這樣的一個(gè)接口,它的本意是想讓我們把這個(gè)任務(wù)的執(zhí)行類和任務(wù)的具體內(nèi)容進(jìn)行解耦。解耦的意思就是讓他們的關(guān)系不那么的密切。這樣的話從架構(gòu)的角度去考慮它的擴(kuò)展性會(huì)更好。所以 runnable 接口其實(shí)它所做的最主要的工作是去描述這個(gè)工作的內(nèi)容,但是和現(xiàn)成的啟動(dòng)、銷毀其實(shí)沒(méi)有關(guān)系。而 thread 類本身它是用于維護(hù)整個(gè)線程的,比如說(shuō)啟動(dòng)、線程狀態(tài)更改,包括最后任務(wù)結(jié)束這些都是由 thread 類去做的。所以它們兩個(gè)之間也就是 runnable 接口和 thread 類之間本身權(quán)責(zé)是很分明的。因此我們從代碼架構(gòu)的角度考慮,不應(yīng)該讓它們過(guò)度的耦合。一旦過(guò)度耦合,未來(lái)就會(huì)發(fā)生很難擴(kuò)展的這種問(wèn)題。所以從代碼架構(gòu)的角度去考慮,實(shí)現(xiàn) runnable 接口這種方式會(huì)更好。
????????第二個(gè)角度在于我們是從新建線程的這種損耗的角度去考慮的,同樣也是實(shí)現(xiàn) runnable 接口這種方式更好。那我們就來(lái)分析一下,我們?nèi)绻褂美^承 thread 類的方式,正如我們剛才代碼所看到的那樣,在這個(gè)是繼承 thread 類的方式。那么我們?nèi)绻肴?dòng)一個(gè)線程,需要把這個(gè)類給拗出來(lái),把它給實(shí)例化出來(lái),并且啟動(dòng)起來(lái)。所以每一次我們?nèi)绻バ陆ㄒ粋€(gè)線程,那通常要去 new 一個(gè) thread 類。但是其實(shí)我們?cè)诰€程池這樣的更高級(jí)的用法中,我們并不是是每一個(gè)任務(wù)都去新建一個(gè)線程的。我們?yōu)榱颂岣哒w的效率,會(huì)讓有線數(shù)量的線程比如說(shuō) 10 個(gè)或者是 20 個(gè)或者是 100 個(gè),這個(gè)數(shù)量可以由我們自己來(lái)確定。但是我們假設(shè)用 10 個(gè)線程,它實(shí)際上是可以執(zhí)行成千上萬(wàn)個(gè)這樣的任務(wù)。而有了這樣的一個(gè)思路之后,我們并不是每次都去新建一個(gè)線程,然后執(zhí)行一個(gè)任務(wù),而是把同樣的一個(gè)線程它去執(zhí)行很多很多個(gè)任務(wù)。所以這樣的話它就減少了新建線程的損耗。因?yàn)樗⒉恍枰バ陆?1000 個(gè)線程,而是只需要用一個(gè)線程去執(zhí)行這 1000 個(gè)任務(wù)就可以了。所以如果我們使用繼承 thread 類的方式,就不得不去把這個(gè)損耗都承擔(dān)起來(lái)。有的時(shí)候我們?cè)?run 方法里面所執(zhí)行的內(nèi)容是比較少的。比如說(shuō)像我們的這個(gè)代碼中,它如果只打印一行話的話,其實(shí)整體而言它的開銷甚至還比不上我們新建一個(gè)現(xiàn)成的開銷。那這樣一來(lái),我們相當(dāng)于是撿了芝麻,丟了西瓜,得不償失了。那假設(shè)我們使用這個(gè)實(shí)現(xiàn) runnable 接口的這種方式,可以把這個(gè)任務(wù)作為一個(gè)參數(shù)直接傳遞給線程池。而線程池里面用固定的線程來(lái)執(zhí)行這些任務(wù),就不需要每次都新建并且銷毀線程了,這樣的話就大大的降低了性能的開銷。所以這是從第二個(gè)角度新建線程的損耗這個(gè)角度去看的。從這個(gè)我們也可以分析出實(shí)現(xiàn) runnable 接口,它要比繼承 thread 類去來(lái)得更好。
????????第三個(gè)好處在于 Java 不支持雙繼承,不支持雙繼承的意思就是說(shuō)在我們的這個(gè)類中,它如果已經(jīng) extends 一個(gè) thread 類了,就不能再讓他去 extends 更多的類了。
3、創(chuàng)建線程的原理是什么?
第一種方法是實(shí)現(xiàn) runnable 接口的方法,然后我們會(huì)看到它最終調(diào)用的是 target.run;第一種,這個(gè)實(shí)現(xiàn) runnable 接口這種方法,它的本質(zhì)在于我們傳入了一個(gè) target 并且最終通過(guò) spread 類調(diào)用了這個(gè) target.run 這個(gè)方法,最終去實(shí)現(xiàn)了我們想要它執(zhí)行的這個(gè)邏輯
方法 2 指的是我們?nèi)ダ^承 spread 類這種方法去實(shí)現(xiàn)線程。那么它的原理是什么樣的?
去繼承 thread 類這種方式,它的原理是整個(gè) run 方法都被重寫,那么它自然也就沒(méi)有調(diào)用target.run這樣的一個(gè)過(guò)程。
假如我們同時(shí)用兩種方法會(huì)怎么樣?
????????當(dāng)同時(shí)使用兩種方法去執(zhí)行的時(shí)候,由于已經(jīng)把父類的這個(gè) run 方法給覆蓋了,所以我們即便傳入了 target 或者說(shuō)傳入了 runnable 它都不會(huì)起到效果,真正執(zhí)行的還是覆蓋了 thread 類的那個(gè) run 方法。
無(wú)論是實(shí)現(xiàn) runnable 接口還是繼承 thread 類,它們本質(zhì)都是一樣的。都是最終會(huì)去執(zhí)行到這個(gè) thread 類里面的這個(gè) run 方法。只不過(guò)如果你是通過(guò)實(shí)現(xiàn) runnable 接口的形式,那么它就會(huì)調(diào)用這個(gè) target 的 run ,如果你去直接重寫,那么它就不會(huì)調(diào)用這三行代碼,而是去執(zhí)行你重寫的這個(gè)代碼。不過(guò)本質(zhì)它都是從 thread 類這個(gè) run 方法里面去來(lái)的。其實(shí)無(wú)論你是實(shí)現(xiàn) runnable 接口還是繼承 thread 類,它本質(zhì)都是一樣的。準(zhǔn)確的講,創(chuàng)建線程它只有一種方式就是構(gòu)建 thread 類。但是不同之處是在于如何實(shí)現(xiàn)線程的執(zhí)行單元。剛才如果是實(shí)現(xiàn) runnable 接口,它是把 runnable 這個(gè)實(shí)例傳給 thread 類去實(shí)現(xiàn),然后再通過(guò) target 這種方式去進(jìn)行中轉(zhuǎn),最終執(zhí)行到 runnable 里面的內(nèi)容。
關(guān)于有多少種實(shí)現(xiàn)多線程的方法,用的最多的是兩種,一個(gè)是實(shí)現(xiàn) runnable 接口,另外一個(gè)是繼承 thread 類。不過(guò)這兩種方法它們背后本質(zhì)是一樣的。而其他的方式比如說(shuō)線程池或者是定時(shí)器,它們是對(duì)于前面兩種方式的一種包裝。
4、線程有哪幾種狀態(tài)? 生命周期是什么?
六種狀態(tài):
- New
第一個(gè)就是 new 這種狀態(tài)代表已創(chuàng)建但還沒(méi)啟動(dòng)的新線程。這個(gè)含義非常明確說(shuō)當(dāng)我們用 NEO thread 新建了一個(gè)線程之后,但是我們還沒(méi)有去執(zhí)行 start 方法,此時(shí)這個(gè)線程就處于 new 的這個(gè)狀態(tài)。事實(shí)上我們用 new thread 建立了這個(gè)線程之后,它還沒(méi)有開始運(yùn)行,但是他已經(jīng)做了一些準(zhǔn)備工作了。但是做完準(zhǔn)備工作之后還沒(méi)有去執(zhí)行 run 方法里面的這個(gè)代碼,因?yàn)闆](méi)有人去執(zhí)行 start 方法,這種情況下它的狀態(tài)是 new
- Runnable
第二種狀態(tài)是 runnable, runnable 相對(duì)而言是比較特殊的一種狀態(tài)。這種狀態(tài)是一旦從 new 調(diào)用了 start 方法之后,它就會(huì)處于 runnable 了,一旦調(diào)用了 start 方法,線程便會(huì)進(jìn)入到 runnable 狀態(tài),也就是說(shuō)我們從 new 到 runnable 而不會(huì)從 new 到 waiting。 Java 中的 runnable 狀態(tài)實(shí)際上就對(duì)應(yīng)到我們操作系統(tǒng)中的兩種狀態(tài),分別是 ready 和 running 也就是說(shuō)我們這邊一個(gè) runnable 它既可以是可運(yùn)行的,又可以是實(shí)際運(yùn)行中的,它有可能正在執(zhí)行,也有可能沒(méi)有在執(zhí)行,那沒(méi)有在執(zhí)行的時(shí)候,它就是其實(shí)是等待著 CPU 為它分配執(zhí)行時(shí)間。
并且還有一種情況,比如說(shuō)我這個(gè)線程已經(jīng)拿到 CPU 資源了對(duì)吧?那么它是 runnable 狀態(tài), CPU 資源是被我們的調(diào)度器不停地在調(diào)度的,所以有的時(shí)候會(huì)突然又被拿走。一旦我們某一個(gè)線程拿到了 CPU 資源正在運(yùn)行了,突然這個(gè) CPU 資源又被搶走了,又被分配給別人了。這個(gè)時(shí)候我們這個(gè)線程還是 runner 這個(gè)狀態(tài),因?yàn)殡m然它并沒(méi)有在運(yùn)行中,但它依然是處于一個(gè)可運(yùn)行的狀態(tài),隨時(shí)隨地它都有可能又被調(diào)度器分配回來(lái) CPU 資源,那我們又可以繼續(xù)運(yùn)行了。所以這些情況下我們的狀態(tài)都是 runnable。
- Block
當(dāng)一個(gè)線程進(jìn)入到被 synchronized 修飾的代碼塊的時(shí)候,并且該鎖已經(jīng)被其他線程所拿走了。我們拿不到這把鎖的時(shí)候,線程的狀態(tài)就是 block 的。進(jìn)入 synchronized 修飾的代碼塊兒,這個(gè) block 僅僅是針對(duì) synchronized 這個(gè)關(guān)鍵字才能進(jìn)入 block 的。因?yàn)楹?synchronized 關(guān)鍵字起到相似效果的還有其他的 lock 各種各樣的鎖,像可重入鎖、讀寫鎖,這些都可以讓一個(gè)線程進(jìn)行到這個(gè)等待的情況。但是那些情況下它絕對(duì)不是 block 的這個(gè)線程狀態(tài)。針對(duì) block 的這個(gè)線程狀態(tài),我們要記住它一定是 synchronized 修飾的。當(dāng)然無(wú)論是 synchronized 修飾的一個(gè)方法或者是代碼塊兒,這都可以。那么只要是一個(gè) synchronized 所保護(hù)的一段代碼中,它且沒(méi)有拿到鎖,陷入到一個(gè)等待的狀態(tài),這種情況下才是 block 的。
- Waiting
這個(gè)是微停狀態(tài)。微停是等待哪些情況會(huì)進(jìn)入到這個(gè)狀態(tài)呢?一方面是沒(méi)有設(shè)置 timeout 參數(shù)的 object.wait 方法。給大家看一下流轉(zhuǎn)圖狀態(tài)間的轉(zhuǎn)化圖示:
我們首先看一下剛才所講過(guò)的 new 然后 thread.start 方法之后進(jìn)入到 runnablerunnable 和 blocked 有兩條線,從 runnable 想進(jìn)到 blocked 需要進(jìn)入到 synchronize 修飾的相關(guān)方法或代碼塊,并且沒(méi)拿到鎖。然后那從 block 回到 runnable 那自然是在剛才進(jìn)入 synchronized 之后,等待鎖的過(guò)程中有人釋放了,于是我拿到了就回到 runnable 現(xiàn)在我們?cè)賮?lái)看一下從 runnable 如何到右邊這個(gè) waiting 狀態(tài)。箭頭的左右指向不同,代表著它狀態(tài)切換的方向也不同。所以大家要看準(zhǔn)箭頭。從左邊往右邊的這個(gè)箭頭。上面說(shuō)了這三種情況,分別是 object 的 wait 方法,第二個(gè)是 thread 的 join 方法,第三個(gè)是 lock support 的 park 方法。有一個(gè)很類似的看 object 的 wait 以及 thread 的 join 但是它里面是有參數(shù)的,這里面參數(shù)不同決定了它狀態(tài)的不同。所以大家注意在這邊從 runnable 到 waiting 的時(shí)候,這里面是不帶參數(shù)的 wait 和 join 方法才有這種情況才會(huì)進(jìn)入到waiting,否則進(jìn)入的可能是 time 的 waiting 這兩種狀態(tài)是不一樣的。另外第三, locksupport 的 park 方法我們可能不經(jīng)常見到這個(gè) locksupport 但是實(shí)際上它是我們很多鎖的底層原理。在這邊我們要掌握的是這三種情況會(huì)讓我們 runnable 進(jìn)入到微信狀態(tài),好要想從這三種狀態(tài)返回回來(lái),那么自然也是很類似的。用 object 的 notify 或者是 object 的 notify all 會(huì)讓我們從 wait 這種情況被喚醒回到 runnable 以及和 locksupportpark 對(duì)應(yīng)的是 locksupport unpark 方法,這些都會(huì)讓我們從等待變回可運(yùn)行狀態(tài)。而中間的這個(gè) join 方法需要等待我們 join 方法所執(zhí)行的那個(gè)線程,它運(yùn)行完畢才會(huì)回來(lái)。
- Time Waiting
依舊使用上面的圖做參考,這個(gè)狀態(tài)叫做計(jì)時(shí)等待。這個(gè)狀態(tài)大家可以理解成和這個(gè)等待狀態(tài)非常類似,它們是一個(gè)很兄弟的狀態(tài)關(guān)系,只不過(guò)一個(gè)是有一定時(shí)間期限的。另外一個(gè)是沒(méi)有時(shí)間期限的,在這邊有時(shí)間期限的有哪些呢?我們看一下這五種情況。第一個(gè)是 sleep 方法,然后就是 object wait 和 threadjoin 這些和剛才的區(qū)別僅僅是多了一個(gè)時(shí)間參數(shù),我們可以指定它是兩秒還是 20 秒或者是 400 毫秒,這些都可以。一旦你指定了,那么它就是一個(gè)計(jì)時(shí)等待。另外剛才的 locksupport 的 park 也有兩個(gè)對(duì)應(yīng)的可以放入時(shí)間參數(shù)的這個(gè)方法,總體而言其實(shí)和 wait 是差不多的,只不過(guò)一個(gè)是帶了時(shí)間參數(shù),而一個(gè)是沒(méi)有帶。那么它的區(qū)別就在于帶了時(shí)間參數(shù)的這種它需要等待超時(shí),它在超時(shí)的情況下會(huì)被系統(tǒng)自動(dòng)喚醒。并且如果在超時(shí)之前就收到了像類似 notify 或 notifyAll 這種情況也可以提前的被喚醒,這就是它的不同之處。它相比于 wait 只能等待被喚醒信號(hào)之外,這種計(jì)時(shí)等待除了可以等待喚醒信號(hào),也可以等待時(shí)間到。所以這兩種情況是比剛才這個(gè) wait一種情況返回的機(jī)會(huì)要更大一些。
那么 waiting 和 time waiting 這兩種和剛才的 block 的大家一看好像是不是也很相似我都在等待一些信號(hào)。但是在這邊區(qū)別在于我們的 blocked 它等待是另外線程釋放一個(gè)排他鎖。而這個(gè) waiting 和 time waiting 他是等待被換喚醒或等待一段被設(shè)置好的時(shí)間,所以這是有所不同的。
- Terminated
最后一個(gè)是我們的被終止?fàn)顟B(tài)或者叫已終止?fàn)顟B(tài)。 terminated 這種狀態(tài)有兩種情況可以到達(dá)。第一種同學(xué)們都可以想到的就是我們 run 方法正常執(zhí)行完畢了,正常退出了,自然是線程進(jìn)入到 terminated 另外一種情況,相對(duì)而言少見一些,出現(xiàn)了一個(gè)沒(méi)有被捕獲的異常,終止了這個(gè) run 方法導(dǎo)致意外終止,這樣的話 run 方法不一定會(huì)執(zhí)行完畢。因?yàn)樗鼒?zhí)行到一半就拋出異常了,但是這種情況下依然會(huì)進(jìn)入到這個(gè) terminated 的狀態(tài)。
以上六種狀態(tài)的代碼演示:
/*** 描述: 演示New、Runnable、Terminated狀態(tài)。*/ public class NewRunnableTerminated {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread();//打印出NEW的狀態(tài)System.out.println(thread.getState());thread.start();//打印出Runnable狀態(tài)System.out.println(thread.getState());Thread.sleep(100);//打印出TERMINATED狀態(tài)System.out.println(thread.getState());} } /*** 描述: 展示Blocked、Waiting、Timed_Waiting狀態(tài)*/ public class BlockedWaitingTimedWaiting implements Runnable {public static void main(String[] args) throws InterruptedException {Runnable runnable = new BlockedWaitingTimedWaiting();Thread t1 = new Thread(runnable);t1.start();Thread t2 = new Thread(runnable);t2.start();Thread.sleep(10);//打印Timed_Waiting狀態(tài),因?yàn)檎趫?zhí)行Thread.sleep(1000);System.out.println(t1.getState());//打印出BLOCKED狀態(tài),因?yàn)閠2拿不到synchronized鎖System.out.println(t2.getState());Thread.sleep(1300);//打印出WAITING狀態(tài),以為執(zhí)行了wait()System.out.println(t1.getState());}@Overridepublic void run() {syn();}private synchronized void syn() {try {Thread.sleep(1000);wait();} catch (InterruptedException e) {e.printStackTrace();}} }一般習(xí)慣而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(計(jì)時(shí)等待)都稱為阻塞狀態(tài)
回到我們的正題:線程有哪幾種狀態(tài)? 生命周期是什么?
其實(shí)答案都在上面的那幅圖里。可以一邊畫一邊解釋這個(gè)圖便能完美解答這個(gè)面試題。
三、分布式的面試題
1、什么是分布式?
面對(duì)這個(gè)問(wèn)題,我們就可以給出這個(gè)廚房的例子了。比如說(shuō)我們開飯店,最開始的時(shí)候,由于客流量很小,我們只有一個(gè)廚師就夠了。這個(gè)廚師他不僅去負(fù)責(zé)做菜,還負(fù)責(zé)切菜、洗菜等等。后面我們發(fā)現(xiàn)一個(gè)廚師不夠,于是我們就雇了多個(gè)廚師。但是這多個(gè)廚師僅僅是簡(jiǎn)單的對(duì)于一個(gè)廚師進(jìn)行了復(fù)制。也就是說(shuō)同樣還是由一個(gè)廚師進(jìn)行所有的工作,包括洗菜、切菜和做菜。那么他并沒(méi)有進(jìn)行分工,后續(xù)飯店發(fā)現(xiàn)這樣的話成本太高,因?yàn)閺N師的工資他往往要比洗菜和切菜的工人要高很多。所以他發(fā)現(xiàn)我們?nèi)绻眯g(shù)業(yè)有專攻,是不是可以讓我們整體的成本下降?比如說(shuō)我們?nèi)テ刚?qǐng)一個(gè)配菜師或者是洗菜工或者是切菜工,那么他就可以在不忙的時(shí)候把所有的菜給洗好,切好準(zhǔn)備好,而廚師就可以專注于他的炒菜了,這樣一來(lái)整體的效率就提高了。那剛才這個(gè)例子其實(shí)就比喻了我們實(shí)際項(xiàng)目的演進(jìn)過(guò)程。最開始一個(gè)廚師對(duì)應(yīng)的就是一個(gè)項(xiàng)目,它大而全。后來(lái)我們發(fā)現(xiàn)一個(gè)項(xiàng)目不夠用了,那我們就去用多臺(tái)機(jī)器,但是每一臺(tái)機(jī)器上部署的都是一樣的內(nèi)容,同樣也是大而全的。這樣會(huì)造成一定資源的浪費(fèi)。所以我們就引出了分布式。我們以一個(gè)公司系統(tǒng)為例,比如說(shuō)公司系統(tǒng)里面分為權(quán)限系統(tǒng)、員工、那請(qǐng)假系統(tǒng)很顯然它的使用頻率要比其他的系統(tǒng)要低得多。那么你就沒(méi)有必要去在每臺(tái)機(jī)器上都去部署一個(gè)請(qǐng)假系統(tǒng),而是只要部署一個(gè)請(qǐng)假系統(tǒng),就足以應(yīng)付所有的流量請(qǐng)求了。相反員工系統(tǒng)用得很頻繁。那么你可以去多部署幾個(gè)員工系統(tǒng),這樣的話我們就可以充分利用機(jī)器資源,也同樣發(fā)揮了分布式的優(yōu)勢(shì)。
2、分布式和單體結(jié)構(gòu)哪個(gè)更好?
其實(shí)這是一個(gè)坑,因?yàn)槊撾x業(yè)務(wù)的技術(shù)選型都是沒(méi)有意義的。我們?cè)趯?duì)比某些技術(shù)哪個(gè)更好,哪個(gè)更差的時(shí)候,一定要結(jié)合我們的業(yè)務(wù),結(jié)合具體的場(chǎng)景。
????????首先對(duì)于傳統(tǒng)的單體架構(gòu)而言,對(duì)于新人的學(xué)習(xí)成本,它的難點(diǎn)在于業(yè)務(wù)邏輯很多,因?yàn)槟氵@一個(gè)項(xiàng)目很大,功能點(diǎn)也特別多。而分布式架構(gòu)由于它都進(jìn)行了拆分,所以它的主要成本在于架構(gòu)的復(fù)雜度上高。而這一點(diǎn)往往是架構(gòu)師或是 leader 所負(fù)責(zé)的。那么底下的開發(fā),他們更多的是關(guān)注某一個(gè)模塊就夠了。
????????在部署和運(yùn)維方面單體的架構(gòu)會(huì)很簡(jiǎn)單,但是對(duì)于分布式架構(gòu)而言,它的部署和運(yùn)維都要復(fù)雜得多。對(duì)于隔離性而言,單體架構(gòu)由于它是渾然一體的,所以有一個(gè)地方出問(wèn)題,整個(gè)就會(huì)出問(wèn)題,正所謂是一損俱損,殃及魚池。而分布式架構(gòu)沒(méi)有這個(gè)缺點(diǎn)。如果某一個(gè)內(nèi)容出現(xiàn)了問(wèn)題,它影響的也僅僅是它自己,對(duì)于其他的部分不會(huì)產(chǎn)生太大的影響。所以故障的影響范圍是比較小的。
????????在架構(gòu)設(shè)計(jì)方面,傳統(tǒng)的單體架構(gòu)它的難度要遠(yuǎn)遠(yuǎn)低于我們的分布式架構(gòu)。而在系統(tǒng)性能方面,由于單體架構(gòu)它所有的調(diào)用、操作都是在內(nèi)部的,也沒(méi)有網(wǎng)絡(luò)通信的開銷,所以它的響應(yīng)其實(shí)是比較快的。但是在同樣資源的情況下,它整體所能承擔(dān)的最大的流量范圍也就是吞吐量是不如我們的分布式架構(gòu)來(lái)得好。因?yàn)槲覀兊姆植际郊軜?gòu)更加充分地利用了資源。在測(cè)試成本這一塊兒,傳統(tǒng)的單體架構(gòu)它的測(cè)試成本是比較低的。而分布式架構(gòu)要想測(cè)試是比較困難的,有的時(shí)候這個(gè)鏈路很長(zhǎng),你甚至都不容易發(fā)現(xiàn)是哪里出了問(wèn)題。
????????在技術(shù)多樣性方面,傳統(tǒng)的單體架構(gòu)技術(shù)肯定是很單一的,而且是封閉的,你要是想引入新的語(yǔ)言幾乎是不可能的。但是分布式架構(gòu)它的技術(shù)多樣性就體現(xiàn)出來(lái)了,它技術(shù)就很多樣,你這個(gè)模塊所用用的技術(shù)在另外一個(gè)模塊兒中不一定要用一樣的,它對(duì)于技術(shù)完全是開放的。
????????在系統(tǒng)擴(kuò)展性方面,單體的架構(gòu)擴(kuò)展性也是比較差的,因?yàn)槟阋胍粋€(gè)新的包可能會(huì)造成依賴的沖突。而分布式架構(gòu)由于相互之間的獨(dú)立性很好,所以你要想進(jìn)行新功能的擴(kuò)展,那么往往是沒(méi)有太大的阻力的。
????????在系統(tǒng)管理成本方面,傳統(tǒng)的單體架構(gòu)由于它架構(gòu)很簡(jiǎn)單,所以管理成本相對(duì)而言就比較低。但是分布式架構(gòu)管理成本確實(shí)要高很多。那我們可以看出,其實(shí)你很難說(shuō)單體就比分布式好,或者說(shuō)單體就比分布式差。往往我們?cè)陧?xiàng)目建立之初的時(shí)候,為了追求項(xiàng)目的快速上線,我們可以選用單體架構(gòu)。因?yàn)槟莻€(gè)時(shí)候我們的業(yè)務(wù)還沒(méi)有很復(fù)雜,我們也不能預(yù)測(cè)在半年之后或者一年之后究竟需要去新增哪些模塊。所以在最開始我們使用單體架構(gòu)是最合適的。
????????最后的總結(jié)一句話:根據(jù)具體的業(yè)務(wù)場(chǎng)景和發(fā)展階段選擇適合自己的技術(shù)。
3、CAP理論是什么?
- C ( Consistency ,一致性)︰讀操作是否總能讀到前一個(gè)寫操作的結(jié)果
- A( Availability,可用性)︰非故障節(jié)點(diǎn)應(yīng)該在合理的時(shí)間內(nèi)作出合理的響應(yīng)
- P( Partition tolerance,分區(qū)容錯(cuò)性): 當(dāng)出現(xiàn)網(wǎng)絡(luò)分區(qū)現(xiàn)象后,系統(tǒng)能夠繼續(xù)運(yùn)行
簡(jiǎn)單的說(shuō)就是:一致性的意思說(shuō)假設(shè)有人操作了某一個(gè)數(shù)據(jù),那么后面想去讀取的時(shí)候,要求是讀取到操作之后的結(jié)果,而不是以前的緩存。 A 可用性的意思說(shuō)你這個(gè)系統(tǒng)是不是隨時(shí)都能對(duì)外提供服務(wù)?如果系統(tǒng)掛了,如果不給響應(yīng)了或者給出錯(cuò)誤響應(yīng)了,那么這個(gè)就叫做不可用。而 P 它指的就是說(shuō)如果節(jié)點(diǎn)與節(jié)點(diǎn)之間相互無(wú)法通信了,是否影響到你整個(gè)系統(tǒng)的運(yùn)行。
4、CAP怎么選?
????????通常可以跟面試官畫一個(gè)這樣的圖,這是三個(gè)有交集的緣。但是特點(diǎn)在于最中間是三個(gè)圓的焦點(diǎn),它是一個(gè)點(diǎn)而不是一個(gè)面。這就反映了 cap 的一個(gè)很重要的特點(diǎn),說(shuō)你不能 cap 三者兼得,只能從中選取兩個(gè)。那這個(gè)就涉及到 cap 如何選擇的問(wèn)題了。由于網(wǎng)絡(luò)是我們?nèi)藶闊o(wú)法完全控制的,也就是說(shuō)網(wǎng)絡(luò)錯(cuò)誤無(wú)法避免。所以從系統(tǒng)的層面去考慮, P 始終是要考慮在內(nèi)的。那么供我們選擇的就是 CP 或者是 ap 我們還記得 C 代表的是一致性,而 A 代表的是可用性。
????????什么場(chǎng)合下可用性會(huì)高于一致性呢?我們來(lái)舉個(gè)例子,比如說(shuō)我們是做一個(gè)圖片網(wǎng)站的,對(duì)外提供的就是各種各樣的圖片。那么我們對(duì)于可用性的要求就會(huì)高于一致性。比如說(shuō)有的時(shí)候我們是允許不一致的,我們?cè)谶@里更新了一張圖片,或許在短時(shí)間內(nèi)其他的用戶拿到的還是舊的圖片還是老版本的圖片,但是這并沒(méi)有太大的問(wèn)題。最終隨著時(shí)間的推移,人人都會(huì)看到最新的版本。但是我們不希望說(shuō)我在更新的時(shí)候其他人就不可用了,他們連網(wǎng)站都訪問(wèn)不了,這個(gè)是我們不希望的。所以在這種情況下,可用性高于一致性。
????????那什么情況下一致性會(huì)高于可用性呢?比如說(shuō)在交易支付這樣的場(chǎng)景中,一致性的要求就特別高。不能說(shuō)我把這個(gè)錢轉(zhuǎn)出去了,已經(jīng)轉(zhuǎn)走了,別人卻還看到的是我救的那個(gè)余額,然后又轉(zhuǎn)走一份這樣的話會(huì)造成很大的問(wèn)題。所以在這種場(chǎng)景下,一致性就高于可用性。所以 cap 怎么選還是取決于我們的業(yè)務(wù)適合自己的才是最好的,孰優(yōu)孰劣并沒(méi)有定論。如果是涉及到錢財(cái),我們的 C 也就是一致性是必須保證的。那如果是不涉及到這種強(qiáng)一致的內(nèi)容,我們就可以優(yōu)先去選擇 A 也就是可用性,這就是和 cap 理論相關(guān)的內(nèi)容。
四、Docker相關(guān)面試題
1、為什么需要Docker ?
????????首先 Docker 它是用來(lái)裝程序及其環(huán)境的一個(gè)容器,所主要解決的問(wèn)題就是環(huán)境配置的問(wèn)題。比如說(shuō)這個(gè)程序在我這臺(tái)電腦上可以良好的運(yùn)行,但是在你那邊卻報(bào)錯(cuò)了,這就是一個(gè)環(huán)境所帶來(lái)的典型的問(wèn)題。那為了解決這種問(wèn)題之前,就誕生了其他的解決方案,比如說(shuō)虛擬機(jī)的解決方案。但是虛擬機(jī)了,太重的意思就是說(shuō)它的成本太高了。
????????我們?yōu)榱吮WC一樣的環(huán)境,需要去模擬出一臺(tái)完整的機(jī)器,包括硬盤包括內(nèi)存,而且它們都是獨(dú)享的,即便你程序不運(yùn)行,它的那部分資源也不能去拿來(lái)共享,那就造成很大程度的浪費(fèi)了。正是因?yàn)橛羞@樣的問(wèn)題,所以我們才需要 Docker 那有了 Docker 之后,Docker就給我們提供了統(tǒng)一的環(huán)境,而且還提供了可以快速擴(kuò)展的彈性伸縮的云浮。這個(gè)指的是說(shuō)如果遇到了雙 11 這樣流量大的情況,那么我們可以利用 Docker 迅速的去擴(kuò)展幾十臺(tái)甚至上百臺(tái)機(jī)器,它們的環(huán)境都是統(tǒng)一的,也就是可以保證程序是可以在上面穩(wěn)定運(yùn)行的。那這樣一來(lái)我們就可以根據(jù)流量的不同進(jìn)行合理的配置,讓我們資源既不浪費(fèi),也不會(huì)出現(xiàn)資源不足的情況。
????????另外 Docker 還有一大好處說(shuō)它可以防止其他用戶的進(jìn)程把服務(wù)器的資源占用過(guò)多,Docker可以做到很好的隔離。并且如果你這里出錯(cuò)了,也不會(huì)影響到其他的用戶。這相比于以前,我們可能出現(xiàn)某一個(gè)程序把 CPU 占滿了,或者把內(nèi)存或者把磁盤占滿了,導(dǎo)致這臺(tái)機(jī)器上的所有的程序都不可用。相比于這種情況,Docker就有很大優(yōu)勢(shì)了。所以這就是 Docker 所帶來(lái)的好處。
2、Docker的架構(gòu)是什么樣的?
最主要的幾個(gè)部分分別是 containerimage 和 registrycontainer 是容器的意思,把 images 啟動(dòng)起來(lái)之后就形成了一個(gè)容器。而 image 是鏡像。鏡像我們可以從鏡像市場(chǎng)也就是右邊的這個(gè) registry 中去獲取到這個(gè)鏡像,包括五幫圖、OS 、Redis、nginx都有。作為我們使用者而言,通常情況下我們要去啟動(dòng)一個(gè) container 然后在里面去運(yùn)行程序。而啟動(dòng) container 的步驟就包括從鏡像市場(chǎng)中下載,還包括把 image 給啟動(dòng)起來(lái),主要就是這樣的一個(gè)流程。而在最左側(cè)是我們的 client 是客戶端,客戶端他負(fù)責(zé)發(fā)送命令去真正執(zhí)行操作的是中間的這邊的 Docker 的服務(wù)端也可以叫做 docker_host。
3、Docker的網(wǎng)絡(luò)模式有哪些?
Docker 的網(wǎng)絡(luò)模式啊一共有這三種,第一種呢是 bridge 叫做橋接,第二種叫做 host ,第三種叫做none。其中 bridge 用的是最多的,也就是說(shuō)我們用外面的主機(jī)的一個(gè)端口號(hào)去映射到容器里面的某一個(gè)端口號(hào),實(shí)現(xiàn)了一座橋,通過(guò)這個(gè)橋大家就可以通信了。第二種 host 的含義是里面的容器不會(huì)獲得一個(gè)獨(dú)立的網(wǎng)絡(luò)資源配置,它和我們外界的主機(jī)使用的是一模一樣的,使用同一個(gè)網(wǎng)絡(luò)。也說(shuō)里面的容器將不會(huì)虛擬出自己的網(wǎng)卡,也不會(huì)配置自己的 IP 而是使用我們宿主機(jī)上的 IP 和端口號(hào),這就是 host 模式。第三種是不需要網(wǎng)絡(luò)的模式,是 none 的模式,如果是選擇這種模式的話,那么就不能和外界有任何通信了。通常情況下我們都會(huì)選擇 bridge 作為我們的網(wǎng)絡(luò)模式。
五、Nginx和Zookeeper相關(guān)面試題
1、Nginx的適用場(chǎng)景有哪些?
nginx 主要有兩個(gè)適用場(chǎng)景,第一個(gè)是 HTTP 的反向代理服務(wù)器,而第二個(gè)就是動(dòng)態(tài)靜態(tài)的資源分離。
HTTP 反向代理服務(wù)器:
????????外面是我們的互聯(lián)網(wǎng)用戶,他們連接到 nginx 然后再由 nginx 進(jìn)行轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)到我們里面的各個(gè)服務(wù)器。那么其中從 nginx 到我們里面各個(gè)服務(wù)器的這個(gè)過(guò)程就叫做反向代理。正是因?yàn)橛辛?nginx 作為我們的反向代理服務(wù)器,我們就可以很好的去進(jìn)行負(fù)載均衡,我們把不同的請(qǐng)求分到不同的服務(wù)器上,讓他們雨露均沾,各自都去處理自己應(yīng)該處理的內(nèi)容。
????????第二個(gè)應(yīng)用場(chǎng)景就是動(dòng)態(tài)靜態(tài)的資源分離。如果我們不進(jìn)行動(dòng)態(tài)靜態(tài)的資源分離的話,那么有很多的靜態(tài)資源也會(huì)去經(jīng)過(guò)我們的 tomcat 處理。那其實(shí)這種處理是沒(méi)有必要的,因?yàn)檫@種資源都是固定且死的,都是固定的,其實(shí)只要直接提供給用戶就可以了。所以有了這個(gè)動(dòng)靜分離之后,我們就可以做到靜態(tài)資源無(wú)需經(jīng)過(guò) tomcat 他們只負(fù)責(zé)處理動(dòng)態(tài)資源,比如說(shuō)后綴為 GIF 這樣的一個(gè)圖片。這種圖片資源 nginx 會(huì)首先識(shí)別到這個(gè)用戶想請(qǐng)求這個(gè)圖片,然后直接把這個(gè)文件就提供給用戶了。同樣有的時(shí)候我們的網(wǎng)站如果不是特別復(fù)雜,完全可以利用這個(gè) nginx 搭建一個(gè)靜態(tài)的資源服務(wù)器。
2、Nginx常用命令有哪些?
- /usr/sbin/nginx啟動(dòng)
- -h幫助
- -c讀取指定配置文件
- -t測(cè)試
- -v版本
- -s信號(hào)
-
- stop 立即停止
-
立即停止的意思就是說(shuō)對(duì)于當(dāng)前已經(jīng)接到的這個(gè)請(qǐng)求也不管了,直接我就停止了不處理了。
-
- quit優(yōu)雅停止
-
優(yōu)雅停止的意思是說(shuō)我們不再接收新的連接了。但是對(duì)于已經(jīng)處理的處理到一半的,我們會(huì)繼續(xù)對(duì)他們提供服務(wù),逐步的讓我們的程序停止下來(lái)。
-
- reload重啟
-
它在我們配置的時(shí)候也經(jīng)常會(huì)用到。比如說(shuō)我們更改了配置文件,就需要利用 reload 命令來(lái)讀取出最新的這個(gè)配置文件的內(nèi)容。
-
- reopen更換日志文件
-
更換日志文件
3、Zookeeper有哪些節(jié)點(diǎn)類型?
直接畫個(gè)圖:
- 持久節(jié)點(diǎn)
- 臨時(shí)節(jié)點(diǎn)
- 順序節(jié)點(diǎn)
對(duì)于樹來(lái)講最重要的就是節(jié)點(diǎn),而它的節(jié)點(diǎn)又分為持久節(jié)點(diǎn)、臨時(shí)節(jié)點(diǎn)和順序節(jié)點(diǎn)。持久節(jié)點(diǎn)的意思是說(shuō)我創(chuàng)建這個(gè)節(jié)點(diǎn)之后它就一直在那里了,除非你把我刪掉。臨時(shí)節(jié)點(diǎn)指的是說(shuō)在鏈接斷開之后會(huì)自動(dòng)的進(jìn)行刪除。而順序節(jié)點(diǎn)在創(chuàng)建的時(shí)候它是有順序的,而且是遞增的。那么我們就可以通過(guò) zokeeper 生成的這個(gè)節(jié)點(diǎn)的號(hào)碼去用于生成一些唯一的 ID 這也是 zookeeper 的一個(gè)應(yīng)用場(chǎng)景。
六、RabbitMQ相關(guān)面試題
1、為什么要用消息隊(duì)列?什么場(chǎng)景用?
消息隊(duì)列的三大作用:第一個(gè)作用就是 系統(tǒng)解耦 。通過(guò)消息隊(duì)列的收發(fā)消息的機(jī)制,我們就可以讓不同的系統(tǒng)之間解耦,我不再需要去調(diào)用你的接口了。我也不必等你返回了,我就只要發(fā)個(gè)消息就可以了,剩下的事情都由我們的消息隊(duì)列去完成。另外消息隊(duì)列還可以用于 異步調(diào)用 。比如說(shuō)我們有一個(gè)功能,它所涉及到的模塊兒特別特別多,可能有十幾個(gè)。那么我們的用戶其實(shí)不關(guān)心后續(xù)的內(nèi)容,所以這個(gè)時(shí)候就可以利用消息隊(duì)列,我們把消息發(fā)出去就可以了,不需要等他們返回。而對(duì)于用戶而言,它的體驗(yàn)就好很多,因?yàn)樗却龝r(shí)間就大幅縮小了。下一個(gè)場(chǎng)景是 流量削峰 ,在高并發(fā)的情況下,有可能短時(shí)間內(nèi)我們會(huì)接到特別多的請(qǐng)求。那我們不應(yīng)該讓這個(gè)請(qǐng)求一下子都進(jìn)來(lái)。這個(gè)時(shí)候我們可以把這些請(qǐng)求都放到我們的消息隊(duì)列中,然后由消息隊(duì)列一個(gè)一個(gè)的后面去逐漸的對(duì)這些消息對(duì)這些請(qǐng)求進(jìn)行消化。這樣一來(lái)我們就很好地去控制了我們機(jī)器的訪問(wèn)壓力,不至于由于過(guò)大的訪問(wèn)量導(dǎo)致我們的機(jī)器宕機(jī)。
2、RabbitMQ核心概念
先它會(huì)有發(fā)送者和消費(fèi)者,發(fā)送者會(huì)把自己的消息發(fā)送到交換機(jī)上,然后由交換機(jī)去把這個(gè)消息放到合適合理的隊(duì)列上。而我們的消費(fèi)者其實(shí)他只去關(guān)心隊(duì)列就可以了,隊(duì)列里有什么他就去消費(fèi)什么,這是消息的最主要的一個(gè)流轉(zhuǎn)的路徑。那么在我們的交換機(jī)和隊(duì)列的外面,會(huì)有一個(gè)概念叫做 virtual host 虛擬主機(jī)的意思。在同一個(gè) rabbitmq 的 server 之下,你可以建立不同的虛擬主機(jī),那它們之間都是相互獨(dú)立的,可以用于不同的業(yè)務(wù)線,這就是消息隊(duì)列的核心概念。
3、交換機(jī)工作模式有哪4種?
第一種叫做 find out 是廣播的意思,如圖所示:
如果我們利用廣播的話,如果我們利用這種交換機(jī)的模式,那么他就會(huì)把這個(gè)消息毫無(wú)差別的發(fā)送到所有綁定的隊(duì)列上,適用于最普通的消息。
第二種工作模式是 direct ,direct 是要根據(jù)我們的 roading key 去精準(zhǔn)匹配的。我們來(lái)看一下圖:
比如說(shuō)我們交換機(jī)的工作模式是 direct 那么如果我們指定了 orange 作為第一個(gè)隊(duì)列的路由鍵,而同時(shí)指定 black 和 green 作為第二個(gè)隊(duì)列的路由鍵。那么在發(fā)送消息的時(shí)候,orange的就會(huì)被放到第一個(gè)去,而 black 和 green 的就會(huì)被放到第二個(gè)隊(duì)列中去。所以這種模式適合精準(zhǔn)匹配。
比如說(shuō)我們?cè)趯?shí)際工作中可能會(huì)出現(xiàn)這樣的場(chǎng)景,我們?nèi)ヌ幚砣罩尽S幸粋€(gè)隊(duì)列只接收錯(cuò)誤日志,而有另外一個(gè)隊(duì)列他接收的是所有的日志,就包括 info error 和 warning 這三個(gè)級(jí)別。那這種場(chǎng)景就很適合去使用 direct 模式。我們把 error 的只發(fā)到第一個(gè)隊(duì)列中,而把 iinfo、error和 warning 的都發(fā)送到第二個(gè)隊(duì)列中,實(shí)現(xiàn)了日志的分離。
其實(shí)在我們的生產(chǎn)中更多的用的是第三種,也就是 topic 模式。 topic 模式它非常的靈活,它可以根據(jù)我們?cè)O(shè)定的內(nèi)容進(jìn)行模糊匹配,并且進(jìn)行相應(yīng)的轉(zhuǎn)發(fā)。在 topic 里面,*代表是一個(gè)單詞,而#號(hào)代表是零個(gè)或者多個(gè)單詞。比如說(shuō)我們舉個(gè)例子:
在 topic 模式下,我們第一個(gè)隊(duì)列只關(guān)心橙色的動(dòng)物,而第二個(gè)隊(duì)列只關(guān)心 lazy 的動(dòng)物以及兔子。那么我們使用這個(gè) topic 模式,它的優(yōu)勢(shì)就體現(xiàn)出來(lái)了。對(duì)于第一個(gè)隊(duì)列而言,只要你是橙色的,那不管你是什么物種,不管你是兔子還是火烈鳥,只要你是orange的都會(huì)匹配過(guò)來(lái)。那么在實(shí)際工作中,我們完全可以把 orange 換成是請(qǐng)假系統(tǒng)里面和請(qǐng)假相關(guān)的信息。那么這樣一來(lái),你這個(gè)隊(duì)列就能把所有和請(qǐng)假相關(guān)的信息都收集到,不會(huì)遺漏。
第四種工作模式是 headers 這種使用的非常少,它是根據(jù)我們消息內(nèi)容中的 headers 來(lái)進(jìn)行匹配,需要我們自定義。那通常情況下我們用不到這一種。
七、微服務(wù)相關(guān)
1、微服務(wù)有哪兩大門派?
- Spring Cloud:眾多子項(xiàng)目
- dubbo:高性能、輕量級(jí)的開源RPC框架,它提供了三大核心能力∶面向接口的遠(yuǎn)程方法調(diào)用,智能容錯(cuò)和負(fù)載均衡,以及服務(wù)自動(dòng)注冊(cè)和發(fā)現(xiàn)
以上也就是說(shuō) dubbo 所提供的能力它只是 spring cloud 的一部分子集。
2、Spring Cloud核心組件有哪些?
3、能畫一下Eureka架構(gòu)嗎?
- Eureka Server和Eureka Client
上面的這個(gè)藍(lán)色的部分是eureka server 而右下角的 service provider 它就是一個(gè) eureka client 它會(huì)注冊(cè)到我們的 eureka server 上面去。左下角的是我們的服務(wù)消費(fèi)者,它先訪問(wèn)到 eureka server 拿到地址,然后再去對(duì)這個(gè)服務(wù)提供者進(jìn)行遠(yuǎn)程調(diào)用,這就是一個(gè)最基本的Eureka 的架構(gòu)。
4、負(fù)載均衡的兩種類型是什么?
????????一種類型是客戶端的負(fù)載均衡。比如說(shuō)客戶端的負(fù)載均衡的意思就是說(shuō)我們?cè)谡?qǐng)求的時(shí)候就已經(jīng)知道了,這三個(gè) IP 地址都能提供服務(wù)。那么我們就一個(gè)一個(gè)的去調(diào)用,或者通過(guò)一定的算法去調(diào)用。但總之這個(gè)決策是在我們調(diào)用方的,這就叫客戶端的負(fù)載均衡。
????????一個(gè)服務(wù)端的負(fù)載均衡。一個(gè)非常典型的例子就是 nginx 對(duì)于普通的廣大的用戶而言,他可不會(huì)進(jìn)行負(fù)載均衡,他就訪問(wèn)你一個(gè)入口就可以了。那么這個(gè)時(shí)候我們就需要用到服務(wù)端的負(fù)載均衡,比如說(shuō)利用 nginx 進(jìn)行合理的轉(zhuǎn)發(fā),讓我們的請(qǐng)求分散開來(lái)。那么剛才我們說(shuō)到了負(fù)載均衡,面試官可能會(huì)接下來(lái)問(wèn)你,你知道有哪些典型的負(fù)載均衡策略呢?比較典型的策略有以下這幾種。第一個(gè)是 random 叫做隨機(jī)策略。隨機(jī)策略顧名思義,他發(fā)送請(qǐng)求的時(shí)候并沒(méi)有一個(gè)具體的規(guī)則,完全是隨機(jī)的。第二個(gè)是用的最多的是輪詢的策略,輪詢的策略就是說(shuō)挨個(gè)的去進(jìn)行請(qǐng)求。第一次我請(qǐng)求一號(hào),第二次請(qǐng)求二號(hào),第三次請(qǐng)求三號(hào),第四次再次回到一號(hào),然后就是這樣一二三周而復(fù)始,叫做輪詢。
????????一種比較高級(jí)是加權(quán),加權(quán)的含義說(shuō)它會(huì)根據(jù)每一個(gè)服務(wù)器的響應(yīng)時(shí)間進(jìn)行動(dòng)態(tài)的調(diào)整。比如說(shuō)你這個(gè)服務(wù)器響應(yīng)特別慢,那我就給你少幾個(gè)請(qǐng)求。如果有其他的服務(wù)器,響應(yīng)很快,我就給多幾個(gè)請(qǐng)求,這樣也可以更大程度上的去發(fā)揮我們機(jī)器的性能。
5、為什么需要斷路器?
比如說(shuō)我們依賴很多服務(wù),但是有一個(gè)服務(wù)突然就不能用了,我們這邊標(biāo)紅的 dependency i 一旦有一個(gè)服務(wù)不可用了之后,
假設(shè)我們沒(méi)有斷錄器,會(huì)發(fā)生什么樣可怕的情況呢?
假設(shè)我們的用戶請(qǐng)求會(huì)用到這個(gè)不可用的 i 那么其實(shí)每一個(gè)請(qǐng)求基本上都是和用戶相關(guān)的,所以都會(huì)訪問(wèn)到 i而這個(gè)i 現(xiàn)在又不可用,所以會(huì)導(dǎo)致你所有的線程幾乎在一瞬間之內(nèi)都卡在了這個(gè)地方。那么這樣一來(lái),現(xiàn)有的用戶他的請(qǐng)求被卡住了,而后面的用戶由于沒(méi)有更多的線程來(lái)處理了,所以后面的用戶也進(jìn)不來(lái),就導(dǎo)致你的整個(gè)服務(wù)在很短的時(shí)間內(nèi)就變得不可用了,發(fā)生很嚴(yán)重的故障。所以我們 需要斷路器的一個(gè)很重要的原因 就是當(dāng)我們發(fā)現(xiàn)某一個(gè)服務(wù)某一個(gè)模塊不可用的時(shí)候,我們把它給摘除掉,不至于影響到我們其他的主要的流程。
6、為什么需要網(wǎng)關(guān)?
主要有這兩個(gè)原因:
????????第一個(gè)是和鑒權(quán)相關(guān)的。如果我們不使用網(wǎng)關(guān),那么每一個(gè)模塊兒自己都要去實(shí)現(xiàn)一套獨(dú)立的鑒權(quán)服務(wù),那這個(gè)通常是一種資源浪費(fèi),而且維護(hù)起來(lái)也很困難。所以我們通過(guò)網(wǎng)關(guān)把這個(gè)功能進(jìn)行統(tǒng)一的收集。
????????第二個(gè),主要的功能是統(tǒng)一對(duì)外增強(qiáng)了安全性。我們?cè)诰€上服務(wù)通常只會(huì)對(duì)外暴露網(wǎng)關(guān)這一個(gè)服務(wù),而其他的都作為內(nèi)部服務(wù)不對(duì)外暴露。那這樣的話外面想訪問(wèn)必須通過(guò)網(wǎng)關(guān)。所以我們只需要在網(wǎng)關(guān)這個(gè)層面去進(jìn)行安全的保護(hù)就可以了。我們可以對(duì)惡意 IP 進(jìn)行攔截,我們同樣也可以對(duì)所有的記錄進(jìn)行打日志。那么由于我們把所有的請(qǐng)求都收集到一起了,所以要想保護(hù)它的安全比分散的時(shí)候容易得多,這就是需要網(wǎng)關(guān)的兩大主要原因。
7、Dubbo的工作流程是什么?
直接畫圖:
在這幅圖中,由數(shù)字標(biāo)出來(lái)的012345,這就代表 double 工作的時(shí)候的最主要的流程。那么我們一個(gè)一個(gè)的來(lái)看,0代表服務(wù),這個(gè)容器啟動(dòng)了,容器啟動(dòng)之后會(huì)把我們的 provider 給啟動(dòng)起來(lái),然后這個(gè) provider 就會(huì)去注冊(cè)中心注冊(cè)上,一旦他注冊(cè)上之后,后續(xù)我們假設(shè)有 consumer 想要去調(diào)用服務(wù)的話,那么他就會(huì)去訂閱這個(gè)地址,我們的注冊(cè)中心就會(huì)把 provider 的地址通知到 consumer 于是 consumer 就可以進(jìn)行調(diào)用了。也就是我們這邊的第四步, invoke 調(diào)用的時(shí)候,如果有多臺(tái),那同樣它也可以進(jìn)行一定的負(fù)載均衡的處理。最后一步是我們的第五步,count它的含義是進(jìn)行數(shù)據(jù)統(tǒng)計(jì),我們的服務(wù)其實(shí)是需要一定的監(jiān)控保障的,無(wú)論是 consumer 還是 provider 那么我們可能會(huì)想知道他們被調(diào)用的次數(shù)是多少,他們運(yùn)行的是否穩(wěn)定,運(yùn)行了多久。那么正是因?yàn)橛羞@樣的需求就有了監(jiān)控。于是 provider 和 consumer 都會(huì)定時(shí)的把自己的一些信息上報(bào)到監(jiān)控中心,這就是 double 工作的主要的流程。
八、鎖分類、死鎖
1、Lock簡(jiǎn)介、地位、作用
本身它是一種鎖,它是一種工具,這種工具專門用來(lái)控制對(duì)共享資源的訪問(wèn)的最常見的類就是 reentlock 其他的實(shí)現(xiàn)可能是在其他鎖的內(nèi)部一般不直接使用。所以我們說(shuō)到 lock 接口,我們就以 reaction 的 lock 這個(gè)為最主要的典型,就可以對(duì)于鎖而言,lock和 synchronized 是兩種最常見的鎖,它們都可以達(dá)到線程安全的目的,但是在使用上和功能上又有比較大的不同。在這一點(diǎn)我們要明確一點(diǎn)說(shuō)它們不是一個(gè)相互替代的關(guān)系,他們不是說(shuō)我后來(lái)的我比你厲害,那我就全盤的代替你,他們有各自適用的場(chǎng)合。對(duì)于 lock 而言,有的時(shí)候可以提供一些 synchronized 不提供的功能高級(jí)功能。但有的時(shí)候我們又沒(méi)必要用這個(gè)高級(jí)功能,直接用 synchronized 就可以了。 lock 接口中最常見的實(shí)現(xiàn)類就是我們的 reentant lock 我們?nèi)绻f(shuō)到 lock 接口,你要舉一個(gè)實(shí)現(xiàn)類的話,你舉它肯定沒(méi)錯(cuò)。
2、Lock主要方法介紹
在Lock中聲明了四個(gè)方法來(lái)獲取鎖:
lock()、 tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()
第一個(gè)方法叫做 lock 方法,這個(gè)方法就是最普通的獲取鎖了。如果說(shuō)這個(gè)鎖已經(jīng)被其他線程的鎖拿到,那么它就等待。但是它有一個(gè)特點(diǎn)這也是我們 lock 的特點(diǎn)。 lock 它不會(huì)像 synchronized 一樣在異常的時(shí)候自動(dòng)釋放鎖。對(duì)于 synchronized 而言,你即便沒(méi)有寫,我在發(fā)生異常的時(shí)候能不能幫我釋放一下鎖沒(méi)有寫也沒(méi)關(guān)系,jvm會(huì)自動(dòng)幫我們釋放,這是隱藏在背后的邏輯。可是對(duì)于我們這個(gè) lock 而言,無(wú)論你是加鎖還是解鎖,你都必須自己主動(dòng)的寫出來(lái),要用代碼來(lái)明示,而不是暗示。所以說(shuō)我們?cè)谑褂?lock 的時(shí)候,最佳實(shí)踐就是你無(wú)論怎么樣都要寫一個(gè)。Try類在 finallay 里面去釋放鎖,保證發(fā)生異常的時(shí)候鎖一定會(huì)被釋放。下面讓我們來(lái)看一下它有一個(gè)什么問(wèn)題,這個(gè)方法它不能被中斷,這會(huì)帶來(lái)很大的隱患。比如說(shuō)我們陷入死鎖了,死鎖說(shuō)兩個(gè)線程互相想拿到對(duì)方持有的鎖。如果我們用這個(gè) lock 方法,它遇到死鎖之后,那它也沒(méi)有辦法自己消除,它會(huì)陷入永久等待。
trylock 方法,這個(gè)方法可以用來(lái)獲取鎖。如果說(shuō)當(dāng)前的鎖沒(méi)有被其他線程所占用,那么我就獲取成功了,它會(huì)返回一個(gè)布爾值返回處代表獲取成功,返回 false 代表獲取失敗。相比于 lock 而言,這樣的方法顯然功能是更強(qiáng)大了,我們可以根據(jù)獲取鎖是否成功來(lái)決定程序后續(xù)要怎么處理。這個(gè)方法會(huì)立刻返回。不是說(shuō)等一段時(shí)間我拿不拿,這個(gè)方法里面是沒(méi)有參數(shù)的,那這個(gè) trylock 會(huì)立刻返回,無(wú)論你拿到還是拿不到,它立刻給你一個(gè)答案,拿不到他也不會(huì)一直在那里一等。
trylock 兄弟方法,里面是多了一個(gè)參數(shù)區(qū)別,就是說(shuō)它會(huì)等一段時(shí)間有一個(gè)超時(shí)時(shí)間,如果在這段時(shí)間內(nèi)拿到鎖,它就返回處。如果還是拿不到,那時(shí)間到了它也會(huì)返回,返回 false 還有一個(gè)方法叫做 lockinterruptibly 這個(gè)方法和上面那個(gè)方法是一樣的,這兩個(gè)方法它們都是聲明了異常,也就是說(shuō)你使用這個(gè)方法你必須 trycache 或者拋出去。但是它和它不同之處就在于它的時(shí)間默認(rèn)為設(shè)置為無(wú)限,所以在等待的過(guò)程中也是可以被打斷的,也是可以去感受到這個(gè)中斷的門 lock 有一個(gè)很好的好處,就是它在燈鎖期間他不是盲目的,也不是愣頭青,也不是不見黃河不死心,他是很靈活的。如果你不想讓他去等鎖了,那么你隨時(shí)可以中斷他在鎖里面。
最后一個(gè)介紹的方法就是 unlock unlock 這個(gè)方法大家一定要注意,就是它最應(yīng)該被寫在 final 類里面。并且我們一旦獲取到鎖,第一件事不是去執(zhí)行,我獲取到鎖了有什么業(yè)務(wù)邏輯,而是寫個(gè) try 再寫個(gè) finally 把我們的 unlock 放在 finally 之后,完成了這個(gè)固定動(dòng)作之后再去寫我們的業(yè)務(wù)邏輯,這個(gè)是非常好的習(xí)慣,否則的話你可能就會(huì)漏掉去解鎖或者是發(fā)生異常就跳過(guò)你的解鎖。那這樣一來(lái)就會(huì)導(dǎo)致你這個(gè)程序陷入死鎖,因?yàn)槟沔i拿了又沒(méi)有釋放,這樣很不好。
3、synchronized和Lock有什么異同?
相同點(diǎn):
主要從兩方方面去回答。第一個(gè)方面是相同點(diǎn)。那么最大的相同點(diǎn)相信小伙伴們都知道他們的目的和作用都是為了保證資源的線程安全。比如說(shuō)我們使用了那么被保護(hù)的代碼塊兒,它就最多只有一個(gè)線程能同時(shí)訪問(wèn),那這樣的話它就保證了安全。同樣 lock 的作用是類似的,是用于保證線程安全的,所以它們都可以被稱為是鎖。那這個(gè)是他們的最基礎(chǔ)的作用是第一個(gè)相同點(diǎn)。而第二個(gè)相同點(diǎn)通常就不太容易考慮到了。第二個(gè)相同點(diǎn)是可重入。可重入是什么意思呢?意思就是說(shuō)當(dāng)我們拿到鎖的這個(gè)線程想再次去獲取這把鎖的時(shí)候,是否需要提前釋放掉我手中的鎖?下面代碼演示一下:
/*** 描述: synchronized可重入*/ public class Reentrant {public synchronized void f1() {System.out.println("f1方法被運(yùn)行了");f2();}public synchronized void f2() {System.out.println("f2方法被運(yùn)行了");}public static void main(String[] args) {Reentrant reentrant = new Reentrant();reentrant.f1();} }
不同點(diǎn):
第一個(gè)不同點(diǎn)就在于它們的用法 synchrniced 它可以用在方法上,同樣也可以用在同步代碼塊兒上。那么這是它的最主要的用法。而對(duì)于 lock 而言,它的用法就不太一樣了,它必須去使用 lock 方法來(lái)加鎖,并且使用 unlock 方法來(lái)解鎖。所以它的加解鎖都顯示的都是很明顯你能看到的什么時(shí)候加鎖,什么時(shí)候解鎖?而 synchronize 它的加解鎖是隱式的,是隱含在其中的。在 Java 代碼中并沒(méi)有很明顯的說(shuō)這個(gè)時(shí)候我要加鎖,那個(gè)時(shí)候我要解鎖這樣的代碼它是沒(méi)有的。
第二個(gè)區(qū)別是加解鎖的順序不同。那由于我們的 lock 它加鎖和解鎖是我們可以程序員去手動(dòng)寫代碼控制的。所以我們比如說(shuō)想先給這三個(gè)鎖加鎖再去給它們反方向的解鎖,這些都是可以去做到的,有靈活度是由我們程序員去掌控的。而 synchronized 的它是由我們的 Java 內(nèi)部去控制的。在進(jìn)入 synchronized 保護(hù)的代碼的時(shí)候,它會(huì)加鎖退出的時(shí)候會(huì)解鎖,而這些都是自動(dòng)的,所以在順序上也不能靈活的調(diào)整。那這就是他們的第二個(gè)不同
第三個(gè)不同是 synchronized 不夠靈活,怎么體現(xiàn)是這樣的。如果我們有一個(gè)鎖已經(jīng)被某一個(gè)線程給獲取了,這是一個(gè)鎖,此時(shí)如果其他線程還想去獲得這個(gè)鎖的話,它只能等待直到上面一個(gè)鎖釋放。那這個(gè)時(shí)候就有一個(gè)問(wèn)題,比如說(shuō)等的時(shí)間可能會(huì)很長(zhǎng),這樣的話你整個(gè)程序的運(yùn)行效率就非常低了,甚至是如果別人他幾天都不釋放鎖,那么你也只能一直等待下去。相反我們的 lock 就很靈活了,它在等鎖的過(guò)程中你如果覺(jué)得時(shí)間太長(zhǎng)了不想等的話,你可以去提前的退出。同樣它的靈活之處還在于他在獲取之前他可以先看一看現(xiàn)在你這個(gè)鎖能不能獲取到?是不是已經(jīng)有線程占有你這個(gè)鎖了?那么如果說(shuō)當(dāng)他發(fā)現(xiàn)此時(shí)獲取不到鎖的話,他可以靈活的去調(diào)整,比如說(shuō)去執(zhí)行其他的邏輯,那這樣的話他就很靈活。所以這是他們的第三個(gè)不同。
第四個(gè)不同是性能上的區(qū)別。在性能上,之前的 Java 版本中, synchrniced 的性能是比較低的,在 Java 5 和 5 之前的版本它都比較低。但是到了 Java 6 以后,我們的 Java 對(duì)于 synchronized 進(jìn)行了性能的優(yōu)化。那么有有了這些優(yōu)化之后,原本新 chronize 的性能確實(shí)是比 lock 要差,但是有了這些優(yōu)化之后,它的性能逐步的去提高。所以到現(xiàn)在我們所使用的 Java 主流版本 synchronized 和 lock 它們的性能并沒(méi)有很明顯的差異,所以這就是它們?cè)谛阅苌系膮^(qū)別。具體指的是早期版本中 synchronized 性能差,現(xiàn)在的版本中性能差異較小。
4、你知道有幾種鎖?
- 共享鎖/獨(dú)占鎖
- 公平鎖/非公平鎖
- 悲觀鎖/樂(lè)觀鎖
- 自旋鎖/非自旋鎖
- 可重入鎖/非可重入鎖
- 可中斷鎖/不可中斷鎖
第一種分類是 共享鎖和獨(dú)占鎖 。這個(gè)含義是說(shuō)這個(gè)鎖是不是可以被共享還是只能被同一個(gè)線程所拿到。那么大部分的鎖都是獨(dú)占鎖,也說(shuō)當(dāng)一個(gè)線程獲取到之后,其他的線程不能來(lái)訪問(wèn)。而共享鎖的一個(gè)最典型的案例就是讀寫鎖。它的含義是說(shuō)在多個(gè)線程同時(shí)去進(jìn)行讀操作的時(shí)候,你們是可以共享這把鎖的,因?yàn)樽x操作并不會(huì)帶來(lái)線程安全問(wèn)題,所以使用共享鎖可以提高效率。排他鎖又有一個(gè)名字叫做獨(dú)占鎖或者叫獨(dú)享鎖。排他鎖獲取了這個(gè)鎖之后,它既能讀又能寫。但是此時(shí)其他線程再也沒(méi)有辦法獲得這個(gè)派他鎖了,只能由他本人去修改數(shù)據(jù),所以保證了線程安全。所以說(shuō)我們舉個(gè)例子,synchronized它本身就是一個(gè)排他鎖,因?yàn)樗@取之后別人獲取不了。但是此時(shí)還有一種叫做共享鎖。共享鎖又可以稱為讀鎖,我們獲取到共享鎖之后我們可以查看查詢,但是我們不能修改也不能刪除,做改動(dòng)的都是不行的。其他線程,同時(shí)如果這個(gè)時(shí)候也只是想讀的話,它是可以同時(shí)獲取到這個(gè)共享鎖的。但是同樣道理,其他線程雖然獲取到這個(gè)共享鎖,它也不能修改也不能刪除。那么對(duì)于這個(gè)而言,最典型的就是 reententreadwritelock 因?yàn)檫@里面有兩把鎖,其中一把獨(dú)鎖是共享鎖,可以有多個(gè)線程同時(shí)持有。而寫鎖是獨(dú)享鎖只能最多有一個(gè)線程持有。下面我們就來(lái)看一下讀寫鎖的作用。在一開始沒(méi)有讀寫鎖之前,假設(shè)我們使用最普通的 reentlock 那么這個(gè)時(shí)候我們確實(shí)是可以保證線程安全的,但是與此同時(shí)也浪費(fèi)了一定的資源。比如說(shuō)多個(gè)線程想同時(shí)讀,多個(gè)線程想同時(shí)讀實(shí)際上并沒(méi)有線程安全問(wèn)題,或者更多的線程,100個(gè)線程想同時(shí)讀都是可以的。我們這個(gè)時(shí)候并沒(méi)有必要給它加鎖,因?yàn)樽x是安全的。可是我們?nèi)绻褂昧?reaction 的 lock 那它是不區(qū)分場(chǎng)景的,它不管你是讀還是寫,都必須要求有了這個(gè)鎖之后才能操作。所以就造成了沒(méi)有意義的同步,浪費(fèi)了時(shí)間,浪費(fèi)了資源。我們?nèi)绻诖嘶A(chǔ)上升級(jí),我們?cè)谧x的地方只用讀鎖,在寫的地方用寫鎖,這樣就非常靈活。如果我們?cè)跊](méi)有協(xié)鎖持有的情況下,我們的讀鎖它是沒(méi)有阻塞的,多個(gè)線程可以同時(shí)來(lái)讀,提高了我們程序的效率。
下一種分類是 公平鎖和非公平鎖。什么是公平和非公平。對(duì)于公平而言,它指的是按照我們現(xiàn)成請(qǐng)求的順序來(lái)分配鎖,你先來(lái)我就給你先分配鎖,很公平也很好理解。但是這里的非公平指的是不是說(shuō)完全亂序,這里大家一定要注意清楚這個(gè)非公平不是說(shuō)我既然不公平,我就特別不公平,我就隨機(jī)好了,不是這個(gè)意思,他這個(gè)非公平指的是我不完全按照請(qǐng)求的順序,只有在一定的情況下他才可以插隊(duì)的。我們這里有一個(gè)注意點(diǎn),就是說(shuō)我們這里的非公平,同樣他其實(shí)內(nèi)心還是一個(gè)好人,他是不提倡插隊(duì)的。他這里的非公平只是在合適的時(shí)機(jī)他允許插隊(duì),不是說(shuō)盲目亂插隊(duì)。那么好小伙伴們肯定會(huì)有一個(gè)疑問(wèn)了,那你說(shuō)合適的時(shí)機(jī)插隊(duì),什么叫做合適的時(shí)機(jī)呢?這個(gè)合適的時(shí)機(jī)你說(shuō)合適就合適,我被插隊(duì)了,我不高興,我說(shuō)不合適。那你說(shuō)以誰(shuí)說(shuō)的為準(zhǔn)呢?所以說(shuō)在這里我們要舉一個(gè)例子,我們舉一個(gè)買火車票被插隊(duì)的例子,用這個(gè)例子就可以說(shuō)明公平和非公平她們的情況。第一個(gè),假設(shè)我們以前還沒(méi)有1236,網(wǎng)上 App 的時(shí)候,大家還是說(shuō)才對(duì),去火車站買那個(gè)時(shí)候是這樣的,其實(shí) 12306 也沒(méi)幾年,我們那個(gè)時(shí)候買火車票,尤其是春運(yùn)的時(shí)候,可是很難搶票的。這個(gè)時(shí)候一個(gè)插隊(duì),那簡(jiǎn)直是影響到我能不能買到票,所以是非常關(guān)鍵的。這個(gè)時(shí)候假設(shè)有這么一個(gè)情況,我們是排在隊(duì)伍的第二位。在我們前面有一個(gè)人他是先于我們排隊(duì),所以他自然是先于我們買票,他買完了票走了。下一個(gè)本來(lái)是我,可是因?yàn)槲医?jīng)過(guò)了徹夜的排隊(duì),那個(gè)時(shí)候買火車票實(shí)際上要徹夜排隊(duì)的提早去的,要不然你買不到。所以那個(gè)時(shí)候其實(shí)我腦袋還是嗡嗡作響,還不是特別清醒,確實(shí)該輪到我了。可是這個(gè)時(shí)候我也沒(méi)有一下子緩過(guò)神來(lái),在那愣住了。這個(gè)時(shí)候第一個(gè)人本來(lái)已經(jīng)走了,他突然回來(lái)又問(wèn)了一下乘務(wù)員說(shuō)我就問(wèn)一句,很快的請(qǐng)問(wèn)那火車幾點(diǎn)發(fā)車就這樣問(wèn)一句,那你說(shuō)這個(gè)叫不叫插隊(duì)?這個(gè)實(shí)際上完全模擬了我們?cè)诰€程中插隊(duì)的情況。我們來(lái)想一下,這種情況下主要是體現(xiàn)了什么呢?體現(xiàn)了第一,由于我從呆蒙的狀態(tài)到緩過(guò)神來(lái)去執(zhí)行,這個(gè)就對(duì)應(yīng)到我們線程從阻塞狀態(tài)被喚醒,這個(gè)是需要一個(gè)長(zhǎng)時(shí)間切換的。而剛才那個(gè)人他是很清醒,他直接來(lái)問(wèn),問(wèn)好之后他就走,其實(shí)并沒(méi)有影響到我們什么東西,因?yàn)槟莻€(gè)時(shí)候就算他不來(lái)問(wèn),我也是腦子不清楚,也沒(méi)辦法買票。這個(gè)反映了我們這邊非公平的意思。我們來(lái)看一下為什么要有非公平?主要是避免了喚醒帶來(lái)的空檔期這里有一個(gè)空檔期的,因?yàn)槲覀優(yōu)槭裁床幌Mi都是公平的呢?畢竟公平是一種好的行為,不公平是不好的對(duì)不對(duì)?但是我們?nèi)绻冀K公平的話,他在把那個(gè)已經(jīng)掛起的線程恢復(fù)過(guò)來(lái)的這段時(shí)間是有開銷的。而這段時(shí)間如果你是公平的話,你要求必須排隊(duì)的,那么這段時(shí)間誰(shuí)都拿不到鎖,誰(shuí)都沒(méi)辦法處理。但是我們假設(shè)我們是可以允許非公平的。我們假設(shè)我們這邊有三個(gè)線程,第一個(gè)線程 A 持有這把鎖。線程 B 請(qǐng)求這把鎖,由于這個(gè)鎖已經(jīng)被 A 持有了,那么 B 自然而然要去休息,假設(shè) A 這個(gè)時(shí)候釋放了,那么 B 就要被換喚醒并且拿到這把鎖。假設(shè)與此同時(shí),突然 C 來(lái)請(qǐng)求這個(gè)鎖。那么由于 C 這個(gè)線程它本身一直是處于喚醒狀態(tài),它也沒(méi)有休息,它是可以立刻執(zhí)行的。那么它很有可能在 B 被完全喚醒之前就已經(jīng)獲得了,并且使用完了并且又釋放掉這種鎖了,這就形成了一種雙贏的局面。為什么叫雙贏呢?第一個(gè),誰(shuí)贏了, C 肯定是贏了, C 沒(méi)有排隊(duì),他拿到鎖了并且用完了釋放了第二個(gè)誰(shuí)贏了,其實(shí) B 也沒(méi)有輸。為什么呢? B 本身這段時(shí)間他知道說(shuō) A 已經(jīng)釋放了,然后 B 喚醒 B 的這個(gè)過(guò)程是耗時(shí)的。那么這段時(shí)間本身這段時(shí)間我既然耗時(shí),我也拿不到鎖,不如就讓給別人。所以說(shuō)對(duì)于 B 而言,它拿到鎖的時(shí)間并沒(méi)有推遲,所以這是一種雙贏的局面,這種插隊(duì)是可以帶來(lái)吞吐量的提升的。說(shuō)到這里,小伙伴們一定明白了,為什么說(shuō)要有非公平鎖,主要因?yàn)樵谖覀兇蠖鄶?shù)的情況下,由于這個(gè)喚醒的過(guò)程這個(gè)開銷膠其實(shí)是比較大的。那在這個(gè)期間它為了增加我們的吞吐量來(lái)把這個(gè)期間也給利用出去,這就是我們非公平設(shè)計(jì)的最根本的原因。
5、對(duì)比公平和非公平的優(yōu)缺點(diǎn)
6、什么是樂(lè)觀鎖和悲觀鎖?
悲觀鎖
- 如果我不鎖住這個(gè)資源,別人就會(huì)來(lái)爭(zhēng)搶,就會(huì)造成數(shù)據(jù)結(jié)果錯(cuò)誤,所以每次悲觀鎖為了確保結(jié)果的正確性,會(huì)在每次獲取并修改數(shù)據(jù)時(shí),把數(shù)據(jù)鎖住,讓別人無(wú)法訪問(wèn)該數(shù)據(jù),這樣就可以確保數(shù)據(jù)內(nèi)容萬(wàn)無(wú)一失
- Java中悲觀鎖的實(shí)現(xiàn)就是synchronized和Lock相關(guān)類
樂(lè)觀鎖
- 認(rèn)為自己在處理操作的時(shí)候不會(huì)有其他線程來(lái)干擾,所以并不會(huì)鎖住被操作對(duì)象
- 在更新的時(shí)候,去對(duì)比在我修改的期間數(shù)據(jù)有沒(méi)有被其他人改變過(guò)如果沒(méi)被改變過(guò),就說(shuō)明真的是只有我自己在操作,那我就正常去修改數(shù)據(jù)
- 如果數(shù)據(jù)和我一開始拿到的不一樣了,說(shuō)明其他人在這段時(shí)間內(nèi)改過(guò)數(shù)據(jù),那我就不能繼續(xù)剛才的更新數(shù)據(jù)過(guò)程了,我會(huì)選擇放棄、報(bào)錯(cuò)、重試等策略
- 樂(lè)觀鎖的實(shí)現(xiàn)一般都是利用CAS算法來(lái)實(shí)現(xiàn)的
舉幾個(gè)典型的例子。悲觀鎖的例子我們剛才介紹過(guò),主要是 synchronized 和 lock 鎖。那么我們看一下樂(lè)觀鎖,樂(lè)觀鎖它也有很多的應(yīng)用場(chǎng)景。比如說(shuō)我們?cè)倥e一個(gè)數(shù)據(jù)庫(kù)的例子,關(guān)于樂(lè)觀鎖和悲觀鎖而言,在數(shù)據(jù)庫(kù)中都有體現(xiàn)。我們先說(shuō)一個(gè)悲觀鎖的體現(xiàn)。對(duì)于悲觀鎖而言呢,我們?cè)跀?shù)據(jù)庫(kù)中如果用了這樣的語(yǔ)句 select for update 那么它就會(huì)把庫(kù)給鎖住。鎖住之后你再去更新。更新的期間其他人不能修改。但是如果我們用 version 來(lái)控制數(shù)據(jù)庫(kù),這就是樂(lè)觀鎖。我們來(lái)看一下怎么寫這個(gè)語(yǔ)句。我們首先需要有一個(gè)字段啊叫做 lock_version 這個(gè)是專門用來(lái)記錄版本號(hào)的。然后啊我們?cè)诓樵冊(cè)兊臅r(shí)候是要把這個(gè)版本號(hào)給查出來(lái),并且在下一次更新的時(shí)候把加 1 的這個(gè)版本號(hào)給更新上去。更新的時(shí)候它會(huì)去檢查 where version 等于1。這個(gè)實(shí)際上就是在檢查。如果在我更新的期間,有其他人已經(jīng)率先修改了,那么由于對(duì)方也同樣會(huì)把這個(gè)新的版本號(hào)更新上去。假設(shè)第二個(gè)線程先更新了,那么他會(huì)看到的現(xiàn)在的版本這個(gè) ID 等于 5 的這條語(yǔ)句的版本,它就是 2 而不是1。所以如果在此期間它被修改過(guò),那么這條語(yǔ)句是不會(huì)生效的。如果它更新的時(shí)候發(fā)現(xiàn) ID 等于5,并且 version 確實(shí)等于1,說(shuō)明在此期間沒(méi)有人去修改。那么很好,我就把我現(xiàn)在的版本號(hào)是 2 給更新上去,這就是在數(shù)據(jù)庫(kù)中利用我們的樂(lè)觀鎖去實(shí)現(xiàn)。
7、自旋鎖和阻塞鎖
什么是自旋鎖?
如果我們不使用自旋鎖,那么我們就需要阻塞或者喚醒一個(gè) Java 線程。那么喚醒它需要我們切換 CPU 的狀態(tài),這個(gè)是需要耗費(fèi)我們的處理器時(shí)間的。那么假設(shè)我們很快我所等待的那個(gè)鎖就會(huì)被釋放,那么其實(shí)不值得我每次都切換這個(gè)狀態(tài)對(duì)不對(duì)?因?yàn)橛锌赡芪規(guī)?lái)切換的開銷比我執(zhí)行那個(gè)代碼還要時(shí)間長(zhǎng),我執(zhí)行代碼也許很簡(jiǎn)單,也許就是一行代碼,但是你開銷可能很大。所以為了應(yīng)對(duì)這種場(chǎng)景,哪種場(chǎng)景就是我們同步資源鎖定時(shí)間很短的場(chǎng)景,我就不必要為了這一小段時(shí)間去切換線程了。因?yàn)榫€程的掛起和恢復(fù)可能讓整個(gè)的這個(gè)操作得不償失。如果我們的物理機(jī)有多個(gè)處理器的話,我們可以讓兩個(gè)以上的線程同時(shí)是并行執(zhí)行的。那么在這種情況下,我們后面請(qǐng)求鎖的那個(gè)線程,他就不放棄 CPU 的執(zhí)行時(shí)間,他去在那里不停地檢測(cè)你,你是不是很快就釋放了?如果你釋放了,我就來(lái)拿到。這樣一來(lái)我 CPU 沒(méi)有釋放,我 CPU 一直在檢測(cè),這樣一來(lái)就避免了那個(gè)切換的過(guò)程。為了讓當(dāng)前線程去檢測(cè),也說(shuō)讓我稍等一下,我們讓當(dāng)前線程進(jìn)行自旋。如果自旋完成后前面那個(gè)已經(jīng)釋放了,那么 OK 我就可以直接獲取到了,避免了線程的開銷。這個(gè)就是自旋鎖。
自旋鎖的缺點(diǎn)
如果我們的鎖占用時(shí)間過(guò)長(zhǎng),那么自旋只會(huì)白白浪費(fèi)處理器。為什么呢?因?yàn)榍懊婺莻€(gè)人家不想釋放,人家不想釋放。輪到你本該去阻塞的,你又不阻塞,你老是占著 CPU 來(lái)問(wèn)我,你問(wèn)我我就告訴你。那么我現(xiàn)在站著的這個(gè)人他就說(shuō)了,別問(wèn)就是不釋放,你別來(lái)問(wèn)我,你問(wèn)我我也不釋放,一個(gè)小時(shí)我也不釋放。那如果是這樣的一個(gè)情況,這種特例的話,我們自旋所它的效率就不高了,因?yàn)樗谧孕倪^(guò)程中一直要消耗 CPU 雖然一開始它的開銷確實(shí)不高,但是隨著自旋的時(shí)間增長(zhǎng),它的開銷線性增長(zhǎng),那逐漸它的開銷就大了。
8、可重入的性質(zhì)
如果我再次去申請(qǐng)這個(gè)鎖的時(shí)候,無(wú)需提前釋放掉我這把鎖,而是可以直接繼續(xù)使用我手里這把鎖再去獲取的話,這個(gè)就叫做可重入。可重入鎖也叫做遞歸鎖,指的是我同一個(gè)線程可以多次獲取同一把鎖。在我們的 Java 中, lock 是一種synchronized ,也是一種可重入鎖。那么這有什么好處呢?首先第一個(gè)它的好處就是可以避免死鎖。為什么這么說(shuō),假設(shè)我們有兩個(gè)方法都被我們的 synchronize 修飾了,或者是被我們同一個(gè)鎖給鎖住了。那么這個(gè)時(shí)候線程 A 運(yùn)行到第一個(gè)方法他拿到這把鎖了。可是這個(gè)時(shí)候他如果想執(zhí)行第二個(gè)方法,這個(gè)方法也是被同樣的鎖鎖住。假設(shè)我們不具備可重入性,那么這個(gè)時(shí)候再去獲取那把鎖你是獲取不到的,因?yàn)檫@把鎖你必須要先釋放才能再獲取。那你如果不具備可重入性的話,這個(gè)時(shí)候就發(fā)生死鎖了,相當(dāng)于我手上拿著這把鎖,我還想獲取這把鎖對(duì)不起,你獲取不到。那么有了可重入性之后,我們就不會(huì)發(fā)生這種現(xiàn)象了,可以避免死鎖的發(fā)生。二點(diǎn)好處就是提高了我們的封裝性。這樣一來(lái)啊我們的枷鎖鎖解鎖就沒(méi)有那么麻煩,避免了一次的解鎖又加鎖,解鎖又加鎖,降低了我們編程的難度。
9、中斷鎖和不可中斷鎖
可中斷鎖說(shuō)你在獲取鎖的時(shí)候,如果期間你不想去獲取了,你覺(jué)得等待的時(shí)間太長(zhǎng)了,你可以中斷它,讓它不再去傻等而不可中斷鎖,它就沒(méi)有這個(gè)功能。一旦你想讓它去獲取鎖,他就必須去一直等,一直等,直到他拿到鎖才可以進(jìn)行其他的操作。
10、什么是死鎖?
- 發(fā)生在并發(fā)中
- 互不相讓:當(dāng)兩個(gè)(或更多)線程(或進(jìn)程)相互持有對(duì)方所需要的資源,又不主動(dòng)釋放,導(dǎo)致所有人都無(wú)法繼續(xù)前進(jìn),導(dǎo)致程序陷入無(wú)盡的阻塞,這就是死鎖。
線程 A 大家看到它在左側(cè)是持有第一把鎖的,但是同時(shí)它想去獲取右側(cè)的這第二把鎖。同樣道理,線程 B 它持有第二把鎖,他想去獲取第一把鎖。如果假設(shè)他們?cè)谶@里不首先讓出自己的鎖,那么就相當(dāng)于陷入了無(wú)窮的等待了。因?yàn)殒i它的特性是只能同時(shí)被一個(gè)線程所擁有。在這種情況下,鎖 1 已經(jīng)被線程 A 拿走了,它就不可能被線程 B 拿走。鎖 2 已經(jīng)被線程 B 拿走了,它也不可能被線程 A 拿走。所以線程 A 和線程 B 他們手握一部分資源,想獲取另一部分資源,可是卻永遠(yuǎn)沒(méi)有辦法讓這個(gè)程序員繼續(xù)下去。
多個(gè)線程造成死鎖的情況
多個(gè)線程和兩個(gè)線程它們情況是類似的,只不過(guò)多個(gè)線程它們相互依賴不再是你依賴我,我依賴你,而是說(shuō)它們要形成一個(gè)環(huán)路,一旦它們形成了這個(gè)環(huán)路,它們依然可能發(fā)生死鎖。在這個(gè)圖中,我們一共有三個(gè)線程,第一個(gè)線程它是拿到了鎖 A 想去獲取鎖 B 第二個(gè)線程是拿到了鎖 B 想去獲取鎖 C 我們來(lái)看一下,假設(shè)是這種情況,我們先考慮前兩個(gè)線程是一個(gè)什么樣的狀態(tài)。那么前兩個(gè)線程,由于第一個(gè)線程他拿到 A 了,這拿得很順利,然后想去拿 B 可是 B 已經(jīng)被我們的第二個(gè)線程拿到了。所以對(duì)于第一個(gè)線程而言,他就說(shuō),那我等一等,沒(méi)關(guān)系對(duì)吧,我等到你閉這個(gè)鎖是不是釋放之后再給我,這就可以。所以說(shuō)第一個(gè)線程他就開始等。那么對(duì)于第二個(gè)線程而言,他拿到了 B 想去拿 C 可是不巧的是,這個(gè) c2 已經(jīng)被我們線程 3 所獲取了。所以說(shuō)這個(gè)時(shí)候第二個(gè)線程他就開始等啦,他想等到 C 被釋放之后他去拿,可是 C 會(huì)不會(huì)釋放呢?我們來(lái)看到線程3,線程 3 他是拿到了 C 想去獲取 A 這個(gè) A 恰恰就形成了環(huán)路了。在這里他想去獲得 A 可是 A 被線程 1 所拿到,線程 1 是不會(huì)輕易釋放 A 的,除非他拿到了 B 線程 2 是不會(huì)釋放 B 的,除非他拿到了 C 線程 3 是不會(huì)釋放 C 的,除非他拿到了 A 這樣一來(lái),他們?nèi)齻€(gè)就相互打架,三個(gè)和尚沒(méi)水喝,說(shuō)的恰恰就是這種情況。這樣一來(lái),多個(gè)線程同樣也會(huì)造成死鎖的情況,因?yàn)樗鼈冎g會(huì)形成一個(gè)鎖的環(huán)路。
編寫一個(gè)死鎖的例子:
/*** 描述: 必然發(fā)生死鎖*/ public class DeadLock implements Runnable {public int flag;static Object o1 = new Object();static Object o2 = new Object();public void run() {System.out.println("開始執(zhí)行");if (flag == 1) {synchronized (o1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("成功獲取到了兩把鎖");}}}if (flag == 2) {synchronized (o2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("成功獲取到了兩把鎖");}}}}public static void main(String[] args) {DeadLock r1 = new DeadLock();DeadLock r2 = new DeadLock();r1.flag = 1;r2.flag = 2;new Thread(r1).start();new Thread(r2).start();} }九、HashMap和final
1、Hashmap為什么不安全?
我們直接看源碼:
會(huì)出現(xiàn)安全問(wèn)題的就是這個(gè)++,雖然這是一行但有三個(gè)操作:
- 第一個(gè)步驟是讀取
- 第二個(gè)步驟是增加
- 第三個(gè)步驟是保存
以i為例:
????????假設(shè)最開始i的值是1,然后線程 1 先去執(zhí)行i++,他會(huì)發(fā)現(xiàn)i 等于1,然后假設(shè)他執(zhí)行第二步進(jìn)行增加,I加 1 它就算出來(lái)了。但是此時(shí)它還沒(méi)有進(jìn)行第三個(gè)步驟保存。它還沒(méi)有保存的時(shí)候線程 2 開始執(zhí)行了,所以這邊的箭頭指向了右側(cè)。那由于線程 1 還沒(méi)有保存,所以此時(shí)線程 2 所讀到的值一定還是 1 而不是2。所以線程 2 拿到 1 之后再去進(jìn)行加,同樣是把i從 1 加到了2。假設(shè)此時(shí)線程 1 又執(zhí)行了,然后線程 1 就會(huì)把i等于 2 給保存回去。同樣最后輪到線程 2 執(zhí)行的時(shí)候,線程 2 會(huì)把 i 等于 2 給保存回去。那這樣一來(lái)兩個(gè)線程執(zhí)行了兩次i++,本來(lái)如果是 1 的話就應(yīng)該變成3,但是最終你會(huì)發(fā)現(xiàn)它變成的是2,那這就導(dǎo)致了線程安全問(wèn)題,導(dǎo)致我們運(yùn)行的結(jié)果都錯(cuò)誤了,這肯定是不行的。所以說(shuō)我們已經(jīng)發(fā)現(xiàn)了,在我們的哈希 map 中,只要你存在這樣的代碼,比如說(shuō)加,并且沒(méi)有對(duì)這個(gè)方法進(jìn)行任何的同步,比如說(shuō) synchronize 或者是鎖這樣的同步它都沒(méi)有。那這樣就可以證明我們的希 map 是不安全的了。當(dāng)然這是第一點(diǎn),也就是在計(jì)算的時(shí)候,我們這個(gè) modcount 可能它計(jì)算是會(huì)不準(zhǔn)確的,這是一個(gè)角度。
????????另外也有其他的角度說(shuō)它線程不安全。還有一個(gè)角度就是在同時(shí) put 的時(shí)候會(huì)導(dǎo)致數(shù)據(jù)丟失。如果有多個(gè)線程同時(shí)對(duì)哈希 map 進(jìn)行賦值的話,并且他們的 key 假設(shè)是一樣的,那么就可能會(huì)發(fā)生沖突,在發(fā)生沖突的時(shí)候就可能會(huì)有一個(gè)線程,它的值直接就丟失了,那這樣的話就造成了數(shù)據(jù)的損失也是不好的。
????????不僅如此它還有可見性問(wèn)題。那可見性問(wèn)題指的就是說(shuō)比如說(shuō)一個(gè)線程對(duì)哈希 map 進(jìn)行了賦值,但是另外一個(gè)線程卻有可能是看不見的。第二個(gè)線程去獲取的時(shí)候可能獲取到的是舊的值,所以這也是一個(gè)很嚴(yán)重的問(wèn)題,這就是哈希 map 它的一個(gè)弊端。那么經(jīng)過(guò)這些分析我們可以得出一個(gè)結(jié)論,說(shuō)由于哈希 map 自身它不是線程安全的,所以我們盡量不要在并發(fā)的情況下去使用它。
2、final的作用是什么?有哪些用法?
- final修飾變量
- final修飾方法
- final修飾類
final的作用:
????????早期: 早期的 Java 版本中,如果用了 final 修飾,它就會(huì)把某一個(gè)方法轉(zhuǎn)為內(nèi)嵌。內(nèi)嵌的意思就是說(shuō)我們用一個(gè)方法去調(diào)用另外一個(gè) final 方法。那么當(dāng)編譯器發(fā)現(xiàn)它是 final 的,它就會(huì)把那個(gè)方法里面的東西全都給挪過(guò)來(lái),相當(dāng)于我們只在同一個(gè)方法內(nèi)就完成了整個(gè)的工作,而不是方法之間調(diào)來(lái)調(diào)去。因?yàn)槲覀冎婪椒ㄖg的調(diào)用它是有一個(gè)性能損耗的,所以這樣一來(lái)可以提高一定的效率。
????????現(xiàn)在: 第一點(diǎn),我們可以修飾一個(gè)類,防止被繼承。第二點(diǎn)我們可以修飾一個(gè)方法,防止被重寫。第三點(diǎn)我們可以修飾一個(gè)變量,防止被修改。而且第二點(diǎn)其實(shí)現(xiàn)在用 final 的一大原因就是為了實(shí)現(xiàn)線程安全,如果我們可以用 final 把對(duì)象做到不可變,那就不再需要額外的同步開銷,這是一個(gè)很劃算的生意。并且第三點(diǎn)就是之前的我們剛才說(shuō)在早期版本中用 final 帶來(lái)的性能提高,在目前我們幾乎是不需要再考慮了。因?yàn)槲覀兡壳暗?JVM 它非常智能,它會(huì)把能優(yōu)化的點(diǎn)都優(yōu)化到。這樣一來(lái)用不用 final 所帶來(lái)的區(qū)別可以說(shuō)是可以忽略不計(jì)的。而且也有人做過(guò)測(cè)試,目前從性能的角度考慮已經(jīng)看不出它的優(yōu)勢(shì)了。目前我們使用它更多的還是基于設(shè)計(jì)的清晰。因?yàn)樾揎椫笪覀兙椭懒诉@個(gè)屬性或者這個(gè)類或者這個(gè)方法,它擁有了 final 語(yǔ)義,也就是我們不希望它被繼承被重寫或者被修改,這是目前使用 final 的原因,而不再是性能原因了。
final的3種用法: 第一種用法是修飾變量,第二種用法是修飾方法,第三種用法是修飾類。
- final instance variable(類中的final屬性)
- final static variable(類中的static final屬性)
- final local variable(方法中的final變量)
三種變量它們最主要的區(qū)別就是在賦值。實(shí)際上一旦一個(gè)屬性被聲明為 final 之后,它的變量就只能被賦值一次,一旦賦值就不能再改變,無(wú)論如何也不能改變。
賦值時(shí)機(jī):
- final instance variable(類中的final屬性)
-
- 第一種是在聲明變量的等號(hào)右邊直接賦值第二種就是構(gòu)造函數(shù)中賦值
-
- 第三就是在類的初始代碼塊中賦值(不常用)
-
- 如果不采用第一種賦值方法,那么就必須在第2、3種挑一個(gè)來(lái)賦值,而不能不賦值,這是final語(yǔ)法所規(guī)定的
- final static variable(類中的static final屬性)
-
- 兩個(gè)賦值時(shí)機(jī)∶除了在聲明變量的等號(hào)右邊直接賦值外,static final變量還可以用static初始代碼塊賦值,但是不能用普通的初始代碼塊賦值
- final local variable(方法中的final變量)
-
- 和前面兩種不同,由于這里的變量是在方法里的,所以沒(méi)有構(gòu)造函數(shù),也不存在初始代碼塊
-
- final local variable不規(guī)定賦值時(shí)機(jī),只要求在使用前必須賦值,這和方法中的非final變量的要求也是一樣的
為什么要規(guī)定賦值時(shí)機(jī)?
我們來(lái)思考一下為什么語(yǔ)法要這繼承這樣?∶如果初始化不賦值,后續(xù)賦值,就是從null變成你的賦值,這就違反final不變的原則了!
總結(jié): 使用它的時(shí)候有三個(gè)途徑,一個(gè)是變量,一個(gè)是方法,一個(gè)是類。尤其是對(duì)于變量而言,還分為三種變量。在這邊會(huì)有類中的屬性,類中的 static 以及方法中的它們各自都有不同的賦值時(shí)機(jī)。但是總結(jié)出來(lái)一旦被賦值,那么它就不可以再變化了。對(duì)于 final 的第二種用法而言,是修飾方法,構(gòu)造方法不允許被修飾。而普通方法被修飾之后,它不能被 override 如果用發(fā)音道去修飾類代表這個(gè)類不可被繼承。最典型的就是我們的 string 它就是發(fā)音道修飾的,它也不可以被繼承。
十、單例模式的八種寫法
1、什么是單例模式?
單例模式指的是保證一個(gè)類只有一個(gè)實(shí)例,并且還提供一個(gè)全局可以訪問(wèn)的入口,這個(gè)就是單例模式了。我們舉個(gè)例子,比如說(shuō)分身術(shù),分身術(shù)分出來(lái)其實(shí)有很多個(gè),但是真正的真身只有一個(gè)。也就是說(shuō)如果我們使用了單例模式看上去每個(gè)地方都能調(diào)用到這個(gè)對(duì)象,但其實(shí)它們背后都是同一個(gè)對(duì)象。
2、為什么需要單例模式?
節(jié)省內(nèi)存和計(jì)算、保證結(jié)果正確、方便管理
3、應(yīng)用場(chǎng)景
????????沒(méi)有狀態(tài)的工具類。比如說(shuō)日志工具類,它就屬于沒(méi)有狀態(tài)的,無(wú)論在哪里使用,其實(shí)我們?nèi)フ{(diào)用它僅僅是讓它幫我們?nèi)ビ涗浫罩拘畔ⅰ3酥?#xff0c;我們也不需要在實(shí)例對(duì)象上存儲(chǔ)任何的狀態(tài)。那么在這種情況下,這種工具類我們使用一個(gè)實(shí)例就夠了,類似的還有像字符串處理工具類或者是日期工具類都可以。那么我們利用單例模式給我們提供一個(gè)統(tǒng)一的入口,使得管理這些工具類就非常的方便。
????????全局的信息類。比如說(shuō)我們用一個(gè)類記錄網(wǎng)站的訪問(wèn)次數(shù),我們不希望有的被記錄在 A 上,有的記錄被記錄在對(duì)象 B 上。那此時(shí)我們用這個(gè)單例模式去做就很合適,類似的還有環(huán)境變量。
4、單例模式的八種寫法
- 餓漢式(靜態(tài)常量)[可用]
- 餓漢式(靜態(tài)代碼塊)[可用]
- 懶漢式(線程不安全)[不可用]
- 懶漢式(線程安全,同步方法)[不推薦用]
- 懶漢式(線程不安全,同步代碼塊)[不可用]
- 雙重檢查[推薦用]
- 靜態(tài)內(nèi)部類[推薦用]
- 枚舉[推薦用]
下面是代碼演示:
1)、餓漢式(靜態(tài)常量)(可用)
public class Singleton1 {private Singleton1() {}private final static Singleton1 INSTANCE = new Singleton1();public static Singleton1 getInstance() {return INSTANCE;} }當(dāng)用戶想去拿到這個(gè)單例的時(shí)候,他會(huì)調(diào)用這邊的 get instance 方法。那么返回的就是這個(gè) instance 而這個(gè) instance 它會(huì)在最開始類加載的時(shí)候就把這個(gè)實(shí)例給初始化出來(lái)。那么你可以在這個(gè)構(gòu)造函數(shù)里面去寫很多的或者說(shuō)更多的初始化的內(nèi)容,無(wú)論是給其中的屬性賦值或者是去計(jì)算或者是去調(diào)用數(shù)據(jù)庫(kù)都可以。但是后續(xù)凡是去使用 get instance 拿到的實(shí)例一定是這個(gè)單例。那這種寫法為什么說(shuō)它可用呢?原因就在于它不具備懶加載的效果。
那什么叫做懶加載啊?懶加載的意思就是說(shuō)在加載這個(gè)類之后,并不一定要立刻的把這個(gè)實(shí)例給初始化出來(lái),可以到運(yùn)用實(shí)例的時(shí)候再初始化出來(lái)。但是我們這種寫法只要加載了這個(gè)類,那么由于我們這邊的 instance 它是 static 修飾的。所以根據(jù) Java 類加載的原則,datect修飾的,在類加載的時(shí)候就會(huì)完成對(duì)于后面這個(gè)實(shí)例的創(chuàng)建,所以它的主要缺點(diǎn)在于沒(méi)有達(dá)到懶加載的效果。
2)、餓漢式(靜態(tài)代碼塊)(可用)
public class Singleton2 {private Singleton2() {}static {INSTANCE = new Singleton2();}private final static Singleton2 INSTANCE;public static Singleton2 getInstance() {return INSTANCE;} }根據(jù)我們類加載的原則,同樣在類加載的時(shí)候會(huì)把靜態(tài)代碼塊兒也就是 static 修飾的這個(gè)大括號(hào)里面的內(nèi)容都執(zhí)行完,所以它就執(zhí)行完了。那么一旦你執(zhí)行完,這個(gè)對(duì)象也就創(chuàng)建出來(lái)了。那有的時(shí)候如果你類加載了,但是其實(shí)你并不需要這個(gè)單例的話,但是這個(gè)時(shí)候由于 static 這個(gè)代碼塊兒一定會(huì)被執(zhí)行,所以這個(gè)實(shí)例它所占用的內(nèi)存包括初始化所帶來(lái)的開銷,其實(shí)都屬于浪費(fèi)了。
所以這種寫法和之前的那種寫法,它們擁有一樣的缺點(diǎn),那就是沒(méi)有實(shí)現(xiàn)懶加載的效果,這就是餓漢式的一個(gè)通病。餓漢式之所以叫餓漢式。說(shuō)明他餓很餓的人一見到食物就會(huì)去吃。所以這個(gè)餓漢式的寫法,一旦在類加載的時(shí)候,就會(huì)把實(shí)例給實(shí)例化出來(lái)。
3)、懶漢式(線程不安全)
public class Singleton3 {private Singleton3() {}private static Singleton3 INSTANCE;public static Singleton3 getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton3();}return INSTANCE;} }這個(gè)就是最簡(jiǎn)單的懶漢式的寫法。那么邏輯上看上去并沒(méi)有問(wèn)題。因?yàn)榈谝粋€(gè)訪問(wèn)這個(gè) get instance 方法的線程,它會(huì)發(fā)現(xiàn) instance 等于 null于是就新建并且返回后面的線程,發(fā)現(xiàn)它不等于 null直接返回并且返回的都是同一個(gè)實(shí)例。
可是這種寫法的問(wèn)題在哪里呢?問(wèn)題就在于,如果有兩個(gè)線程同時(shí)的訪問(wèn)到這一行代碼,也就是他們同時(shí)去判斷 instance 是不是等于 null那么假設(shè)此時(shí)這個(gè) instance 還沒(méi)有被初始化,也就是這兩個(gè)線程都是第一次去訪問(wèn)這個(gè) instance 那么這個(gè)時(shí)候由于他們都是同時(shí)在這邊,所以他們都會(huì)同時(shí)的判斷。你確實(shí)等于null,于是他們都會(huì)進(jìn)入到這一行語(yǔ)句中。這樣一來(lái),我們就創(chuàng)建了兩個(gè)實(shí)例,第一個(gè)線程會(huì)創(chuàng)建一個(gè) singleton3 而第二個(gè)線程也會(huì)創(chuàng)建一個(gè) singleton3。那這樣一來(lái)就違背了我們單例模式的初衷和原則。我們最大的原則就是只有一個(gè)實(shí)例不能有兩個(gè)實(shí)例。那現(xiàn)在一旦兩個(gè)線程同時(shí)去訪問(wèn)的話,就會(huì)導(dǎo)致你這個(gè)單例模式失效,所以這是線程不安全的。
4)、懶漢式(線程安全,同步方法)(不推薦)
public class Singleton4 {private Singleton4() {}private static Singleton4 INSTANCE;public synchronized static Singleton4 getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton4();}return INSTANCE;} }第一個(gè)線程它全都執(zhí)行完畢了。第二個(gè)線程進(jìn)來(lái),它就不可能再看到這個(gè) instance 為 null 了。因?yàn)榈谝粋€(gè)線程執(zhí)行完畢之后,它已經(jīng)把它實(shí)例化完畢了。所以第二個(gè)線程看到 OK 你不等于鬧,于是就返回了。所以就避免了之前的兩個(gè)實(shí)例的這種問(wèn)題。所以這種寫法是線程安全的,是可以使用的。
但是說(shuō)它可以使用的同時(shí)我們同樣又標(biāo)出了不推薦。所以這種寫法它的問(wèn)題在哪里呢?
系統(tǒng)并發(fā)量比較大,那么大家都排隊(duì)的話這個(gè)效率就太低了。每個(gè)線程想獲取這個(gè)類的單例的時(shí)候都要進(jìn)行同步,那多個(gè)線程還不能同時(shí)的進(jìn)行獲取。那假設(shè)我們線程多一點(diǎn),可能會(huì)導(dǎo)致在獲取這個(gè)實(shí)例的時(shí)候發(fā)生擁堵。那其實(shí)這種麻煩是沒(méi)有必要的,我們并不需要讓他每次都進(jìn)行同步。
5)、假如我們升級(jí)一下(同步的范圍盡量縮小),上面的代碼
public class Singleton5 {private Singleton5() {}private static Singleton5 INSTANCE;public static Singleton5 getInstance() {if (INSTANCE == null) {synchronized (Singleton5.class) {INSTANCE = new Singleton5();}}return INSTANCE;} }這個(gè)也不是線程安全的,我們來(lái)想象一種情況,那假設(shè)有兩個(gè)線程同時(shí)的去走到了這一行語(yǔ)句,并且他們都判斷出來(lái) instance 等于 null 于是他們都會(huì)進(jìn)入到 synchronized 代碼塊的外面,雖然根據(jù)規(guī)定不能有兩個(gè)線程同時(shí)的去執(zhí)行這里面的語(yǔ)句。但是假設(shè)第一個(gè)線程已經(jīng)執(zhí)行完了,里面的語(yǔ)句,第二個(gè)線程此時(shí)就會(huì)進(jìn)去并且再次執(zhí)行這個(gè)語(yǔ)句。這樣一來(lái)還是生成了兩個(gè)實(shí)例。所以這種寫法它的初衷是好,他想把我們的同步的范圍盡量縮小,這樣的話效率盡可能的可以提高,但是是線程不安全的,那么線程不安全的話肯定是不能使用的。
6)、雙重檢查(推薦)
public class Singleton6 {private Singleton6() {}private static volatile Singleton6 INSTANCE;public static Singleton6 getInstance() {if (INSTANCE == null) {synchronized (Singleton6.class) {if (INSTANCE == null) {INSTANCE = new Singleton6();}}}return INSTANCE;} }首先我們?cè)賮?lái)看剛才我們所講到的那種情況,當(dāng)兩個(gè)線程同時(shí)的去訪問(wèn)到這一行,并且都發(fā)現(xiàn)它等于鬧,于是一個(gè)線程會(huì)等待在這個(gè)代碼塊的外面,另外一個(gè)線程去執(zhí)行。這樣當(dāng)它執(zhí)行完畢之后,這個(gè) instance 就被實(shí)例化了。此時(shí)第二個(gè)線程再進(jìn)來(lái)他還會(huì)檢查一下,他此時(shí)一定會(huì)發(fā)現(xiàn)這個(gè) instance 不等于鬧。因?yàn)榍懊婺莻€(gè)線程已經(jīng)把它初始化完畢了,所以它就會(huì)跳過(guò)這個(gè)創(chuàng)建實(shí)例的這個(gè)過(guò)程。然后返回返回的也是第一個(gè)線程所創(chuàng)建的那個(gè)實(shí)例,所以這樣一來(lái)就不會(huì)出現(xiàn)多個(gè)實(shí)例的情況了。
現(xiàn)在我們就來(lái)看一下為什么需要使用 volatile 新建一個(gè)對(duì)象,比如說(shuō)我們?nèi)?zhí)行一個(gè) newsingle6,這就是一個(gè)典型的新建對(duì)象。那么新建一個(gè)對(duì)象,其實(shí)會(huì)有三個(gè)步驟,而不是我們所表面上看到的這一個(gè)步驟。哪三個(gè)步驟呢?
- 1.新建一個(gè)對(duì)象,但還未初始化
- 2.調(diào)用構(gòu)造函數(shù)等來(lái)初始化該對(duì)象
- 3.把對(duì)象指向引用
問(wèn)題:
????????由于 CPU 的優(yōu)化或者是編譯器的優(yōu)化,這三步其實(shí)可能它的順序會(huì)被調(diào)換,也就是說(shuō)看上去是 123 的步驟,有一定可能性會(huì)變成132。一旦它的順序變成了132。也說(shuō)我們?cè)谶@邊它是先新建對(duì)象,但是還沒(méi)有初始化,但是他就把這個(gè)對(duì)象指向這個(gè)引用了。那此時(shí)這個(gè)對(duì)象其實(shí)還沒(méi)有調(diào)用構(gòu)造函數(shù),但是對(duì)于我們判斷它等不等于 null 這個(gè)時(shí)候它的結(jié)果已經(jīng)是不等于null了。
????????現(xiàn)在我們就來(lái)模擬這種場(chǎng)景,假設(shè)第一個(gè)線程它先執(zhí)行,然后它去判斷等不等于null。由于是第一次執(zhí)行,所以它等于null,它就進(jìn)來(lái)了這個(gè)同步代碼塊兒。那么進(jìn)來(lái)了同步代碼塊之后,他再次判斷等不等于到依然等于到,于是他就去創(chuàng)建。那這里我們已經(jīng)知道了,他其實(shí)背后是有三步的,于是假設(shè)他執(zhí)行完了第一步,然后跳到第三步來(lái)執(zhí)行,第二步還沒(méi)有執(zhí)行。那么此時(shí)這個(gè) instance 已經(jīng)不等于null了,但是它還沒(méi)有執(zhí)行真正的構(gòu)造函數(shù),所以它的很多的屬性還沒(méi)有被初始化。此時(shí)假設(shè)有另外一個(gè)線程進(jìn)來(lái)了,它剛剛進(jìn)入到這個(gè)方法之后,第一步就是去判斷它是不是等于null。那此時(shí)由于它不等于null,因?yàn)榍懊嫖覀冎v過(guò)這個(gè)對(duì)象已經(jīng)被指向了這個(gè)引用,所以對(duì)于第二個(gè)線程而言,它看到的確實(shí)不等于null,于是他就把這個(gè)對(duì)象給返回了。但是這個(gè)對(duì)象返回的時(shí)候,其實(shí)這個(gè)對(duì)象還沒(méi)有執(zhí)行構(gòu)作函數(shù)里面的內(nèi)容,所以它還沒(méi)有初始化完畢。那么第二個(gè)線程拿到這樣一個(gè)半成品的對(duì)象去使用的話,自然就會(huì)報(bào)錯(cuò)了。
總結(jié): 第一點(diǎn)是第一重檢查的作用在于提高效率。第二點(diǎn)在于第二重檢查的作用在于保證線程安全。而第三點(diǎn)在于volatile ,它的作用主要是為了防止重排序所帶來(lái)的問(wèn)題。有了它之后就可以自動(dòng)的避免重排序,保證了線程安全。
7)、靜態(tài)內(nèi)部類寫法(推薦用)
public class Singleton7 {private Singleton7() {}private static class SingletonInstance {private static Singleton7 INSTANCE = new Singleton7();}public static Singleton7 getInstance() {return SingletonInstance.INSTANCE;} }那我們來(lái)看一下,這種寫法的關(guān)鍵點(diǎn)在于它有一個(gè)內(nèi)部類,并且在這個(gè)內(nèi)部類里面去把我們的 instance 給實(shí)例化了。那用這種寫法的好處在于外面這個(gè)類被裝載的時(shí)候,里面這個(gè)類并不會(huì)被裝載,所以它就實(shí)現(xiàn)了懶加載。那么只有調(diào)用 get instance 方法的時(shí)候,它會(huì)去訪問(wèn)到里面的這個(gè) instance 實(shí)例,此時(shí)才會(huì)把它給加載出來(lái),所以它就避免了內(nèi)存的浪費(fèi)。那這種寫法我們可以在項(xiàng)目中使用是沒(méi)有問(wèn)題的。
8)、枚舉單例模式
public enum Singleton8 {// 1.寫法簡(jiǎn)潔,只需要Singleton8.INSTANCE,就可以進(jìn)行操作了// 2.線程安全,Java 虛擬機(jī)所保證// 3.防止反射,Java規(guī)定枚舉是不允許被反射創(chuàng)建的,所以它天然的就保證了反射時(shí)候的安全性。INSTANCE; }不同的寫法對(duì)比:
- 餓漢︰簡(jiǎn)單,但是沒(méi)有l(wèi)azy loading(懶加載)
- 懶漢︰有線程安全問(wèn)題
- 靜態(tài)內(nèi)部類∶可用
- 雙重檢查∶面試用
- 枚舉:最好
總結(jié)
- 上一篇: 闭环控制 matlab仿真,反馈闭环控制
- 下一篇: 汉字与GBK内码互转工具(支持批量转换)