Java多線程與併發之面試常問題

JAVA多線程與併發


進程與線程的區別

進程是資源分配的最小單位,線程是CPU調度的最小單位

  • 所有與進程相關的資源,都被記錄在PCB(進程控制塊)中
  • 進程是搶佔處理機的調度單位;線程屬於某個進程,共享其資源
  • 線程只由堆棧寄存器、程序計數器和TCB(線程控制塊)組成

總結:

  • 線程不能看做獨立應用,而進程可看做獨立應用
  • 進程有獨立的地址空間,相互不影響,線程只是進程的不同執行路徑
  • 線程沒有獨立的地址空間,多進程的程序比多線程的程序健壯
  • 進程的開銷比線程大,切換代價高

Java進程和線程的關係

  • Java對操作系統的功能進行封裝,包括進程和線程
  • 運行一個程序會產生一個進程,進程包含至少一個線程
  • 每個進程對應一個JVM實例,多個線程共享JVM裏的堆
  • Java採用單線程編程模型,程序會自動創建主線程
  • 主線程可以創建子線程,原則上要後於子線程完成執行

start和run的區別

  • 調用start()方法會創建一個新的子線程並啓動
  • run()方法只是Thread的一個普通方法的調用(注:還是在主線程裏面執行)

Thread和Runnable

  • Thread是實現了Runnable接口的類,使得run支持多線程
    public class Thread implements Runnable
  • 因爲類的單一繼承原則,推薦多使用Runnable接口

如何給run()方法傳參

實現方式有三種

  • 構造函數傳參
  • 成員變量傳參
  • 回調函數傳參

如何實現處理線程的返回值

實現的方式主要有三種:

  • 主線程等待法
  • 使用Thread類的join()阻塞當前線程以等待子線程處理完畢
  • 通過Callable接口實現:通過Future Or 線程池獲取

Java線程的六個狀態

  • 新建(New):創建後尚未啓動的線程的狀態
  • 運行(Runnable):包含Running 和Ready
  • 無限期等待(Waiting):不會被分配CPU執行時間,需要顯示被喚醒
  • 限期等待(Timed Waiting):在一定時間後會由系統自動喚醒
  • 阻塞(Blocked):等待獲取排它鎖
  • 結束(Terminated):已終止線程的狀態,線程已經結束執行

Sleep和wait的區別

  • sleep是Thread的方法,wait是Object類中定義的方法
  • sleep()方法可以在任何地方使用
  • wait()方法只能在synchronized方法或synchronized塊中使用
  • Thread.sleep只會讓出CPU,不會導致鎖行爲的改變
  • Object.wait不僅讓出CPU,還會釋放已經佔有的同步資源鎖

notify和notifyAll的區別

鎖池EntryList:假設線程A已經獲得了某個對象(不是類)的鎖,而其他線程B,C想要調用這個對象的某個synchronized方法(或者塊),由於B,C線程在進入對象的synchronized方法之前必須獲得該對象鎖的擁有權,而恰巧該對象的鎖剛好被線程A所佔用,此時B,C線程就會被阻塞,進入一個地方去等待鎖的釋放,這個地方就是鎖池。

等待池WaitSet:假設線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖,同時線程A就進入到了該對象的等待池中,進入到等待池中的線程不會去競爭該對象的鎖。

  • notifyAll會讓所有出於等待池WaitSet的線程全部進入鎖池EntryList去競爭獲取鎖的機會
  • notify只會隨機選取一個處於等待池中的線程進入鎖池去競爭獲取鎖的機會

Yield與join的區別

當調用Thread.yeild()函數時,會給線程調度器一個當前線程願意讓出CPU使用的暗示,但是線程調度器可能會忽略這個暗示。並不會讓出當前線程的鎖。

  • yield是一個靜態的原生(native)方法
  • yield不能保證是的當前正在運行的線程迅速轉換到可運行的狀態,僅能從運行態轉換到可運行態,而不能是等待或阻塞。

join方法可以使得一個線程在另一個線程結束後再執行。當前線程將阻塞直到這個線程實例完成了再執行。

  • join方法可設置超時,使得join()方法的影響在特定超時後無效,如,join(50)。注:join(0),並不是等待0秒,而是等待無限時間,等價join()。
  • join方法必須在線程start()方法調用之後纔有意義
  • join方法的原理,就是調用了相應線程的wait方法

如何中斷線程

已經被拋棄的方法:

  • 通過調用stop()方法停止線層(原因:不安全,會釋放掉鎖)
  • 通過調用suspend()和resume()方法

目前使用的方法:

  • 調用interrput(),通知線程應該中斷了:1.如果線程出於被阻塞的狀態,那麼線程將立即退出被阻塞狀態,並拋出一個InterruputedException異常。2.如果線程出於正常的活動狀態,那麼會將該線程的中斷標誌設置爲true。被設置中斷標誌的線程將繼續正常運行,不受影響。
  • 需要被調用的線程配合中斷:1.在正常運行任務時,經常檢查本線程的中斷標誌位,如果被設置了中斷標誌就自行停止線程。2.如果線程處於正常活動狀態,那麼會將該線程的中斷標誌設置爲true。被設置中斷標誌的線程將繼續正常運行,不受影響。

Synchronized

線程安全出現的原因:

  • 存在共享數據(也稱爲臨界資源)
  • 存在多線程共同操作這些共享數據

解決線程安全的根本辦法:同一時刻有且只有一個線程在操作共享數據,其他線程必須等到該線程處理完數據之後再對共享數據進行操作,引入了互斥鎖

互斥鎖的特性:

  • 互斥性:即在同一個時間只允許一個線程持有某個對象鎖,通過這種特性來實現多線程的協調機制,這樣在同一時間只有一個線程對需要同步的代碼塊(複合操作)進行訪問。互斥性也成爲操作的原子性。
  • 可見性:必須確保在鎖被釋放之前,對共享變量所做的修改,對於隨後獲得該鎖的另一個線程是可見的(即在獲得鎖時應獲得最新的共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作,從而引起不一致性。
  • synchronized鎖的不是代碼,是對象。

根據獲取的鎖分類:

  • 獲取對象鎖:1、同步代碼塊(synchronized(this),synchronized(類實例對象)),鎖是小括號()中的實例對象。2、同步非靜態方法(synchronized method),鎖是當前對象的實例對象。
  • 獲取類鎖:1、同步代碼塊(synchronized(類.class)),鎖是小括號()中的類對象(class對象)。2、同步靜態方法(synchronized static method),鎖是當前對象的類對象(class對象)。
  • 有線程訪問對象的同步代碼塊時,另外的線程可以訪問該對象的非同步代碼塊
  • 若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另一個線程訪問對象的同步方法,會被阻塞
  • 類鎖和對象鎖互補干擾

synchronized底層實現原理:

  • Monitor:每個java對象天生自帶了一把看不見的鎖(c++實現)
  • Monitor鎖的競爭、獲取與釋放
  • 自旋鎖:緣由,1、許多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得。2、通過讓線程執行忙循環等待鎖的釋放,不讓出CPU。3、若鎖被其他線程長時間佔用,會帶來許多性能上的開銷
  • 自適應自旋鎖:1、自旋的次數不固定。2、由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
  • 鎖消除:JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖
  • 鎖粗化:通過擴大鎖的範圍,避免反覆的加鎖和解鎖

鎖的內存語義:

  • 當線程釋放鎖時,Java內存模型會把該線程對應的本地內存中的共享變量刷新到主內存中去;
  • 而當線程獲得鎖時,Java內存模型會把該線程對應的本地內存置爲無效,從而使得監視器保護的臨界區代碼必須從主內存中讀取共享變量。

synchronized的四種狀態:

  • 無鎖
  • 偏向鎖:減少同一線程獲取鎖的代價,大多數情況下,鎖不存在多線程競爭,總是由同一線程多層次獲得。核心思想:如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時MarkWord的結構也變爲偏向鎖結構,當該線程再次請求鎖時,無需任何同步操作,即獲取鎖的過程只需要檢查Markword的鎖標記位爲偏向鎖以及當前線程Id等於Markword的ThreadId 即可,這樣就省去了大量有關鎖申請的操作。不適合鎖競爭比較激烈的多線程場合
  • 輕量級鎖:由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。適應場景:線程交替執行同步代碼塊。若存在同一時間訪問同一鎖的情況,就導致輕量級鎖膨脹爲重量級鎖
  • 重量級鎖

AQS:

AQS提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架,AbstractQueuedSynchronizer中對state的操作是原子的,且不能被繼承。所有的同步機制的實現均依賴於對改變量的原子操作。爲了實現不同的同步機制,我們需要創建一個非共有的(non-public internal)擴展了AQS類的內部輔助類來實現相應的同步邏輯,AbstractQueuedSynchronizer並不實現任何同步接口,它提供了一些可以被具體實現類直接調用的一些原子操作方法來重寫相應的同步邏輯。AQS同時提供了互斥模式(exclusive)和共享模式(shared)兩種不同的同步邏輯。

ReentrantLock:

  • jdk1.5後引入了ReentrantLock(再入鎖),位於java.util.concurrent.locks包
  • 和CountDownLatch、FutrueTask、Semaphore一樣基於AQS實現
  • 能夠實現比synchronized更細粒度的控制,如控制fairness
  • 調用lock()之後,必須調用unlock()釋放鎖
  • 性能未必比synchronized高,並且也是可重入的
  • ReentrantLock公平性的設置:參數爲true時,1、傾向於將鎖賦予等待時間最久的線程。2、公平鎖:獲取鎖的順序按先後調用lock方法的順序。3、synchronized並不是公平性鎖

synchronized和ReentrantLock的區別:

  • synchronized是關鍵字,ReentrantLock是類
  • ReentrantLock可以對獲取鎖的等待時間進行設置,避免死鎖
  • ReentrantLock可以獲取各種鎖的信息
  • ReentrantLock可以靈活的實現多路通知

synchronized和volatile的區別

  • volatile本質是告訴JVM當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞直到該線程完成變量操作爲止
  • volatile僅能使用在變量級別;synchronized則可以使用在變量、方法和類級別
  • volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量修改的可見性和原子性
  • volatile不會造成現成的阻塞;synchronized可能造成線程的阻塞
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量可被編譯器優化

線程間的通訊方式

本質上有兩大類:共享內存機制和消息通信機制。

  • 同步:多個線程通過synchronized關鍵字這種方式來實現線程間的通信。如:線程A需要等待線程B執行完method方法後,線程A才能執行這個方法,以此實現線程A,B之間的通訊。
  • while輪詢的方式(不建議使用):線程A不斷地改變條件,線程B不停地通過while語句檢測某個條件(這個條件與線程A的操作有關)是否成立 ,從而實現了線程間的通信。缺點:浪費資源,線程B會不停的while
  • wait/notify機制:線程A需要線程B完成某任務在執行時,線程A調用wait()方法,進入等待池中,等待線程B的喚醒。線程B完成某任務後(這個任務線程A所需要的),調用notify將其喚醒。優點:比起while輪詢方法,更加的節約資源。缺點:通知過早,會打亂程序的執行邏輯。即線程B先於線程A佔用CPU,但是此時線程A併爲執行
  • 管道通信:就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通信

Java內存模型

java內存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實的存在,它描述一組規範或者規則,通過這組規範定義了程序中各個變量的訪問方式。

JMM中的主內存:

  • 存儲Java實例對象
  • 包括成員變量、類信息、常量、靜態變量等
  • 數據共享的區域,多線程併發操作時會引發一系列的安全問題

JMM中的工作內存:

  • 存儲當前方法的所有本地變量信息,本地變量對其他線程不可見
  • 字節碼行號指示器、Native方法信息
  • 屬於線程私有數據區域,不存在線層安全問題

JMM與java內存區域劃分是不同的概念層次:

  • JMM描述的是一組規則,圍繞原子性,有序性,可見性展開
  • 相似點:存在共享區域和私有區域

JMM如何解決可見性問題:

  • 在單線程環境下不能改變程序運行的結果
  • 存在數據依賴關係的不允許重排序
  • 無法通過happens-before原則推導出來的,才能進行指令重排序
  • 如果操作A happens-before 操作B,那麼操作A在內存上所做的操作對操作B來說都是可見的

happens-before

  • 程序次序原則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖的lock操作
  • volatile變量規則:對一個變量的寫操作先行發生於後面對這個讀操作
  • 傳遞規則:如果操作A先行發生於操作B,操作B先行發生於操作C,則A先行發生於C
  • 線程啓動原則:Thread對象的start()方法先行發生於此線程的每一個動作
  • 線程中斷原則:對線程interrupt()方法調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 線程終結原則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  • 對象終結原則:一個對象的初始化完成先行發生於他的finalize()方法的開始

CAS

compare and swap:

  • 包含三個操作數,內存位置(V),預期原值(A)和新值(B)
  • J.U.C的atomic包提供了常用的原子性數據類型以及引用、數組等相關類型和更新操作工具,是很多線程安全程序的首選
  • Unsafe類雖然提供CAS服務,但因能夠操縱任意內存地址讀寫而有隱患
  • Java9以後,可以使用Variable Handle API 來代替Unsafe
  • 缺點:若循環時間長,則開銷很大,只能保證一個共享變量的原子操作,ABA問題(解決:通過版本來解決ABA問題,AtomicStampedReference)

JAVA線程池

爲什麼要使用線程池

  • 降低資源消耗
  • 提高線程的可管理性
  • 提高響應速度;

J.U.C的三個Executor接口

  • Executor:運行新任務的簡單接口,將任務提交和任務執行細節解耦
  • ExecutorService:具備管理執行器和任務生命週期的方法,提交任務機制更完善
  • ScheduledExecutorService:支持Future和定期執行任務

線程池的狀態:

  • RUNNING:能接受新提交的任務,並且也能處理阻塞隊列中的任務
  • SHUTDOWN;不再接受新提交的任務,但可以處理存量任務
  • STOP:不再接受新提交的任務,也不處理存量任務
  • TIDYING:所有的任務都已經終止
  • TERMINATED:teriminated()方法執行完後進入該狀態

線程池的大小如何選定:

  • CPU密集型:線程數=按照核數或者核數+1 設定
  • I/O密集型:線程數 = CPU核數 *(1+平均等待時間/平均工作時間)

線程池的參數

  • corePoolSize:線程池中的核心線程數,當提交一個任務時,線程池創建一個新線程執行任務,直到當前線程數等於corePoolSize;如果當前線程數爲corePoolSize,繼續提交的任務被保存到阻塞隊列中,等待被執行;如果執行了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啓動所有核心線程。
  • maximumPoolSize:線程池中允許的最大線程數。如果當前阻塞隊列滿了,且繼續提交任務,則創建新的線程執行任務,前提是當前線程數小於maximumPoolSize;
  • keepAliveTime:線程空閒時的存活時間,即當線程沒有任務執行時,繼續存活的時間;默認情況下,該參數只在線程數大於corePoolSize時纔有用;
  • unit:keepAliveTime的單位;
  • workQueue:用來保存等待被執行的任務的阻塞隊列,且任務必須實現Runable接口,在JDK中提供瞭如下阻塞隊列:
    1、ArrayBlockingQueue:基於數組結構的有界阻塞隊列,按FIFO排序任務;
    2、LinkedBlockingQuene:基於鏈表結構的阻塞隊列,按FIFO排序任務,吞吐量通常要高於ArrayBlockingQuene;
    3、SynchronousQuene:一個不存儲元素的阻塞隊列,每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene;
    4、priorityBlockingQuene:具有優先級的無界阻塞隊列;
  • threadFactory:創建線程的工廠,通過自定義的線程工廠可以給每個新建的線程設置一個具有識別度的線程名。
  • handler:線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工作線程,如果繼續提交任務,必須採取一種策略處理該任務,線程池提供了4種策略:
    1、AbortPolicy:直接拋出異常,默認策略;
    2、CallerRunsPolicy:用調用者所在的線程來執行任務;
    3、DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務;
    4、DiscardPolicy:直接丟棄任務;
    當然也可以根據應用場景實現RejectedExecutionHandler接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。
  • Exectors:工廠類提供了線程池的初始化接口
    • newFixedThreadPool(int nThreads) 指定工作線程數量的線程池
    • newCachedThreadPool()處理大量短時間工作任務的線程池。1:試圖緩存線程並重用,當無緩存線程可用時,就會創建新的工作線程;2:如果線程閒置的時間超過閾值,則會被終止並移除緩存;3、系統長時間閒置的時候,不會消耗什麼資源
    • newSingleThreadExcutor()創建唯一的工作者線程來執行任務,如果線程異常結束,會有另一個線程取代它
    • newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize)定時或者週期性的工作制度,兩者的區別在於單一工作線程還是多個線程
    • newWorkStealingPool()內部會構建ForkJoinPool,利用working-stealing算法,並行地處理任務,不保證處理順序

線程池的任務提交:線程池框架提供了兩種方式提交任務,根據不同的業務需求選擇不同的方式。

  • Executor.execute():通過Executor.execute()方法提交的任務,必須實現Runnable接口,該方式提交的任務不能獲取返回值,因此無法判斷任務是否執行成功。
  • ExecutorService.submit():通過ExecutorService.submit()方法提交的任務,可以獲取任務執行完的返回值。

線程池任務的執行:具體的執行流程如下:

  • 1、workerCountOf方法根據ctl的低29位,得到線程池的當前線程數,如果線程數小於corePoolSize,則執行addWorker方法創建新的線程執行任務;否則執行步驟(2);
  • 2、如果線程池處於RUNNING狀態,且把提交的任務成功放入阻塞隊列中,則執行步驟(3),否則執行步驟(4);
  • 3、再次檢查線程池的狀態,如果線程池沒有RUNNING,且成功從阻塞隊列中刪除任務,則執行reject方法處理任務;
  • 4、執行addWorker方法創建新的線程執行任務,如果addWoker執行失敗,則執行reject方法處理任務;

addWoker方法實現的前半部分:

1、判斷線程池的狀態,如果線程池的狀態值大於或等SHUTDOWN,則不處理提交的任務,直接返回;

2、通過參數core判斷當前需要創建的線程是否爲核心線程,如果core爲true,且當前線程數小於corePoolSize,則跳出循環,開始創建新的線程,具體實現如下:

線程池的工作線程通過Woker類實現,在ReentrantLock鎖的保證下,把Woker實例插入到HashSet後,並啓動Woker中的線程,其中Worker類設計如下:

  • 1、繼承了AQS類,可以方便的實現工作線程的中止操作;
  • 2、實現了Runnable接口,可以將自身作爲一個任務在工作線程中執行;
  • 3、當前提交的任務firstTask作爲參數傳入Worker的構造方法;

runWorker方法是線程池的核心:

  • 1、線程啓動之後,通過unlock方法釋放鎖,設置AQS的state爲0,表示運行中斷;
  • 2、獲取第一個任務firstTask,執行任務的run方法,不過在執行任務之前,會進行加鎖操作,任務執行完會釋放鎖;
  • 3、在執行任務的前後,可以根據業務場景自定義beforeExecute和afterExecute方法;
  • 4、firstTask執行完成之後,通過getTask方法從阻塞隊列中獲取等待的任務,如果隊列中沒有任務,getTask方法會被阻塞並掛起,不會佔用cpu資源;

getTask實現:

  • 1、workQueue.take:如果阻塞隊列爲空,當前線程會被掛起等待;當隊列中有任務加入時,線程被喚醒,take方法返回任務,並執行;
  • 2、workQueue.poll:如果在keepAliveTime時間內,阻塞隊列還是沒有任務,則返回null;
    所以,線程池中實現的線程可以一直執行由用戶提交的任務。

Future和Callable實現:在實際業務場景中,Future和Callable基本是成對出現的,Callable負責產生結果,Future負責獲取結果。

  • 1、Callable接口類似於Runnable,只是Runnable沒有返回值。
  • 2、Callable任務除了返回正常結果之外,如果發生異常,該異常也會被返回,即Future可以拿到異步執行任務各種結果;
  • 3、Future.get方法會導致主線程阻塞,直到Callable任務執行完成;

協程

協程(Coroutine)這個詞其實有很多叫法,比如有的人喜歡稱爲纖程(Fiber),或者綠色線程(GreenThread)。其實究其本質,對於協程最直觀的解釋是線程的線程。雖然讀上去有點拗口,但本質上就是這樣。

協程的核心在於調度那塊由他來負責解決,遇到阻塞操作,立刻放棄掉,並且記錄當前棧上的數據,阻塞完後立刻再找一個線程恢復棧並把阻塞的結果放到這個線程上去跑,這樣看上去好像跟寫同步代碼沒有任何差別,這整個流程可以稱爲coroutine,而跑在由coroutine負責調度的線程稱爲Fiber。

早期,在JVM上實現協程一般會使用kilim,不過這個工具已經很久不更新了,現在常用的工具是Quasar。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章