面試系列之線程篇

線程和進程?

進程是資源分配和調度的最小獨立單元,線程是CPU調度的基本單元;

一個進程可以包含多個線程,多個線程共享該進程的資源;

線程可以看作是輕量級的進程;

 

進程間通信的方式?

volatile,synchronized,wait/notifyAll,管道輸入/輸出流,thread.join(),ThreadLocal

 

聊聊synchronized和lock?

synchronized是Java中的一個關鍵字,通過該關鍵字修飾來達到同步的效果,被synchronized修飾的代碼塊會隱式的加鎖解鎖,對開發者都是透明的,底層是基於進入和退出Monitor對象來實現的同步,會在代碼塊前後形成monitorenter和monitorexit兩條指令,分別進行加鎖解鎖(可重入鎖);

進入時執行monitorenter獲取鎖計算器+1,退出時執行monitorexit計數器-1;

lock則是一個對象鎖,開發者需要顯式的獲取鎖釋放鎖,常見的有ReentrantLock,lock對象鎖是一種支持可中斷的鎖,而synchronized則不支持,lock在發生異常時如果沒有主動的去釋放鎖會導致死鎖;synchronized會自動釋放異常線程佔有的鎖,不會發生死鎖;

lock可以獲取鎖的狀態,synchronized不可以;

 

wait和sleep的區別?

wait和sleep都是針對線程的,都可以達到讓線程等待或者說暫時性停止的效果,但是這兩個方法又是不同的,主要體現在下面幾點:

  • wait方法是Object類的,sleep是線程類(Thread)的靜態方法

  • wait執行等待釋放鎖,sleep睡眠不釋放鎖

  • wait需要被notify/notifyAll喚醒,sleep時間失效後會自動喚醒

 

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

設定一個tempNumber用來指定當前獲得執行機會的線程編號,每個線程執行完就更新這個number,指明下一個要有執行權利的線程,同時喚醒所有等待的線程;

每個線程啓動之後,寫一個while判斷tempNumber是否等於自己的number值,如果不是則wait,否則就執行線程;

 

多線程產生死鎖的4個必要原因?

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

請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放,就例如AB線程各自持有對方的鎖,卻需要等待對方釋放鎖才能釋放自己的鎖;

不剝奪條件:線程已經獲得了資源,在沒有結束之前,不允許其他線程獲得資源,也就是線程對共享資源的獨佔性;

循環等待條件:多個線程之間形成環形等待的情況,即多個線程陷入死循環;

如何避免死鎖?

指定線程獲取鎖的順序即可避免死鎖,同一時刻只能有且僅有唯一一個線程獲得鎖的機會;

 

聊聊你對volatile的認識?

該關鍵字可以保證變量的可見性,但是不保證原子性;

被volatile修飾的變量,當線程處理時,會將線程本地內存設置爲無效,強制要求線程去主內存操作變量;

被volatile修飾的變量在操作時會通過生成lock指令來形成內存屏障,禁止JVM進行指令重排序操作;

 

談談你對ThreadLocal的理解?

當使用 ThreadLocal 維護變量時,爲每個使用該變量的線程提供了一份獨立的變量副本,所以每一個線程都可以獨立的操作自己的副本,而不會影響其他線程對應的副本;

ThreadLocal內部實現機制:每個線程內部都維護一個ThreadLocalMap,每個ThreadLocalMap又包含若干個Entry,Entry 的 Key 是一個 ThreadLocal 實例,Value 是一個線程特有對象。Entry 的作用是爲所屬線程建立起一個 ThreadLocal 實例與一個線程特有對象之間的對應關係;

 

聊聊CountDownLatch和CyclicBarrier

作用:用於監聽某些初始化操作,等待初始化執行完畢,通知主線程繼續工作。允許一個或多個線程等待其他線程完成操作;

怎麼做:CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量。每當一個線程完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務;

CountDownLatch的await()方法,會使線程等待,直到計數器爲0,結束等待

CountDownLatch的countDown()計數器 -1

 

CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續執行;

CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更爲複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓線程們重新執行一次;

 

聊聊Semaphore?

信號量是用來控制同時訪問特定資源的線程數量,通過協調各個線程,以保證合理的使用公共資源

 

聊聊Exchanger?

Exchanger主要是用於線程之間進行數據交換的,提供一個同步點,在這個同步點,兩個線程可以交換彼此的數據;兩個線程通過Exchanger方法來進行交換,當第一個線程先執行Exchanger()方法,他會一直等待第二個線程也執行Exchanger方法,然後兩個線程交換數據;

 

聊聊Java實現原子操作的CAS?

CAS:又稱無鎖算法,全稱Compare-and-Swap(比較並交換),依賴處理器的彙編指令cmpxchg(比較交換)來完成,當CAS指令執行時,當且僅當內存位置V符合舊預期值時A時,處理器纔會用新值B去更新V的值,否則就不執行更新,但是無論是否更新V,都會返回V的舊值,該操作過程就是一個原子操作;

什麼是CAS的BAB問題,如何解決?

使用CAS時因爲會先去檢查內存位置的舊值A有沒有發生變化,發生變化則更新最新值B,但是存在一種情況就是,初次讀取內存舊值時是A,再次檢查之前這段期間,如果內存位置的值發生過從A變成B再變回A的過程,我們就會錯誤的檢查到舊值還是A,認爲沒有發生變化,其實已經發生過A-B-A得變化;

ABA解決方案:使用版本號,即1A-2B-3A,這樣就會發現1A到3A的變化,不存在ABA變化無感知問題,JDK的atomic包中提供一個帶有標記的原子引用類AtomicStampedReference來解決ABA問題,它可以通過控制變量值得版本號來保證CAS的正確性。該類的compareAndSet方法會首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果都相等則以原子方式該引用和該標誌的值進行更新;

 

聊聊隊列同步器AQS?

隊列同步器是用來構建鎖或者其他同步組件的基礎元素,主要是使用一個int成員變量來表示同步狀態,通過一個FIFO隊列來完成資源的獲取線程的排隊工作,隊列同步器是面向鎖的實現的;

同步器依賴內部的同步隊列(FIFO)來完成同步狀態的管理,過程如下:當前線程獲取同步狀態失敗時,同步器就會將當前線程以及等待狀態的信息構成一個節點並加入到同步隊列中,同時阻塞當前線程,當同步狀態釋放時,會將首節點的線程喚醒,再次嘗試獲取同步狀態;

 

聊聊你所熟悉的併發容器有哪些?

ConcurrentHashMap:線程安全高效的HashMap,內部採用鎖分段技術來優化,每個段其實就是一個小的HashTable,他們有自己各自的鎖,只要多個修改操作發生在不同的段上,他們之間就可以併發的進行;

ConcurrentLinkedQueue:一個基於鏈表節點實現的無界限安全隊列(不允許Null元素存在,有null存在則鏈表就會斷了),該隊列的元素遵循先進先出(FIFO)原則,它採用了無等待算法(CAS)來實現,不會發生阻塞,適用於高併發場景;

BlockingQueue:支持阻塞的插入和移除操作的隊列,這是一個接口,具體有下面幾種實現類:

  • ArrayBlockingQueue :由數組結構組成的有界阻塞隊列;

  • LinkedBlockingQueue :由鏈表結構組成的有界阻塞隊列;

  • PriorityBlockingQueue :支持優先級排序的無界阻塞隊列;

  • DelayQueue:使用優先級隊列實現的無界阻塞隊列;

  • SynchronousQueue:不存儲元素的阻塞隊列;

  • LinkedTransferQueue:鏈表結構組成的無界阻塞隊列;

  • LinkedBlockingDeque:鏈表結構組成的雙向阻塞隊列;

 

Java提供的線程池有哪幾種?

線程池在JDK的JUC包下有個線程池工廠類Executors,通過這個工廠類我們可以創建多種不同的線程池實例,主要有下面4種:

  • newFixedThreadPool(corePoolSize):該方法返回一個固定數量的線程池,線程數量始終不變,當有任務提交時,若線程池中有空閒線程,則立即執行,若沒有,則會被緩存在一個任務隊列中等待有空閒的線程再去執行;(核心線程數等於最大線程數,默認空閒時間爲0,空閒立馬銷燬);

  • newSingleThreadExecutor():創建一個線程數量始終爲1的線程池,若空閒則執行,否則入隊列等待被執行;(核心線程數量爲1,最大線程數量也爲1,空閒等待時間爲0);

  • newCachedThreadPool():返回一個可根據實際情況調整線程個數的線程池,不限制最大線程數量,若有任務則沒線程時則創建線程,每個線程空閒等待時間爲60秒,60秒後如果該線程沒有任務可執行,則被回收;(核心線程數量爲0,最大線程數量爲最大,空閒等待時間爲60s);

  • newScheduledThreadPool(corePoolSize):返回一個ScheduledExecutorService對象,該線程池可以指定線程數量,他調了父類super的方法裏面還是一個ThreadPoolExecutor,這種方式創建的線程池中每個線程都有定時器的功能;

 

線程池的工作原理是什麼?

我們創建一個線程池之後可以通過submit方法向線程池提交任務,線程池內部維護的工作者線程的數量就是該線程池的線程池大小,有 3 種形態:

  • 當前線程池大小:表示線程池中實際工作者線程的數量

  • 最大線程池大小:表示線程池中允許存在的工作者線程的數量上限

  • 核心線程池大小:表示一個不大於最大線程池大小的工作者線程數量上限

新任務提交給線程池時:首先判斷線程池中是否有空閒線程,如果有則直接使用線程;如果沒有則判斷線程池當前線程數是否小於核心線程池,小於則創建新的線程,否則加入到等待隊列中排隊;當等待隊列滿時,則判斷總線程數是否超過最大線程池大小,如果沒有則創建新的線程,如果有則採取拒絕策略;

JDK提供的線程池的拒絕策略有哪些?

  • AbortPolicy策略:該策略直接拋出異常,阻止系統工作;

  • CallerRunsPolicy策略:只要線程池未關閉,該策略直接在調用者線程中運行當前被丟棄的任務。顯然這樣不會真的丟棄任務,但是,調用者線程性能可能急劇下降;

  • DiscardOledestPolicy策略:丟棄最老的一個請求任務,並嘗試再次提交當前任務;

  • DiscardPolicy策略:默默的丟棄無法處理的任務,不予任何處理,當做什麼都沒發生的樣子;

 

如何自定義線程池?

public ThreadPoolExecutor(int corePoolSize, //線程池核心線程數量
                 int maximumPoolSize, //最大線程數量
                 long keepAliveTime, //線程空閒等待時間
                 TimeUnit unit, //線程空閒等待時間的單位
                 BlockingQueue<Runnable> workQueue, //存放待執行任務的隊列即等待隊列
                 RejectedExecutionHandler handler ) { //拒絕任務的處理策略
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), handler);
   }

 

線程的生命週期中,狀態是如何轉移的?

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