天天看點

Java面試總結——Java并發

1、程序與線程的差別:(重點掌握)

答:程序與線程之間的主要差別可以總結如下。

  • 程序是一個“執行中的程式”,是系統進行資源配置設定和排程的一個獨立機關
  • 線程是程序的一個實體,一個程序中一般擁有多個線程。線程之間共享位址空間和其它資源(是以通信和同步等操作,線程比程序更加容易)
  • 線程一般不擁有系統資源,但是也有一些必不可少的資源(使用ThreadLocal存儲)
  • 線程上下文的切換比程序上下文切換要快很多。

知識點:

線程上下文切換比程序上下文切換快的原因,可以總結如下:

  • 程序切換時,涉及到目前程序的CPU環境的儲存和新被排程運作程序的CPU環境的設定
  • 線程切換時,僅需要儲存和設定少量的寄存器内容,不涉及存儲管理方面的操作

應用場景:

程序:需要安全穩定時用程序,需要速度時用程序,既要速度又要安全。

線程:I/O密集型,多核。

解析:

程序與線程的差別算是一個開場題目,旨在考察大家對程序與線程的了解,因為我們的多線程是指在一個程序中的多個線程。

前面我們說線程之間共享一個程序的資源和位址空間,那麼線程可以擁有獨屬于自己的資源嗎?

答:可以的,通過ThreadLocal可以存儲線程的特有對象,也就是屬于目前線程的資源。

程序之間常見的通信方式:

  • 通過使用套接字Socket來實作不同機器間的程序通信
  • 通過映射一段可以被多個程序通路的共享記憶體來進行通信
  • 通過寫程序和讀程序利用管道進行通信

2、多線程與單線程的關系:

多線程與單線程之間的關系可以概括如下。

  • 多線程是指在一個程序中,并發執行了多個線程,每個線程都實作了不同的功能
  • 在單核CPU中,将CPU分為很小的時間片,在每一時刻隻能有一個線程在執行,是一種微觀上輪流占用CPU的機制。由于CPU輪詢的速度非常快,是以看起來像是“同時”在執行一樣
  • 多線程會存線上程上下文切換,會導緻程式執行速度變慢
  • 多線程不會提高程式的執行速度,反而會降低速度。但是對于使用者來說,可以減少使用者的等待響應時間,提高了資源的利用效率

解析:

搞清楚多線程和單線程之間的差別,有助于我們了解為什麼要使用多線程并發程式設計。多線程并發利用了CPU輪詢時間片的特點,在一個線程進入阻塞狀态時,可以快速切換到其餘線程執行其餘操作,這有利于提高資源的使用率,最大限度的利用系統提供的處理能力,有效減少了使用者的等待響應時間。

但是,多線程并發程式設計也會帶來資料的安全問題,線程之間的競争也會導緻線程死鎖和鎖死等活性故障。線程之間的上下文切換也會帶來額外的開銷等問題。

3、線程的狀态有哪些?

待補充

4、多線程程式設計中的常用函數的比較和特性總結如下。

sleep 和 wait 的差別:

  • sleep方法:是Thread類的靜态方法,目前線程将睡眠n毫秒,線程進入阻塞狀态。當睡眠時間到了,會解除阻塞,進入可運作狀态,等待CPU的到來。睡眠不釋放鎖(如果有的話)。
  • wait方法:是Object的方法,必須與synchronized關鍵字一起使用,線程進入阻塞狀态,當notify或者notifyall被調用後,會解除阻塞。但是,隻有重新占用互斥鎖之後才會進入可運作狀态。睡眠時,會釋放互斥鎖。

join 方法:目前線程調用,則其它線程全部停止,等待目前線程執行完畢,接着執行。

yield 方法:該方法使得線程放棄目前分得的 CPU 時間。但是不使線程阻塞,即線程仍處于可執行狀态,随時可能再次分得 CPU 時間。

解析:

這個題目主要是考察 sleep和wait方法所處的類是哪個,并且考察其在休眠的時候對于互斥鎖的處理。

5、線程活性故障有哪些?

答:由于資源的稀缺性或者程式自身的問題導緻線程一直處于非Runnable狀态,并且其處理的任務一直無法完成的現象被稱為是線程活性故障。常見的線程活性故障包括死鎖,鎖死,活鎖與線程饑餓。

解析:

每一個線程都有其特定的任務處理邏輯。由于資源的稀缺性或者資源本身的一些特性,導緻多個線程需要共享一些排他性資源,比如說處理器,資料庫連接配接等。當出現資源争用的時候,部分線程會進入等待狀态。接下來,讓我們依次介紹各種形式的線程活性故障吧。

6、線程死鎖:(重點掌握)

死鎖是最常見的一種線程活性故障。死鎖的起因是多個線程之間互相等待對方而被永遠暫停(處于非Runnable)。死鎖的産生必須滿足如下四個必要條件:

  • 資源互斥:一個資源每次隻能被一個線程使用
  • 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放
  • 非搶占條件:線程已經獲得的資源,在未使用完之前,不能強行剝奪
  • 循環等待條件:若幹線程之間形成一種頭尾相接的循環等待資源關系

那麼,如何避免死鎖的發生?

  • 粗鎖法:使用一個粒度粗的鎖來消除“請求與保持條件”,缺點是會明顯降低程式的并發性能并且會導緻資源的浪費。
  • 鎖排序法:(必須回答出來的點)指定擷取鎖的順序,比如某個線程隻有獲得A鎖和B鎖,才能對某資源進行操作,在多線程條件下,如何避免死鎖?通過指定鎖的擷取順序,比如規定,隻有獲得A鎖的線程才有資格擷取B鎖,按順序擷取鎖就可以避免死鎖。這通常被認為是解決死鎖很好的一種方法。
  • 使用顯式鎖中的ReentrantLock.try(long,TimeUnit)來申請鎖。ReentrantLock.tryLock(long,TimeUnit)允許我們為鎖申請這個操作指定一個逾時時間。在逾時時間内,如果相應的鎖申請成功,那麼該方法傳回 true; 如果在tryLock(long,TimeUnit)執行的那一刻相應的鎖正被其他線程持有,那麼該方法會使目前線程暫停,直到這個鎖被申請成功(此時該方法傳回 true) 或者等待時間超過指定的逾時時間(此時該方法傳回false)。是以,使用tryLock(long,TimeUnit)來申請鎖可以避免一個線程無限制地等待另外一個線程持有的資源,進而最終能夠消除死鎖産生的必要條件中的“占用并等待資源” 。

死鎖總結:

關于線程活性故障中最常見的死鎖,我們必須熟悉其産生的4個必要條件,根據必要條件還應該掌握其避免死鎖的方法,鎖排序法請大家務必熟練掌握。

線程鎖死:

線程鎖死是另一種常見的線程活性故障,與線程死鎖不可以混為一談。線程鎖死的定義如下:

線程鎖死是指等待線程由于喚醒其所需的條件永遠無法成立,或者其他線程無法喚醒這個線程而一直處于非運作狀态(線程并未終止)導緻其任務 一直無法進展。

線程死鎖和線程鎖死的外部表現是一緻的,即故障線程一直處于非運作狀态使得其所執行的任務沒有進展。但是鎖死的産生條件和線程死鎖不一樣,即使産生死鎖的4個必要條件都沒有發生,線程鎖死仍然可能已經發生。

線程鎖死分為了如下兩種:

  • 信号丢失鎖死:

信号丢失鎖死是因為沒有對應的通知線程來将等待線程喚醒,導緻等待線程一直處于等待狀态。

典型例子是等待線程在執行Object.wait( )/Condition.await( )前沒有對保護條件進行判斷,而此時保護條件實際上可能已經成立,此後可能并無其他線程更新相應保護條件涉及的共享變量使其成立并通知等待線程,這就使得等待線程一直處于等待狀态,進而使其任務一直無法進展。

  • 嵌套螢幕鎖死:

嵌套螢幕鎖死是由于嵌套鎖導緻等待線程永遠無法被喚醒的一種故障。

比如一個線程,隻釋放了内層鎖Y.wait(),但是沒有釋放外層鎖X; 但是通知線程必須先獲得外層鎖X,才可以通過 Y.notifyAll()來喚醒等待線程,這就導緻出現了嵌套等待現象。

活鎖:

活鎖是一種特殊的線程活性故障。當一個線程一直處于運作狀态,但是其所執行的任務卻沒有任何進展稱為活鎖。比如,一個線程一直在申請其所需要的資源,但是卻無法申請成功。

線程饑餓:

線程饑餓是指線程一直無法獲得其所需的資源導緻任務一直無法運作的情況。線程排程模式有公平排程和非公平排程兩種模式。線上程的非公平排程模式下,就可能出現線程饑餓的情況。

線程活性故障總結:

  • 線程饑餓發生時,如果線程處于可運作狀态,也就是其一直在申請資源,那麼就會轉變為活鎖
  • 隻要存在一個或多個線程因為擷取不到其所需的資源而無法進展就是線程饑餓,是以線程死鎖其實也算是線程饑餓

1、Java記憶體模型是怎麼樣保證原子性、可見性、有序性?

多線程環境下的線程安全主要展現在原子性,可見性與有序性方面。

Java面試總結——Java并發

原子性:

原子操作是指一系列的操作,要麼全部發生,要麼全部不發生,JMM 保證對除 long 和 double 外的基礎資料類型的讀寫操作是原子性的。另外關鍵字 synchronized 也可以提供原子性保證。synchronized 的原子性是通過 Java 的兩個進階的位元組碼指令 monitorenter 和 monitorexit 來保證的。

可見性

JMM 可見性的保證,一個是通過 synchronized,另外一個就是 volatile。volatile 強制變量的指派會同步重新整理回主記憶體,強制變量的讀取會從主記憶體重新加載,保證不同的線程總是能夠看到該變量的最新值。

有序性

有序性是指一個處理器上運作的線程所執行的記憶體通路操作在另外一個處理器上運作的線程來看是否有序的問題。對有序性的保證,主要通過 volatile 和一系列 happens-before 原則。volatile 的另一個作用就是阻止指令重排序,這樣就可以保證變量讀寫的有序性。

happens-before 原則包括一系列規則,如:

  • 程式順序原則,即一個線程内必須保證語義串行性;
  • 鎖規則,即對同一個鎖的解鎖一定發生在再次加鎖之前;
  • happens-before 原則的傳遞性、線程啟動、中斷、終止規則等。

知識點:

在單處理器中,為什麼也會出現可見性的問題呢?

單處理器中,由于是多線程并發程式設計,是以會存線上程的上下文切換,線程會将對變量的更新當作上下文存儲起來,導緻其餘線程無法看到該變量的更新。是以單處理器下的多線程并發程式設計也會出現可見性問題的。

可見性如何保證?

  • 目前處理器需要重新整理處理器緩存,使得其餘處理器對變量所做的更新可以同步到目前的處理器緩存中
  • 目前處理器對共享變量更新之後,需要沖刷處理器緩存,使得該更新可以被寫入處理器緩存中

重排序:

為了提高程式執行的性能,Java編譯器在其認為不影響程式正确性的前提下,可能會對源代碼順序進行一定的調整,導緻程式運作順序與源代碼順序不一緻。

重排序是對記憶體讀寫操作的一種優化,在單線程環境下不會導緻程式的正确性問題,但是多線程環境下可能會影響程式的正确性。

重排序舉例:

Instance instance = new Instance()都發生了啥?

具體步驟如下所示三步:

  • 在堆記憶體上配置設定對象的記憶體空間
  • 在堆記憶體上初始化對象
  • 設定instance指向剛配置設定的記憶體位址

第二步和第三步可能會發生重排序,導緻引用型變量指向了一個不為null但是也不完整的對象。(在多線程下的單例模式中,我們必須通過volatile來禁止指令重排序)

2、談談你對synchronized關鍵字的了解。

Java面試總結——Java并發

補充:是一種非公平排程方式,如果新來的線程占用該資源的時間不長,那麼它完全有可能在被喚醒的線程繼續執行前釋放相應的資源,進而不影響該被喚醒的線程申請資源。喚醒的資源消耗大。

知識點:

JVM對資源的排程分為公平排程和非公平排程方式。公平排程方式:按照申請的先後順序授予資源的獨占權。

3、談談你對volatile關鍵字的了解。

主要有兩個方面:

第一個:操作變量,并不會拷貝副本。即對變量的指派,會被強制重新整理到主存中,對變量的讀取會從主記憶體中重新加載。

第二個:可以阻止指令重排,避免讀取到引用到未對象初始化的對象。

4、ReentrantLock和synchronized的差別

ReentrantLock是顯示鎖,其提供了一些内部鎖不具備的特性,但并不是内部鎖的替代品。顯式鎖支援公平和非公平的排程方式,預設采用非公平排程。

synchronized 内部鎖簡單,但是不靈活。顯示鎖支援在一個方法内申請鎖,并且在另一個方法裡釋放鎖。顯示鎖定義了一個tryLock()方法,嘗試去擷取鎖,成功傳回true,失敗并不會導緻其執行的線程被暫停而是直接傳回false,即可以避免死鎖。

1、Java中的線程池有了解嗎?

java.util.concurrent.ThreadPoolExecutor類就是一個線程池。利用線程複用的思想,避免線程反複建立,而造成的資源消耗,同時也便于管理。用戶端調用ThreadPoolExecutor.submit(Runnable task)送出任務。

線程池的參數:

Java面試總結——Java并發

JDK對各個字段的解釋:

  • corePoolSize:核心線程數
  • maximumPoolSize:最大線程數
  • keepAliveTime :線程空閑但是保持不被回收的時間
  • unit:時間機關
  • workQueue:存儲線程的隊列
  • threadFactory:建立線程的工廠
  • handler:拒絕政策
Java面試總結——Java并發

線程池的執行流程:核心 -> 阻塞 ->  最大 -> 拒絕

2、線程池有那些?(*)

Java面試總結——Java并發

3、ThreadLocal有了解嗎?

使用ThreadLocal維護變量時,其為每個使用該變量的線程提供獨立的變量副本,是以每一個線程都可以獨立的改變自己的副本,而不會影響其他線程對應的副本。

ThreadLocal内部實作機制:

  • 每個線程内部都會維護一個類似HashMap的對象,稱為ThreadLocalMap,裡邊會包含若幹了Entry(K-V鍵值對),相應的線程被稱為這些Entry的屬主線程
  • Entry的Key是一個ThreadLocal執行個體,Value是一個線程特有對象。Entry的作用是為其屬主線程建立起一個ThreadLocal執行個體與一個線程特有對象之間的對應關系
  • Entry對Key的引用是弱引用;Entry對Value的引用是強引用。

4、Atmoic有了解嗎?

介紹Atomic之前先來看一個問題吧,i++操作是線程安全的嗎?

i++操作并不是線程安全的,它是一個複合操作,包含三個步驟:

  • 拷貝i的值到臨時變量
  • 臨時變量++操作
  • 拷貝回原始變量i

這是一個複合操作,不能保證原子性,是以這不是線程安全的操作。那麼如何實作原子自增等操作呢?

這裡就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子類了。AtomicInteger類提供了getAndIncrement和incrementAndGet等原子性的自增自減等操作。Atomic等原子類内部使用了CAS來保證原子性。

5、CountDownLatch和CyclicBarrier有了解嗎?(*)

兩個關鍵字經常放在一起比較和考察,下邊我們分别介紹。

CountDownLatch是一個倒計時協調器,它可以實作一個或者多個線程等待其餘線程完成一組特定的操作之後,繼續運作。

CountDownLatch的内部實作如下:

  • CountDownLatch内部維護一個計數器,CountDownLatch.countDown()每被執行一次都會使計數器值減少1。
  • 當計數器不為0時,CountDownLatch.await()方法的調用将會導緻執行線程被暫停,這些線程就叫做該CountDownLatch上的等待線程。
  • CountDownLatch.countDown()相當于一個通知方法,當計數器值達到0時,喚醒所有等待線程。當然對應還有指定等待時間長度的CountDownLatch.await( long , TimeUnit)方法。

CyclicBarrier是一個栅欄,可以實作多個線程互相等待執行到指定的地點,這時候這些線程會再接着執行,在實際工作中可以用來模拟高并發請求測試。

可以認為是這樣的,當我們爬山的時候,到了一個平坦處,前面隊伍會稍作休息,等待後邊隊伍跟上來,當最後一個爬山夥伴也達到該休息地點時,所有人同時開始從該地點出發,繼續爬山。

CyclicBarrier的内部實作如下:

  • 使用CyclicBarrier實作等待的線程被稱為參與方(Party),參與方隻需要執行CyclicBarrier.await()就可以實作等待,該栅欄維護了一個顯示鎖,可以識别出最後一個參與方,當最後一個參與方調用await()方法時,前面等待的參與方都會被喚醒,并且該最後一個參與方也不會被暫停。
  • CyclicBarrier内部維護了一個計數器變量count = 參與方的個數,調用await方法可以使得count -1。當判斷到是最後一個參與方時,調用singalAll喚醒所有線程。

什麼是happened-before原則?

JVM虛拟機對内部鎖有哪些優化?

如何進行無鎖化程式設計?

CAS以及如何解決ABA問題?

AQS(AbstractQueuedSynchronizer)的原理與實作。

Java面試總結——Java并發

繼續閱讀