多線程和併發編程
1) 什麼是線程?
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麼用十個線程完成該任務只需10毫秒。
2) 線程和進程有什麼區別?
線程是進程的子集,一個進程可以有很多線程,每條線程併發執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。
3)線程的實現方式?使用哪個更好?
有兩種方式,一是繼承Thread類,二是實現Runnable接口。
java不支持類的多繼承,但是允許實現多個接口,所以繼承了Thread類就不能繼承其他類,而且Thread類實際上也是實現了Runnable接口,所以最好使用Runnable接口實現線程。
4)Thread類種的start()和run()方法有什麼區別?
start()方法用來啓動新創建的線程,而且start()方法內部調用了run()方法;當調用run()方法的時候,只會在原來的線程中調用,並沒有啓動新的線程。
5)什麼是線程安全性?如何編寫線程安全的代碼?
定義:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼這個類就可以稱爲是線程安全的。
編寫線程安全的代碼,其核心在於要對狀態訪問操作進行管理,特別是對共享的和可變的狀態的訪問。
無狀態的變量一定是線程安全的,即沒有共享變量
6)什麼是競態條件?舉例說明
競態條件就是,由於不正確的執行時序,而出現不正確的結果。
最常見的競態條件類型就是“先檢查後執行”操作,即通過一個可能失效的觀測結果來決定下一步的動作。
比如,++count操作,看上去是一個操作,但其實這個操作並非是原子的,實際上它包含了3個獨立的操作,讀取count,將其+1,將計算結果寫入count,這是一個“讀取-修改-寫入”的操作序列,並且其結果狀態依賴於之前的狀態。
7)什麼是內置鎖?
每個java對象都可以用作一個實現同步的鎖,這些鎖稱爲內置鎖。
其使用方式就是使用synchronized關鍵字、synchronized 方法或者 synchronized 代碼塊。線程在進入同步代碼塊之前自動獲得鎖,在退出同步代碼塊時自動釋放鎖,獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
8)什麼是可見性問題?如何解決?
一個共享變量,如果線程A做了修改,線程B隨後讀取到的還是舊值,導致線程讀取到髒數據,這就是可見性問題。
解決:
1. 加鎖:內置鎖可以保證可見性,確保某個線程以一種可預測的方式來查看另一個線程的執行結果。
2. 使用volatile變量:可以確保將變量的更新操作 通知到其他線程。
9)什麼是volatile變量?什麼時候使用volatile?
volatile是一種輕量級的鎖,它能夠保證可見性,但不能保證原子性。
volatile變量,可以確保將變量的更新操作通知到其他線程。把變量聲明爲volatile類型後,編譯器和運行時都會注意到這個變量是共享的,因此不會將該變量上的操作和其他內存操作一起重排序。volatile變量不會被緩存在寄存器或其他處理器不可見的地方,因此讀取volatile類型的變量總是返回最新寫入的值。
什麼時候應該使用volatile變量:
1. 對變量的寫入操作不依賴變量的當前值(如++count)
2. 該變量不會與其他狀態變量一起納入不變性條件中(如(lower,upper)上下限,volatile只能保證lower和upper的寫入能被其他線程看到,但是不能保證其中一個變量寫入時,另一個變量的值不發生變化)
3. 在訪問變量時不需要加鎖。
(volatile變量通常用做某個操作完成、發生中斷或者狀態的標誌)
volatile特性:
1. 保證此變量對所有線程都是可見的;
2. 禁止指令重排序
10)什麼是ThreadLocal?
ThreadLocal是線程局部變量,爲解決多線程的併發問題提供了一種新的思路。
ThreadLocal提供了set、get等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。
如何實現爲每個線程維護變量副本?
在ThreadLocal類中有一個Map(ThreadLocalMap),用於存儲每個線程的變量副本,Map中元素的key爲線程對象,而value對應線程的變量副本。
11)ThreadLocal與同步機制的比較:
對於多線程資源共享的問題,
同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。
同步機制只提供了一份變量,讓不同的線程排隊訪問;而ThreadLocal爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
12)什麼是不可變對象?
如果某個對象在被創建後,其狀態就不能被修改,就可以稱這個對象爲不可變對象。他們的不變性條件是由構造函數創建的,不可變對象一定是線程安全的。
不可變對象的條件:
1. 對象創建後,其狀態就不能被修改。
2.對象的所有域都是final類型。
3.對象是被正確創建的。
13)Runnable和Callable的區別?
1. Runnable執行方法是run(),無返回值,有異常不能拋出,只能捕獲;
2. Callable執行方法是call(),有返回值,可以拋出異常。
14)線程有哪些狀態 或者 線程的生命週期?
線程從創建、運行到結束一共是5個狀態,即:新建狀態(New)、就緒狀態(Runnable)、運行狀態(Running)、阻塞狀態(Blocked)、死亡狀態(Dead)。
15)CountDownLatch(閉鎖)、CyclicBarrier(柵欄) 、 Semaphore(信號量)有什麼不同?
1. CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,不過側重點不同:
CountDownLatch一般用於某個線程A等待若干個線程執行完任務之後,它才執行;
CyclicBarrier一般用於一組線程相互等待至某個狀態,然後一組線程再同時執行;
2. Semaphore其實和鎖有點類似,它一般用於控制對某組資源的訪問權限。
可以通過CountdownLatch同時啓動多個線程。
16)如何終止一個線程?
java中終止一個線程一共有3個方法,即:stop、interrupt、設置標誌位。
1.Thread的stop()方法是一個被廢棄的方法,因爲stop會強行把執行一半的線程終止,導致線程資源不能被正確釋放。
2. 使用Boolean類型的變量,設置標誌位,來終止線程。
3. 使用線程中斷機制,線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。
17) 一個線程發生運行時異常會怎麼樣?
Java中Throwable分爲Exception和Error: 出現Error的情況下,程序會停止運行。 Exception分爲RuntimeException和非運行時異常。 非運行時異常必須處理,比如thread中sleep()時,必須處理InterruptedException異常,才能通過編譯。 而RuntimeException可以處理也可以不處理,因爲編譯並不能檢測該類異常,比如NullPointerException、ArithmeticException、ArrayIndexOutOfBoundException等。
所以這裏存在兩種情形:
① 如果該異常被捕獲或拋出,則程序繼續運行。
② 如果異常沒有被捕獲該線程將會停止執行。
Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler,並將線程和異常作爲參數傳遞給handler的uncaughtException()方法進行處理。
18)如何在多個線程間共享數據?
1,如果每個線程執行的代碼相同,可以使用同一個Runnable對象,這個Runnable對象中有那個共享數據,例如,賣票系統就可以這麼做。
2,如果每個線程執行的代碼不同,這時候需要用不同的Runnable對象,例如,設計4個線程。其中兩個線程每次對j增加1,另外兩個線程對j每次減1,銀行存取款
19) notify和notifyAll有什麼區別?
在調用notify時,JVM會從這個條件隊列上等待的多個線程中選擇一個來喚醒;
而調用notifyAll時,則會喚醒所有在這個條件隊列上等待的線程。
由於多個線程可以基於不同的條件謂詞在同一個條件隊列上等待,如果使用notify,容易導致類似於信號丟失的問題,因此大多數情況下,應該優先選擇notifyAll而不是單個的notify。
20)ConcurrentHashMap?
ConcurrentHashMap採用了分段鎖的設計,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭。相比於對整個Map加鎖的設計,分段鎖大大的提高了高併發環境下的處理能力。但同時,由於不是對整個Map加鎖,導致一些需要掃描整個Map的方法(如size())需要使用特殊的實現,另外一些方法(如clear())甚至放棄了對一致性的要求。
ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。ConcurrentHashMap中的HashEntry相對於HashMap中的Entry有一定的差異性:HashEntry中的value以及next都被volatile修飾,這樣在多線程讀寫過程中能夠保持它們的可見性。
併發度實際上就是ConcurrentHashMap中的分段鎖個數,即Segment[]的數組長度。ConcurrentHashMap默認的併發度爲16,但用戶也可以在構造函數中設置併發度。
ConcurrentHashMap在JDK8中進行了巨大改動,摒棄了Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用CAS算法。它沿用了與它同時期的HashMap版本的思想,底層依然由“數組+鏈表+紅黑樹“的思想。
在ConcurrentHashMap中並沒有實現對Map加鎖以提供獨佔訪問,因此在大多數情況下,用ConcurrentHashMap來替代同步map能進一步提供代碼的可伸縮性,只有當應用程序需要加鎖map以進行獨佔訪問時,才應該放棄使用ConcurrentHashMap。
21)什麼是BlockingQueue(阻塞隊列)?
阻塞隊列指在隊列的基礎上增加了支持阻塞的插入和移除操作的隊列。
提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。如果隊列已經滿了,put時將阻塞直到有空間可用,如果隊列爲空,take時將阻塞直到有元素可用。隊列可以是有界的,也可以是無界的。常用有界隊列ArrayBlockingQueue、LinkedBlockingQueue 以及支持優先級的無界隊列PriorityBlockingQueue
阻塞隊列常用於生產者-消費者模式,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。簡而言之,阻塞隊列是生產者用來存放元素、消費者獲取元素的容器。
22)什麼是生產者-消費者模式?
某個模塊負責產生數據,另一個模塊負責處理數據,可以將產生數據的模塊稱爲生產者,處理數據的模塊稱爲消費者。在生產者與消費者之間在加個緩衝區,形象的稱爲倉庫,生產者負責往倉庫了進商品,而消費者負責從倉庫裏拿商品,這就構成了生產者消費者模式。
優點:
1.解耦
2.支持併發
3.支持忙閒不均
23)爲什麼wait, notify 和 notifyAll這些方法不在thread類裏面?
由於wait,notify和notifyAll都是鎖級別的操作,鎖屬於對象,所以把他們定義在Object類中。
24)wait()、notify()、notifyAll()、yield()、sleep()、join()、interrupt() 區別?
wait() -- 讓當前線程處於“等待(阻塞)狀態”,會立即釋放它所持有對象的鎖,直到其他線程調用此對象的notify() 或notifyAll()喚醒線程;
notify() -- 喚醒在此對象監視器上等待的單個線程。
notifyAll() -- 喚醒在此對象監視器上等待的所有線程。
yield() -- 讓當前線程由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行權,不會釋放鎖;但是,並不能保證在當前線程調用yield()之後,其它具有相同優先級的線程就一定能獲得執行權;也有可能是當前線程又進入到“運行狀態”繼續運行 。
sleep() -- 讓當前線程休眠,即當前線程會從“運行狀態”進入到“休眠(阻塞)狀態”,不會釋放鎖。sleep()會指定休眠時間,當線程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”。
join() -- 讓“主線程”等待“子線程”結束之後才能繼續運行。
interrupt() -- 中斷線程。InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。對某一線程調用interrupt()時,如果該線程正在執行普通的代碼,那麼該線程根本就不會拋出InterruptedException。但是,一旦該線程進入到wait()/sleep()/join()後,就會立刻拋出InterruptedException。
25)interrupt()、interrupted()的區別?
interrupt()是用來設置中斷狀態的。返回true說明中斷狀態被設置了而不是被清除了。 java的中斷並不是真正的中斷線程,而只設置標誌位來通知用戶。如果你捕獲到中斷異常,說明當前線程已經被中斷,不需要繼續保持中斷位。調用sleep、wait等此類可中斷方法時,一旦方法拋出InterruptedException,當前調用該方法的線程的中斷狀態就會被JVM自動清除了,就是說我們調用該線程的isInterrupted ()方法時是返回false。如果你想保持中斷狀態,可以再次調用interrupt方法設置中斷狀態。
interrupted是靜態方法,返回的是當前線程的中斷狀態。
26)爲什麼應該在循環中檢查等待條件?
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,在條件謂詞不爲真的情況下也可以反覆醒來,因此必須在一個循環中調用wait,並在每次迭代中都測試條件謂詞,如果條件謂詞不爲真,就繼續等待或失敗。
27)線程池的作用?如何創建線程池?
線程池就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。
作用:
1、可以限定線程的個數,以防由於線程過多導致系統運行緩慢或崩潰
2、線程池不需要每次都去創建或銷燬線程,節約了資源、響應時間更快
創建:
可以使用Executors類中的靜態工廠方法(newCachedThreadPool 、newFixedThreadPool、newScheduledThreadPool 、newSingleThreadExecutor ),也可以使用ThreadPoolExecutor類(提供了4個構造器)
28)什麼是死鎖?發生死鎖的條件是什麼?如何避免死鎖?
死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法執行下去。
死鎖的發生必須滿足以下四個條件:
- 互斥條件:一個資源每次只能被一個線程使用。
- 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:線程已獲得的資源,在末使用完之前,不能強行剝奪。
- 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關係。
避免死鎖:
首先找出在什麼地方將獲取多個鎖,然後對所有這些事例進行全局分析,確保他們在整個程序中獲取鎖的順序保持一致,儘可能的使用開放調用,從而避免死鎖。
29) 死鎖、活鎖有什麼區別?
死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法執行下去。
活鎖是另一種形式的活躍性問題,它雖然不會阻塞線程,但也不會繼續執行,因爲線程將不斷重複執行相同的操作,而且總會失敗。
30)如何減少鎖的競爭?
1.減少鎖的持有時間,比如可以將一些與鎖無關的代碼移出同步代碼塊。
2.降低鎖的請求頻率,可以通過鎖分解和鎖分段技術實現。
如果一個鎖需要保護多個相互獨立的狀態變量,就可以將這個鎖分解爲多個鎖,並且每個鎖只能保護一個變量,從而提高可伸縮性,最終降低每個鎖的請求頻率。
某些情況下,可以將鎖分解進一步擴展爲鎖分段,即對一組獨立對象上的鎖進行分解,如ConcurrentHashMap實現了鎖分段技術。
3.使用帶有協調機制的獨佔鎖。比如併發容器、讀-寫鎖、不可變對象、原子變量。
ReadWriteLock:實現了一種在多個讀取操作以及單個寫入操作情況下的加鎖機制,如果多個讀取操作都不會修改共享資源,那麼這些讀取操作可以同時訪問該共享資源,但在執行寫入操作時必須以獨佔方式獲取鎖。
原子變量類:提供了在整數或對象應用上的細粒度原子操作,並使用了現代處理器中提供的底層併發原語(如CAS)。
31)內置鎖(synchronized) 和 顯式鎖(ReentrantLock)的區別?
1. 基本使用:
Java中通過Synchronized實現內置鎖,內置鎖獲得鎖和釋放鎖是隱式的,線程進入同步代碼塊或方法的時候會自動獲得鎖,在退出同步代碼塊或方法時會釋放鎖;
ReentrantLock是顯示鎖,需要顯示的進行 lock 以及 unlock 操作。
2.通信:
與Synchronized配套使用的通信方法通常有wait()、notify()/notifyAll()。
與ReentrantLock搭配的通行方式是Condition,Condition是被綁定到Lock上的,必須使用lock.newCondition()才能創建一個Condition。Synchronized能實現的通信方式,Condition都可以實現,而Condition的優秀之處在於它可以爲多個線程間建立不同的Condition,比如對象的讀/寫Condition。
3.編碼:
Synchronized編碼模式比較簡單,不必顯示的獲得鎖,釋放鎖。
ReentrantLock必須在finally塊中釋放鎖,否則如果被保護的代碼中拋出了異常,這個鎖永遠都無法釋放。
4.靈活性:
內置鎖在進入同步塊時,採取的是無限等待的策略,一旦開始等待,就既不能中斷也不能取消,容易產生飢餓與死鎖的問題;
ReentrantLock支持可輪詢的、可定時的、可中斷的鎖獲取操作,lockInterruptibly()方法能夠在獲得鎖的同時保持對中斷的響應;tryLock()方法可以使得線程在等待一段時間後,如果還未獲得鎖,就停止等待而非一直等待,可以更好的解決飢餓與死鎖的問題。另外ReentrantLock提供了兩種公平性選擇,即公平鎖和非公平鎖。在公平的鎖中,如果有另一個線程持有這個鎖或有其他線程在隊列中等待這個鎖,那麼發出請求的線程將放入隊列中,在非公平的鎖中,只有當鎖被某個線程持有時,發出請求的線程將放入隊列中。
5.性能:
Synchronized是JVM的內置屬性,它能執行一些優化,JVM可以通過線程轉儲來幫助識別死鎖的發生,僅當內置鎖不能滿足需求時,才考慮使用顯式鎖。
32)讀寫鎖?
涉及接口:ReadWriteLock
實現:ReentrantReadWriteLock
優勢:提供程序可伸縮性
33)AbstractQueuedSynchronizer(AQS)?
AQS其實就是一個可以給我們實現鎖的框架。可以說Lock的子類實現都是基於AQS的,在LOCK包中的相關鎖(常用的有ReentrantLock、 ReadWriteLock)都是基於AQS來構建
34)什麼是CAS?
比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。 CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。
優勢:高效的解決了原子操作問題。
缺點:
1.循環時間長開銷很大。
2.只能保證一個共享變量的原子操作。
3.ABA問題。(如果內存地址V初次讀取的值是A,並且在準備賦值的時候檢查到它的值仍然爲A,那我們就能說它的值沒有被其他線程改變過了嗎? 如果在這段期間它的值曾經被改成了B,後來又被改回爲A,那CAS操作就會誤認爲它從來沒有被改變過。這個漏洞稱爲CAS操作的“ABA”問題)
ABA問題可以使用JDK的併發包中的AtomicStampedReference和 AtomicMarkableReference來解決。
35)什麼是原子變量類?
原子變量比鎖的粒度更細,量級更輕,將發生競爭的範圍縮小到單個變量上,能夠支持原子的和有條件的讀-改-寫操作。
java.util.concurrent.atomic包下,常用:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference
常用方法:incrementAndGet() 加一 、decrementAndGet() 減一,compareAndSet(expect, update) 比較並交換
36)樂觀鎖和悲觀鎖的區別?
樂觀鎖:
總是認爲不會產生併發問題,每次讀取數據的總認爲不會有其他線程對數據進行修改,因此不會加鎖,但是在更新時會判斷其他線程是否對數據進行修改,一般會使用版本號機制和CAS操作實現。
version方式:一般是在數據表中加上一個version字段,表示數據被修改的次數,當數據被修改時,version加1。當線程A進行更新操作時,在讀取數據的同時也會讀取version值,在提交更新時,判斷讀取到的version值和當前version值是否相等,若相等則更新,否則重試更新操作,直到更新成功。
CAS操作方式:比較並替換,有3個操作數,內存值V,舊的預期值A,將要替換的新值B,只有當V和A相等時才替換,若不等,則重試(自旋操作)。
悲觀鎖:
總是認爲會產生併發問題,每次讀寫數據時都認爲其他線程會修改,因此加鎖,當其他線程想要訪問數據時,都要被堵塞。可以依靠數據庫實現,如讀鎖、寫鎖、行鎖等,都是在操作之前加鎖,synchronized也是悲觀鎖。
樂觀鎖適合於讀操作比較多的場景,悲觀鎖適合於寫操作比較多的場景。
37)樂觀鎖如何保證數據一致性?
可以通過版本號機制實現。
一般是在數據表中加上一個version字段,表示數據被修改的次數,當數據被修改時,version加1。當線程A進行更新操作時,在讀取數據的同時也會讀取version值,在提交更新時,判斷讀取到的version值和當前version值是否相等,若相等則更新,否則重試更新操作,直到更新成功。