肝了一夜的66道并發多線程面試題,你不來個666嗎?
大家好,我是狼王,一個愛打球的程序員
我花了點時間整理了一些多線程,并發相關的面試題,雖然不是很多,但是偶爾看看還是很有用的哦!
話不多說,直接開整!
01 什么是線程?
線程是操作系統能夠進⾏運算調度的最⼩單位,它被包含在進程之中,是進程中的實際運作單位,可以使⽤多線程對進⾏運算提速。
02 什么是線程安全和線程不安全?
線程安全:
就是多線程訪問時,采⽤了加鎖機制,當⼀個線程訪問該類的某個數據時,進⾏保護,其他線程不能進⾏訪問,直到該線程讀取完,其他線程才可使⽤。不會出現數據不⼀致或者數據污染。Vector 是⽤同步⽅法來實現線程安全的, ⽽和它相似的ArrayList不是線程安全的。
線程不安全:
就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據 線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,⽽⽆寫操作,⼀般來說,這個全局變量是線程安全的;若有多個線程同時執⾏寫操作,⼀般都需要考慮線程同步,否則的話就可能影響線程安全。
03 什么是自旋鎖?
自旋鎖是SMP架構中的⼀種low-level的同步機制。
當線程A想要獲取⼀把自旋鎖⽽該鎖⼜被其它線程鎖持有時,線程A會在⼀個循環中自旋以檢測鎖是不是已經可⽤了。
自旋鎖需要注意:
- 由于自旋時不釋放CPU,因⽽持有自旋鎖的線程應該盡快釋放自旋鎖,否則等待該自旋鎖的線程會⼀直在那⾥自旋,這就會浪費CPU時間。
- 持有自旋鎖的線程在sleep之前應該釋放自旋鎖以便其它線程可以獲得自旋鎖。
⽬前的JVM實現自旋會消耗CPU,如果⻓時間不調⽤doNotify⽅法,doWait⽅發會⼀直自旋,CPU會消耗太⼤。
自旋鎖⽐較適⽤于鎖使⽤者保持鎖時間⽐較短的情況,這種情況自旋鎖的效率⽐較⾼。
自旋鎖是⼀種對多處理器相當有效的機制,⽽在單處理器⾮搶占式的系統中基本上沒有做⽤。
04 什么是CAS?
- CAS(compare and swap)的縮寫,中⽂翻譯成⽐較并交換。
- CAS 不通過JVM,直接利⽤java本地⽅ JNI(Java Native Interface為JAVA本地調⽤),直接調⽤CPU 的cmpxchg(是匯編指令)指令。
- 利⽤CPU的CAS指令,同時借助JNI來完成Java的⾮阻塞算法,實現原⼦操作。其它原⼦操作都是利⽤類似的特性完成的。
- 整個java.util.concurrent都是建⽴在CAS之上的,因此對于synchronized阻塞算法,J.U.C在性能上有了很⼤的提升。
- CAS是項樂觀鎖技術,當多個線程嘗試使⽤CAS同時更新同⼀個變量時,只有其中⼀個線程能更新變量的值,⽽其它線程都失敗,失敗的線程并不會被掛起,⽽是被告知這次競爭中失敗,并可以再次嘗試。
- 使⽤CAS在線程沖突嚴重時,會⼤幅降低程序性能;CAS只適合于線程沖突較少的情況使⽤。
- synchronized在jdk1.6之后,已經改進優化。synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋后阻塞,競爭切換后繼續競爭鎖,稍微犧牲了公平性,但獲得了⾼吞吐量。在線程沖突較少的情況下,可以獲得和CAS類似的性能;⽽線程沖突嚴重的情況下,性能遠⾼于CAS。
05 什么是樂觀鎖和悲觀鎖?
1.悲觀鎖
Java在JDK1.5之前都是靠synchronized關鍵字保證同步的,這種通過使⽤⼀致的鎖定協議來協調對共享狀態的訪問,可以確保⽆論哪個線程持有共享變量的鎖,都采⽤獨占的⽅式來訪問這些變量。獨占鎖其實就是⼀種悲觀鎖,所以可以說synchronized是悲觀鎖。
2.樂觀鎖
樂觀鎖( Optimistic Locking)其實是⼀種思想。相對悲觀鎖⽽⾔,樂觀鎖假設認為數據⼀般情況下不會造成沖突,所以在數據進⾏提交更新的時候,才會正式對數據的沖突與否進⾏檢測,如果發現沖突了,則讓返回⽤戶錯誤的信息,讓⽤戶決定如何去做。memcached使⽤了cas樂觀鎖技術保證數據⼀致性。
06 什么是AQS?
1、AbstractQueuedSynchronizer簡稱AQS,是⼀個⽤于構建鎖和同步容器的框架。事實上concurrent包內許多類都是基于AQS構建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現同步容器時設計的⼤量細節問題。
2、AQS使⽤⼀個FIFO的隊列表示排隊等待鎖的線程,隊列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何線程關聯。其他的節點與等待線程關聯,每個節點維護⼀個等待狀態waitStatus。
07 什么是原⼦操作?在Java Concurrency API中有哪些原⼦類(atomic classes)?
- 原⼦操作是指⼀個不受其他操作影響的操作任務單元。原⼦操作是在多線程環境下避免數據不⼀致必須的⼿段。
- int++并不是⼀個原⼦操作,所以當⼀個線程讀取它的值并加1時,另外⼀個線程有可能會讀到之前的值,這就會引發錯誤。
- 為了解決這個問題,必須保證增加操作是原⼦的,在JDK1.5之前我們可以使⽤同步技術來做到這⼀點。
到JDK1.5,java.util.concurrent.atomic包提供了int和long類型的裝類,它們可以自動的保證對于他們的操作是原⼦的并且不需要使⽤同步。
08 什么是Executors框架?
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創建⼀個可緩存線程池,如果線程池⻓度超過處理需要,可靈活回收空閑線程,若⽆可回收,則新建線程。newFixedThreadPool 創建⼀個定⻓線程池,可控制線程最⼤并發數,超出的線程會在隊列中等待。newScheduledThreadPool 創建⼀個定⻓線程池,⽀持定時及周期性任務執⾏。newSingleThreadExecutor 創建⼀個單線程化的線程池,它只會⽤唯⼀的⼯作線程來執⾏任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執⾏。
09 什么是阻塞隊列?如何使⽤阻塞隊列來實現⽣產者-消費者模型?
1、JDK7提供了7個阻塞隊列。(也屬于并發容器)
ArrayBlockingQueue :⼀個由數組結構組成的有界阻塞隊列。LinkedBlockingQueue :⼀個由鏈表結構組成的有界阻塞隊列。PriorityBlockingQueue :⼀個⽀持優先級排序的⽆界阻塞隊列。DelayQueue:⼀個使⽤優先級隊列實現的⽆界阻塞隊列。SynchronousQueue:⼀個不存儲元素的阻塞隊列。LinkedTransferQueue:⼀個由鏈表結構組成的⽆界阻塞隊列。LinkedBlockingDeque:⼀個由鏈表結構組成的雙向阻塞隊列。
2、概念:阻塞隊列是⼀個在隊列基礎上⼜⽀持了兩個附加操作的隊列。
3、2個附加操作:
3.1. ⽀持阻塞的插⼊⽅法:隊列滿時,隊列會阻塞插⼊元素的線程,直到隊列不滿。
3.2 ⽀持阻塞的移除⽅法:隊列空時,獲取元素的線程會等待隊列變為⾮空。
10 什么是Callable和Future?
1、Callable 和 Future 是⽐較有趣的⼀對組合。當我們需要獲取線程的執⾏結果時,就需要⽤到它們。Callable⽤于產⽣結果,Future⽤于獲取結果。
2、Callable接⼝使⽤泛型去定義它的返回類型。Executors類提供了⼀些有⽤的⽅法去在線程池中執⾏Callable內的任務。由于Callable任務是并⾏的,必須等待它返回的結果。java.util.concurrent.Future對象解決了這個問題。
3、在線程池提交Callable任務后返回了⼀個Future對象,使⽤它可以知道Callable任務的狀態和得到Callable返回的執⾏結果。Future提供了get()⽅法,等待Callable結束并獲取它的執⾏結果。
11 什么是FutureTask?
1、FutureTask可⽤于異步獲取執⾏結果或取消執⾏任務的場景。通過傳⼊Runnable或者Callable的任務給FutureTask,直接調⽤其run⽅法或者放⼊線程池執⾏,之后可以在外部通過FutureTask的get⽅法異步獲取執⾏結果,因此,FutureTask⾮常適合⽤于耗時的計算,主線程可以在完成自⼰的任務后,再去獲取結果。另外,FutureTask還可以確保即使調⽤了多次run⽅法,它都只會執⾏⼀次Runnable或者Callable任務,或者通過cancel取消FutureTask的執⾏等。
2、futuretask可⽤于執⾏多任務、以及避免⾼并發情況下多次創建數據機鎖的出現。
12 什么是同步容器和并發容器的實現?
同步容器:
1、主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。
2、鎖的粒度為當前對象整體。
3、迭代器是及時失敗的,即在迭代的過程中發現被修改,就會拋出ConcurrentModificationException。
并發容器:
1、主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
2、鎖的粒度是分散的、細粒度的,即讀和寫是使⽤不同的鎖。
3、迭代器具有弱⼀致性,即可以容忍并發修改,不會拋出ConcurrentModificationException。
- ConcurrentHashMap 采⽤分段鎖技術,同步容器中,是⼀個容器⼀個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分成若⼲段,每段維護⼀個鎖,以達到⾼效的并發訪問;
13 什么是多線程的上下⽂切換?
1、多線程:是指從軟件或者硬件上實現多個線程的并發技術。
2、多線程的好處:
- 使⽤多線程可以把程序中占據時間⻓的任務放到后臺去處理,如圖⽚、視屏的下載 發揮多核處理器的優勢,并發執⾏讓系統運⾏的更快、更流暢,⽤戶體驗更好
3、多線程的缺點:
- ⼤量的線程降低代碼的可讀性;更多的線程需要更多的內存空間, 當多個線程對同⼀個資源出現爭奪時候要注意線程安全的問題。
4、多線程的上下⽂切換:
- CPU通過時間⽚分配算法來循環執⾏任務,當前任務執⾏⼀個時間⽚后會切換到下⼀個任務。但是,在切換前會保存上⼀個任務的狀態,以便下次切換回這個任務時,可以再次加載這個任務的狀態。
14 ThreadLocal的設計理念與作⽤?
Java中的ThreadLocal類允許我們創建只能被同⼀個線程讀寫的變量。因此,如果⼀段代碼含有⼀個ThreadLocal變量的引⽤,即使兩個線程同時執⾏這段代碼,它們也⽆法訪問到對⽅的ThreadLocal變量。
概念:線程局部變量。在并發編程的時候,成員變量如果不做任何處理其實是線程不安全的,各個線程都在操作同⼀個變量,顯然是不⾏的,并且我們也知道volatile這個關鍵字也是不能保證線程安全的。那么在有⼀種情況之下,我們需要滿⾜這樣⼀個條件:變量是同⼀個,但是每個線程都使⽤同⼀個初始值,也就是使⽤同⼀個變量的⼀個新的副本。這種情況之下ThreadLocal就⾮常適⽤,⽐如說DAO的數據庫連接,我們知道DAO是單例的,那么他的屬性Connection就不是⼀個線程安全的變量。⽽我們每個線程都需要使⽤他,并且各自使⽤各自的。這種情況,ThreadLocal就⽐較好的解決了這個問題。
原理:從本質來講,就是每個線程都維護了⼀個map,⽽這個map的key就threadLocal,⽽值就是我們set的那個值,每次線程在get的時候,都從自⼰的變量中取值,既然從自⼰的變量中取值,那肯定就不存在線程安全問題,總體來講,ThreadLocal這個變量的狀態根本沒有發⽣變化,他僅僅是充當⼀個key的⻆⾊,另外提供給每⼀個線程⼀個初始值。
實現機制:每個Thread對象內部都維護了⼀個ThreadLocalMap這樣⼀個ThreadLocal的Map,可以存放若⼲個 ThreadLocal。
15 ThreadPool(線程池)⽤法與優勢?
ThreadPool 優點:
減少了創建和銷毀線程的次數,每個⼯作線程都可以被重復利⽤,可執⾏多個任務 可以根據系統的承受能⼒,調整線程池中⼯作線線程的數⽬,防⽌因為因為消耗過多的內存,⽽把服務器累趴下(每個線程需要⼤約1MB內存,線程開的越多,消耗的內存也就越⼤,最后死機)
------減少在創建和銷毀線程上所花的時間以及系統資源的開銷
------如不使⽤線程池,有可能造成系統創建⼤量線程⽽導致消耗完系統內存
- Java⾥⾯線程池的頂級接⼜是Executor,但是嚴格意義上講Executor并不是⼀個線程池,⽽只是⼀個執⾏線程的⼯具。真正的線程池接⼜是ExecutorService。
當線程數⼩于corePoolSize時,創建線程執⾏任務。
當線程數⼤于等于corePoolSize并且workQueue沒有滿時,放⼊workQueue中
線程數⼤于等于corePoolSize并且當workQueue滿時,新任務新建線程運⾏,線程總數要⼩于maximumPoolSize
當線程總數等于maximumPoolSize并且workQueue滿了的時候執⾏handler的rejectedExecution。也就是拒絕策略。
16 Concurrent包⾥的其他東⻄:ArrayBlockingQueue、CountDownLatch等等。
1、ArrayBlockingQueue 數組結構組成的有界阻塞隊列。
2、CountDownLatch 允許⼀個或多個線程等待其他線程完成操作;join⽤于讓當前執⾏線程等待join線程執⾏結束。其實現原理是不停檢查join線程是否存活,如果join線程存活則讓當前線程永遠wait。
17 synchronized和ReentrantLock的區別?
基礎知識:
1.可重⼊鎖。可重⼊鎖是指同⼀個線程可以多次獲取同⼀把鎖。ReentrantLocksynchronized都是可重⼊鎖。
2.可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,⽽ReentrantLock則提供了中斷功能。公平鎖與⾮公平鎖。公平鎖是指多個線程同時嘗試獲取同⼀把鎖時,獲取鎖的順序按照線程達到的順序,⽽⾮公平鎖則允許線程“插隊”。synchronized是⾮公平鎖,⽽ReentrantLock的默認實現是⾮公平鎖,但是也可以設置為公平鎖。
3.CAS操作(CompareAndSwap)。CAS操作簡單的說就是⽐較并交換。CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。⽆論哪種情況,它都會在 CAS 指令之前返回該位置的值。CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”
4.Synchronized: isynchronized是java內置的關鍵字,它提供了⼀種獨占的加鎖⽅式。synchronized的獲取和釋放鎖由JVM實現,⽤戶不需要顯示的釋放鎖,⾮常⽅便。然⽽synchronized也有⼀定的局限性:
當線程嘗試獲取鎖的時候,如果獲取不到鎖會⼀直阻塞。
如果獲取鎖的線程進⼊休眠或者阻塞,除⾮當前線程異常,否則其他線程嘗試獲取鎖必須⼀直等待。
5.ReentrantLock:
ReentrantLock它是JDK 1.5之后提供的API層⾯的互斥鎖,需要lock()和unlock()⽅法配合try/finally語句塊來完成。
等待可中斷避免,出現死鎖的情況(如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false)
公平鎖與⾮公平鎖多個線程等待同⼀個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖⾮公平鎖,ReentrantLock默認的構造函數是創建的⾮公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性能不是很好。
18 Semaphore有什么作⽤?
Semaphore就是⼀個信號量,它的作⽤是限制某段代碼塊的并發數
19 Java Concurrency API中的Lock接⼝(Lock interface)是什么?對⽐同步它有什么優勢?
1、Lock接⼝⽐同步⽅法和同步塊提供了更具擴展性的鎖操作。他們允許更靈活的結構,可以具有完全不同的性質,并且可以⽀持多個相關類的條件對象。
2、它的優勢有:
可以使鎖更公平 可以使線程在等待鎖的時候響應中斷 可以讓線程嘗試獲取鎖,并在⽆法獲取鎖的時候⽴即返回或者等待⼀段時間 可以在不同的范圍,以不同的順序獲取和釋放鎖
20 Hashtable的size()⽅法中明明只有⼀條語句”return count”,為什么還要做同步?
1、同⼀時間只能有⼀條線程執⾏固定類的同步⽅法,但是對于類的⾮同步⽅法,可以多條線程同時訪問。所以,這樣就有問題了,可能線程A在執⾏Hashtable的put⽅法添加數據,線程B則可以正常調⽤size()⽅法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能線程A添加了完了數據,但是沒有對size++,線程B就已經讀取size了,那 么對于線程B來說讀取到的size⼀定是不準確的。
2、⽽給size()⽅法加了同步之后,意味著線程B調⽤size()⽅法只有在線程A調⽤put⽅法完畢之后才可以調⽤,這樣就保證了線程安全性。
21 ConcurrentHashMap的并發度是什么?
1、⼯作機制(分⽚思想):它引⼊了⼀個“分段鎖”的概念,具體可以理解為把⼀個⼤的Map拆分成N個⼩的segment,根據key.hashCode()來決定把key放到哪個HashTable中。可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。
2、應⽤:當讀>寫時使⽤,適合做緩存,在程序啟動時初始化,之后可以被多個線程訪問;
3、hash沖突:
簡介:HashMap中調⽤hashCode()⽅法來計算hashCode。由于在Java中兩個不同的對象可能有⼀樣的hashCode,所以不同的鍵可能有⼀樣hashCode,從⽽導致沖突的產⽣。hash沖突解決:使⽤平衡樹來代替鏈表,當同⼀hash中的元素數量超過特定的值便會由鏈表切換到平衡樹
4、⽆鎖讀:ConcurrentHashMap之所以有較好的并發性是因為ConcurrentHashMap是⽆鎖讀和加鎖寫,并且利⽤了分段鎖(不是在所有的entry上加鎖,⽽是在⼀部分entry上加鎖);
讀之前會先判斷count(jdk1.6),其中的count是被volatile修飾的(當變量被volatile修飾后,每次更改該變量的時候會將更改結果寫到系統主內存中,利⽤多處理器的緩存⼀致性,其他處理器會發現自⼰的緩存⾏對應的內存地址被修改,就會將自⼰處理器的緩存⾏設置為失效,并強制從系統主內存獲取最新的數據。),故可以實現⽆鎖讀。
5、ConcurrentHashMap的并發度就是segment的⼤⼩,默認為16,這意味著最多同時可以有16條線程操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最⼤優勢。
22 ReentrantReadWriteLock讀寫鎖的使⽤?
1、讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm自⼰控制的,你只要上好相應的鎖即可。
2、如果你的代碼只讀數據,可以很多⼈同時讀,但不能同時寫,那就上讀鎖;
3、如果你的代碼修改數據,只能有⼀個⼈在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
23 CyclicBarrier和CountDownLatch的⽤法及區別?
CyclicBarrier和CountDownLatch 都位于java.util.concurrent 這個包下
24 LockSupport⼯具?
LockSupport是JDK中⽐較底層的類,⽤來創建鎖和其他同步⼯具類的基本線程阻塞。java鎖和同步器框架的核⼼ AQS:AbstractQueuedSynchronizer,就是通過調⽤ LockSupport .park()和 LockSupport .unpark()實現線程的阻塞和喚醒的。
25 Condition接⼝及其實現原理?
在java.util.concurrent包中,有兩個很特殊的⼯具類,Condition和ReentrantLock,使⽤過的⼈都知道,ReentrantLock(重⼊鎖)是jdk的concurrent包提供的⼀種獨占鎖的實現。
我們知道在線程的同步時可以使⼀個線程阻塞⽽等待⼀個信號,同時放棄鎖使其他線程可以能競爭到鎖。
在synchronized中我們可以使⽤Object的wait()和notify⽅法實現這種等待和喚醒。
但是在Lock中怎么實現這種wait和notify呢?答案是Condition,學習Condition主要是為了⽅便以后學習blockqueue和concurrenthashmap的源碼,同時也進⼀步理解ReentrantLock。
26 Fork/Join框架的理解?
1、Fork就是把⼀個⼤任務切分為若⼲⼦任務并⾏的執⾏。
2、Join就是合并這些⼦任務的執⾏結果,最后得到這個⼤任務的結果。
27 wait()和sleep()的區別?
1、sleep() ⽅法是線程類(Thread)的靜態⽅法,讓調⽤線程進⼊睡眠狀態,讓出執⾏機會給其他線程,等到休眠時間結束后,線程進⼊就緒狀態和其他線程⼀起競爭cpu的執⾏時間。
因為sleep() 是static靜態的⽅法,他不能改變對象的機鎖,當⼀個synchronized塊中調⽤了sleep() ⽅法,線程雖然進⼊休眠,但是對象的機鎖沒有被釋放,其他線程依然⽆法訪問這個對象。
2、wait() wait()是Object類的⽅法,當⼀個線程執⾏到wait⽅法時,它就進⼊到⼀個和該對象相關的等待池,同時釋放對象的機鎖,使得其他線程能夠訪問,可以通過notify,notifyAll⽅法來喚醒等待的線程。
28 線程的五個狀態(五種狀態,創建、就緒、運⾏、阻塞和死亡)?
線程通常都有五種狀態,創建、就緒、運⾏、阻塞和死亡。
第⼀是創建狀態。在⽣成線程對象,并沒有調⽤該對象的start⽅法,這是線程處于創建狀態。
第⼆是就緒狀態。當調⽤了線程對象的start⽅法之后,該線程就進⼊了就緒狀態,但是此時線程調度程序還沒有把該線程設置為當前線程,此時處于就緒狀態。在線程運⾏之后,從等待或者睡眠中回來之后,也會處于就緒狀態 。第三是運⾏狀態。線程調度程序將處于就緒狀態的線程設置為當前線程,此時線程就進⼊了運⾏狀態,開始運⾏run函數當中的代碼。
第四是阻塞狀態。線程正在運⾏的時候,被暫停,通常是為了等待某個時間的發⽣(⽐如說某項資源就緒)之后再繼續運⾏。sleep,suspend,wait等⽅法都可以導致線程阻塞。
第五是死亡狀態。如果⼀個線程的run⽅法執⾏結束或者調⽤stop⽅法后,該線程就會死亡。對于已經死亡的線程,⽆法再使⽤start⽅法令其進⼊就緒。
29 start()⽅法和run()⽅法的區別?
1、start()⽅法來啟動⼀個線程,真正實現了多線程運⾏。
2、如果直接調⽤run(),其實就相當于是調⽤了⼀個普通函數⽽已,直接調⽤run()⽅法必須等待run()⽅法執⾏完畢才能執⾏下⾯的代碼,所以執⾏路徑還是只有⼀條,根本就沒有線程的特征,所以在多線程執⾏時要使⽤start()⽅法⽽不是run()⽅法。
30 Runnable接⼝和Callable接⼝的區別?
Runnable接⼝中的run()⽅法的返回值是void,它做的事情只是純粹地去執⾏run()⽅法中的代碼⽽已;
Callable接⼝中的call()⽅法是有返回值的,是⼀個泛型,和Future、FutureTask配合可以⽤來獲取異步執⾏的結果。
31 volatile關鍵字的作⽤?
多線程主要圍繞可⻅性和原⼦性兩個特性⽽展開,使⽤volatile關鍵字修飾的變量,保證了其在多線程之間的可⻅性,即每次讀取到volatile變量,⼀定是最新的數據。
代碼底層執⾏不像我們看到的⾼級語⾔—-Java程序這么簡單,它的執⾏是Java代碼–>字節碼–>根據字節碼執⾏對應的C/C++代碼–>C/C++代碼被編譯成匯編語⾔–>和硬件電路交互,現實中,為了獲取更好的性能JVM可能會對指令進⾏重排序,多線程下可能會出現⼀些意想不到的問題。使⽤volatile則會對禁⽌語義重排序,當然這也⼀定程度上降低了代碼執⾏效率。
32 Java中如何獲取到線程dump⽂件?
死循環、死鎖、阻塞、⻚⾯打開慢等問題,查看線程dump是最好的解決問題的途徑。所謂線程dump也就是線程堆棧,獲取到線程堆棧有兩步:
獲取到線程的pid,可以通過使⽤jps命令,在Linux環境下還可以使⽤ps -ef | grep java
打印線程堆棧,可以通過使⽤jstack pid命令,在Linux環境下還可以使⽤kill -3 pid
另外提⼀點,Thread類提供了⼀個getStackTrace()⽅法也可以⽤于獲取線程堆棧。這是⼀個實例⽅法,因此此⽅法是和具體線程實例綁定的,每次獲取到的是具體某個線程當前運⾏的堆棧。
33 線程和進程有什么區別?
進程是系統進⾏資源分配的基本單位,有獨⽴的內存地址空間
線程是CPU獨⽴運⾏和獨⽴調度的基本單位,沒有單獨地址空間,有獨⽴的棧,局部變量,寄存器, 程序計數器等。
創建進程的開銷⼤,包括創建虛擬地址空間等需要⼤量系統資源
創建線程開銷⼩,基本上只有⼀個內核對象和⼀個堆棧。
⼀個進程⽆法直接訪問另⼀個進程的資源;同⼀進程內的多個線程共享進程的資源。
進程切換開銷⼤,線程切換開銷⼩;進程間通信開銷⼤,線程間通信開銷⼩。
線程屬于進程,不能獨⽴執⾏。每個進程⾄少要有⼀個線程,成為主線程
34 線程實現的⽅式有⼏種(四種)?
繼承Thread類,重寫run⽅法
實現Runnable接⼝,重寫run⽅法,實現Runnable接⼝的實現類的實例對象作為Thread構造函數的target
實現Callable接⼝通過FutureTask包裝器來創建Thread線程
通過線程池創建線程
35 高并發、任務執⾏時間短的業務怎樣使⽤線程池?并發不⾼、任務執⾏時間⻓的業務怎樣使⽤線程池?并發⾼業務執⾏時間⻓的業務怎樣使⽤線程池?
高并發、任務執⾏時間短的業務:線程池線程數可以設置為CPU核數+1,減少線程上下⽂的切換。
并發不⾼、任務執⾏時間⻓的業務要區分開看:
- 假如是業務時間⻓集中在IO操作上,也就是IO密集型的任務,因為IO操作并不占⽤CPU,所以不要讓所有的CPU閑下來,可以加⼤線程池中的線程數⽬,讓CPU處理更多的業務
- 假如是業務時間⻓集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)⼀樣吧,線程池中的線程數設置得少⼀些,減少線程上下⽂的切換
并發⾼、業務執⾏時間⻓,解決這種類型任務的關鍵不在于線程池⽽在于整體架構的設計,看看這些業務⾥⾯某些數據是否能做緩存是第⼀步,增加服務器是第⼆步,⾄于線程池的設置,設置參考(2)。最后,業務執⾏時間⻓的問題, 也可能需要分析⼀下,看看能不能使⽤中間件對任務進⾏拆分和解耦。
36 如果你提交任務時,線程池隊列已滿,這時會發⽣什么?
1、如果你使⽤的LinkedBlockingQueue,也就是⽆界隊列的話,沒關系,繼續添加任務到阻塞隊列中等待執⾏,因為LinkedBlockingQueue可以近乎認為是⼀個⽆窮⼤的隊列,可以⽆限存放任務;
2、如果你使⽤的是有界隊列⽐⽅說ArrayBlockingQueue的話,任務⾸先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使⽤拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy。
37 鎖的等級:⽅法鎖、對象鎖、類鎖?
⽅法鎖(synchronized修飾⽅法時):
通過在⽅法聲明中加⼊ synchronized關鍵字來聲明 synchronized ⽅法。
synchronized ⽅法控制對類成員變量的訪問;
每個類實例對應⼀把鎖,每個 synchronized ⽅法都必須獲得調⽤該⽅法的類實例的鎖⽅能執⾏,否則所屬線程阻塞,⽅法⼀旦執⾏,就獨占該鎖,直到從該⽅法返回時才將鎖釋放,此后被阻塞的線程⽅能獲得該鎖,重新進⼊可執⾏狀態。這種機制確保了同⼀時刻對于每⼀個類實例,其所有聲明為 synchronized 的成員函數中⾄多只有⼀個處于可執⾏狀態,從⽽有效避免了類成員變量的訪問沖突。
對象鎖(synchronized修飾⽅法或代碼塊):
當⼀個對象中有synchronized method或synchronized block的時候調⽤此對象的同步⽅法或進⼊其同步區域時,就必須先獲得對象鎖。如果此對象的對象鎖已被其他調⽤者占⽤,則需要等待此鎖被釋放。(⽅法鎖也是對象鎖)
java的所有對象都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。線程進⼊synchronized⽅法的時候獲取該對象的鎖,當然如果已經有線程獲取了這個對象的鎖,那么當前線程會等待;synchronized⽅法正常返回或者拋異常⽽終⽌,JVM會自動釋放對象鎖。這⾥也體現了⽤synchronized來加鎖的1個好處,⽅法拋異常的時候,鎖仍然可以由JVM來自動釋放。
類鎖(synchronized 修飾靜態的⽅法或代碼塊):
由于⼀個class不論被實例化多少次,其中的靜態⽅法和靜態變量在內存中都只有⼀份。所以,⼀旦⼀個靜態的⽅法被申明為synchronized。此類所有的實例化對象在調⽤此⽅法,共⽤同⼀把鎖,我們稱之為類鎖。
對象鎖是⽤來控制實例⽅法之間的同步,類鎖是⽤來控制靜態⽅法(或靜態變量互斥體)之間的同步
38 如果同步塊內的線程拋出異常會發⽣什么?
synchronized⽅法正常返回或者拋異常⽽終⽌,JVM會自動釋放對象鎖
39 并發編程(concurrency)并⾏編程(parallellism)有什么區別?
解釋⼀:并⾏是指兩個或者多個事件在同⼀時刻發⽣;⽽并發是指兩個或多個事件在同⼀時間間隔發⽣。
解釋⼆:并⾏是在不同實體上的多個事件,并發是在同⼀實體上的多個事件。
解釋三:在⼀臺處理器上“同時”處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分布式集群所以并發編程的⽬標是充分的利⽤處理器的每⼀個核,以達到最⾼的處理性能。
40 如何保證多線程下 i++ 結果正確?
volatile只能保證你數據的可⻅性,獲取到的是最新的數據,不能保證原⼦性;
⽤AtomicInteger保證原⼦性。
synchronized既能保證共享變量可⻅性,也可以保證鎖內操作的原⼦性。
41 ⼀個線程如果出現了運⾏時異常會怎么樣?
如果這個異常沒有被捕獲的話,這個線程就停⽌執⾏了。
另外重要的⼀點是:如果這個線程持有某個對象的監視器,那么這個對象監視器會被⽴即釋放.
42 如何在兩個線程之間共享數據?
通過在線程之間共享對象就可以了,然后通過wait/notify/notifyAll、await/signal/signalAll進⾏喚起和等待,⽐⽅說阻塞隊列BlockingQueue就是為線程之間共享數據⽽設計的。
43 ⽣產者消費者模型的作⽤是什么?
通過平衡⽣產者的⽣產能⼒和消費者的消費能⼒來提升整個系統的運⾏效率,這是⽣產者消費者模型最重要的作⽤。
解耦,這是⽣產者消費者模型附帶的作⽤,解耦意味著⽣產者和消費者之間的聯系少,聯系越少越可以獨自發展⽽不需要受到相互的制約。
44 怎么喚醒⼀個阻塞的線程?
如果線程是因為調⽤了wait()、sleep()或者join()⽅法⽽導致的阻塞;
suspend與resume:Java廢棄 suspend() 去掛起線程的原因,是因為 suspend() 在導致線程暫停的同時,并不會去釋放任何鎖資源。其他線程都⽆法訪問被它占⽤的鎖。直到對應的線程執⾏ resume() ⽅法后,被掛起的線程才能繼續,從⽽其它被阻塞在這個鎖的線程才可以繼續執⾏。但是,如果 resume() 操作出現在 suspend() 之前執⾏,那么線程將⼀直處于掛起狀態,同時⼀直占⽤鎖,這就產⽣了死鎖。⽽且,對于被掛起的線程,它的線程狀態居然還是 Runnable。
wait與notify:wait與notify必須配合synchronized使⽤,因為調⽤之前必須持有鎖,wait會⽴即釋放鎖,notify則是同步塊執⾏完了才釋放
await與singal:Condition類提供,⽽Condition對象由new ReentLock().newCondition()獲得,與wait和notify相同,因為使⽤Lock鎖后⽆法使⽤wait⽅法
park與unpark:LockSupport是⼀個⾮常⽅便實⽤的線程阻塞⼯具,它可以在線程任意位置讓線程阻塞。和Thread.suspenf()相⽐,它彌補了由于resume()在前發⽣,導致線程⽆法繼續執⾏的情況。和Object.wait()相⽐,它不需要先獲得某個對象的鎖,也不會拋出IException異常。可以喚醒指定線程。如果線程遇到了IO阻塞,⽆能為⼒,因為IO是操作系統實現的,Java代碼并沒有辦法直接接觸到操作系統。
45 Java中⽤到的線程調度算法是什么?
搶占式。⼀個線程⽤完CPU之后,操作系統會根據線程優先級、線程饑餓情況等數據算出⼀個總的優先級并分配下⼀個時間⽚給某個線程執⾏。
46 單例模式的線程安全性?
⽼⽣常談的問題了,⾸先要說的是單例模式的線程安全意味著:某個類的實例在多線程環境下只會被創建⼀次出來。單例模式有很多種的寫法,我總結⼀下:
(1)餓漢式單例模式的寫法:線程安全
(2)懶漢式單例模式的寫法:⾮線程安全
(3)雙檢鎖單例模式的寫法:線程安全
47 線程類的構造⽅法、靜態塊是被哪個線程調⽤的?
線程類的構造⽅法、靜態塊是被new這個線程類所在的線程所調⽤的,⽽run⽅法⾥⾯的代碼才是被線程自身所調⽤的。
48 同步⽅法和同步塊,哪個是更好的選擇?
同步塊是更好的選擇,因為它不會鎖住整個對象(當然也可以讓它鎖住整個對象)。同步⽅法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停⽌執⾏并需要等待獲得這個對象上的鎖。
synchronized(this)以及⾮static的synchronized⽅法(⾄于static synchronized⽅法請往下看),只能防⽌多個線程同時執⾏同⼀個對象的同步代碼段。
如果要鎖住多個對象⽅法,可以鎖住⼀個固定的對象,或者鎖住這個類的Class對象。
synchronized鎖住的是括號⾥的對象,⽽不是代碼。對于⾮static的synchronized⽅法,鎖的就是對象本身也就是this。
49 如何檢測死鎖?怎么預防死鎖?
概念:是指兩個或兩個以上的進程在執⾏過程中,因爭奪資源⽽造成的⼀種互相等待的現象,若⽆外⼒作⽤,它們都將⽆法推進下去。此時稱系統處于死鎖;
死鎖的四個必要條件:
互斥條件:進程對所分配到的資源不允許其他進程進⾏訪問,若其他進程訪問該資源,只能等待,直⾄占有該資源的進程使⽤完成后釋放該資源
請求和保持條件:進程獲得⼀定的資源之后,⼜對其他資源發出請求,但是該資源可能被其他進程占有,此時請求阻塞,但⼜對自⼰獲得的資源保持不放
不可剝奪條件:是指進程已獲得的資源,在未完成使⽤之前,不可被剝奪,只能在使⽤完后自⼰釋放
環路等待條件:是指進程發⽣死鎖后,若⼲進程之間形成⼀種頭尾相接的循環等待資源關系
死鎖產⽣的原因:
因競爭資源發⽣死鎖 現象:系統中供多個進程共享的資源的數⽬不⾜以滿⾜全部進程的需要時,就會引起對諸資源的競爭⽽發⽣死鎖現象
進程推進順序不當發⽣死鎖
檢查死鎖:
有兩個容器,⼀個⽤于保存線程正在請求的鎖,⼀個⽤于保存線程已經持有的鎖。每次加鎖之前都會做如下檢測
檢測當前正在請求的鎖是否已經被其它線程持有,如果有,則把那些線程找出來
遍歷第⼀步中返回的線程,檢查自⼰持有的鎖是否正被其中任何⼀個線程請求,如果第⼆步返回真,表示出現了死鎖
死鎖的解除與預防:
控制不要讓四個必要條件成⽴。
50 HashMap在多線程環境下使⽤需要注意什么?
要注意死循環的問題,HashMap的put操作引發擴容,這個動作在多線程并發下會發⽣線程死循環的問題。
1、HashMap不是線程安全的;Hashtable線程安全,但效率低,因為是Hashtable是使⽤synchronized的,所有線程競爭同⼀把鎖;⽽ConcurrentHashMap不僅線程安全⽽且效率⾼,因為它包含⼀個segment數組,將數據分段存儲,給每⼀段數據配⼀把鎖,也就是所謂的鎖分段技術。
2、HashMap為何線程不安全:
put時key相同導致其中⼀個線程的value被覆蓋;
多個線程同時擴容,造成數據丟失;
多線程擴容時導致Node鏈表形成環形結構造成.next()死循環,導致CPU利⽤率接近100%;
3、ConcurrentHashMap最⾼效;
51 什么是守護線程?有什么⽤?
守護線程(即daemon thread),是個服務線程,準確地來說就是服務其他的線程,這是它的作⽤——⽽其他的線程只有⼀種,那就是⽤戶線程。所以java⾥線程分2種,
1、守護線程,⽐如垃圾回收線程,就是最典型的守護線程。
2、⽤戶線程,就是應⽤程序⾥的自定義線程。
52 如何實現線程串⾏執⾏?
a. 為了控制線程執⾏的順序,如ThreadA->ThreadB->ThreadC->ThreadA循環執⾏三個線程,我們需要確定喚醒、等待的順序。這時我們可以同時使⽤ Obj.wait()、Obj.notify()與synchronized(Obj)來實現這個⽬標。
線程中持有上⼀個線程類的對象鎖以及自⼰的鎖,由于這種依賴關系,該線程執⾏需要等待上個對象釋放鎖,從⽽ 保證類線程執⾏的順序。
b. 通常情況下,wait是線程在獲取對象鎖后,主動釋放對象鎖,同時本線程休眠,直到有其它線程調⽤對象的notify()喚醒該線程,才能繼續獲取對象鎖,并繼續執⾏。⽽notify()則是對等待對象鎖的線程的喚醒操作。但值得注意的是notify()調⽤后,并不是⻢上就釋放對象鎖,⽽是在相應的synchronized(){}語句塊執⾏結束。釋放對象鎖后,JVM會在執⾏wait()等待對象鎖的線程中隨機選取⼀線程,賦予其對象鎖,喚醒線程,繼續執⾏。
53 可以運⾏時kill掉⼀個線程嗎?
a. 不可以,線程有5種狀態,新建(new)、可運⾏(runnable)、運⾏中(running)、阻塞(block)、死亡(dead)。
b. 只有當線程run⽅法或者主線程main⽅法結束,⼜或者拋出異常時,線程才會結束⽣命周期。
54 關于synchronized
在某個對象的所有synchronized⽅法中,在某個時刻只能有⼀個唯⼀的⼀個線程去訪問這些synchronized⽅法
如果⼀個⽅法是synchronized⽅法,那么該synchronized關鍵字表示給當前對象上鎖(即this)相當于synchronized(this){}
如果⼀個synchronized⽅法是static的,那么該synchronized表示給當前對象所對應的class對象上鎖(每個類不管⽣成多少對象,其對應的class對象只有⼀個)
55 分步式鎖,程序數據庫中死鎖機制及解決⽅案
基本原理:⽤⼀個狀態值表示鎖,對鎖的占⽤和釋放通過狀態值來標識。
三種分布式鎖:
第一種:Zookeeper:
基于zookeeper瞬時有序節點實現的分布式鎖,其主要邏輯如下。⼤致思想即為:每個客戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的⽬錄下,⽣成⼀個唯⼀的瞬時有序節點。判斷是否獲取鎖的⽅式很簡單,只需要判斷有序節點中序號最⼩的⼀個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖⽆法釋放,⽽產⽣的死鎖問題
【優點】鎖安全性⾼,zk可持久化,且能實時監聽獲取鎖的客戶端狀態。⼀旦客戶端宕機,則瞬時節點隨之消失,zk因⽽能第⼀時間釋放鎖。這也省去了⽤分布式緩存實現鎖的過程中需要加⼊超時時間判斷的這⼀邏輯。
【缺點】性能開銷⽐較⾼。因為其需要動態產⽣、銷毀瞬時節點來實現鎖功能。所以不太適合直接提供給⾼并發的場景使⽤。
【實現】可以直接采⽤zookeeper第三⽅庫curator即可⽅便地實現分布式鎖。
【適⽤場景】對可靠性要求⾮常⾼,且并發程度不⾼的場景下使⽤。如核⼼數據的定時全量/增量同步等。)
第二種memcached:
memcached帶有add函數,利⽤add函數的特性即可實現分布式鎖。add和set的區別在于:如果多線程并發set,則每個set都會成功,但最后存儲的值以最后的set的線程為準。⽽add的話則相反,add會添加第⼀個到達的值,并返回true,后續的添加則都會返回false。利⽤該點即可很輕松地實現分布式鎖。
【優點】并發⾼效
【缺點】 memcached采⽤列⼊LRU置換策略,所以如果內存不夠,可能導致緩存中的鎖信息丟失。memcached⽆法持久化,⼀旦重啟,將導致信息丟失。
【使⽤場景】⾼并發場景。需要 1)加上超時時間避免死鎖; 2)提供⾜夠⽀撐鎖服務的內存空間; 3)穩定的集群化管理。
第三種Redis:
redis分布式鎖即可以結合zk分布式鎖鎖⾼度安全和memcached并發場景下效率很好的優點,其實現⽅式和memcached類似,采⽤setnx即可實現。需要注意的是,這⾥的redis也需要設置超時時間,以避免死鎖。可以利⽤jedis客戶端實現。
數據庫死鎖機制和解決⽅案:
死鎖:死鎖是指兩個或者兩個以上的事務在執⾏過程中,因爭奪鎖資源⽽造成的⼀種互相等待的現象。
處理機制:解決死鎖最有⽤最簡單的⽅法是不要有等待,將任何等待都轉化為回滾,并且事務重新開始。但是有可能影響并發性能。
------超時回滾,innodb_lock_wait_time設置超時時間;
------wait-for-graph⽅法:跟超時回滾⽐起來,這是⼀種更加主動的死鎖檢測⽅式。InnoDB引擎也采⽤這種⽅式。
56 spring單例為什么沒有安全問題(ThreadLocal)
1、ThreadLocal:spring使⽤ThreadLocal解決線程安全問題;ThreadLocal會為每⼀個線程提供⼀個獨⽴的變量副本,從⽽隔離了多個線程對數據的訪問沖突。因為每⼀個線程都擁有自⼰的變量副本,從⽽也就沒有必要對該變量進⾏同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。概括起來說,對于多線程資源共享的問題,同步機制采⽤了“以時間換空間”的⽅式,⽽ThreadLocal采⽤了“以空間換時間”的⽅式。前者僅提供⼀份變量,讓不同的線程排隊訪問,⽽后者為每⼀個線程都提供了⼀份變量,因此可以同時訪問⽽互不影響。在很多情況下,ThreadLocal⽐直接使⽤synchronized同步機制解決線程安全問題更簡單,更⽅便,且結果程序擁有更⾼的并發性。
2、單例:⽆狀態的Bean(⽆狀態就是⼀次操作,不能保存數據。⽆狀態對象(Stateless Bean),就是沒有實例變量的對象,不能保存數據,是不變類,是線程安全的。)適合⽤不變模式,技術就是單例模式,這樣可以共享實例,提⾼性能。
57 線程池原理
使⽤場景:假設⼀個服務器完成⼀項任務所需時間為:T1-創建線程時間,T2-在線程中執⾏任務的時間,T3-銷毀線程時間。如果T1+T3遠⼤于T2,則可以使⽤線程池,以提⾼服務器性能;
組成:
線程池管理器(ThreadPool):⽤于創建并管理線程池,包括 創建線程池,銷毀線程池,添加新任務;
⼯作線程(PoolWorker):線程池中線程,在沒有任務時處于等待狀態,可以循環的執⾏任務;
任務接⼝(Task):每個任務必須實現的接⼝,以供⼯作線程調度任務的執⾏,它主要規定了任務的⼊⼝,任務執⾏完后的收尾⼯作,任務的執⾏狀態等;
任務隊列(taskQueue):⽤于存放沒有處理的任務。提供⼀種緩沖機制。
原理:線程池技術正是關注如何縮短或調整T1,T3時間的技術,從⽽提⾼服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者⼀些空閑的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
⼯作流程:
1、線程池剛創建時,⾥⾯沒有⼀個線程(也可以設置參數prestartAllCoreThreads啟動預期數量主線程)。任務隊列是作為參數傳進來的。不過,就算隊列⾥⾯有任務,線程池也不會⻢上執⾏它們。
2、當調⽤ execute() ⽅法添加⼀個任務時,線程池會做如下判斷:
如果正在運⾏的線程數量⼩于 corePoolSize,那么⻢上創建線程運⾏這個任務;
如果正在運⾏的線程數量⼤于或等于 corePoolSize,那么將這個任務放⼊隊列;
如果這時候隊列滿了,⽽且正在運⾏的線程數量⼩于 maximumPoolSize,那么還是要創建⾮核⼼線程⽴刻運⾏這個任務;
如果隊列滿了,⽽且正在運⾏的線程數量⼤于或等于 maximumPoolSize,那么線程池會拋出異常RejectExecutionException。
3、當⼀個線程完成任務時,它會從隊列中取下⼀個任務來執⾏。
4、當⼀個線程⽆事可做,超過⼀定的時間(keepAliveTime)時,線程池會判斷,如果當前運⾏的線程數⼤于corePoolSize,那么這個線程就被停掉。所以線程池的所有任務完成后,它最終會收縮到 corePoolSize 的⼤⼩。
58 java鎖多個對象
例如:在銀⾏系統轉賬時,需要鎖定兩個賬戶,這個時候,順序使⽤兩個synchronized可能存在死鎖的情況
59 java線程如何啟動
1、繼承Thread類;
2、實現Runnable接⼝;
3、直接在函數體內:
⽐較:
1、實現Runnable接⼝優勢:
1)適合多個相同的程序代碼的線程去處理同⼀個資源
2)可以避免java中的單繼承的限制
3)增加程序的健壯性,代碼可以被多個線程共享,代碼和數據獨⽴。
2、繼承Thread類優勢:
1)可以將線程類抽象出來,當需要使⽤抽象⼯⼚模式設計時。
2)多線程同步
3、在函數體使⽤優勢
1)⽆需繼承thread或者實現Runnable,縮⼩作⽤域。
60 java中加鎖的⽅式有哪些,如何實現怎么個寫法?
1、java中有兩種鎖:⼀種是⽅法鎖或者對象鎖(在⾮靜態⽅法或者代碼塊上加鎖),第⼆種是類鎖(在靜態⽅法或者class上加鎖);
2、注意:其他線程可以訪問未加鎖的⽅法和代碼;synchronized同時修飾靜態⽅法和實例⽅法,但是運⾏結果是交替進⾏的,這證明了類鎖和對象鎖是兩個不⼀樣的鎖,控制著不同的區域,它們是互不⼲擾的。
61 如何保證數據不丟失
1、使⽤消息隊列,消息持久化;
2、添加標志位:未處理 0,處理中 1,已處理 2。定時處理。
62、ThreadLocal為什么會發⽣內存泄漏?
1、OOM實現:
ThreadLocal的實現是這樣的:每個Thread 維護⼀個 ThreadLocalMap 映射表,這個映射表的 key 是 ThreadLocal實例本身,value 是真正需要存儲的 Object。2、也就是說 ThreadLocal 本身并不存儲值,它只是作為⼀個 key 來讓線程從 ThreadLocalMap 獲取 value。值得注意的是圖中的虛線,表示 ThreadLocalMap 是使⽤ ThreadLocal 的弱引⽤作為 Key 的,弱引⽤的對象在 GC 時會被回收。
ThreadLocalMap使⽤ThreadLocal的弱引⽤作為key,如果⼀個ThreadLocal沒有外部強引⽤來引⽤它,那么系統 GC的時候,這個ThreadLocal勢必會被回收,這樣⼀來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會⼀直存在⼀條強引⽤鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠⽆法回收,造成內存泄漏。
3、預防辦法:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap⾥所有key為null的value。但是這些被動的預防措施并不能保證不會內存泄漏:
(1)使⽤static的ThreadLocal,延⻓了ThreadLocal的⽣命周期,可能導致內存泄漏。
(2)分配使⽤了ThreadLocal⼜不再調⽤get(),set(),remove()⽅法,那么就會導致內存泄漏,因為這塊內存⼀直存在。
63 jdk8中對ConcurrentHashmap的改進
Java 7為實現并⾏訪問,引⼊了Segment這⼀結構,實現了分段鎖,理論上最⼤并發度與Segment個數相等。
Java 8為進⼀步提⾼并發性,摒棄了分段鎖的⽅案,⽽是直接使⽤⼀個⼤的數組。同時為了提⾼哈希碰撞下的尋址性能,Java 8在鏈表⻓度超過⼀定閾值(8)時將鏈表(尋址時間復雜度為O(N))轉換為紅⿊樹(尋址時間復雜度為O(long(N)))。
64 concurrent包下有哪些類?
ConcurrentHashMap、Future、FutureTask、AtomicInteger…
65 線程a,b,c,d運⾏任務,怎么保證當a,b,c線程執⾏完再執⾏d線程?
1、CountDownLatch類
⼀個同步輔助類,常⽤于某個條件發⽣后才能執⾏后續進程。給定計數初始化CountDownLatch,調⽤countDown()⽅法,在計數到達零之前,await⽅法⼀直受阻塞。
重要⽅法為countdown()與await();
2、join⽅法
將線程B加⼊到線程A的尾部,當A執⾏完后B才執⾏。
3、notify、wait⽅法,Java中的喚醒與等待⽅法,關鍵為synchronized代碼塊,參數線程間應相同,也常⽤Object作為參數。
66 ⾼并發系統如何做性能優化?如何防⽌庫存超賣?
1、⾼并發系統性能優化:優化程序,優化服務配置,優化系統配置
盡量使⽤緩存,包括⽤戶緩存,信息緩存等,多花點內存來做緩存,可以⼤量減少與數據庫的交互,提⾼性能。
⽤jprofiler等⼯具找出性能瓶頸,減少額外的開銷。
優化數據庫查詢語句,減少直接使⽤hibernate等⼯具的直接⽣成語句(僅耗時較⻓的查詢做優化)。
優化數據庫結構,多做索引,提⾼查詢效率。
統計的功能盡量做緩存,或按每天⼀統計或定時統計相關報表,避免需要時進⾏統計的功能。
能使⽤靜態⻚⾯的地⽅盡量使⽤,減少容器的解析(盡量將動態內容⽣成靜態html來顯示)。
解決以上問題后,使⽤服務器集群來解決單臺的瓶頸問題。
2.防⽌庫存超賣:
悲觀鎖:在更新庫存期間加鎖,不允許其它線程修改;
數據庫鎖:select xxx for update;
分布式鎖;
樂觀鎖:使⽤帶版本號的更新。每個線程都可以并發修改,但在并發時,只有⼀個線程會修改成功,其它會返回失敗。
redis watch:監視鍵值對,作⽤時如果事務提交exec時發現監視的監視對發⽣變化,事務將被取消。
消息隊列:通過 FIFO 隊列,使修改庫存的操作串⾏化。
總結:
總的來說,不能把壓⼒放在數據庫上,所以使⽤ “select xxx for update” 的⽅式在⾼并發的場景下是不可⾏的。FIFO 同步隊列的⽅式,可以結合庫存限制隊列⻓,但是在庫存較多的場景下,⼜不太適⽤。所以相對來說,我會傾向于選擇:樂觀鎖 / 緩存鎖 / 分布式鎖的⽅式。