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


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

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

 

Ask、現在有T1、T2、T3三個線程,你怎樣保證T2在T1執行完后執行,T3在T2執行完后執行?

join方法

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

join有兩個超時特性的方法,如果在超時時間內thread還沒有執行結束,則從該超時方法返回

 

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

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

 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中什么是線程調度?

1、首先簡單說下java內存模型:Java中所有變量都儲存在主存中,對於所有線程都是共享的(因為在同一進程中),每個線程都有自己的工作內存或本地內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內存中進行,而線程之間無法相互直接訪問,變量傳遞均需要通過主存完成,但是在程序內部可以互相調用(通過對象方法),所有線程間的通信相對簡單,速度也很快。

                              

                                                                       java內存模型

2、進程間的內部數據和狀態都是相互完全獨立的,因此進程間通信大多數情況是必須通過網絡實現。線程本身的數據,通常只有寄存器數據,以及一個程序執行時使用的堆棧,所以線程的切換比進程切換的負擔要小。

3、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

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM