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