高併發編程

(一)高併發編程基礎知識

這裏涉及到一些基礎的概念,我從新捧起了一下《實戰 Java 高併發程序設計》這一本書,感受到心潮澎湃,這或許就是筆者敘述功底紮實的魅力吧,喜歡。對於併發的基礎能夠參照一下我以前寫過的一篇博文:Java學習筆記(4)——併發基礎html

1)多線程和單線程的區別和聯繫?

答:java

  1. 在單核 CPU 中,將 CPU 分爲很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。程序員

  2. 多線程會存在線程上下文切換,會致使程序執行速度變慢,即採用一個擁有兩個線程的進程執行所須要的時間比一個線程的進程執行兩次所須要的時間要多一些。面試

結論:即採用多線程不會提升程序的執行速度,反而會下降速度,可是對於用戶來講,能夠減小用戶的響應時間。算法

面試官:那使用多線程有什麼優點?編程

解析:儘管面臨不少挑戰,多線程有一些優勢仍然使得它一直被使用,而這些優勢咱們應該瞭解。設計模式

答:緩存

(1)資源利用率更好安全

想象一下,一個應用程序須要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件須要5秒,處理一個文件須要2秒。處理兩個文件則須要:服務器

1| 5秒讀取文件A 2| 2秒處理文件A 3| 5秒讀取文件B 4| 2秒處理文件B 5| --------------------- 6| 總共須要14秒

從磁盤中讀取文件的時候,大部分的CPU時間用於等待磁盤去讀取數據。在這段時間裏,CPU很是的空閒。它能夠作一些別的事情。經過改變操做的順序,就可以更好的使用CPU資源。看下面的順序:

1| 5秒讀取文件A 2| 5秒讀取文件B + 2秒處理文件A 3| 2秒處理文件B 4| --------------------- 5| 總共須要12秒

CPU等待第一個文件被讀取完。而後開始讀取第二個文件。當第二文件在被讀取的時候,CPU會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU大部分時間是空閒的。

總的說來,CPU可以在等待IO的時候作一些其餘的事情。這個不必定就是磁盤IO。它也能夠是網絡的IO,或者用戶輸入。一般狀況下,網絡和磁盤的IO比CPU和內存的IO慢的多。

(2)程序設計在某些狀況下更簡單

在單線程應用程序中,若是你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每一個文件讀取和處理的狀態。相反,你能夠啓動兩個線程,每一個線程處理一個文件的讀取和操做。線程會在等待磁盤讀取文件的過程當中被阻塞。在等待的時候,其餘的線程可以使用CPU去處理已經讀取完的文件。其結果就是,磁盤老是在繁忙地讀取不一樣的文件到內存中。這會帶來磁盤和CPU利用率的提高。並且每一個線程只須要記錄一個文件,所以這種方式也很容易編程實現。

(3)程序響應更快

有時咱們會編寫一些較爲複雜的代碼(這裏的複雜不是說複雜的算法,而是複雜的業務邏輯),例如,一筆訂單的建立,它包括插入訂單數據、生成訂單趕快找、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊「訂購」按鈕開始,就要等待這些操做所有完成才能看到訂購成功的結果。可是這麼多業務操做,如何可以讓其更快地完成呢?

在上面的場景中,可使用多線程技術,即將數據一致性不強的操做派發給其餘線程處理(也可使用消息隊列),如生成訂單快照、發送郵件等。這樣作的好處是響應用戶請求的線程可以儘量快地處理完成,縮短了響應時間,提高了用戶體驗。

多線程還有一些優點也顯而易見:
① 進程以前不能共享內存,而線程之間共享內存(堆內存)則很簡單。
② 系統建立進程時須要爲該進程從新分配系統資源,建立線程則代價小不少,所以實現多任務併發時,多線程效率更高.
③ Java語言自己內置多線程功能的支持,而不是單純第做爲底層系統的調度方式,從而簡化了多線程編程.

2)多線程必定快嗎?

答:不必定。

好比,咱們嘗試使用並行和串行來分別執行累加的操做觀察是否並行執行必定比串行執行更快:

如下是我測試的結果,能夠看出,當不超過1百萬的時候,並行是明顯比串行要慢的,爲何併發執行的速度會比串行慢呢?這是由於線程有建立和上下文切換的開銷。

3)什麼是同步?什麼又是異步?

解析:這是對多線程基礎知識的考察

答:同步和異步一般用來形容一次方法調用。

同步方法調用一旦開始,調用者必須等到方法返回後,才能繼續後續的行爲。這就好像是咱們去商城買一臺空調,你看中了一臺空調,因而就跟售貨員下了單,而後售貨員就去倉庫幫你調配物品,這天你熱的實在不行,就催着商家趕忙發貨,因而你就在商店裏等着,知道商家把你和空調都送回家,一次愉快的購物才結束,這就是同步調用。

而異步方法更像是一個消息傳遞,一旦開始,方法調用就會當即返回,調用者就能夠繼續後續的操做。回到剛纔買空調的例子,咱們能夠坐在裏打開電腦,在網上訂購一臺空調。當你完成網上支付的時候,對你來講購物過程已經結束了。雖然空調尚未送到家,可是你的任務都已經完成了。商家接到你的訂單後,就會加緊安排送貨,固然這一切已經跟你無關了,你已經支付完成,想什麼就能去幹什麼了,出去溜達幾圈都不成問題。等送貨上門的時候,接到商家電話,回家一趟簽收便可。這就是異步調用。

面試官:那併發(Concurrency)和並行(Parallelism)的區別呢?

解析:並行性和併發性是既類似又有區別的兩個概念。

答:並行性是指兩個或多個事件在同一時刻發生。而併發性是指連個或多個事件在同一時間間隔內發生。

在多道程序環境下,併發性是指在一段時間內宏觀上有多個程序在同時運行,但在單處理機環境下(一個處理器),每一時刻卻僅能有一道程序執行,故微觀上這些程序只能是分時地交替執行。例如,在1秒鐘時間內,0-15ms程序A運行;15-30ms程序B運行;30-45ms程序C運行;45-60ms程序D運行,所以能夠說,在1秒鐘時間間隔內,宏觀上有四道程序在同時運行,但微觀上,程序A、B、C、D是分時地交替執行的。

若是在計算機系統中有多個處理機,這些能夠併發執行的程序就能夠被分配到多個處理機上,實現併發執行,即利用每一個處理機處理一個可併發執行的程序。這樣,多個程序即可以同時執行。以此就能提升系統中的資源利用率,增長系統的吞吐量。

4)線程和進程的區別:(必考)

答:

  1. 進程是一個 「執行中的程序」,是系統進行資源分配和調度的一個獨立單位;
  2. 線程是進程的一個實體,一個進程中擁有多個線程,線程之間共享地址空間和其它資源(因此通訊和同步等操做線程比進程更加容易);
  3. 線程上下文的切換比進程上下文切換要快不少。
    • (1)進程切換時,涉及到當前進程的 CPU 環境的保存和新被調度運行進程的 CPU 環境的設置。

    • (2)線程切換僅須要保存和設置少許的寄存器內容,不涉及存儲管理方面的操做。

面試官:進程間如何通信?線程間如何通信?

答:進程間通信依靠 IPC 資源,例如管道(pipes)、套接字(sockets)等;

線程間通信依靠 JVM 提供的 API,例如 wait()、notify()、notifyAll() 等方法,線程間還能夠經過共享的主內存來進行值的傳遞。

關於線程和進程有一篇寫得很是不錯的文章,不過是英文的,我進行了翻譯,相信閱讀以後會對進程和線程有不同的理解:線程和進程基礎——翻譯文

5)什麼是阻塞(Blocking)和非阻塞(Non-Blocking)?

答:阻塞和非阻塞一般用來形容多線程間的相互影響。好比一個線程佔用了臨界區資源,那麼其餘全部須要這個而資源的線程就必須在這個臨界區中進行等待。等待會致使線程掛起,這種狀況就是阻塞。此時,若是佔用資源的線程一直不肯意釋放資源,那麼其餘全部阻塞在這個臨界區上的線程都不能工做。

非阻塞的意思與之相反,它強調沒有一個線程能夠妨礙其餘線程執行。全部的線程都會嘗試不斷前向執行。

面試官:臨界區是什麼?

答:臨界區用來表示一種公共資源或者說是共享資源,能夠被多個線程使用。可是每一次,只能有一個線程使用它,一旦臨界區資源被佔用,其餘線程要想使用這個資源,就必須等待。

好比,在一個辦公室裏有一臺打印機,打印機一次只能執行一個任務。若是小王和小明同時須要打印文件,很顯然,若是小王先下發了打印任務,打印機就開始打印小王的文件了,小明的任務就只能等待小王打印結束後才能打印,這裏的打印機就是一個臨界區的例子。

在並行程序中,臨界區資源是保護的對象,若是意外出現打印機同時執行兩個打印任務,那麼最可能的結果就是打印出來的文件就會是損壞的文件,它既不是小王想要的,也不是小明想要的。

6)什麼是死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock)?

答:死鎖、飢餓和活鎖都屬於多線程的活躍性問題,若是發現上述幾種狀況,那麼相關線程可能就再也不活躍,也就說它可能很難再繼續往下執行了。

  1. 死鎖應該是最糟糕的一種狀況了,它表示兩個或者兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。

  2. 飢餓是指某一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行。好比:
    1)它的線程優先級可能過低,而高優先級的線程不斷搶佔它須要的資源,致使低優先級的線程沒法工做。在天然界中,母雞餵食雛鳥時,很容易出現這種狀況,因爲雛鳥不少,食物有限,雛鳥之間的食物競爭可能很是厲害,小雛鳥由於常常搶不到食物,有可能會被餓死。線程的飢餓也很是相似這種狀況。
    2)另一種多是,某一個線程一直佔着關鍵資源不放,致使其餘須要這個資源的線程沒法正常執行,這種狀況也是飢餓的一種。
    與死鎖相比,飢餓仍是有可能在將來一段時間內解決的(好比高優先級的線程已經完成任務,再也不瘋狂的執行)

  3. 活鎖是一種很是有趣的狀況。不知道你們是否是有遇到過這樣一種狀況,當你要坐電梯下樓,電梯到了,門開了,這時你正準備出去,但不巧的是,門外一我的擋着你的去路,他想進來。因而你很紳士的靠左走,避讓對方,但同時對方也很紳士,但他靠右走但願避讓你。結果,大家又撞上了。因而乎,大家都意識到了問題,但願儘快避讓對方,你當即向右走,他也當即向左走,結果又撞上了!不過介於人類的只能,我相信這個動做重複 二、 3 次後,你應該能夠順利解決這個問題,由於這個時候,你們都會本能的對視,進行交流,保證這種狀況再也不發生。
    但若是這種狀況發生在兩個線程間可能就不會那麼幸運了,若是線程的智力不夠,且都秉承着 「謙讓」 的原則,主動將資源釋放給他人使用,那麼就會出現資源不斷在兩個線程中跳動,而沒有一個線程能夠同時拿到全部的資源而正常執行。這種狀況就是活鎖。

7)多線程產生死鎖的 4 個必要條件?

答:

  1. 互斥條件:一個資源每次只能被一個線程使用;

  2. 請求與保持條件:一個線程因請求資源而阻塞時,對已得到的資源保持不放;

  3. 不剝奪條件:進程已經得到的資源,在未使用完以前,不能強行剝奪;

  4. 循環等待條件:若干線程之間造成一種頭尾相接的循環等待資源關係。

面試官:如何避免死鎖?(常常接着問這個問題哦~)

答:指定獲取鎖的順序,舉例以下:

  1. 好比某個線程只有得到 A 鎖和 B 鎖才能對某資源進行操做,在多線程條件下,如何避免死鎖?

  2. 得到鎖的順序是必定的,好比規定,只有得到 A 鎖的線程纔有資格獲取 B 鎖,按順序獲取鎖就能夠避免死鎖!!!

8)如何指定多個線程的執行順序?

解析:面試官會給你舉個例子,如何讓 10 個線程按照順序打印 0123456789?(寫代碼實現)

答:

  1. 設定一個 orderNum,每一個線程執行結束以後,更新 orderNum,指明下一個要執行的線程。而且喚醒全部的等待線程。

  2. 在每個線程的開始,要 while 判斷 orderNum 是否等於本身的要求值!!不是,則 wait,是則執行本線程。

9)Java 中線程有幾種狀態?

答:六種(查看 Java 源碼也能夠看到是 6 種),而且某個時刻 Java 線程只能處於其中的一個狀態。

  1. 新建(NEW)狀態:表示新建立了一個線程對象,而此時線程並無開始執行。

  2. 可運行(RUNNABLE)狀態:線程對象建立後,其它線程(好比 main 線程)調用了該對象的 start() 方法,才表示線程開始執行。當線程執行時,處於 RUNNBALE 狀態,表示線程所需的一切資源都已經準備好了。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取 cpu 的使用權。

  3. 阻塞(BLOCKED)狀態:若是線程在執行過程終於到了 synchronized 同步塊,就會進入 BLOCKED 阻塞狀態,這時線程就會暫停執行,直到得到請求的鎖。

  4. 等待(WAITING)狀態:當線程等待另外一個線程通知調度器一個條件時,它本身進入等待狀態。在調用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時,就會出現這種狀況;

  5. 計時等待(TIMED_WAITING)狀態:Object.wait、Thread.join、Lock.tryLock和Condition.await 等方法有超時參數,還有 Thread.sleep 方法、LockSupport.parkNanos 方法和 LockSupport.parkUntil 方法,這些方法會致使線程進入計時等待狀態,若是超時或者出現通知,都會切換會可運行狀態;

  6. 終止(TERMINATED)狀態:當線程執行完畢,則進入該狀態,表示結束。

注意:從 NEW 狀態出發後,線程不能再回到 NEW 狀態,同理,處於 TERMINATED 狀態的線程也不能再回到 RUNNABLE 狀態。


(二)高併發編程-JUC 包

在 Java 5.0 提供了 java.util.concurrent(簡稱 JUC )包,在此包中增長了在併發編程中很經常使用的實用工具類,用於定義相似於線程的自定義子系統,包括線程池、異步 IO 和輕量級任務框架。

1)sleep( ) 和 wait( n)、wait( ) 的區別:

答:

  1. sleep 方法:是 Thread 類的靜態方法,當前線程將睡眠 n 毫秒,線程進入阻塞狀態。當睡眠時間到了,會解除阻塞,進行可運行狀態,等待 CPU 的到來。睡眠不釋放鎖(若是有的話);

  2. wait 方法:是 Object 的方法,必須與 synchronized 關鍵字一塊兒使用,線程進入阻塞狀態,當 notify 或者 notifyall 被調用後,會解除阻塞。可是,只有從新佔用互斥鎖以後纔會進入可運行狀態。睡眠時,釋放互斥鎖。

2)synchronized 關鍵字:

答:底層實現:

  1. 進入時,執行 monitorenter,將計數器 +1,釋放鎖 monitorexit 時,計數器-1;

  2. 當一個線程判斷到計數器爲 0 時,則當前鎖空閒,能夠佔用;反之,當前線程進入等待狀態。

含義:(monitor 機制)

Synchronized 是在加鎖,加對象鎖。對象鎖是一種重量鎖(monitor),synchronized 的鎖機制會根據線程競爭狀況在運行時會有偏向鎖(單一線程)、輕量鎖(多個線程訪問 synchronized 區域)、對象鎖(重量鎖,多個線程存在競爭的狀況)、自旋鎖等。

該關鍵字是一個幾種鎖的封裝。

3)volatile 關鍵字:

答:該關鍵字能夠保證可見性不保證原子性。

功能:

  1. 主內存和工做內存,直接與主內存產生交互,進行讀寫操做,保證可見性;

  2. 禁止 JVM 進行的指令重排序。

解析:關於指令重排序的問題,能夠查閱 DCL 雙檢鎖失效相關資料。

4)volatile 能使得一個非原子操做變成原子操做嗎?

答:能。

一個典型的例子是在類中有一個 long 類型的成員變量。若是你知道該成員變量會被多個線程訪問,如計數器、價格等,你最好是將其設置爲 volatile。爲何?由於 Java 中讀取 long 類型變量不是原子的,須要分紅兩步,若是一個線程正在修改該 long 變量的值,另外一個線程可能只能看到該值的一半(前 32 位)。可是對一個 volatile 型的 long 或 double 變量的讀寫是原子。

面試官:volatile 修飾符的有過什麼實踐?

答:

  1. 一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是64位寬,所以對這兩種類型的讀是分爲兩部分的,第一次讀取第一個 32 位,而後再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的。

  2. volatile 修復符的另外一個做用是提供內存屏障(memory barrier),例如在分佈式框架中的應用。簡單的說,就是當你寫一個 volatile 變量以前,Java 內存模型會插入一個寫屏障(write barrier),讀一個 volatile 變量以前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫以前,也能保證任何數值的更新對全部線程是可見的,由於內存屏障會將其餘全部寫的值更新到緩存。

5)ThreadLocal(線程局部變量)關鍵字:

答:當使用 ThreadLocal 維護變量時,其爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立的改變本身的副本,而不會影響其餘線程對應的副本。

ThreadLocal 內部實現機制:

  1. 每一個線程內部都會維護一個相似 HashMap 的對象,稱爲 ThreadLocalMap,裏邊會包含若干了 Entry(K-V 鍵值對),相應的線程被稱爲這些 Entry 的屬主線程;

  2. Entry 的 Key 是一個 ThreadLocal 實例,Value 是一個線程特有對象。Entry 的做用便是:爲其屬主線程創建起一個 ThreadLocal 實例與一個線程特有對象之間的對應關係;

  3. Entry 對 Key 的引用是弱引用;Entry 對 Value 的引用是強引用。

6)線程池有了解嗎?(必考)

答:java.util.concurrent.ThreadPoolExecutor 類就是一個線程池。客戶端調用 ThreadPoolExecutor.submit(Runnable task) 提交任務,線程池內部維護的工做者線程的數量就是該線程池的線程池大小,有 3 種形態:

  • 當前線程池大小 :表示線程池中實際工做者線程的數量;
  • 最大線程池大小 (maxinumPoolSize):表示線程池中容許存在的工做者線程的數量上限;
  • 核心線程大小 (corePoolSize ):表示一個不大於最大線程池大小的工做者線程數量上限。
  1. 若是運行的線程少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊;

  2. 若是運行的線程等於或者多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不是添加新線程;

  3. 若是沒法將請求加入隊列,即隊列已經滿了,則建立新的線程,除非建立此線程超出 maxinumPoolSize, 在這種狀況下,任務將被拒絕。

面試官:咱們爲何要使用線程池?

答:

  1. 減小建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。

  2. 能夠根據系統的承受能力,調整線程池中工做線程的數目,放置由於消耗過多的內存,而把服務器累趴下(每一個線程大約須要 1 MB 內存,線程開的越多,消耗的內存也就越大,最後死機)

面試官:核心線程池內部實現瞭解嗎?

答:對於核心的幾個線程池,不管是 newFixedThreadPool() 方法,newSingleThreadExecutor() 仍是 newCachedThreadPool() 方法,雖然看起來建立的線程有着徹底不一樣的功能特色,但其實內部實現均使用了 ThreadPoolExecutor 實現,其實都只是 ThreadPoolExecutor 類的封裝。

爲什麼 ThreadPoolExecutor 有如此強大的功能呢?咱們能夠來看一下 ThreadPoolExecutor 最重要的構造函數:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

函數的參數含義以下:

  • corePoolSize:指定了線程池中的線程數量
  • maximumPoolSize:指定了線程池中的最大線程數量
  • keepAliveTime:當線程池線程數量超過 corePoolSize 時,多餘的空閒線程的存活時間。即,超過了 corePoolSize 的空閒線程,在多長時間內,會被銷燬。
  • unit: keepAliveTime 的單位。
  • workQueue:任務隊列,被提交但還沒有被執行的任務。
  • threadFactory:線程工廠,用於建立線程,通常用默認的便可。
  • handler:拒絕策略。當任務太多來不及處理,如何拒絕任務。

7)Atomic關鍵字:

答:可使基本數據類型以原子的方式實現自增自減等操做。參考博客:concurrent.atomic包下的類AtomicInteger的使用

8)建立線程有哪幾種方式?

答:有兩種建立線程的方法:一是實現Runnable接口,而後將它傳遞給Thread的構造函數,建立一個Thread對象;二是直接繼承Thread類。

面試官:兩種方式有什麼區別呢?

  1. 繼承方式:
    • (1)Java中類是單繼承的,若是繼承了Thread了,該類就不能再有其餘的直接父類了.
    • (2)從操做上分析,繼承方式更簡單,獲取線程名字也簡單.(操做上,更簡單)
    • (3)從多線程共享同一個資源上分析,繼承方式不能作到.
  2. 實現方式:
  • (1)Java中類能夠多實現接口,此時該類還能夠繼承其餘類,而且還能夠實現其餘接口(設計上,更優雅).
    • (2)從操做上分析,實現方式稍微複雜點,獲取線程名字也比較複雜,得使用Thread.currentThread()來獲取當前線程的引用.
    • (3)從多線程共享同一個資源上分析,實現方式能夠作到(是否共享同一個資源).

9)run() 方法和 start() 方法有什麼區別?

答:start() 方法會新建一個線程並讓這個線程執行 run() 方法;而直接調用 run() 方法知識做爲一個普通的方法調用而已,它只會在當前線程中,串行執行 run() 中的代碼。

10)你怎麼理解線程優先級?

答:Java 中的線程能夠有本身的優先級。優先極高的線程在競爭資源時會更有優點,更可能搶佔資源,固然,這只是一個機率問題。若是運行很差,高優先級線程可能也會搶佔失敗。

因爲線程的優先級調度和底層操做系統有密切的關係,在各個平臺上表現不一,而且這種優先級產生的後果也可能不容易預測,沒法精準控制,好比一個低優先級的線程可能一直搶佔不到資源,從而始終沒法運行,而產生飢餓(雖然優先級低,可是也不能餓死它啊)。所以,在要求嚴格的場合,仍是須要本身在應用層解決線程調度的問題。

在 Java 中,使用 1 到 10 表示線程優先級,通常可使用內置的三個靜態標量表示:

public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10;

數字越大則優先級越高,但有效範圍在 1 到 10 之間,默認的優先級爲 5 。

11)在 Java 中如何中止一個線程?

答:Java 提供了很豐富的 API 但沒有爲中止線程提供 API 。

JDK 1.0 原本有一些像 stop(),suspend() 和 resume() 的控制方法可是因爲潛在的死鎖威脅所以在後續的 JDK 版本中他們被棄用了,以後 Java API 的設計者就沒有提供一個兼容且線程安全的方法來中止任何一個線程。

當 run() 或者 call() 方法執行完的時候線程會自動結束,若是要手動結束一個線程,你能夠用 volatile 布爾變量來退出 run() 方法的循環或者是取消任務來中斷線程。

12)多線程中的忙循環是什麼?

答:忙循環就是程序員用循環讓一個線程等待,不像傳統方法 wait(),sleep() 或yield() 它們都放棄了 CPU 控制權,而忙循環不會放棄 CPU,它就是在運行一個空循環。這麼作的目的是爲了保留 CPU 緩存。

在多核系統中,一個等待線程醒來的時候可能會在另外一個內核運行,這樣會重建緩存,爲了不重建緩存和減小等待重建的時間就可使用它了。

13)10 個線程和 2 個線程的同步代碼,哪一個更容易寫?

答:從寫代碼的角度來講,二者的複雜度是相同的,由於同步代碼與線程數量是相互獨立的。可是同步策略的選擇依賴於線程的數量,由於越多的線程意味着更大的競爭,因此你須要利用同步技術,如鎖分離,這要求更復雜的代碼和專業知識。

14)你是如何調用 wait()方法的?使用 if 塊仍是循環?爲何?

答:wait() 方法應該在循環調用,由於當線程獲取到 CPU 開始執行的時候,其餘條件可能尚未知足,因此在處理前,循環檢測條件是否知足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:

// The standard idiom for using the wait method synchronized (obj) { while (condition does not hold) obj.wait(); // (Releases lock, and reacquires on wakeup) ... // Perform action appropriate to condition }

參見 Effective Java 第 69 條,獲取更多關於爲何應該在循環中來調用 wait 方法的內容。

15)什麼是多線程環境下的僞共享(false sharing)?

答:僞共享是多線程系統(每一個處理器有本身的局部緩存)中一個衆所周知的性能問題。僞共享發生在不一樣處理器的上的線程對變量的修改依賴於相同的緩存行,以下圖所示:

僞共享問題很難被發現,由於線程可能訪問徹底不一樣的全局變量,內存中卻碰巧在很相近的位置上。如其餘諸多的併發問題,避免僞共享的最基本方式是仔細審查代碼,根據緩存行來調整你的數據結構。

16)用 wait-notify 寫一段代碼來解決生產者-消費者問題?

解析:這是常考的基礎類型的題,只要記住在同步塊中調用 wait() 和 notify()方法,若是阻塞,經過循環來測試等待條件。

答:

import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; /** * Java program to solve Producer Consumer problem using wait and notify * method in Java. Producer Consumer is also a popular concurrency design pattern. * * @author Javin Paul */ public class ProducerConsumerSolution { public static void main(String args[]) { Vector sharedQueue = new Vector(); int size = 4; Thread prodThread = new Thread(new Producer(sharedQueue, size), "Producer"); Thread consThread = new Thread(new Consumer(sharedQueue, size), "Consumer"); prodThread.start(); consThread.start(); } } class Producer implements Runnable { private final Vector sharedQueue; private final int SIZE; public Producer(Vector sharedQueue, int size) { this.sharedQueue = sharedQueue; this.SIZE = size; } @Override public void run() { for (int i = 0; i < 7; i++) { System.out.println("Produced: " + i); try { produce(i); } catch (InterruptedException ex) { Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex); } } } private void produce(int i) throws InterruptedException { // wait if queue is full while (sharedQueue.size() == SIZE) { synchronized (sharedQueue) { System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + sharedQueue.size()); sharedQueue.wait(); } } // producing element and notify consumers synchronized (sharedQueue) { sharedQueue.add(i); sharedQueue.notifyAll(); } } } class Consumer implements Runnable { private final Vector sharedQueue; private final int SIZE; public Consumer(Vector sharedQueue, int size) { this.sharedQueue = sharedQueue; this.SIZE = size; } @Override public void run() { while (true) { try { System.out.println("Consumed: " + consume()); Thread.sleep(50); } catch (InterruptedException ex) { Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex); } } } private int consume() throws InterruptedException { // wait if queue is empty while (sharedQueue.isEmpty()) { synchronized (sharedQueue) { System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + sharedQueue.size()); sharedQueue.wait(); } } // Otherwise consume element and notify waiting producer synchronized (sharedQueue) { sharedQueue.notifyAll(); return (Integer) sharedQueue.remove(0); } } }  Output: Produced: 0 Queue is empty Consumer is waiting , size: 0 Produced: 1 Consumed: 0 Produced: 2 Produced: 3 Produced: 4 Produced: 5 Queue is full Producer is waiting , size: 4 Consumed: 1 Produced: 6 Queue is full Producer is waiting , size: 4 Consumed: 2 Consumed: 3 Consumed: 4 Consumed: 5 Consumed: 6 Queue is empty Consumer is waiting , size: 0

17)用 Java 寫一個線程安全的單例模式(Singleton)?

解析:有多種方法,但重點掌握的是雙重校驗鎖。

答:

1.餓漢式單例

餓漢式單例是指在方法調用前,實例就已經建立好了。下面是實現代碼:

public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }

2.加入 synchronized 的懶漢式單例

所謂懶漢式單例模式就是在調用的時候纔去建立這個實例,咱們在對外的建立實例方法上加如 synchronized 關鍵字保證其在多線程中很好的工做:

public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 

3.使用靜態內部類的方式建立單例

這種方式利用了 classloder 的機制來保證初始化 instance 時只有一個線程,它跟餓漢式的區別是:餓漢式只要 Singleton 類被加載了,那麼 instance 就會被實例化(沒有達到 lazy loading 的效果),而這種方式是 Singleton 類被加載了,instance 不必定被初始化。只有顯式經過調用 getInstance() 方法時纔會顯式裝載 SingletonHoder 類,從而實例化 singleton

public class Singleton { private Singleton() { } private static class SingletonHolder {// 靜態內部類 private static Singleton singleton = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.singleton; } }

4.雙重校驗鎖

爲了達到線程安全,又能提升代碼執行效率,咱們這裏能夠採用DCL的雙檢查鎖機制來完成,代碼實現以下:

public class Singleton { private static Singleton singleton; private Singleton() { } public static Singleton getInstance(){ if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } } 

這種是用雙重判斷來建立一個單例的方法,那麼咱們爲何要使用兩個if判斷這個對象當前是否是空的呢 ?由於當有多個線程同時要建立對象的時候,多個線程有可能都中止在第一個if判斷的地方,等待鎖的釋放,而後多個線程就都建立了對象,這樣就不是單例模式了,因此咱們要用兩個if來進行這個對象是否存在的判斷。

5.使用 static 代碼塊實現單例

靜態代碼塊中的代碼在使用類的時候就已經執行了,因此能夠應用靜態代碼塊的這個特性的實現單例設計模式。

public class Singleton{ private static Singleton instance = null; private Singleton(){} static{ instance = new Singleton(); } public static Singleton getInstance() { return instance; } } 

6.使用枚舉數據類型實現單例模式

枚舉enum和靜態代碼塊的特性類似,在使用枚舉時,構造方法會被自動調用,利用這一特性也能夠實現單例:

public class ClassFactory{ private enum MyEnumSingleton{ singletonFactory; private MySingleton instance; private MyEnumSingleton(){//枚舉類的構造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } public static MySingleton getInstance(){ return MyEnumSingleton.singletonFactory.getInstance(); } } 

小結:關於 Java 中多線程編程,線程安全等知識一直都是面試中的重點和難點,還須要熟練掌握。


參考資料:

① 知名互聯網公司校招 Java 開發崗面試知識點解析
② 最近5年133個Java面試問題列表
③ 《實戰 Java 高併發程序設計 —— 葛一鳴 郭超 編著》

轉自:https://www.cnblogs.com/wmyskxz/p/9021597.html

相關文章
相關標籤/搜索