1. 程式人生 > >GOvoid java併發程式設計實戰 java併發程式設計的藝術

GOvoid 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,讀取用讀鎖,寫用寫鎖

既然讀的時候可以多人訪問,那麼為什麼還要加讀鎖呢?當然要加鎖了,否則在寫時去讀,可能不正確-(寫的時候不能去讀)

讀寫鎖的作用為,當我們加上寫鎖時,其他執行緒被阻塞,只有一個寫操作在執行,當我們加上讀鎖後,它是不會限制多個讀執行緒去訪問的。也就是get和put之間是互斥的,put與任何執行緒均為互斥,但是get與get執行緒間並不是互斥的。其實加讀寫鎖的目的是同一把鎖的讀鎖既可以與寫鎖互斥,讀鎖之間還可以共享。

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特性也會引起安全問題,這就是的原因!

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