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

 

JAVA多線程與併發


進程與線程的區別

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

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

總結:c++

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

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()方法

目前使用的方法:app

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

Synchronized

線程安全出現的緣由:

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

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

互斥鎖的特性:

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

根據獲取的鎖分類:

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

synchronized底層實現原理:

  • Monitor:每一個java對象天生自帶了一把看不見的鎖(c++實現)
  • Monitor鎖的競爭、獲取與釋放
  • 自旋鎖:原因,一、許多狀況下,共享數據的鎖定狀態持續時間較短,切換線程不值得。二、經過讓線程執行忙循環等待鎖的釋放,不讓出CPU。三、若鎖被其餘線程長時間佔用,會帶來許多性能上的開銷
  • 自適應自旋鎖:一、自旋的次數不固定。二、由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
  • 鎖消除: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時,一、傾向於將鎖賦予等待時間最久的線程。二、公平鎖:獲取鎖的順序按前後調用lock方法的順序。三、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中提供了以下阻塞隊列:
    一、ArrayBlockingQueue:基於數組結構的有界阻塞隊列,按FIFO排序任務;
    二、LinkedBlockingQuene:基於鏈表結構的阻塞隊列,按FIFO排序任務,吞吐量一般要高於ArrayBlockingQuene;
    三、SynchronousQuene:一個不存儲元素的阻塞隊列,每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQuene;
    四、priorityBlockingQuene:具備優先級的無界阻塞隊列;
  • threadFactory:建立線程的工廠,經過自定義的線程工廠能夠給每一個新建的線程設置一個具備識別度的線程名。
  • handler:線程池的飽和策略,當阻塞隊列滿了,且沒有空閒的工做線程,若是繼續提交任務,必須採起一種策略處理該任務,線程池提供了4種策略:
    一、AbortPolicy:直接拋出異常,默認策略;
    二、CallerRunsPolicy:用調用者所在的線程來執行任務;
    三、DiscardOldestPolicy:丟棄阻塞隊列中靠最前的任務,並執行當前任務;
    四、DiscardPolicy:直接丟棄任務;
    固然也能夠根據應用場景實現RejectedExecutionHandler接口,自定義飽和策略,如記錄日誌或持久化存儲不能處理的任務。
  • Exectors:工廠類提供了線程池的初始化接口
    • newFixedThreadPool(int nThreads) 指定工做線程數量的線程池
    • newCachedThreadPool()處理大量短期工做任務的線程池。1:試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程;2:若是線程閒置的時間超過閾值,則會被終止並移除緩存;三、系統長時間閒置的時候,不會消耗什麼資源
    • newSingleThreadExcutor()建立惟一的工做者線程來執行任務,若是線程異常結束,會有另外一個線程取代它
    • newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize)定時或者週期性的工做制度,二者的區別在於單一工做線程仍是多個線程
    • newWorkStealingPool()內部會構建ForkJoinPool,利用working-stealing算法,並行地處理任務,不保證處理順序

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

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

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

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

addWoker方法實現的前半部分:

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

二、經過參數core判斷當前須要建立的線程是否爲核心線程,若是core爲true,且當前線程數小於corePoolSize,則跳出循環,開始建立新的線程,具體實現以下:

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

  • 一、繼承了AQS類,能夠方便的實現工做線程的停止操做;
  • 二、實現了Runnable接口,能夠將自身做爲一個任務在工做線程中執行;
  • 三、當前提交的任務firstTask做爲參數傳入Worker的構造方法;

runWorker方法是線程池的核心:

  • 一、線程啓動以後,經過unlock方法釋放鎖,設置AQS的state爲0,表示運行中斷;
  • 二、獲取第一個任務firstTask,執行任務的run方法,不過在執行任務以前,會進行加鎖操做,任務執行完會釋放鎖;
  • 三、在執行任務的先後,能夠根據業務場景自定義beforeExecute和afterExecute方法;
  • 四、firstTask執行完成以後,經過getTask方法從阻塞隊列中獲取等待的任務,若是隊列中沒有任務,getTask方法會被阻塞並掛起,不會佔用cpu資源;

getTask實現:

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

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

  • 一、Callable接口相似於Runnable,只是Runnable沒有返回值。
  • 二、Callable任務除了返回正常結果以外,若是發生異常,該異常也會被返回,即Future能夠拿到異步執行任務各類結果;
  • 三、Future.get方法會致使主線程阻塞,直到Callable任務執行完成;

協程

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

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

早期,在JVM上實現協程通常會使用kilim,不過這個工具已經好久不更新了,如今經常使用的工具是Quasar,而本文章會所有基於Quasar來介紹。

相關文章
相關標籤/搜索