java 多線程總結篇4——鎖機制

在開發Java多線程應用程序中,各個線程之間因爲要共享資源,必須用到鎖機制。Java提供了多種多線程鎖機制的實現方式,常見的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每種機制都有優缺點與各自的適用場景,必須熟練掌握他們的特色才能在Java多線程應用開發時駕輕就熟。——《Java鎖機制詳解》html

線程同步有關的類圖關係可用如下的圖總結:java

一、Java Concurrency API 中的 Lock 接口是什麼?對比同步它有什麼優點?c++

Lock接口比同步方法和同步塊(這裏的同步就是考察Synchronized關鍵字)提供了更具擴展性的鎖操做。Lock不是Java語言內置的,synchronized是Java語言的關鍵字,所以是內置特性,Lock是一個類,經過這個類能夠實現同步訪問;他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。它的優點有:可使鎖更公平;可使線程在等待鎖的時候響應中斷;可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間;能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖。redis

關於API及代碼的例子請移步:《java併發編程Lock》。經常使用接口方法以下:
算法

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

首先lock()方法是日常使用得最多的一個方法,就是用來獲取鎖。若是鎖已被其餘線程獲取,則進行等待。因爲在前面講到若是採用Lock,必須主動去釋放鎖,而且在發生異常時,不會自動釋放鎖。所以通常來講,使用Lock必須在try{}catch{}塊中進行,而且將釋放鎖的操做放在finally塊中進行,以保證鎖必定被被釋放,防止死鎖的發生。一般使用Lock來進行同步的話,是如下面這種形式去使用的:數據庫

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待。tryLock(long time, TimeUnit unit)方法和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。編程

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //若是不能獲取鎖,則直接作其餘事情
}

lockInterruptibly()方法比較特殊,當經過這個方法去獲取鎖時,若是線程正在等待獲取鎖,則這個線程可以響應中斷,即中斷線程的等待狀態。也就使說,當兩個線程同時經過lock.lockInterruptibly()想獲取某個鎖時,倘若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法可以中斷線程B的等待過程。因爲lockInterruptibly()的聲明中拋出了異常,因此lock.lockInterruptibly()必須放在try塊中或者在調用lockInterruptibly()的方法外聲明拋出InterruptedException。緩存

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }

ReentrantLock,意思是「可重入鎖」,ReentrantLock是惟一實現了Lock接口的類,而且ReentrantLock提供了更多的方法。如下給出一個ReentrantLock的運行實例:安全

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意這個地方,聲明爲類的屬性
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         //能夠用Java箭頭函數特性改寫上述冗餘代碼:
         // new Thread(){()->Thread.currentThread}.start();
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"獲得了鎖");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"釋放了鎖");
            lock.unlock();
        }
    }

上文中提到了Lock接口以及對象,使用它,很優雅的控制了競爭資源的安全訪問,可是這種鎖不區分讀寫,稱這種鎖爲普通鎖。爲了提升性能,Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,若是沒有寫鎖的狀況下,讀是無阻塞的,在必定程度上提升了程序的執行效率。Java中讀寫鎖有個接口java.util.concurrent.locks. ReadWriteLock,也有具體的實現ReentrantReadWriteLock,於是會有下面的提問:數據結構

二、ReadWriteLock是什麼?

讀寫鎖:分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm本身控制的,咱們只要上好相應的鎖便可。若是你的代碼只讀數據,能夠不少人同時讀,但不能同時寫,那就上讀鎖;若是你的代碼修改數據,只能有一我的在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!讀寫鎖接口:ReadWriteLock,它的具體實現類爲:ReentrantReadWriteLock

《ReadWriteLock場景應用》:在多線程的環境下,對同一份數據進行讀寫,會涉及到線程安全的問題。好比在一個線程讀取數據的時候,另一個線程在寫數據,而致使先後數據的不一致性;一個線程在寫數據的時候,另外一個線程也在寫,一樣也會致使線程先後看到的數據的不一致性。這時候能夠在讀寫方法中加入互斥鎖,任什麼時候候只能容許一個線程的一個讀或寫操做,而不容許其餘線程的讀或寫操做,這樣是能夠解決這樣以上的問題,可是效率卻大打折扣了。由於在真實的業務場景中,一份數據,讀取數據的操做次數一般高於寫入數據的操做,而線程與線程間的讀讀操做是不涉及到線程安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就好了。API調用請移步

三、鎖機制有什麼用

有些業務邏輯在執行過程當中要求對數據進行排他性的訪問,因而須要經過一些機制保證在此過程當中數據被鎖住不會被外界修改,這就是所謂的鎖機制。

四、什麼是樂觀鎖(Optimistic Locking)?如何實現樂觀鎖?如何避免ABA問題

悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量,像數據庫若是提供相似於write_condition機制的其實都是提供的樂觀鎖。

五、解釋如下名詞:重排序,自旋鎖,偏向鎖,輕量級鎖,可重入鎖,公平鎖,非公平鎖,樂觀鎖,悲觀鎖

重入鎖(ReentrantLock是一種遞歸無阻塞的同步機制。重入鎖,也叫作遞歸鎖,指的是同一線程 外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖。

自旋鎖,因爲自旋鎖使用者通常保持鎖時間很是短,所以選擇自旋而不是睡眠是很是必要的,自旋鎖的效率遠高於互斥鎖。如何旋轉呢?何爲自旋鎖,就是若是發現鎖定了,不是睡眠等待,而是採用讓當前線程不停地的在循環體內執行實現的,當循環的條件被其餘線程改變時 才能進入臨界區。

偏向鎖(Biased Locking)是Java6引入的一項多線程優化,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,同步鎖只有一個線程訪問,不存在多線程爭用的狀況,則線程是不須要觸發同步的,這種狀況下,就會給線程加一個偏向鎖。 若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。

重入鎖(ReentrantLock)是一種遞歸無阻塞的同步機制,也叫作遞歸鎖,指的是同一線程 外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。 在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖。

公平鎖,就是很公平,在併發環境中,每一個線程在獲取鎖時會先查看此鎖維護的等待隊列,若是爲空,或者當前線程線程是等待隊列的第一個,就佔有鎖,不然就會加入到等待隊列中,之後會按照FIFO的規則從隊列中取到本身

非公平鎖比較粗魯,上來就直接嘗試佔有鎖,若是嘗試失敗,就再採用相似公平鎖那種方式。

六、何時應該使用可重入鎖?

場景1:若是已加鎖,則再也不重複加鎖。a、忽略重複加鎖。b、用在界面交互時點擊執行較長時間請求操做時,防止屢次點擊致使後臺重複執行(忽略重複觸發)。以上兩種狀況多用於進行非重要任務防止重複執行,(如:清除無用臨時文件,檢查某些資源的可用性,數據備份操做等)

場景2:若是發現該操做已經在執行,則嘗試等待一段時間,等待超時則不執行(嘗試等待執行)這種其實屬於場景2的改進,等待得到鎖的操做有一個時間的限制,若是超時則放棄執行。用來防止因爲資源處理不當長時間佔用致使死鎖狀況(你們都在等待資源,致使線程隊列溢出)。

場景3:若是發現該操做已經加鎖,則等待一個一個加鎖(同步執行,相似synchronized)這種比較常見你們也都在用,主要是防止資源使用衝突,保證同一時間內只有一個操做可使用該資源。但與synchronized的明顯區別是性能優點(伴隨jvm的優化這個差距在減少)。同時Lock有更靈活的鎖定方式,公平鎖與不公平鎖,而synchronized永遠是公平的。這種狀況主要用於對資源的爭搶(如:文件操做,同步消息發送,有狀態的操做等)

場景4:可中斷鎖。synchronized與Lock在默認狀況下是不會響應中斷(interrupt)操做,會繼續執行完。lockInterruptibly()提供了可中斷鎖來解決此問題。(場景3的另外一種改進,沒有超時,只能等待中斷或執行完畢)這種狀況主要用於取消某些操做對資源的佔用。如:(取消正在同步運行的操做,來防止不正常操做長時間佔用形成的阻塞)

七、簡述鎖的等級方法鎖、對象鎖、類鎖

方法鎖(synchronized修飾方法時)經過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。synchronized 方法控制對類成員變量的訪問: 每一個類實例對應一把鎖,每一個 synchronized 方法都必須得到調用該方法的類實例的鎖方能執行,不然所屬線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能得到該鎖,從新進入可執行狀態。這種機制確保了同一時刻對於每個類實例,其全部聲明爲 synchronized 的成員函數中至多隻有一個處於可執行狀態,從而有效避免了類成員變量的訪問衝突。

對象鎖(synchronized修飾方法或代碼塊)當一個對象中有synchronized method或synchronized block的時候調用此對象的同步方法或進入其同步區域時,就必須先得到對象鎖。若是此對象的對象鎖已被其餘調用者佔用,則須要等待此鎖被釋放。(方法鎖也是對象鎖)。java的全部對象都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。線程進入synchronized方法的時候獲取該對象的鎖,固然若是已經有線程獲取了這個對象的鎖,那麼當前線程會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放對象鎖。這裏也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然能夠由JVM來自動釋放。 

類鎖(synchronized修飾靜態的方法或代碼塊),因爲一個class不論被實例化多少次,其中的靜態方法和靜態變量在內存中都只有一份。因此,一旦一個靜態的方法被申明爲synchronized。此類全部的實例化對象在調用此方法,共用同一把鎖,咱們稱之爲類鎖。對象鎖是用來控制實例方法之間的同步,類鎖是用來控制靜態方法(或靜態變量互斥體)之間的同步。類鎖只是一個概念上的東西,並非真實存在的,它只是用來幫助咱們理解鎖定實例方法和靜態方法的區別的。java類可能會有不少個對象,可是隻有1個Class對象,也就是說類的不一樣實例之間共享該類的Class對象。Class對象其實也僅僅是1個java對象,只不過有點特殊而已。因爲每一個java對象都有1個互斥鎖,而類的靜態方法是須要Class對象。因此所謂的類鎖,不過是Class對象的鎖而已。獲取類的Class對象有好幾種,最簡單的就是[類名.class]的方式。

八、Java中活鎖和死鎖有什麼區別?

死鎖:是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。死鎖發生的四個條件

一、互斥條件:線程對資源的訪問是排他性的,若是一個線程對佔用了某資源,那麼其餘線程必須處於等待狀態,直到資源被釋放。

二、請求和保持條件:線程T1至少已經保持了一個資源R1佔用,但又提出對另外一個資源R2請求,而此時,資源R2被其餘線程T2佔用,因而該線程T1也必須等待,但又對本身保持的資源R1不釋放。

三、不剝奪條件:線程已得到的資源,在未使用完以前,不能被其餘線程剝奪,只能在使用完之後由本身釋放。

四、環路等待條件:在死鎖發生時,必然存在一個「進程-資源環形鏈」,即:{p0,p1,p2,...pn},進程p0(或線程)等待p1佔用的資源,p1等待p2佔用的資源,pn等待p0佔用的資源。(最直觀的理解是,p0等待p1佔用的資源,而p1而在等待p0佔用的資源,因而兩個進程就相互等待)

活鎖:是指線程1可使用資源,但它很禮貌,讓其餘線程先使用資源,線程2也可使用資源,但它很紳士,也讓其餘線程先使用資源。這樣你讓我,我讓你,最後兩個線程都沒法使用資源。

九、如何確保 N 個線程能夠訪問 N 個資源同時又不致使死鎖?

預防死鎖,預先破壞產生死鎖的四個條件。互斥不可能破壞,因此有以下3種方法:

1.破壞,請求和保持條件1.1)進程等全部要請求的資源都空閒時才能申請資源,這種方法會使資源嚴重浪費(有些資源可能僅在運行初期或結束時才使用,甚至根本不使用)1.2)容許進程獲取初期所需資源後,便開始運行,運行過程當中再逐步釋放本身佔有的資源。好比有一個進程的任務是把數據複製到磁盤中再打印,前期只須要得到磁盤資源而不須要得到打印機資源,待複製完畢後再釋放掉磁盤資源。這種方法比上一種好,會使資源利用率上升。

2.破壞,不可搶佔條件。這種方法代價大,實現複雜

3.破壞,循壞等待條件。對各進程請求資源的順序作一個規定,避免相互等待。這種方法對資源的利用率比前兩種都高,可是前期要爲設備指定序號,新設備加入會有一個問題,其次對用戶編程也有限制

十、死鎖與飢餓的區別?

相同點:兩者都是因爲競爭資源而引發的。

不一樣點:

  • 從進程狀態考慮,死鎖進程都處於等待狀態,忙等待(處於運行或就緒狀態)的進程並不是處於等待狀態,但卻可能被餓死;
  • 死鎖進程等待永遠不會被釋放的資源,餓死進程等待會被釋放但卻不會分配給本身的資源,表現爲等待時限沒有上界(排隊等待或忙式等待);
  • 死鎖必定發生了循環等待,而餓死則否則。這也代表經過資源分配圖能夠檢測死鎖存在與否,但卻不能檢測是否有進程餓死;
  • 死鎖必定涉及多個進程,而飢餓或被餓死的進程可能只有一個。
  • 在飢餓的情形下,系統中有至少一個進程能正常運行,只是飢餓進程得不到執行機會。而死鎖則可能會最終使整個系統陷入死鎖並崩潰

十一、怎麼檢測一個線程是否擁有鎖?

java.lang.Thread中有一個方法叫holdsLock(),它返回true若是當且僅當當前線程擁有某個具體對象的鎖

Object o = new Object(); 

@Test 
public void test1() throws Exception { 

    new Thread(new Runnable() { 

        @Override 
        public void run() { 
            synchronized(o) { 
                System.out.println("child thread: holdLock: " +  
                    Thread.holdsLock(o)); 
            } 
        } 
    }).start(); 

    System.out.println("main thread: holdLock: " + Thread.holdsLock(o)); 
    Thread.sleep(2000); 

} 
main thread: holdLock: false
child thread: holdLock: true

十二、如何實現分佈式鎖?

基於數據庫實現分佈式鎖

基於緩存(redis,memcached,tair)實現分佈式鎖

基於Zookeeper實現分佈式鎖

能夠參考詳情《分佈式鎖的幾種實現方式》 、 《分佈式鎖的3種方式》

1三、有哪些無鎖數據結構,他們實現的原理是什麼?

java 1.5提供了一種無鎖隊列(wait-free/lock-free)ConcurrentLinkedQueue,可支持多個生產者多個消費者線程的環境:網上別人本身實現的一種無鎖算法隊列,原理和jdk官方的ConcurrentLinkedQueue類似:經過volatile關鍵字來保證數據惟一性(注:java的volatile和c++的volatile關鍵字是兩碼事!),可是裏面又用到atomic,感受有點boost::lockfree::queue的風格,估計參考了boost的代碼來編寫這個java無鎖隊列。

1四、Executors類是什麼? Executor和Executors的區別

正如上面所說,這三者均是 Executor 框架中的一部分。Java 開發者頗有必要學習和理解他們,以便更高效的使用 Java 提供的不一樣類型的線程池。總結一下這三者間的區別,以便你們更好的理解:

  • Executor 和 ExecutorService 這兩個接口主要的區別是:ExecutorService 接口繼承了 Executor 接口,是 Executor 的子接口
  • Executor 和 ExecutorService 第二個區別是:Executor 接口定義了 execute()方法用來接收一個Runnable接口的對象,而 ExecutorService 接口中的 submit()方法能夠接受Runnable和Callable接口的對象。
  • Executor 和 ExecutorService 接口第三個區別是 Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法能夠經過一個 Future 對象返回運算結果。
  • Executor 和 ExecutorService 接口第四個區別是除了容許客戶端提交一個任務,ExecutorService 還提供用來控制線程池的方法。好比:調用 shutDown() 方法終止線程池。能夠經過 《Java Concurrency in Practice》 一書瞭解更多關於關閉線程池和如何處理 pending 的任務的知識。
  • Executors 類提供工廠方法用來建立不一樣類型的線程池。好比: newSingleThreadExecutor() 建立一個只有一個線程的線程池,newFixedThreadPool(int numOfThreads)來建立固定線程數的線程池,newCachedThreadPool()能夠根據須要建立新的線程,但若是已有線程是空閒的會重用已有線程。
Executor ExecutorService
Executor 是 Java 線程池的核心接口,用來併發執行提交的任務 ExecutorService 是 Executor 接口的擴展,提供了異步執行和關閉線程池的方法
提供execute()方法用來提交任務 提供submit()方法用來提交任務
execute()方法無返回值 submit()方法返回Future對象,可用來獲取任務執行結果
不能取消任務 能夠經過Future.cancel()取消pending中的任務
沒有提供和關閉線程池有關的方法 提供了關閉線程池的方法

1六、什麼是Java線程轉儲(Thread Dump),如何獲得它?

線程轉儲是一個JVM活動線程的列表,它對於分析系統瓶頸和死鎖很是有用。

有不少方法能夠獲取線程轉儲——使用Profiler,Kill -3命令,jstack工具等等。我更喜歡jstack工具,由於它容易使用而且是JDK自帶的。因爲它是一個基於終端的工具,因此咱們能夠編寫一些腳本去定時的產生線程轉儲以待分析。

1七、如何在Java中獲取線程堆棧?

Java虛擬機提供了線程轉儲(thread dump)的後門,經過這個後門能夠把線程堆棧打印出來。一般咱們將堆棧信息重定向到一個文件中,便於咱們分析,因爲信息量太大,極可能超出控制檯緩衝區的最大行數限制形成信息丟失。這裏介紹一個jdk自帶的打印線程堆棧的工具,jstack用於打印出給定的Java進程ID或core file或遠程調試服務的Java堆棧信息。(Java問題定位之Java線程堆棧分析

示例:$jstack –l 23561 >> xxx.dump
命令 : $jstack [option] pid >> 文件 

>>表示輸出到文件尾部,實際運行中,每每一次dump的信息,還不足以確認問題,建議產生三次dump信息,若是每次dump都指向同一個問題,咱們才肯定問題的典型性。

1八、說出 3 條在 Java 中使用線程的最佳實踐

  • 給你的線程起個有意義的名字。這樣能夠方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor這種名字比Thread-1. Thread-2 and Thread-3好多了,給線程起一個和它要完成的任務相關的名字,全部的主要框架甚至JDK都遵循這個最佳實踐。
  • 避免鎖定和縮小同步的範圍鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。所以相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
  • 多用同步類少用wait和notify首先,CountDownLatch, Semaphore, CyclicBarrier和Exchanger這些同步類簡化了編碼操做,而用wait和notify很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。
  • 多用併發集合少用同步集合,這是另一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,因此在併發編程時使用併發集合效果更好。若是下一次你須要用到map,你應該首先想到用ConcurrentHashMap。

1九、【情景開放題】

實際項目中使用多線程舉例。你在多線程環境中遇到的常見的問題是什麼?你是怎麼解決它的?

 

請說出與線程同步以及線程調度相關的方法

 

程序中有3個 socket,須要多少個線程來處理

 

假若有一個第三方接口,有不少個線程去調用獲取數據,如今規定每秒鐘最多有 10 個線程同時調用它,如何作到

 

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

 

如何確保 main() 方法所在的線程是 Java 程序最後結束的線程

 

很是多個線程(多是不一樣機器),相互之間須要等待協調才能完成某種工做,問怎麼設計這種協調方案

 

你須要實現一個高效的緩存,它容許多個用戶讀,但只容許一個用戶寫,以此來保持它的完整性,你會怎樣去實現它

相關文章
相關標籤/搜索