面試/筆試之多線程面試問題集錦

寫在前面:
找工作告一段落,期間經(jīng)歷了很多事情,也思考了許多問題,最后也收獲了一些沉甸甸的東西 —— 成長和一些來自阿里、百度、京東(sp)、華為等廠的Offer。好在一切又回到正軌,接下來要好好總結(jié)一番才不枉這段經(jīng)歷,遂將此過程中筆者的一些筆試/面試心得、干貨發(fā)表出來,與眾共享之。在此特別要感謝CSDN以及廣大朋友的支持,我將堅持記錄并分享自己所學(xué)、所想、所悟,央請大家不吝賜教,提出您寶貴的意見和建議,以期共同探討提高。
摘要:
本文對面試/筆試過程中經(jīng)常會被問到的一些關(guān)于并發(fā)編程的問題進行了梳理和總結(jié),包括線程池、并發(fā)控制鎖、并發(fā)容器和隊列同步器等基礎(chǔ)知識點,一方面方便自己溫故知新,另一方面也希望為找工作的同學(xué)們提供一個復(fù)習(xí)參考。關(guān)于這塊內(nèi)容的初步了解和掌握,大家可以閱讀《Java并發(fā)編程的藝術(shù)》、《《Java多線程編程核心技術(shù)》和《Java并發(fā)編程實戰(zhàn)》三本書,重點掌握J(rèn).U.C并發(fā)框架。
版權(quán)聲明:
1、如何停止一個線程
使用volatile變量終止正常運行的線程 + 拋異常法/Return法
組合使用interrupt方法與interruptted/isinterrupted方法終止正在運行的線程 + 拋異常法/Return法
使用interrupt方法終止 正在阻塞中的 線程
2、何為線程安全的類?
在線程安全性的定義中,最核心的概念就是 正確性。當(dāng)多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么這個類就是線程安全的。
3、為什么線程通信的方法wait(), notify()和notifyAll()被定義在Object類里?
Object lock = new Object(); synchronized (lock) { lock.wait(); ... }
Wait-notify機制是在獲取對象鎖的前提下不同線程間的通信機制。在Java中,任意對象都可以當(dāng)作鎖來使用,由于鎖對象的任意性,所以這些通信方法需要被定義在Object類里。
4、為什么wait(), notify()和notifyAll()必須在同步方法或者同步塊中被調(diào)用?
wait/notify機制是依賴于Java中Synchronized同步機制的,其目的在于確保等待線程從Wait()返回時能夠感知通知線程對共享變量所作出的修改。如果不在同步范圍內(nèi)使用,就會拋出java.lang.IllegalMonitorStateException的異常。
5、并發(fā)三準(zhǔn)則
- 異常不會導(dǎo)致死鎖現(xiàn)象:當(dāng)線程出現(xiàn)異常且沒有捕獲處理時,JVM會自動釋放當(dāng)前線程占用的鎖,因此不會由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象,同時還會釋放CPU;
- 鎖的是對象而非引用;
- 有wait必有notify;
6、如何確保線程安全?
在Java中可以有很多方法來保證線程安全,諸如:
- 通過加鎖(Lock/Synchronized)保證對臨界資源的同步互斥訪問;
- 使用volatile關(guān)鍵字,輕量級同步機制,但不保證原子性;
- 使用不變類 和 線程安全類(原子類,并發(fā)容器,同步容器等)。
7、volatile關(guān)鍵字在Java中有什么作用
volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新,即保證了內(nèi)存的可見性,除此之外還能 禁止指令重排序。此外,synchronized關(guān)鍵字也可以保證內(nèi)存可見性。
指令重排序問題在并發(fā)環(huán)境下會導(dǎo)致線程安全問題,volatile關(guān)鍵字通過禁止指令重排序來避免這一問題。而對于Synchronized關(guān)鍵字,其所控制范圍內(nèi)的程序在執(zhí)行時獨占的,指令重排序問題不會對其產(chǎn)生任何影響,因此無論如何,其都可以保證最終的正確性。
8、ThreadLocal及其引發(fā)的內(nèi)存泄露
ThreadLocal是Java中的一種線程綁定機制,可以為每一個使用該變量的線程都提供一個變量值的副本,并且每一個線程都可以獨立地改變自己的副本,而不會與其它線程的副本發(fā)生沖突。
每個線程內(nèi)部有一個 ThreadLocal.ThreadLocalMap 類型的成員變量 threadLocals,這個 threadLocals 存儲了與該線程相關(guān)的所有 ThreadLocal 變量及其對應(yīng)的值,也就是說,ThreadLocal 變量及其對應(yīng)的值就是該Map中的一個 Entry,更直白地,threadLocals中每個Entry的Key是ThreadLocal 變量本身,而Value是該ThreadLocal變量對應(yīng)的值。
(1). ThreadLocal可能引起的內(nèi)存泄露
下面是ThreadLocalMap的部分源碼,我們可以看出ThreadLocalMap里面對Key的引用是弱引用。那么,就存在這樣的情況:當(dāng)釋放掉對threadlocal對象的強引用后,map里面的value沒有被回收,但卻永遠(yuǎn)不會被訪問到了,因此ThreadLocal存在著內(nèi)存泄露問題。
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } ... }
看下面的圖示, 實線代表強引用,虛線代表弱引用。每個thread中都存在一個map,map的類型是上文提到的ThreadLocal.ThreadLocalMap,該map中的key為一個ThreadLocal實例。這個Map的確使用了弱引用,不過弱引用只是針對key,每個key都弱引用指向ThreadLocal對象。一旦把threadlocal實例置為null以后,那么將沒有任何強引用指向ThreadLocal對象,因此ThreadLocal對象將會被 Java GC 回收。但是,與之關(guān)聯(lián)的value卻不能回收,因為存在一條從current thread連接過來的強引用。 只有當(dāng)前thread結(jié)束以后, current thread就不會存在棧中,強引用斷開,Current Thread、Map及value將全部被Java GC回收。
所以,得出一個結(jié)論就是:只要這個線程對象被Java GC回收,就不會出現(xiàn)內(nèi)存泄露。但是如果只把ThreadLocal引用指向null而線程對象依然存在,那么此時Value是不會被回收的,這就發(fā)生了我們認(rèn)為的內(nèi)存泄露。比如,在使用線程池的時候,線程結(jié)束是不會銷毀的而是會再次使用的,這種情形下就可能出現(xiàn)ThreadLocal內(nèi)存泄露。
Java為了最小化減少內(nèi)存泄露的可能性和影響,在ThreadLocal進行g(shù)et、set操作時會清除線程Map里所有key為null的value。所以最怕的情況就是,ThreadLocal對象設(shè)null了,開始發(fā)生“內(nèi)存泄露”,然后使用線程池,線程結(jié)束后被放回線程池中而不銷毀,那么如果這個線程一直不被使用或者分配使用了又不再調(diào)用get/set方法,那么這個期間就會發(fā)生真正的內(nèi)存泄露。因此,最好的做法是:在不使用該ThreadLocal對象時,及時調(diào)用該對象的remove方法去移除ThreadLocal.ThreadLocalMap中的對應(yīng)Entry。
9、什么是死鎖(Deadlock)?如何分析和避免死鎖?
死鎖是指兩個以上的線程永遠(yuǎn)阻塞的情況,這種情況產(chǎn)生至少需要兩個以上的線程和兩個以上的資源。
分析死鎖,我們需要查看Java應(yīng)用程序的線程轉(zhuǎn)儲。我們需要找出那些狀態(tài)為BLOCKED的線程和他們等待的資源。每個資源都有一個唯一的id,用這個id我們可以找出哪些線程已經(jīng)擁有了它的對象鎖。下面列舉了一些JDK自帶的死鎖檢測工具:
(1). Jconsole:JDK自帶的圖形化界面工具,主要用于對 Java 應(yīng)用程序做性能分析和調(diào)優(yōu)。
(2). Jstack:JDK自帶的命令行工具,主要用于線程Dump分析。
(3). VisualVM:JDK自帶的圖形化界面工具,主要用于對 Java 應(yīng)用程序做性能分析和調(diào)優(yōu)。
10、什么是Java Timer類?如何創(chuàng)建一個有特定時間間隔的任務(wù)?
Timer是一個調(diào)度器,可以用于安排一個任務(wù)在未來的某個特定時間執(zhí)行或周期性執(zhí)行。TimerTask是一個實現(xiàn)了Runnable接口的抽象類,我們需要去繼承這個類來創(chuàng)建我們自己的定時任務(wù)并使用Timer去安排它的執(zhí)行。
Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("abc"); } }, 200000 , 1000);
11、什么是線程池?如何創(chuàng)建一個Java線程池?
一個線程池管理了一組工作線程,同時它還包括了一個用于放置等待執(zhí)行的任務(wù)的隊列。線程池可以避免線程的頻繁創(chuàng)建與銷毀,降低資源的消耗,提高系統(tǒng)的反應(yīng)速度。java.util.concurrent.Executors提供了幾個java.util.concurrent.Executor接口的實現(xiàn)用于創(chuàng)建線程池,其主要涉及四個角色:
- 線程池:Executor
- 工作線程:Worker線程,Worker的run()方法執(zhí)行Job的run()方法
- 任務(wù)Job:Runable和Callable
- 阻塞隊列:BlockingQueue
阻塞隊列:BlockingQueue
1). 線程池Executor
Executor及其實現(xiàn)類是用戶級的線程調(diào)度器,也是對任務(wù)執(zhí)行機制的抽象,其將任務(wù)的提交與任務(wù)的執(zhí)行分離開來,核心實現(xiàn)類包括ThreadPoolExecutor(用來執(zhí)行被提交的任務(wù))和ScheduledThreadPoolExecutor(可以在給定的延遲后執(zhí)行任務(wù)或者周期性執(zhí)行任務(wù))。Executor的實現(xiàn)繼承鏈條為:(父接口)Executor -> (子接口)ExecutorService -> (實現(xiàn)類)[ ThreadPoolExecutor + ScheduledThreadPoolExecutor ]。
2). 任務(wù)Runable/Callable
Runnable(run)和Callable(call)都是對任務(wù)的抽象,但是Callable可以返回任務(wù)執(zhí)行的結(jié)果或者拋出異常。
3). 任務(wù)執(zhí)行狀態(tài)Future
Future是對任務(wù)執(zhí)行狀態(tài)和結(jié)果的抽象,核心實現(xiàn)類是furtureTask (所以它既可以作為Runnable被線程執(zhí)行,又可以作為Future得到Callable的返回值) ;
(1). 使用Callable+Future獲取執(zhí)行結(jié)果
ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task(); Future<Integer> result = executor.submit(task); System.out.println("task運行結(jié)果" + result.get()); class Task implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("子線程在進行計算"); Thread.sleep(3000); int sum = 0; for(int i=0;i<100;i++) sum += i; return sum; }
(2). 使用Callable + FutureTask獲取執(zhí)行結(jié)果
ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task(); FutureTask<Integer> futureTask = new FutureTask<Integer>(task); executor.submit(futureTask); System.out.println("task運行結(jié)果"+futureTask.get()); class Task implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("子線程在進行計算"); Thread.sleep(3000); int sum = 0; for(int i=0;i<100;i++) sum += i; return sum; }
4). 四種常用的線程池
(1). FixedThreadPool
用于創(chuàng)建使用固定線程數(shù)的ThreadPool,corePoolSize = maximumPoolSize = n(固定的含義),阻塞隊列為LinkedBlockingQueue。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
(2). SingleThreadExecutor
用于創(chuàng)建一個單線程的線程池,corePoolSize = maximumPoolSize = 1,阻塞隊列為LinkedBlockingQueue。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
(3). CachedThreadPool
用于創(chuàng)建一個可緩存的線程池,corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE,阻塞隊列為SynchronousQueue(沒有容量的阻塞隊列,每個插入操作必須等待另一個線程對應(yīng)的移除操作,反之亦然)。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
(4). ScheduledThreadPoolExecutor
用于創(chuàng)建一個大小無限的線程池,此線程池支持定時以及周期性執(zhí)行任務(wù)的需求。
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue()); }
5). 線程池的飽和策略
當(dāng)阻塞隊列滿了,且沒有空閑的工作線程,如果繼續(xù)提交任務(wù),必須采取一種策略處理該任務(wù),線程池提供了4種策略:
- AbortPolicy:直接拋出異常,默認(rèn)策略;
- CallerRunsPolicy:用調(diào)用者所在的線程來執(zhí)行任務(wù);
- DiscardOldestPolicy:丟棄阻塞隊列中最老的任務(wù),并執(zhí)行當(dāng)前任務(wù);
- DiscardPolicy:直接丟棄任務(wù);
當(dāng)然也可以根據(jù)應(yīng)用場景實現(xiàn)RejectedExecutionHandler接口,自定義飽和策略,如記錄日志或持久化存儲不能處理的任務(wù)。
6). 線程池調(diào)優(yōu)
- 設(shè)置最大線程數(shù),防止線程資源耗盡;
- 使用有界隊列,從而增加系統(tǒng)的穩(wěn)定性和預(yù)警能力(飽和策略);
- 根據(jù)任務(wù)的性質(zhì)設(shè)置線程池大?。篊PU密集型任務(wù)(CPU個數(shù)個線程),IO密集型任務(wù)(CPU個數(shù)兩倍的線程),混合型任務(wù)(拆分)。
12、CAS : CAS自旋volatile變量,是一種很經(jīng)典的用法。
CAS,Compare and Swap即比較并交換,設(shè)計并發(fā)算法時常用到的一種技術(shù)。CAS有3個操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。CAS是通過unsafe類的compareAndSwap (JNI, Java Native Interface) 方法實現(xiàn)的,該方法包括四個參數(shù):第一個參數(shù)是要修改的對象,第二個參數(shù)是對象中要修改變量的偏移量,第三個參數(shù)是修改之前的值,第四個參數(shù)是預(yù)想修改后的值。
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、循環(huán)時間長開銷大和只能保證一個共享變量的原子操作。
ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。
不適用于競爭激烈的情形中:并發(fā)越高,失敗的次數(shù)會越多,CAS如果長時間不成功,會極大的增加CPU的開銷。因此CAS不適合競爭十分頻繁的場景。
只能保證一個共享變量的原子操作:當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,因此可以把多個變量放在一個對象里來進行CAS操作。
13、AQS : 隊列同步器
隊列同步器(AbstractQueuedSynchronizer)是用來構(gòu)建鎖和其他同步組件的基礎(chǔ)框架,技術(shù)是 CAS自旋Volatile變量:它使用了一個Volatile成員變量表示同步狀態(tài),通過CAS修改該變量的值,修改成功的線程表示獲取到該鎖;若沒有修改成功,或者發(fā)現(xiàn)狀態(tài)state已經(jīng)是加鎖狀態(tài),則通過一個Waiter對象封裝線程,添加到等待隊列中,并掛起等待被喚醒。
同步器是實現(xiàn)鎖的關(guān)鍵,子類通過繼承同步器并實現(xiàn)它的抽象方法來管理同步狀態(tài),利用同步器實現(xiàn)鎖的語義。特別地,鎖是面向鎖使用者的,它定義了使用者與鎖交互的接口,隱藏了實現(xiàn)細(xì)節(jié);同步器面向的是鎖的實現(xiàn)者,它簡化了鎖的實現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了鎖的使用者與鎖的實現(xiàn)者所需關(guān)注的領(lǐng)域。
一般來說,自定義同步器要么是獨占方式,要么是共享方式,他們也只需實現(xiàn)tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現(xiàn)獨占和共享兩種方式,如ReentrantReadWriteLock。
同步器的設(shè)計是基于 模板方法模式 的,也就是說,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現(xiàn)中,并調(diào)用同步器提供的模板方法,而這些模板方法將會調(diào)用使用者重寫的方法。
AQS維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。這里volatile是核心關(guān)鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:getState()、setState()以及compareAndSetState()。
AQS定義了兩種資源共享方式:Exclusive(獨占,只有一個線程能執(zhí)行,如ReentrantLock)和Share(共享,多個線程可同時執(zhí)行,如Semaphore/CountDownLatch)。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了。自定義同步器實現(xiàn)時主要實現(xiàn)以下幾種方法:
isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現(xiàn)它;
tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false;
tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false;
tryAcquireShared(int):共享方式。嘗試獲取資源。負(fù)數(shù)表示失?。?表示成功,但沒有剩余可用資源;正數(shù)表示成功,且有剩余資源;
tryReleaseShared(int):共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。
以ReentrantLock為例,state初始化為0,表示未鎖定狀態(tài)。A線程lock()時,會調(diào)用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖。當(dāng)然,釋放鎖之前,A線程自己是可以重復(fù)獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態(tài)的。
14、Java Concurrency API中的Lock接口(Lock interface)是什么?對比同步它有什么優(yōu)勢?
synchronized是Java的關(guān)鍵字,是Java的內(nèi)置特性,在JVM層面實現(xiàn)了對臨界資源的同步互斥訪問。Synchronized的語義底層是通過一個monitor對象來完成的,線程執(zhí)行monitorenter/monitorexit指令完成鎖的獲取與釋放。而Lock是一個Java接口(API如下圖所示),是基于JDK層面實現(xiàn)的,通過這個接口可以實現(xiàn)同步訪問,它提供了比synchronized關(guān)鍵字更靈活、更廣泛、粒度更細(xì)的鎖操作,底層是由AQS實現(xiàn)的。二者之間的差異總結(jié)如下:
- 實現(xiàn)層面:synchronized(JVM層面)、Lock(JDK層面)
- 響應(yīng)中斷:Lock 可以讓等待鎖的線程響應(yīng)中斷,而使用synchronized時,等待的線程會一直等待下去,不能夠響應(yīng)中斷;
- 立即返回:可以讓線程嘗試獲取鎖,并在無法獲取鎖的時候立即返回或者等待一段時間,而synchronized卻無法辦到;
- 讀寫鎖:Lock可以提高多個線程進行讀操作的效率
- 可實現(xiàn)公平鎖:Lock可以實現(xiàn)公平鎖,而sychronized天生就是非公平鎖
- 顯式獲取和釋放:synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;
15、Condition
Condition可以用來實現(xiàn)線程的分組通信與協(xié)作。以生產(chǎn)者/消費者問題為例,
wait/notify/notifyAll:在隊列為空時,通知所有線程;在隊列滿時,通知所有線程,防止生產(chǎn)者通知生產(chǎn)者,消費者通知消費者的情形產(chǎn)生。
await/signal/signalAll:將線程分為消費者線程和生產(chǎn)者線程兩組:在隊列為空時,通知生產(chǎn)者線程生產(chǎn);在隊列滿時,通知消費者線程消費。
16、什么是阻塞隊列?如何使用阻塞隊列來實現(xiàn)生產(chǎn)者-消費者模型?
java.util.concurrent.BlockingQueue的特性是:當(dāng)隊列是空的時,從隊列中獲取或刪除元素的操作將會被阻塞,或者當(dāng)隊列是滿時,往隊列里添加元素的操作會被阻塞。特別地,阻塞隊列不接受空值,當(dāng)你嘗試向隊列中添加空值的時候,它會拋出NullPointerException。另外,阻塞隊列的實現(xiàn)都是線程安全的,所有的查詢方法都是原子的并且使用了內(nèi)部鎖或者其他形式的并發(fā)控制。
BlockingQueue 接口是java collections框架的一部分,它主要用于實現(xiàn)生產(chǎn)者-消費者問題。特別地,SynchronousQueue是一個沒有容量的阻塞隊列,每個插入操作必須等待另一個線程的對應(yīng)移除操作,反之亦然。CachedThreadPool使用SynchronousQueue把主線程提交的任務(wù)傳遞給空閑線程執(zhí)行。
17、同步容器(強一致性)
同步容器指的是 Vector、Stack、HashTable及Collections類中提供的靜態(tài)工廠方法創(chuàng)建的類。其中,Vector實現(xiàn)了List接口,Vector實際上就是一個數(shù)組,和ArrayList類似,但是Vector中的方法都是synchronized方法,即進行了同步措施;Stack也是一個同步容器,它的方法也用synchronized進行了同步,它實際上是繼承于Vector類;HashTable實現(xiàn)了Map接口,它和HashMap很相似,但是HashTable進行了同步處理,而HashMap沒有。
Collections類是一個工具提供類,注意,它和Collection不同,Collection是一個頂層的接口。在Collections類中提供了大量的方法,比如對集合或者容器進行排序、查找等操作。最重要的是,在它里面提供了幾個靜態(tài)工廠方法來創(chuàng)建同步容器類,如下圖所示:
18、什么是CopyOnWrite容器(弱一致性)?
CopyOnWrite容器即寫時復(fù)制的容器,適用于讀操作遠(yuǎn)多于修改操作的并發(fā)場景中。通俗的理解是當(dāng)我們往一個容器添加元素的時候,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進行Copy,復(fù)制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發(fā)的讀,而不需要加鎖,因為當(dāng)前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
從JDK1.5開始Java并發(fā)包里提供了兩個使用CopyOnWrite機制實現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器主要存在兩個弱點:
容器對象的復(fù)制需要一定的開銷,如果對象占用內(nèi)存過大,可能造成頻繁的YoungGC和Full GC;
CopyOnWriteArrayList不能保證數(shù)據(jù)實時一致性,只能保證最終一致性。
19、ConcurrentHashMap (弱一致性)
ConcurrentHashMap的弱一致性主要是為了提升效率,也是一致性與效率之間的一種權(quán)衡。要成為強一致性,就得到處使用鎖,甚至是全局鎖,這就與Hashtable和同步的HashMap一樣了。ConcurrentHashMap的弱一致性主要體現(xiàn)在以下幾方面:
get操作是弱一致的:get操作只能保證一定能看到已完成的put操作;
clear操作是弱一致的:在清除完一個segments之后,正在清理下一個segments的時候,已經(jīng)清理的segments可能又被加入了數(shù)據(jù),因此clear返回的時候,ConcurrentHashMap中是可能存在數(shù)據(jù)的。
public void clear() { for (int i = 0; i < segments.length; ++i) segments[i].clear(); }
ConcurrentHashMap中的迭代操作是弱一致的(未遍歷的內(nèi)容發(fā)生變化可能會反映出來):在遍歷過程中,如果已經(jīng)遍歷的數(shù)組上的內(nèi)容變化了,迭代器不會拋出ConcurrentModificationException異常。如果未遍歷的數(shù)組上的內(nèi)容發(fā)生了變化,則有可能反映到迭代過程中。
20、happens-before
happens-before 指定了兩個操作間的執(zhí)行順序:如果 A happens before B,那么Java內(nèi)存模型將向程序員保證 —— A 的執(zhí)行順序排在 B 之前,并且 A 操作的結(jié)果將對 B 可見,其具體包括如下8條規(guī)則:
程序順序規(guī)則:單線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
管程鎖定規(guī)則:一個unlock操作先行發(fā)生于對同一個鎖的lock操作;
volatile變量規(guī)則:對一個Volatile變量的寫操作先行發(fā)生于對這個變量的讀操作;
線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的其他動作;
線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生;
線程終止規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行;
對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于它的finalize()方法的開始;
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;
21、鎖優(yōu)化技術(shù)
鎖優(yōu)化技術(shù)的目的在于線程之間更高效的共享數(shù)據(jù),解決競爭問題,更好提高程序執(zhí)行效率。
自旋鎖(上下文切換代價大):互斥鎖 -> 阻塞 –> 釋放CPU,線程上下文切換代價較大 + 共享變量的鎖定時間較短 == 讓線程通過自旋等一會兒,自旋鎖
鎖粗化(一個大鎖優(yōu)于若干小鎖):一系列連續(xù)操作對同一對象的反復(fù)頻繁加鎖/解鎖會導(dǎo)致不必要的性能損耗,建議粗化鎖
一般而言,同步范圍越小越好,這樣便于其他線程盡快拿到鎖,但仍然存在特例。
偏向鎖(有鎖但當(dāng)前情形不存在競爭):消除數(shù)據(jù)在無競爭情況下的同步原語,提高帶有同步但無競爭的程序性能。
鎖消除(有鎖但不存在競爭,鎖多余):JVM編譯優(yōu)化,將不存在數(shù)據(jù)競爭的鎖消除
22、主線程等待子線程運行完畢再運行的方法
(1). Join
Thread提供了讓一個線程等待另一個線程完成的方法 — join()方法。當(dāng)在某個程序執(zhí)行流程中調(diào)用其它線程的join()方法時,調(diào)用線程將被阻塞,直到被join()方法加入的join線程執(zhí)行完畢為止,在繼續(xù)運行。join()方法的實現(xiàn)原理是不停檢查join線程是否存活,如果join線程存活則讓當(dāng)前線程永遠(yuǎn)等待。直到j(luò)oin線程完成后,線程的this.notifyAll()方法會被調(diào)用。
(2). CountDownLatch
Countdown Latch允許一個或多個線程等待其他線程完成操作。CountDownLatch的構(gòu)造函數(shù)接收一個int類型的參數(shù)作為計數(shù)器,如果你想等待N個點完成,這里就傳入N。當(dāng)我們調(diào)用countDown方法時,N就會減1,await方法會阻塞當(dāng)前線程,直到N變成0。這里說的N個點,可以使用N個線程,也可以是1個線程里的N個執(zhí)行步驟。
(3). Sleep
用sleep方法,讓主線程睡眠一段時間,當(dāng)然這個睡眠時間是主觀的時間,是我們自己定的,這個方法不推薦,但是在這里還是寫一下,畢竟是解決方法。
引用:
ThreadLocal可能引起的內(nèi)存泄露
java自帶的監(jiān)控工具VisualVM一
深入分析java線程池的實現(xiàn)原理
Java并發(fā)之AQS詳解
《為什么ConcurrentHashMap是弱一致的》
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
- 這篇文章主要介紹了Java多線程與并發(fā)面試題(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-09-26
- 這篇文章主要介紹了這四十道阿里的多線程面試題,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-02-19