java併發編程實戰 java併發編程的藝術 閱讀隨筆

java線程池說明 http://www.oschina.net/question/565065_86540php

java中斷機制 http://ifeve.com/java-interrupt-mechanism/java

 

Ask、如今有T一、T二、T3三個線程,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?linux

join方法程序員

若是一個線程A執行了thread.join()語句,其含義是當前線程A等待thread線程終止後才從thread.join()返回面試

join有兩個超時特性的方法,若是在超時時間內thread尚未執行結束,則從該超時方法返回算法

 

Ask、在Java中Lock接口比synchronized塊的優點是什麼?你須要實現一個高效的緩存,它容許多個用戶讀,但只容許一個用戶寫,以此來保持它的完整性,你會怎樣去實現它?編程

 java se 5以後,併發包中新增了Lock接口用來實現鎖功能,提供與synchronized關鍵字相似的同步功能,只是在使用時須要顯式地獲取和釋放鎖。雖然缺乏了synchronized的便捷性,單擁有了鎖獲取與釋放的可操做性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized不具有的特性。windows

 lock接口在多線程和併發編程中最大的優點是它們爲讀和寫分別提供了鎖,它能知足你寫像ConcurrentHashMap這樣的高性能數據結構和有條件的阻塞。數組

 咱們能夠分析一下jdk8中的讀寫鎖的源碼緩存

在這以前,咱們須要瞭解一下AbstractQueuedSynchronizer隊列同步器,是用來構建鎖或者其餘同步組件的基礎框架,它使用一個int成員變量表示同步狀態,經過內置的FIFO隊列完成資源獲取線程的排隊工做。

同步器的主要使用方式是繼承,子類經過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程當中免不了要對同步狀態進行更改,這時就須要使用同步器提供的3個方法來進行操做,getState()、setState(int newState)、compareAndSetState(int expect,int update),由於它們能保證狀態的改變是安全的。

同步器通常是做爲子類的內部靜態類(待會兒詳見讀寫鎖實現),同步器自身沒有實現任何同步接口,僅僅定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既能夠支持獨佔式地獲取同步狀態,也能夠支持共享式地獲取同步狀態,這樣就能夠方便實現不一樣類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

同步器有一些能夠重寫的方法,好比 tryAcquire獨佔式獲取同步狀態 tryRealease獨佔式釋放同步狀態 tryAcquireShared共享式獲取同步狀態 tryRealeaseShared共享式釋放同步狀態 isHeldExclusively 是否被當前線程所獨佔

還提供了一些模板方法,獨佔式獲取同步狀態、獨佔式釋放同步狀態、響應中斷的、響應超時的等,還有共享式的一系列模板方法。

這些都是不一樣類型同步組件的基礎。

咱們來看一下ReentrantReadWriteLock的源碼

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;

讀寫鎖成員有readerLock、writeLock以及sync,都是ReentrantReadWriteLock的內部類

sync就是繼承實現了同步器中的 tryAcquire、tryRealease、tryAcquireShared、tryRealeaseShared等方法,分別用於readerLock、writeLock使用

abstract static class Sync extends AbstractQueuedSynchronizer
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

以readLock爲例

public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;

protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}

public void lock() {
sync.acquireShared(1);
}

public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

public boolean tryLock() {
return sync.tryReadLock();
}

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void unlock() {
sync.releaseShared(1);
}


public Condition newCondition() {
throw new UnsupportedOperationException();
}

public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}

 

以上,咱們能夠了解,要實現高效緩存,多人讀,一人寫,就能夠用ReentrantReadWriteLock,讀取用讀鎖,寫用寫鎖

既然讀的時候能夠多人訪問,那麼爲何還要加讀鎖呢?固然要加鎖了,不然在寫時去讀,可能不正確-(寫的時候不能去讀)

讀寫鎖的做用爲,當咱們加上寫鎖時,其餘線程被阻塞,只有一個寫操做在執行,當咱們加上讀鎖後,它是不會限制多個讀線程去訪問的。也就是getput之間是互斥的,put與任何線程均爲互斥,可是getget線程間並非互斥的。其實加讀寫鎖的目的是同一把鎖的讀鎖既能夠與寫鎖互斥,讀鎖之間還能夠共享。

 

Ask、在java中wait和sleep方法的不一樣?

sleep()方法,屬於Thread類中的。而wait()方法,則是屬於Object類中的。

在調用sleep()方法的過程當中,線程不會釋放對象鎖。

而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備

Wait一般被用於線程間交互,sleep一般被用於暫停執行

 

Ask、用Java實現阻塞隊列

阻塞隊列是一個支持兩個附加操做的隊列,即支持阻塞的插入和移除方法

java最新jdk中目前有以下幾種阻塞隊列

ArrayBlockingQueue 一個由數組結構組成的有界阻塞隊列,按照FIFO原則對元素進行排序

LinkedBlockingQueue 一個由鏈表結構組成的有界阻塞隊列,FIFO

PriorityBlockingQueue 一個支持優先級排序的無界阻塞隊列,能夠自定義類實現compareTo()方法指定元素排序規則,或者促使或PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序

DelayQueue 一個使用優先級隊列實現的無界阻塞隊列,使用PriorityQueue實現,隊列中元素必須實現Delayed接口,建立元素時能夠指定多久才能從隊列中獲取當前元素,只有在延遲期滿才能從隊列中提取元素。用於緩存系統的設計(保存緩存元素的有效期)、定時任務調度(保存當天將會執行的任務以及執行時間,一旦從DelayQueue中獲取到任務就開始執行。TimerQueue就是使用DelayQueue實現的)

SynchronousQueue 一個不存儲元素的阻塞隊列,每一個put操做必須等待一個take操做,不然不能繼續添加元素。支持公平訪問隊列,默認狀況下線程採用非公平策略訪問隊列,構造時能夠經過構造參數指定公平訪問

LinkedTransferQueue 一個由鏈表結構組成的無界阻塞隊列,多了tryTransfer和transfer方法。

transfer方法,若是當前有消費者正在等待接收元素,transfer方法能夠把生產者傳入的元素馬上transfer給消費者。若是沒有消費者在等待,transfer方法會將元素存放在隊列的tail節點,並等到該元素被消費者消費了才返回。

tryTransfer方法,用來試探生產者傳入的元素是否能直接傳給消費者

LinkedBlockingDeque 一個由鏈表結構組成的雙向阻塞隊列。隊列兩端均可以插入和移除元素,雙向隊列由於多了一個操做隊列的入口,在多線程同時入隊時,也就減小了一半的競爭。初始化時能夠設置容量防止其過分膨脹。

 

本身實現阻塞隊列時,能夠用Object的wait()方法、notify()方法或者Lock中Condition的await()、signal()方法,他們均可以實現等待/通知模式

wait()和notify()必須在synchronized的代碼塊中使用 由於只有在獲取當前對象的鎖時才能進行這兩個操做 不然會報異常

而await()和signal()通常與Lock()配合使用(Condition con = lock.newCondition(); lock.lock();con.await() ),也必須先lock.lock()或者lock.lockInterruptibly()獲取鎖以後,才能await或者signal,不然會報異常

 

Ask、用Java寫代碼來解決生產者——消費者問題

與阻塞隊列相似,也能夠直接用阻塞隊列來實現

 

Ask、什麼是原子操做,Java中的原子操做是什麼?

原子操做的描述是: 多個線程執行一個操做時,其中任何一個線程要麼徹底執行完此操做,要麼沒有執行此操做的任何步驟 ,那麼這個操做就是原子的。

Java中的原子操做包括:

1)除long和double以外的基本類型的賦值操做

2)全部引用reference的賦值操做

3)java.concurrent.Atomic.* 包中全部類的一切操做。

可是java對long和double的賦值操做是非原子操做!!long和double佔用的字節數都是8,也就是64bits。在32位操做系統上對64位的數據的讀寫要分兩步完成,每一步取32位數據。這樣對double和long的賦值操做就會有問題:若是有兩個線程同時寫一個變量內存,一個進程寫低32位,而另外一個寫高32位,這樣將致使獲取的64位數據是失效的數據。所以須要使用volatile關鍵字來防止此類現象。volatile自己不保證獲取和設置操做的原子性,僅僅保持修改的可見性。可是java的內存模型保證聲明爲volatile的long和double變量的get和set操做是原子的,具體後面再分析。(from http://www.iteye.com/topic/213794

 

jdk1.5開始提供atomic包,裏面有13個原子操做類,4中類型,基本都是使用Unsafe實現的包裝類。Unsafe是jni方法

原子更新基本類型類 AtomicBoolean 原子更新布爾類型 AtomicInteger 原子更新整型 AtomicLong 原子更新長整型

原子更新數組 AtomicIntegerArray 原子更新整型數組裏的元素 AtomicLongArray 原子更新長整型數組裏的元素 AtomicReferenceArray 原子更新引用類型數組裏的元素

原子更新引用類型 AtomicReference 原子更新引用類型 AtomicReferenceFieldUpdater 原子更新引用類型裏的字段 AtomicMarkableReference 原則更新帶有標記位的引用類型

原子更新字段類  AtomicIntegerFieldUpdater 原子更新整型的字段的更新器 AtomicLongFieldUpdater 原子更新長整型的字段的更新器 AtomicStampedUpdater 原子更新帶有版本號的引用類型

 

Ask、Java中的volatile關鍵是什麼做用?怎樣使用它?在Java中它跟synchronized方法有什麼不一樣?

若是一個字段被聲明爲volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。保證了共享變量的可見性,當一個線程修改一個共享變量時,另一個線程能讀到這個修改後的值。

JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,本地內存中存儲了該線程讀/寫共享變量的副本。(本地內存是JMM的一個抽象概念,並不真實存在,涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化)

JMM經過控制主內存與每一個線程的本地內存之間的交互,來提供內存可見性。

執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。從java源代碼到最終實際執行的指令序列,會分別經歷3種重排序

1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。

2)指令級並行的重排序。現代處理器採用指令級並行技術ILP,將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。

3)內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,使得加載和存儲操做看上去多是在亂序執行。

對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。對於處理器,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,來禁止特定類型的處理器重排序。

 

happens-before規則中有一條

volatile變量規則:對於一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。

注:兩個操做之間具備happens-before關係,並不意味着前一個操做要在後一個操做以前執行!僅僅要求前一個操做(執行的結果)對後一個操做可見,且前一個操做按順序排在第二個操做以前。

即JMM容許的重排序能夠發生在兩個happens-before操做上。

 

理解volatile特性,能夠把對volatile變量的單個讀/寫,當作是使用同一個鎖對這些單個讀/寫操做作了同步。即get與set方法都加上synchronized

鎖的happens-before規則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性。這意味着,對一個volatile變量的讀,老是能看到任意線程對這個volatile變量最後的寫入。

所得語義決定了臨界區代碼的執行具備原子性,這意味着,即便是64位的long型和double型變量,只要是volatile變量,對該變量的讀/寫就具備原子性。

簡而言之,volatile具備以下特性

1)可見性,對一個volatile變量的讀,老是能看到任意線程對這個volatile變量最後的寫入

2)原子性,對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性

 

jdk5開始,volatile寫與鎖的釋放有相同內存語義,volatile讀與鎖的獲取有相同內存語義。

volatile寫的內存語義:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存

volatile讀的內存語義:當讀一個valatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。(內存屏障,一組處理器指令,用於實現對內存操做的順序限制)

每一個volatile寫操做前面插入一個StoreStore屏障,保證在volatile寫以前,其前面的全部普通寫操做已經對任意處理器可見,由於StoreStore屏障會將上面全部的普通寫在volatile寫以前刷新到主內存。

每一個volatile寫操做後面插入一個StoreLoad屏障,此屏障的做用是避免volatile寫與後面可能有的volatile讀/寫操做重排序

每一個volatile讀操做後面插入一個LoadLoad屏障,用來禁止處理器把上面的volatile讀與下面的普通讀重排序。

每一個volatile讀操做後面插入一個LoadStore屏障,用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

 

Ask、什麼是競爭條件?你怎樣發現和解決競爭?

多個線程或者進程在讀寫一個共享數據時結果依賴於它們執行的相對時間,這種情形叫作競爭。

競爭條件發生在當多個進程或者線程在讀寫數據時,其最終的的結果依賴於多個進程的指令執行順序。

舉一個例子:

咱們日常編程常常遇到的修改某個字段,這個操做在庫存那裏尤其突出,當兩個單子同時修改庫存的時候,這時就造成了競爭條件,若是不作同步處理,這裏十有八九就是錯誤的了,由於若是兩個單子同時出庫,而出庫的數量恰好大於庫存數量,這裏就會出現問題。(固然,還有幾種狀況會出現問題,咱們這裏只是爲了舉一個競爭條件的例子)

再好比多個線程操做A帳戶往B帳戶轉帳,若是沒作同步處理,最後會發現,錢總帳對不上。

發現:有共享變量時會發生競爭

解決:進行同步處理,原子操做

 

Ask、你將如何使用thread dump?你將如何分析Thread dump?

在UNIX中你可使用kill -3,而後thread dump將會打印日誌,在windows中你可使用」CTRL+Break」。很是簡單和專業的線程面試問題,可是若是他問你怎樣分析它,就會很棘手。

dump 文件裏,值得關注的線程狀態有:
  1. 死鎖,Deadlock(重點關注) 
  2. 執行中,Runnable   
  3. 等待資源,Waiting on condition(重點關注) 
  4. 等待獲取監視器,Waiting on monitor entry(重點關注)
  5. 暫停,Suspended
  6. 對象等待中,Object.wait() 或 TIMED_WAITING
  7. 阻塞,Blocked(重點關注)  
  8. 中止,Parked

 

Ask、爲何咱們調用start()方法時會執行run()方法,爲何咱們不能直接調用run()方法?

這是另外一個很是經典的java多線程面試問題。這也是我剛開始寫線程程序時候的困惑。如今這個問題一般在電話面試或者是在初中級Java面試的第一輪被問到。這個問題的回答應該是這樣的,當你調用start()方法時你將建立新的線程,而且執行在run()方法裏的代碼。可是若是你直接調用run()方法,它不會建立新的線程也不會執行調用線程的代碼。閱讀我以前寫的《start與run方法的區別》這篇文章來得到更多信息。

1) start:
  用start方法來啓動線程,真正實現了多線程運行,這時無需等待run方法體代碼執行完畢而直接繼續執行下面的代碼。經過調用Thread類的start()方法來啓動一個線程,這時此線程處於就緒(可運行)狀態,並無運行,一旦獲得cpu時間片,就開始執行run()方法,這裏方法 run()稱爲線程體,它包含了要執行的這個線程的內容,Run方法運行結束,此線程隨即終止。
2) run:
  run()方法只是類的一個普通方法而已,若是直接調用Run方法,程序中依然只有主線程這一個線程,其程序執行路徑仍是隻有一條,仍是要順序執行,仍是要等待run方法體執行完畢後纔可繼續執行下面的代碼,這樣就沒有達到寫線程的目的。總結:調用start方法方可啓動線程,而run方法只是thread的一個普通方法調用,仍是在主線程裏執行。這兩個方法應該都比較熟悉,把須要並行處理的代碼放在run()方法中,start()方法啓動線程將自動調用 run()方法,這是由jvm的內存機制規定的。而且run()方法必須是public訪問權限,返回值類型爲void.。

 

Ask、Java中你怎樣喚醒一個阻塞的線程?

這是個關於線程和阻塞的棘手的問題,它有不少解決方法。若是線程遇到了IO阻塞,我而且不認爲有一種方法能夠停止線程。若是線程由於調用wait()、sleep()、或者join()方法而致使的阻塞,你能夠中斷線程,而且經過拋出InterruptedException來喚醒它。我以前寫的《How to deal with blocking methods in java》有不少關於處理線程阻塞的信息。

 

Ask、在Java中CycliBarriar和CountdownLatch有什麼區別?

這個線程問題主要用來檢測你是否熟悉JDK5中的併發包。這兩個的區別是CyclicBarrier能夠重複使用已經經過的障礙,而CountdownLatch不能重複使用。

 

等待多線程完成的CountdownLatch

容許一個或多個線程等待其餘線程完成操做。JDK1.5以後的併發包中提供的CountdownLatch能夠實現join的功能,而且比join的功能更多。

好比定義一個CountDownLatch c = new CountDownLatch(n);

n能夠表明n個線程,每一個線程執行的最後加上c.countDown(),n會減一。

另外一個線程須要等待這n個線程執行結束,就加上c.await(),則該線程阻塞,直到n變成0,即n個線程都執行完畢。若是不想讓該線程阻塞太長時間,則能夠經過await(long time,TimeUnit unit)方法指定時間,等待特定時間後,就再也不阻塞。

一個線程調用countDown方法happens-before另一個線程調用await方法

注:計數器必須大於等於0,只是等於0時,計數器就是零,調用await方法時不會阻塞當前線程。CountDownLatch不可能從新初始化或者修改內部計數器的值。

 

同步屏障CyclicBarrier

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

CyclicBarrier默認的構造方法CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每一個線程調用await方法告訴CyclicBarrier我已經到達了屏障,而後當前線程阻塞。

CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrierAction),用於在線程到達屏障時,優先執行barrierAction就方便處理更復雜的業務場景。好比分別處理每一個文件中的數據,最後經過barrierAction來對數據進行彙總。

 

區別:CountDownLatch的計數器只能使用一次,而CyclicBarrier得計數器可使用reset()方法重置,因此,CyclicBarrier能處理更爲複雜的業務場景。好比,若是計算髮生錯誤,能夠重置計數器,並讓線程從新執行一次。

CyclicBarrier還提供其餘有用的方法,用來得到阻塞的線程數量以及瞭解阻塞的線程是否被中斷等。

 

Ask、 什麼是不可變對象,它對寫併發應用有什麼幫助?

另外一個多線程經典面試問題,並不直接跟線程有關,但間接幫助不少。這個java面試問題能夠變的很是棘手,若是他要求你寫一個不可變對象,或者問你爲何String是不可變的。

不可變對象(immutable objects),後面文章我將使用immutable objects來代替不可變對象!

那麼什麼是immutable objects?什麼又是mutable Objects呢?

immutable Objects就是那些一旦被建立,它們的狀態就不能被改變的Objects,每次對他們的改變都是產生了新的immutable的對象,而mutable Objects就是那些建立後,狀態能夠被改變的Objects.

舉個例子:String和StringBuilder,String是immutable的,每次對於String對象的修改都將產生一個新的String對象,而原來的對象保持不變,而StringBuilder是mutable,由於每次對於它的對象的修改都做用於該對象自己,並無產生新的對象。

但有的時候String的immutable特性也會引發安全問題,這就是密碼應該存放在字符數組中而不是String中的緣由!

immutable objects 比傳統的mutable對象在多線程應用中更具備優點,它不只可以保證對象的狀態不被改變,並且還能夠不使用鎖機制就能被其餘線程共享。

實際上JDK自己就自帶了一些immutable類,好比String,Integer以及其餘包裝類。爲何說String是immutable的呢?好比:java.lang.String 的trim,uppercase,substring等方法,它們返回的都是新的String對象,而並非直接修改原來的對象。

如何在Java中寫出Immutable的類?

要寫出這樣的類,須要遵循如下幾個原則:

1)immutable對象的狀態在建立以後就不能發生改變,任何對它的改變都應該產生一個新的對象。

2)Immutable類的全部的屬性都應該是final的。

3)對象必須被正確的建立,好比:對象引用在對象建立過程當中不能泄露(leak)。

4)對象應該是final的,以此來限制子類繼承父類,以免子類改變了父類的immutable特性。

5)若是類中包含mutable類對象,那麼返回給客戶端的時候,返回該對象的一個拷貝,而不是該對象自己(該條能夠歸爲第一條中的一個特例)

固然不徹底遵照上面的原則也可以建立immutable的類,好比String的hashcode就不是final的,但它能保證每次調用它的值都是一致的,不管你多少次計算這個值,它都是一致的,由於這些值的是經過計算final的屬性得來的!

另外,若是你的Java類中存在不少可選的和強制性的字段,你也可使用建造者模式來建立一個immutable的類。

下面是一個例子:

public final class Contacts {

private final String name;

private final String mobile;

public Contacts(String name, String mobile) {

this.name = name; this.mobile = mobile;

}

public String getName(){

return name;

}

public String getMobile(){

return mobile;

}

}

咱們爲類添加了final修飾,從而避免由於繼承和多態引發的immutable風險。

上面是最簡單的一種實現immutable類的方式,能夠看到它的全部屬性都是final的。

有時候你要實現的immutable類中可能包含mutable的類,好比java.util.Date,儘管你將其設置成了final的,可是它的值仍是能夠被修改的,爲了不這個問題,咱們建議返回給用戶該對象的一個拷貝,這也是Java的最佳實踐之一。下面是一個建立包含mutable類對象的immutable類的例子:

public final class ImmutableReminder{

private final Date remindingDate;

public ImmutableReminder (Date remindingDate) {

if(remindingDate.getTime() < System.currentTimeMillis()){

throw new IllegalArgumentException("Can not set reminder」 + 「 for past time: " + remindingDate);

}

this.remindingDate = new Date(remindingDate.getTime());

}

public Date getRemindingDate() {

return (Date) remindingDate.clone();

}

}

上面的getRemindingDate()方法能夠看到,返回給用戶的是類中的remindingDate屬性的一個拷貝,這樣的話若是別人經過getRemindingDate()方法得到了一個Date對象,而後修改了這個Date對象的值,那麼這個值的修改將不會致使ImmutableReminder類對象中remindingDate值的修改。

使用Immutable類的好處:
1)Immutable對象是線程安全的,能夠不用被synchronize就在併發環境中共享

2)Immutable對象簡化了程序開發,由於它無需使用額外的鎖機制就能夠在線程間共享

3)Immutable對象提升了程序的性能,由於它減小了synchroinzed的使用

4)Immutable對象是能夠被重複使用的,你能夠將它們緩存起來重複使用,就像字符串字面量和整型數字同樣。你可使用靜態工廠方法來提供相似於valueOf()這樣的方法,它能夠從緩存中返回一個已經存在的Immutable對象,而不是從新建立一個。

immutable也有一個缺點就是會製造大量垃圾,因爲他們不能被重用並且對於它們的使用就是」用「而後」扔「,字符串就是一個典型的例子,它會創造不少的垃圾,給垃圾收集帶來很大的麻煩。固然這只是個極端的例子,合理的使用immutable對象會創造很大的價值。

 

看完以上的分析以後,屢次提到final

對於final域,編譯器和處理器遵照兩個重排序規則。

1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。

2)初次讀一個final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

final域的重排序規則能夠確保:在引用變量爲任意線程可見以前,該引用變量指向的對象的final域已經在構造函數中被正確初始化了。其實,要獲得這個效果,還須要一個保證:在構造函數內部,不能讓這個被構造對象的引用爲其餘線程所見,也就是對象引用不能再構造函數中「溢出」。

由於在構造函數返回前,被構造對象的引用不能爲其餘線程所見,由於此時的final域可能尚未被初始化。在構造函數返回後,任意線程都將保證能看到final域正確初始化以後的值。

舊的內存模型中一個缺陷就是final域的值會改變,JDK5以後,加強了final的語義,增長了寫和讀重排序規則,能夠爲java程序員提供初始化安全保證:只要對象是正確構造的(被構造對象的引用在構造函數中沒有溢出),那麼不須要使用同步,就能夠保證任意線程都能看到這個final域在構造函數中被初始化以後的值。

 

Ask、你在多線程環境中遇到的常見的問題是什麼?你是怎麼解決它的?

多線程和併發程序中常遇到的有Memory-interface、競爭條件、死鎖、活鎖和飢餓。問題是沒有止境的,若是你弄錯了,將很難發現和調試。這是大多數基於面試的,而不是基於實際應用的Java線程問題。

 

 

 

Ask、在java中綠色線程和本地線程區別?

1.什麼是綠色線程?

綠色線程(Green Thread)是一個相對於操做系統線程(Native Thread)的概念。
操做系統線程(Native Thread)的意思就是,程序裏面的線程會真正映射到操做系統的線程,線程的運行和調度都是由操做系統控制的
綠色線程(Green Thread)的意思是,程序裏面的線程不會真正映射到操做系統的線程,而是由語言運行平臺自身來調度。
當前版本的Python語言的線程就能夠映射到操做系統線程。當前版本的Ruby語言的線程就屬於綠色線程,沒法映射到操做系統的線程,所以Ruby語言的線程的運行速度比較慢。
難道說,綠色線程要比操做系統線程要慢嗎?固然不是這樣。事實上,狀況可能正好相反。Ruby是一個特殊的例子。線程調度器並非很成熟。 
目前,線程的流行實現模型就是綠色線程。好比,stackless Python,就引入了更加輕量的綠色線程概念。在線程併發編程方面,不管是運行速度仍是併發負載上,都優於Python。
另外一個更著名的例子就是ErLang(愛立信公司開發的一種開源語言)。 
ErLang的綠色線程概念很是完全。ErLang的線程不叫Thread,而是叫作Process。這很容易和進程混淆起來。這裏要注意區分一下。 
ErLang Process之間根本就不須要同步。由於ErLang語言的全部變量都是final的,不容許變量的值發生任何變化。所以根本就不須要同步。 
final變量的另外一個好處就是,對象之間不可能出現交叉引用,不可能構成一種環狀的關聯,對象之間的關聯都是單向的,樹狀的。所以,內存垃圾回收的算法效率也很是高。這就讓ErLang可以達到Soft Real Time(軟實時)的效果。這對於一門支持內存垃圾回收的語言來講,可不是一件容易的事情

2.Java世界中的綠色線程

所謂綠色線程更多的是一個邏輯層面的概念,依賴於虛擬機來實現。操做系統對於虛擬機內部如何進行線程的切換並不清楚,從虛擬機外部來看,或者說站在操做系統的角度看,這些都是不可見的。能夠把虛擬機看做一個應用程序,程序的代碼自己來創建和維護針對不一樣線程的堆棧,指令計數器和統計信息等等。這個時候的線程僅僅存在於用戶級別的應用程序中,不須要進行系統級的調用,也不依賴於操做系統爲線程提供的具體功能。綠色線程主要是爲了移植方便,可是會增長虛擬機的複雜度。總的來講,它把線程的實現對操做系統屏蔽,處在用戶級別的實現這個層次上。綠色線程模型的一個特色就是多CPU也只能在某一時刻僅有一個線程運行。
本機線程簡單地說就是和操做系統的線程對應,操做系統徹底瞭解虛擬機內部的線程。對於windows操做系統,一個java虛擬機的線程對應一個本地線程,java線程調度依賴於操做系統線程。對於solaris,複雜一些,由於後者自己提供了用戶級和系統級兩個層次的線程庫。依賴於操做系統增長了對於平臺的依賴性,可是虛擬機實現相對簡單些,並且能夠充分利用多CPU實現多線程同時處理。

 

Ask、線程與進程的區別?

 

Ask、 什麼是多線程中的上下文切換?

即便是單核處理器也支持多線程執行代碼,CPU經過給每一個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,由於時間片很是短,因此CPU經過不停地切換線程執行,讓咱們感受多個線程同時執行,時間片通常爲幾十毫秒ms。

CPU經過時間片分配算法來循環執行任務,當前任務執行一個時間片以後會切換到下一個任務。可是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,能夠再加載這個任務的狀態。因此任務從保存到再加載的過程就是一次上下文切換。

 

Ask、死鎖與活鎖的區別,死鎖與飢餓的區別?

活鎖指的是任務或者執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。 活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,所謂的「活」, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
活鎖能夠認爲是一種特殊的飢餓。 下面這個例子在有的文章裏面認爲是活鎖。實際上這只是一種飢餓。由於沒有體現出「活」的特色。 假設事務T2再不斷的重複嘗試獲取鎖R,那麼這個就是活鎖。
若是 事務T1封鎖了數據R,事務T2又請求封鎖R,因而T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。而後T4又請求封鎖R,當T3釋放了R上的封鎖以後,系統又批准了T4的請求......T2可能永遠等待。
活鎖應該是一系列進程在 輪詢地等待某個不可能爲真的條件爲真。活鎖的時候進程是不會blocked,這會致使耗盡CPU資源。
解決協同活鎖的一種方案是調整重試機制。
 
好比引入一些隨機性。例如若是檢測到衝突,那麼就暫停隨機的必定時間進行重試。這回大大減小碰撞的可能性。 典型的例子是以太網的 CSMA/CD檢測機制。
另外爲了不可能的 死鎖,適當加入必定的重試次數也是有效的解決辦法。儘管這在業務上會引發一些複雜的邏輯處理。
好比約定重試機制避免再次衝突。 例如自動駕駛的防碰撞系統(假想的例子),能夠根據序列號約定檢測到相撞風險時,序列號小的飛機朝上飛, 序列號大的飛機朝下飛。

 

死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。
 
死鎖發生的條件
  • 互斥條件:線程對資源的訪問是排他性的,若是一個線程對佔用了某資源,那麼其餘線程必須處於等待狀態,直到資源被釋放。
  • 請求和保持條件:線程T1至少已經保持了一個資源R1佔用,但又提出對另外一個資源R2請求,而此時,資源R2被其餘線程T2佔用,因而該線程T1也必須等待,但又對本身保持的資源R1不釋放。
  • 不剝奪條件:線程已得到的資源,在未使用完以前,不能被其餘線程剝奪,只能在使用完之後由本身釋放。
  • 環路等待條件:在死鎖發生時,必然存在一個「進程-資源環形鏈」,即:{p0,p1,p2,...pn},進程p0(或線程)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,因而兩個進程就相互等待)

避免死鎖方法:一次封鎖法和 順序封鎖法。

一次封鎖法要求每一個事務必須一次將全部要使用的數據所有加鎖,不然就不能繼續執行。
一次封鎖法雖然能夠有效地防止死鎖的發生,但也存在問題,一次就將之後要用到的所有數據加鎖,勢必擴大了封鎖的範圍,從而下降了系統的併發度。

順序封鎖法是預先對數據對象規定一個封鎖順序,全部事務都按這個順序實行封鎖。
順序封鎖法能夠有效地防止死鎖,但也一樣存在問題。事務的封鎖請求能夠隨着事務的執行而動態地決定,很難事先肯定每個事務要封鎖哪些對象,所以也就很難按規定的順序去施加封鎖。

 
什麼是活鎖
活鎖:是指線程1可使用資源,但它很禮貌,讓其餘線程先使用資源,線程2也可使用資源,但它很紳士,也讓其餘線程先使用資源。這樣你讓我,我讓你,最後兩個線程都沒法使用資源。
避免活鎖的簡單方法是採用先來先服務的策略。當多個事務請求封鎖同一數據對象時,封鎖子系統按請求封鎖的前後次序對事務排隊,數據對象上的鎖一旦釋放就批准申請隊列中第一個事務得到鎖。
什麼是飢餓
飢餓:是指若是線程T1佔用了資源R,線程T2又請求封鎖R,因而T2等待。T3也請求資源R,當T1釋放了R上的封鎖後,系統首先批准了T3的請求,T2仍然等待。而後T4又請求封鎖R,當T3釋放了R上的封鎖以後,系統又批准了T4的請求......,T2可能永遠等待。

 

Ask、Java中用到的線程調度算法是什麼?

JVM調度的模式有兩種:分時調度和搶佔式調度。

    分時調度是全部線程輪流得到CPU使用權,並平均分配每一個線程佔用CPU的時間;

    搶佔式調度是根據線程的優先級別來獲取CPU的使用權。JVM的線程調度模式採用了搶佔式模式。既然是搶佔調度,那麼咱們就能經過設置優先級來「有限」的控制線程的運行順序,注意「有限」一次。

 

Ask、 在Java中什麼是線程調度?

一、首先簡單說下java內存模型:Java中全部變量都儲存在主存中,對於全部線程都是共享的(由於在同一進程中),每一個線程都有本身的工做內存或本地內存(Working Memory),工做內存中保存的是主存中某些變量的拷貝,線程對全部變量的操做都是在工做內存中進行,而線程之間沒法相互直接訪問,變量傳遞均須要經過主存完成,可是在程序內部能夠互相調用(經過對象方法),全部線程間的通訊相對簡單,速度也很快。

                              

                                                                       java內存模型

二、進程間的內部數據和狀態都是相互徹底獨立的,所以進程間通訊大多數狀況是必須經過網絡實現。線程自己的數據,一般只有寄存器數據,以及一個程序執行時使用的堆棧,因此線程的切換比進程切換的負擔要小。

三、CPU對於各個線程的調度是隨機的(分時調度),在Java程序中,JVM負責線程的調度。 線程調度是指按照特定的機制爲多個線程分配CPU的使用權,也就是實際執行的時候是線程,所以CPU調度的最小單位是線程,而資源分配的最小單位是進程。

 

Ask、在線程中你怎麼處理不可捕捉異常?

在java多線程程序中,全部線程都不容許拋出未捕獲的checked exception,也就是說各個線程須要本身把本身的checked exception處理掉。這一點是經過java.lang.Runnable.run()方法聲明(由於此方法聲明上沒有throw exception部分)進行了約束。可是線程依然有可能拋出unchecked exception,當此類異常跑拋出時,線程就會終結,而對於主線程和其餘線程徹底不受影響,且徹底感知不到某個線程拋出的異常(也是說徹底沒法catch到這個異常)。JVM的這種設計源自於這樣一種理念:「線程是獨立執行的代碼片段,線程的問題應該由線程本身來解決,而不要委託到外部。」基於這樣的設計理念,在Java中,線程方法的異常(不管是checked仍是unchecked exception),都應該在線程代碼邊界以內(run方法內)進行try catch並處理掉.

 

但若是線程確實沒有本身try catch某個unchecked exception,而咱們又想在線程代碼邊界以外(run方法以外)來捕獲和處理這個異常的話,java爲咱們提供了一種線程內發生異常時可以在線程代碼邊界以外處理異常的回調機制,即Thread對象提供的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法。

經過該方法給某個thread設置一個UncaughtExceptionHandler,能夠確保在該線程出現異常時能經過回調UncaughtExceptionHandler接口的public void uncaughtException(Thread t, Throwable e) 方法來處理異常,這樣的好處或者說目的是能夠在線程代碼邊界以外(Thread的run()方法以外),有一個地方能處理未捕獲異常。可是要特別明確的是:雖然是在回調方法中處理異常,但這個回調方法在執行時依然還在拋出異常的這個線程中!另外還要特別說明一點:若是線程是經過線程池建立,線程異常發生時UncaughtExceptionHandler接口不必定會當即回調。

 

比之上述方法,還有一種編程上的處理方式能夠借鑑,即,有時候主線程的調用方可能只是想知道子線程執行過程當中發生過哪些異常,而不必定會處理或是當即處理,那麼發起子線程的方法能夠把子線程拋出的異常實例收集起來做爲一個Exception的List返回給調用方,由調用方來根據異常狀況決定如何應對。不過要特別注意的是,此時子線程早以終結。

 

Ask、 什麼是線程組,爲何在Java中不推薦使用?

 

Ask、爲何使用Executor框架比使用應用建立和管理線程好?

 

Ask、 在Java中Executor和Executors的區別?

 

Ask、 如何在Windows和Linux上查找哪一個線程使用的CPU時間最長?

 windows上面用任務管理器看,linux下能夠用top 這個工具看。固然若是你要查找具體的進程,能夠用ps命令,好比查找java:ps -ef |grep java

相關文章
相關標籤/搜索