1. 程式人生 > >面試/筆試第四彈 —— 多執行緒面試問題集錦

面試/筆試第四彈 —— 多執行緒面試問題集錦

寫在前面:

  找工作告一段落,期間經歷了很多事情,也思考了許多問題,最後也收穫了一些沉甸甸的東西 —— 成長和一些來自阿里、百度、京東(sp)、華為等廠的Offer。好在一切又回到正軌,接下來要好好總結一番才不枉這段經歷,遂將此過程中筆者的一些筆試/面試心得、乾貨發表出來,與眾共享之。在此特別要感謝CSDN以及廣大朋友的支援,我將堅持記錄並分享自己所學、所想、所悟,央請大家不吝賜教,提出您寶貴的意見和建議,以期共同探討提高。

摘要:

  本文對面試/筆試過程中經常會被問到的一些關於併發程式設計的問題進行了梳理和總結,包括執行緒池、併發控制鎖、併發容器和佇列同步器等基礎知識點,一方面方便自己溫故知新,另一方面也希望為找工作的同學們提供一個複習參考。關於這塊內容的初步瞭解和掌握,大家可以閱讀《Java併發程式設計的藝術》、《《Java多執行緒程式設計核心技術》和《Java併發程式設計實戰》三本書,重點掌握J.U.C併發框架。

1、如何停止一個執行緒

  • 使用volatile變數終止正常執行的執行緒 + 拋異常法/Return法

  • 組合使用interrupt方法與interruptted/isinterrupted方法終止正在執行的執行緒 + 拋異常法/Return法

  • 使用interrupt方法終止 正在阻塞中的 執行緒

2、何為執行緒安全的類?

  線上程安全性的定義中,最核心的概念就是 正確性。當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。

3、為什麼執行緒通訊的方法wait(), notify()和notifyAll()被定義在Object類裡?

Object lock = new Object();
synchronized (lock) {
    lock.wait();
    ...
}

  Wait-notify機制是在獲取物件鎖的前提下不同執行緒間的通訊機制。在Java中,任意物件都可以當作鎖來使用,由於鎖物件的任意性,所以這些通訊方法需要被定義在Object類裡。

4、為什麼wait(), notify()和notifyAll()必須在同步方法或者同步塊中被呼叫?

  wait/notify機制是依賴於Java中Synchronized同步機制的,其目的在於確保等待執行緒從Wait()返回時能夠感知通知執行緒對共享變數所作出的修改。如果不在同步範圍內使用,就會丟擲java.lang.IllegalMonitorStateException的異常。

5、併發三準則

  • 異常不會導致死鎖現象:當執行緒出現異常且沒有捕獲處理時,JVM會自動釋放當前執行緒佔用的鎖,因此不會由於異常導致出現死鎖現象,同時還會釋放CPU;

  • 鎖的是物件而非引用;

  • 有wait必有notify;

6、如何確保執行緒安全?

  在Java中可以有很多方法來保證執行緒安全,諸如:

  • 通過加鎖(Lock/Synchronized)保證對臨界資源的同步互斥訪問;
  • 使用volatile關鍵字,輕量級同步機制,但不保證原子性;
  • 使用不變類 和 執行緒安全類(原子類,併發容器,同步容器等)。

7、volatile關鍵字在Java中有什麼作用

  volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理,即保證了記憶體的可見性,除此之外還能 禁止指令重排序。此外,synchronized關鍵字也可以保證記憶體可見性。

  指令重排序問題在併發環境下會導致執行緒安全問題,volatile關鍵字通過禁止指令重排序來避免這一問題。而對於Synchronized關鍵字,其所控制範圍內的程式在執行時獨佔的,指令重排序問題不會對其產生任何影響,因此無論如何,其都可以保證最終的正確性。

8、ThreadLocal及其引發的記憶體洩露

  ThreadLocal是Java中的一種執行緒繫結機制,可以為每一個使用該變數的執行緒都提供一個變數值的副本,並且每一個執行緒都可以獨立地改變自己的副本,而不會與其它執行緒的副本發生衝突。

  每個執行緒內部有一個 ThreadLocal.ThreadLocalMap 型別的成員變數 threadLocals,這個 threadLocals 儲存了與該執行緒相關的所有 ThreadLocal 變數及其對應的值,也就是說,ThreadLocal 變數及其對應的值就是該Map中的一個 Entry,更直白地,threadLocals中每個Entry的Key是ThreadLocal 變數本身,而Value是該ThreadLocal變數對應的值。

(1). ThreadLocal可能引起的記憶體洩露

  下面是ThreadLocalMap的部分原始碼,我們可以看出ThreadLocalMap裡面對Key的引用是弱引用。那麼,就存在這樣的情況:當釋放掉對threadlocal物件的強引用後,map裡面的value沒有被回收,但卻永遠不會被訪問到了,因此ThreadLocal存在著記憶體洩露問題。

    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 回收。但是,與之關聯的value卻不能回收,因為存在一條從current thread連線過來的強引用。 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開,Current Thread、Map及value將全部被Java GC回收。

           ThreadLocal可能引起的記憶體洩露.jpg-127.3kB

  所以,得出一個結論就是:只要這個執行緒物件被Java GC回收,就不會出現記憶體洩露。但是如果只把ThreadLocal引用指向null而執行緒物件依然存在,那麼此時Value是不會被回收的,這就發生了我們認為的記憶體洩露。比如,在使用執行緒池的時候,執行緒結束是不會銷燬的而是會再次使用的,這種情形下就可能出現ThreadLocal記憶體洩露。  

  Java為了最小化減少記憶體洩露的可能性和影響,在ThreadLocal進行get、set操作時會清除執行緒Map裡所有key為null的value。所以最怕的情況就是,ThreadLocal物件設null了,開始發生“記憶體洩露”,然後使用執行緒池,執行緒結束後被放回執行緒池中而不銷燬,那麼如果這個執行緒一直不被使用或者分配使用了又不再呼叫get/set方法,那麼這個期間就會發生真正的記憶體洩露。因此,最好的做法是:在不使用該ThreadLocal物件時,及時呼叫該物件的remove方法去移除ThreadLocal.ThreadLocalMap中的對應Entry。

9、什麼是死鎖(Deadlock)?如何分析和避免死鎖?

  死鎖是指兩個以上的執行緒永遠阻塞的情況,這種情況產生至少需要兩個以上的執行緒和兩個以上的資源。

  分析死鎖,我們需要檢視Java應用程式的執行緒轉儲。我們需要找出那些狀態為BLOCKED的執行緒和他們等待的資源。每個資源都有一個唯一的id,用這個id我們可以找出哪些執行緒已經擁有了它的物件鎖。下面列舉了一些JDK自帶的死鎖檢測工具:

(1). Jconsole:JDK自帶的圖形化介面工具,主要用於對 Java 應用程式做效能分析和調優。

           Jconsole死鎖.png-26.7kB

(2). Jstack:JDK自帶的命令列工具,主要用於執行緒Dump分析。

           這裡寫圖片描述

(3). VisualVM:JDK自帶的圖形化介面工具,主要用於對 Java 應用程式做效能分析和調優。

           VisualVM死鎖.jpg-240.8kB

10、什麼是Java Timer類?如何建立一個有特定時間間隔的任務?

  Timer是一個排程器,可以用於安排一個任務在未來的某個特定時間執行或週期性執行。TimerTask是一個實現了Runnable介面的抽象類,我們需要去繼承這個類來建立我們自己的定時任務並使用Timer去安排它的執行。

Timer timer = new Timer();
timer.schedule(new TimerTask() {
        public void run() {
            System.out.println("abc");
        }
}, 200000 , 1000);

11、什麼是執行緒池?如何建立一個Java執行緒池?

  一個執行緒池管理了一組工作執行緒,同時它還包括了一個用於放置等待執行的任務的佇列。執行緒池可以避免執行緒的頻繁建立與銷燬,降低資源的消耗,提高系統的反應速度。java.util.concurrent.Executors提供了幾個java.util.concurrent.Executor介面的實現用於建立執行緒池,其主要涉及四個角色:

  • 執行緒池:Executor
  • 工作執行緒:Worker執行緒,Worker的run()方法執行Job的run()方法
  • 任務Job:Runable和Callable
  • 阻塞佇列:BlockingQueue

               執行緒池的處理流程.jpg-25.3kB

1). 執行緒池Executor

  Executor及其實現類是使用者級的執行緒排程器,也是對任務執行機制的抽象,其將任務的提交與任務的執行分離開來,核心實現類包括ThreadPoolExecutor(用來執行被提交的任務)和ScheduledThreadPoolExecutor(可以在給定的延遲後執行任務或者週期性執行任務)。Executor的實現繼承鏈條為:(父介面)Executor -> (子介面)ExecutorService -> (實現類)[ ThreadPoolExecutor + ScheduledThreadPoolExecutor ]。

2). 任務Runable/Callable

  Runnable(run)和Callable(call)都是對任務的抽象,但是Callable可以返回任務執行的結果或者丟擲異常。

3). 任務執行狀態Future

  Future是對任務執行狀態和結果的抽象,核心實現類是furtureTask (所以它既可以作為Runnable被執行緒執行,又可以作為Future得到Callable的返回值) ;

           FutureAPI.png-9.4kB

(1). 使用Callable+Future獲取執行結果

    ExecutorService executor = Executors.newCachedThreadPool();
    Task task = new Task();
    Future<Integer> result = executor.submit(task);
    System.out.println("task執行結果" + 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獲取執行結果

    ExecutorService executor = Executors.newCachedThreadPool();
     Task task = new Task();
     FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
     executor.submit(futureTask);  
    System.out.println("task執行結果"+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

  用於建立使用固定執行緒數的ThreadPool,corePoolSize = maximumPoolSize = n(固定的含義),阻塞佇列為LinkedBlockingQueue。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

(2). SingleThreadExecutor

  用於建立一個單執行緒的執行緒池,corePoolSize = maximumPoolSize = 1,阻塞佇列為LinkedBlockingQueue。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

(3). CachedThreadPool

  用於建立一個可快取的執行緒池,corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE,阻塞佇列為SynchronousQueue(沒有容量的阻塞佇列,每個插入操作必須等待另一個執行緒對應的移除操作,反之亦然)。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

(4). ScheduledThreadPoolExecutor

  用於建立一個大小無限的執行緒池,此執行緒池支援定時以及週期性執行任務的需求。

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

5). 執行緒池的飽和策略

  當阻塞佇列滿了,且沒有空閒的工作執行緒,如果繼續提交任務,必須採取一種策略處理該任務,執行緒池提供了4種策略:

  • AbortPolicy:直接丟擲異常,預設策略;

  • CallerRunsPolicy:用呼叫者所在的執行緒來執行任務;

  • DiscardOldestPolicy:丟棄阻塞佇列中最老的任務,並執行當前任務;

  • DiscardPolicy:直接丟棄任務;

      當然也可以根據應用場景實現RejectedExecutionHandler介面,自定義飽和策略,如記錄日誌或持久化儲存不能處理的任務。

6). 執行緒池調優

  • 設定最大執行緒數,防止執行緒資源耗盡;

  • 使用有界佇列,從而增加系統的穩定性和預警能力(飽和策略);

  • 根據任務的性質設定執行緒池大小:CPU密集型任務(CPU個數個執行緒),IO密集型任務(CPU個數兩倍的執行緒),混合型任務(拆分)。

12、CAS : CAS自旋volatile變數,是一種很經典的用法。

  CAS,Compare and Swap即比較並交換,設計併發演算法時常用到的一種技術。CAS有3個運算元,記憶體值V,舊的預期值A,新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。CAS是通過unsafe類的compareAndSwap (JNI, Java Native Interface) 方法實現的,該方法包括四個引數:第一個引數是要修改的物件,第二個引數是物件中要修改變數的偏移量,第三個引數是修改之前的值,第四個引數是預想修改後的值。

  CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、迴圈時間長開銷大和只能保證一個共享變數的原子操作。

  • ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

  • 不適用於競爭激烈的情形中:併發越高,失敗的次數會越多,CAS如果長時間不成功,會極大的增加CPU的開銷。因此CAS不適合競爭十分頻繁的場景。

  • 只能保證一個共享變數的原子操作:當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,因此可以把多個變數放在一個物件裡來進行CAS操作。

13、AQS : 佇列同步器

  佇列同步器(AbstractQueuedSynchronizer)是用來構建鎖和其他同步元件的基礎框架,技術是 CAS自旋Volatile變數:它使用了一個Volatile成員變量表示同步狀態,通過CAS修改該變數的值,修改成功的執行緒表示獲取到該鎖;若沒有修改成功,或者發現狀態state已經是加鎖狀態,則通過一個Waiter物件封裝執行緒,新增到等待佇列中,並掛起等待被喚醒。

  同步器是實現鎖的關鍵,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,利用同步器實現鎖的語義。特別地,鎖是面向鎖使用者的,它定義了使用者與鎖互動的介面,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了鎖的使用者與鎖的實現者所需關注的領域。

  一般來說,自定義同步器要麼是獨佔方式,要麼是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支援自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。

  同步器的設計是基於 模板方法模式 的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器提供的模板方法,而這些模板方法將會呼叫使用者重寫的方法。

           AQS.png-26.4kB

  AQS維護了一個volatile int state(代表共享資源)和一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列)。這裡volatile是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:getState()、setState()以及compareAndSetState()。

  AQS定義了兩種資源共享方式:Exclusive(獨佔,只有一個執行緒能執行,如ReentrantLock)和Share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch)。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

  • isHeldExclusively():該執行緒是否正在獨佔資源。只有用到condition才需要去實現它;

  • tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false;

  • tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false;

  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源;

  • tryReleaseShared(int):共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

      以ReentrantLock為例,state初始化為0,表示未鎖定狀態。A執行緒lock()時,會呼叫tryAcquire()獨佔該鎖並將state+1。此後,其他執行緒再tryAcquire()時就會失敗,直到A執行緒unlock()到state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖。當然,釋放鎖之前,A執行緒自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。

14、Java Concurrency API中的Lock介面(Lock interface)是什麼?對比同步它有什麼優勢?

  synchronized是Java的關鍵字,是Java的內建特性,在JVM層面實現了對臨界資源的同步互斥訪問。Synchronized的語義底層是通過一個monitor物件來完成的,執行緒執行monitorenter/monitorexit指令完成鎖的獲取與釋放。而Lock是一個Java介面(API如下圖所示),是基於JDK層面實現的,通過這個介面可以實現同步訪問,它提供了比synchronized關鍵字更靈活、更廣泛、粒度更細的鎖操作,底層是由AQS實現的。二者之間的差異總結如下:

  • 實現層面:synchronized(JVM層面)、Lock(JDK層面)

  • 響應中斷:Lock 可以讓等待鎖的執行緒響應中斷,而使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;

  • 立即返回:可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間,而synchronized卻無法辦到;

  • 讀寫鎖:Lock可以提高多個執行緒進行讀操作的效率

  • 可實現公平鎖:Lock可以實現公平鎖,而sychronized天生就是非公平鎖

  • 顯式獲取和釋放:synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;

               LOCK API.png-36.5kB

15、Condition

  Condition可以用來實現執行緒的分組通訊與協作。以生產者/消費者問題為例,

  • wait/notify/notifyAll:在佇列為空時,通知所有執行緒;在佇列滿時,通知所有執行緒,防止生產者通知生產者,消費者通知消費者的情形產生。

  • await/signal/signalAll:將執行緒分為消費者執行緒和生產者執行緒兩組:在佇列為空時,通知生產者執行緒生產;在佇列滿時,通知消費者執行緒消費。

               Condition.png-66.2kB

16、什麼是阻塞佇列?如何使用阻塞佇列來實現生產者-消費者模型?

  java.util.concurrent.BlockingQueue的特性是:當佇列是空的時,從佇列中獲取或刪除元素的操作將會被阻塞,或者當佇列是滿時,往佇列裡新增元素的操作會被阻塞。特別地,阻塞佇列不接受空值,當你嘗試向佇列中新增空值的時候,它會丟擲NullPointerException。另外,阻塞佇列的實現都是執行緒安全的,所有的查詢方法都是原子的並且使用了內部鎖或者其他形式的併發控制。

  BlockingQueue 介面是java collections框架的一部分,它主要用於實現生產者-消費者問題。特別地,SynchronousQueue是一個沒有容量的阻塞佇列,每個插入操作必須等待另一個執行緒的對應移除操作,反之亦然。CachedThreadPool使用SynchronousQueue把主執行緒提交的任務傳遞給空閒執行緒執行。

17、同步容器(強一致性)

  同步容器指的是 Vector、Stack、HashTable及Collections類中提供的靜態工廠方法建立的類。其中,Vector實現了List介面,Vector實際上就是一個數組,和ArrayList類似,但是Vector中的方法都是synchronized方法,即進行了同步措施;Stack也是一個同步容器,它的方法也用synchronized進行了同步,它實際上是繼承於Vector類;HashTable實現了Map介面,它和HashMap很相似,但是HashTable進行了同步處理,而HashMap沒有。

  Collections類是一個工具提供類,注意,它和Collection不同,Collection是一個頂層的介面。在Collections類中提供了大量的方法,比如對集合或者容器進行排序、查詢等操作。最重要的是,在它裡面提供了幾個靜態工廠方法來建立同步容器類,如下圖所示:

           同步容器.png-145.7kB

18、什麼是CopyOnWrite容器(弱一致性)?

  CopyOnWrite容器即寫時複製的容器,適用於讀操作遠多於修改操作的併發場景中。通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡新增元素,新增完元素之後,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

  從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器主要存在兩個弱點:

  • 容器物件的複製需要一定的開銷,如果物件佔用記憶體過大,可能造成頻繁的YoungGC和Full GC;

  • CopyOnWriteArrayList不能保證資料實時一致性,只能保證最終一致性。

19、ConcurrentHashMap (弱一致性)

  ConcurrentHashMap的弱一致性主要是為了提升效率,也是一致性與效率之間的一種權衡。要成為強一致性,就得到處使用鎖,甚至是全域性鎖,這就與Hashtable和同步的HashMap一樣了。ConcurrentHashMap的弱一致性主要體現在以下幾方面:

  • get操作是弱一致的:get操作只能保證一定能看到已完成的put操作;

               ConcurrentHashMap的弱一致性.png-136.8kB

  • clear操作是弱一致的:在清除完一個segments之後,正在清理下一個segments的時候,已經清理的segments可能又被加入了資料,因此clear返回的時候,ConcurrentHashMap中是可能存在資料的。
public void clear() {
    for (int i = 0; i < segments.length; ++i)
        segments[i].clear();
}
  • ConcurrentHashMap中的迭代操作是弱一致的(未遍歷的內容發生變化可能會反映出來):在遍歷過程中,如果已經遍歷的陣列上的內容變化了,迭代器不會丟擲ConcurrentModificationException異常。如果未遍歷的陣列上的內容發生了變化,則有可能反映到迭代過程中。

20、happens-before

  happens-before 指定了兩個操作間的執行順序:如果 A happens before B,那麼Java記憶體模型將向程式設計師保證 —— A 的執行順序排在 B 之前,並且 A 操作的結果將對 B 可見,其具體包括如下8條規則:

  • 程式順序規則:單執行緒內,按照程式程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作;

  • 管程鎖定規則:一個unlock操作先行發生於對同一個鎖的lock操作;

  • volatile變數規則:對一個Volatile變數的寫操作先行發生於對這個變數的讀操作;

  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的其他動作;

  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;

  • 執行緒終止規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;

  • 物件終結規則:一個物件的初始化完成先行發生於它的finalize()方法的開始;

  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;

21、鎖優化技術

  鎖優化技術的目的在於執行緒之間更高效的共享資料,解決競爭問題,更好提高程式執行效率。

  • 自旋鎖(上下文切換代價大):互斥鎖 -> 阻塞 –> 釋放CPU,執行緒上下文切換代價較大 + 共享變數的鎖定時間較短 == 讓執行緒通過自旋等一會兒,自旋鎖

  • 鎖粗化(一個大鎖優於若干小鎖):一系列連續操作對同一物件的反覆頻繁加鎖/解鎖會導致不必要的效能損耗,建議粗化鎖
    一般而言,同步範圍越小越好,這樣便於其他執行緒儘快拿到鎖,但仍然存在特例。

  • 偏向鎖(有鎖但當前情形不存在競爭):消除資料在無競爭情況下的同步原語,提高帶有同步但無競爭的程式效能。

  • 鎖消除(有鎖但不存在競爭,鎖多餘):JVM編譯優化,將不存在資料競爭的鎖消除

22、主執行緒等待子執行緒執行完畢再執行的方法

(1). Join

  Thread提供了讓一個執行緒等待另一個執行緒完成的方法 — join()方法。當在某個程式執行流程中呼叫其它執行緒的join()方法時,呼叫執行緒將被阻塞,直到被join()方法加入的join執行緒執行完畢為止,在繼續執行。join()方法的實現原理是不停檢查join執行緒是否存活,如果join執行緒存活則讓當前執行緒永遠等待。直到join執行緒完成後,執行緒的this.notifyAll()方法會被呼叫。

(2). CountDownLatch

  Countdown Latch允許一個或多個執行緒等待其他執行緒完成操作。CountDownLatch的建構函式接收一個int型別的引數作為計數器,如果你想等待N個點完成,這裡就傳入N。當我們呼叫countDown方法時,N就會減1,await方法會阻塞當前執行緒,直到N變成0。這裡說的N個點,可以使用N個執行緒,也可以是1個執行緒裡的N個執行步驟。

           CountDownLatch的使用.png-53.9kB

(3). Sleep

  用sleep方法,讓主執行緒睡眠一段時間,當然這個睡眠時間是主觀的時間,是我們自己定的,這個方法不推薦,但是在這裡還是寫一下,畢竟是解決方法。

引用: