鍍金池/ 教程/ Java/ 無阻塞算法
Slipped Conditions
阻塞隊列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競態(tài)條件與臨界區(qū)
多線程的優(yōu)點
CAS
線程通信
如何創(chuàng)建并運行 java 線程
阿姆達爾定律
避免死鎖
信號量
多線程的代價
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內存模型
線程安全與共享資源
Java 同步塊

無阻塞算法

在并發(fā)上下文中,非阻塞算法是一種允許線程在阻塞其他線程的情況下訪問共享狀態(tài)的算法。在絕大多數(shù)項目中,在算法中如果一個線程的掛起沒有導致其它的線程掛起,我們就說這個算法是非阻塞的。

為了更好的理解阻塞算法和非阻塞算法之間的區(qū)別,我會先講解阻塞算法然后再講解非阻塞算法。

阻塞并發(fā)算法

一個阻塞并發(fā)算法一般分下面兩步:

  • 執(zhí)行線程請求的操作
  • 阻塞線程直到可以安全地執(zhí)行操作

很多算法和并發(fā)數(shù)據(jù)結構都是阻塞的。例如,java.util.concurrent.BlockingQueue 的不同實現(xiàn)都是阻塞數(shù)據(jù)結構。如果一個線程要往一個阻塞隊列中插入一個元素,隊列中沒有足夠的空間,執(zhí)行插入操作的線程就會阻塞直到隊列中有了可以存放插入元素的空間。

下圖演示了一個阻塞算法保證一個共享數(shù)據(jù)結構的行為:

http://wiki.jikexueyuan.com/project/java-concurrent/images/18.png" alt="" />

非阻塞并發(fā)算法

一個非阻塞并發(fā)算法一般包含下面兩步:

  • 執(zhí)行線程請求的操作
  • 通知請求線程操作不能被執(zhí)行

Java 也包含幾個非阻塞數(shù)據(jù)結構。AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference 都是非阻塞數(shù)據(jù)結構的例子。

下圖演示了一個非阻塞算法保證一個共享數(shù)據(jù)結構的行為:

http://wiki.jikexueyuan.com/project/java-concurrent/images/19.png" alt="" />

非阻塞算法 vs 阻塞算法

阻塞算法和非阻塞算法的主要不同在于上面兩部分描述的它們的行為的第二步。換句話說,它們之間的不同在于當請求操作不能夠執(zhí)行時阻塞算法和非阻塞算法會怎么做。

阻塞算法會阻塞線程知道請求操作可以被執(zhí)行。非阻塞算法會通知請求線程操作不能夠被執(zhí)行,并返回。

一個使用了阻塞算法的線程可能會阻塞直到有可能去處理請求。通常,其它線程的動作使第一個線程執(zhí)行請求的動作成為了可能。 如果,由于某些原因線程被阻塞在程序某處,因此不能讓第一個線程的請求動作被執(zhí)行,第一個線程會阻塞——可能一直阻塞或者直到其他線程執(zhí)行完必要的動作。

例如,如果一個線程產(chǎn)生往一個已經(jīng)滿了的阻塞隊列里插入一個元素,這個線程就會阻塞,直到其他線程從這個阻塞隊列中取走了一些元素。如果由于某些原因,從阻塞隊列中取元素的線程假定被阻塞在了程序的某處,那么,嘗試往阻塞隊列中添加新元素的線程就會阻塞,要么一直阻塞下去,要么知道從阻塞隊列中取元素的線程最終從阻塞隊列中取走了一個元素。

非阻塞并發(fā)數(shù)據(jù)結構

在一個多線程系統(tǒng)中,線程間通常通過一些數(shù)據(jù)結構”交流“。例如可以是任何的數(shù)據(jù)結構,從變量到更高級的俄數(shù)據(jù)結構(隊列,棧等)。為了確保正確,并發(fā)線程在訪問這些數(shù)據(jù)結構的時候,這些數(shù)據(jù)結構必須由一些并發(fā)算法來保證。這些并發(fā)算法讓這些數(shù)據(jù)結構成為并發(fā)數(shù)據(jù)結構

如果某個算法確保一個并發(fā)數(shù)據(jù)結構是阻塞的,它就被稱為是一個阻塞算法。這個數(shù)據(jù)結構也被稱為是一個阻塞,并發(fā)數(shù)據(jù)結構。

如果某個算法確保一個并發(fā)數(shù)據(jù)結構是非阻塞的,它就被稱為是一個非阻塞算法。這個數(shù)據(jù)結構也被稱為是一個非阻塞,并發(fā)數(shù)據(jù)結構。

每個并發(fā)數(shù)據(jù)結構被設計用來支持一個特定的通信方法。使用哪種并發(fā)數(shù)據(jù)結構取決于你的通信需要。在接下里的部分,我會引入一些非阻塞并發(fā)數(shù)據(jù)結構,并講解它們各自的適用場景。通過這些并發(fā)數(shù)據(jù)結構工作原理的講解應該能在非阻塞數(shù)據(jù)結構的設計和實現(xiàn)上一些啟發(fā)。

Volatile 變量

Java 中的 volatile 變量是直接從主存中讀取值的變量。當一個新的值賦給一個 volatile 變量時,這個值總是會被立即寫回到主存中去。這樣就確保了,一個 volatile 變量最新的值總是對跑在其他 CPU 上的線程可見。其他線程每次會從主存中讀取變量的值,而不是比如線程所運行 CPU 的 CPU 緩存中。

colatile 變量是非阻塞的。修改一個 volatile 變量的值是一耳光原子操作。它不能夠被中斷。不過,在一個 volatile 變量上的一個 read-update-write 順序的操作不是原子的。因此,下面的代碼如果由多個線程執(zhí)行可能導致競態(tài)條件。

volatile myVar = 0;
...
int temp = myVar;
temp++;
myVar = temp;

首先,myVar 這個 volatile 變量的值被從主存中讀出來賦給了 temp 變量。然后,temp 變量自增 1。然后,temp 變量的值又賦給了 myVar 這個 volatile 變量這意味著它會被寫回到主存中。

如果兩個線程執(zhí)行這段代碼,然后它們都讀取 myVar 的值,加 1 后,把它的值寫回到主存。這樣就存在 myVar 僅被加 1,而沒有被加 2 的風險。

你可能認為你不會寫像上面這樣的代碼,但是在實踐中上面的代碼等同于如下的代碼:

myVar++;

執(zhí)行上面的代碼時,myVar 的值讀到一個 CPU 寄存器或者一個本地 CPU 緩存中,myVar 加 1,然后這個 CPU 寄存器或者 CPU 緩存中的值被寫回到主存中。

單個寫線程的情景

在一些場景下,你僅有一個線程在向一個共享變量寫,多個線程在讀這個變量。當僅有一個線程在更新一個變量,不管有多少個線程在讀這個變量,都不會發(fā)生競態(tài)條件。因此,無論時候當僅有一個線程在寫一個共享變量時,你可以把這個變量聲明為 volatile。

當多個線程在一個共享變量上執(zhí)行一個 read-update-write 的順序操作時才會發(fā)生競態(tài)條件。如果你只有一個線程在執(zhí)行一個 raed-update-write 的順序操作,其他線程都在執(zhí)行讀操作,將不會發(fā)生競態(tài)條件。

下面是一個單個寫線程的例子,它沒有采取同步手段但任然是并發(fā)的。

public class SingleWriterCounter{
    private volatile long count = 0;

    /**
     *Only one thread may ever call this method
     *or it will lead to race conditions
     */
     public void inc(){
         this.count++;
     }

     /**
      *Many reading threads may call this method
      *@return
      */
      public long count(){
          return this.count;
      }
}

多個線程訪問同一個 Counter 實例,只要僅有一個線程調用 inc()方法,這里,我不是說在某一時刻一個線程,我的意思是,僅有相同的,單個的線程被允許去調用 inc()>方法。多個線程可以調用 count()方法。這樣的場景將不會發(fā)生任何競態(tài)條件。

下圖,說明了線程是如何訪問 count 這個 volatile 變量的。

http://wiki.jikexueyuan.com/project/java-concurrent/images/20.png" alt="" />

基于 volatile 變量更高級的數(shù)據(jù)結構

使用多個 volatile 變量去創(chuàng)建數(shù)據(jù)結構是可以的,構建出的數(shù)據(jù)結構中每一個 volatile 變量僅被一個單個的線程寫,被多個線程讀。每個 volatile 變量可能被一個不同的線程寫(但僅有一個)。使用像這樣的數(shù)據(jù)結構多個線程可以使用這些 volatile 變量以一個非阻塞的方法彼此發(fā)送信息。

下面是一個簡單的例子:

public class DoubleWriterCounter{
    private volatile long countA = 0;
    private volatile long countB = 0;

    /**
     *Only one (and the same from thereon) thread may ever call this method,
     *or it will lead to race conditions.
     */
     public void incA(){
         this.countA++;
     }

     /**
      *Only one (and the same from thereon) thread may ever call this method, 
      *or it will  lead to race conditions.
      */
      public void incB(){
          this.countB++;
      }

      /**
       *Many reading threads may call this method
       */
      public long countA(){
          return this.countA;
      }

     /**
      *Many reading threads may call this method
      */
      public long countB(){
          return this.countB;
      }
}

如你所見,DoubleWriterCoounter 現(xiàn)在包含兩個 volatile 變量以及兩對自增和讀方法。在某一時刻,僅有一個單個的線程可以調用 inc(),僅有一個單個的線程可以訪問 incB()。不過不同的線程可以同時調用 incA()和 incB()。countA()和 countB()可以被多個線程調用。這將不會引發(fā)競態(tài)條件。

DoubleWriterCoounter 可以被用來比如線程間通信。countA 和 countB 可以分別用來存儲生產(chǎn)的任務數(shù)和消費的任務數(shù)。下圖,展示了兩個線程通過類似于上面的一個數(shù)據(jù)結構進行通信的。

http://wiki.jikexueyuan.com/project/java-concurrent/images/21.png" alt="" />

聰明的讀者應該已經(jīng)意識到使用兩個 SingleWriterCounter 可以達到使用 DoubleWriterCoounter 的效果。如果需要,你甚至可以使用多個線程和 SingleWriterCounter 實例。

使用 CAS 的樂觀鎖

如果你確實需要多個線程區(qū)寫同一個共享變量,volatile 變量是不合適的。你將會需要一些類型的排它鎖(悲觀鎖)訪問這個變量。下面代碼演示了使用 Java 中的同步塊進行排他訪問的。

public class SynchronizedCounter{
    long count = 0;

    public void inc(){
        synchronized(this){
            count++;
        }
    }

    public long count(){
        synchronized(this){
            return this.count;
        }
    }
}

注意,inc()和 count()方法都包含一個同步塊。這也是我們像避免的東西——同步塊和 wait()-notify 調用等。

我們可以使用一種 Java 的原子變量來代替這兩個同步塊。在這個例子是 AtomicLong。下面是 SynchronizedCounter 類的 AtomicLong 實現(xiàn)版本。

import java.util.concurrent.atomic.AtomicLong;

public class AtomicLong{
    private AtomicLong count = new AtomicLong(0);

    public void inc(){
        boolean updated = false;
        while(!updated){
            long prevCount = this.count.get();
            updated = this.count.compareAndSet(prevCount, prevCount + 1);
        }
    }

    public long count(){
        return this.count.get();
    }
}

這個版本僅僅是上一個版本的線程安全版本。這一版我們感興趣的是 inc()方法的實現(xiàn)。inc()方法中不再含有一個同步塊。而是被下面這些代碼替代:

boolean updated = false;
while(!updated){
    long prevCount = this.count.get();
    updated = this.count.compareAndSet(prevCount, prevCount + 1);
}

上面這些代碼并不是一個原子操作。也就是說,對于兩個不同的線程去調用 inc()方法,然后執(zhí)行 long prevCount = this.count.get()語句,因此獲得了這個計數(shù)器的上一個 count。但是,上面的代碼并沒有包含任何的競態(tài)條件。

秘密就在于 while 循環(huán)里的第二行代碼。compareAndSet()方法調用是一個原子操作。它用一個期望值和 AtomicLong 內部的值去比較,如果這兩個值相等,就把 AtomicLong 內部值替換為一個新值。compareAndSet()通常被 CPU 中的 compare-and-swap 指令直接支持。因此,不需要去同步,也不需要去掛起線程。

假設,這個 AtomicLong 的內部值是 20。然后,兩個線程去讀這個值,都嘗試調用 compareAndSet(20, 20 + 1)。盡管 compareAndSet()是一個原子操作,這個方法也會被這兩個線程相繼執(zhí)行(某一個時刻只有一個)。

第一個線程會使用期望值 20(這個計數(shù)器的上一個值)與 AtomicLong 的內部值進行比較。由于兩個值是相等的,AtomicLong 會更新它的內部值至 21(20 + 1 )。變量 updated 被修改為 true,while 循環(huán)結束。

現(xiàn)在,第二個線程調用 compareAndSet(20, 20 + 1)。由于 AtomicLong 的內部值不再是 20,這個調用將不會成功。AtomicLong 的值不會再被修改為 21。變量,updated 被修改為 false,線程將會再次在 while 循環(huán)外自旋。這段時間,它會讀到值 21 并企圖把值更新為 22。如果在此期間沒有其它線程調用 inc()。第二次迭代將會成功更新 AtomicLong 的內部值到 22。

為什么稱它為樂觀鎖

上一部分展現(xiàn)的代碼被稱為樂觀鎖(optimistic locking)。樂觀鎖區(qū)別于傳統(tǒng)的鎖,有時也被稱為悲觀鎖。傳統(tǒng)的鎖會使用同步塊或其他類型的鎖阻塞對臨界區(qū)域的訪問。一個同步塊或鎖可能會導致線程掛起。

樂觀鎖允許所有的線程在不發(fā)生阻塞的情況下創(chuàng)建一份共享內存的拷貝。這些線程接下來可能會對它們的拷貝進行修改,并企圖把它們修改后的版本寫回到共享內存中。如果沒有其它線程對共享內存做任何修改, CAS 操作就允許線程將它的變化寫回到共享內存中去。如果,另一個線程已經(jīng)修改了共享內存,這個線程將不得不再次獲得一個新的拷貝,在新的拷貝上做出修改,并嘗試再次把它們寫回到共享內存中去。

稱之為“樂觀鎖”的原因就是,線程獲得它們想修改的數(shù)據(jù)的拷貝并做出修改,在樂觀的假在此期間沒有線程對共享內存做出修改的情況下。當這個樂觀假設成立時,這個線程僅僅在無鎖的情況下完成共享內存的更新。當這個假設不成立時,線程所做的工作就會被丟棄,但任然不使用鎖。

樂觀鎖使用于共享內存競用不是非常高的情況。如果共享內存上的內容非常多,僅僅因為更新共享內存失敗,就用浪費大量的 CPU 周期用在拷貝和修改上。但是,如果砸共享內存上有大量的內容,無論如何,你都要把你的代碼設計的產(chǎn)生的爭用更低。

樂觀鎖是非阻塞的

我們這里提到的樂觀鎖機制是非阻塞的。如果一個線程獲得了一份共享內存的拷貝,當嘗試修改時,發(fā)生了阻塞,其它線程去訪問這塊內存區(qū)域不會發(fā)生阻塞。

對于一個傳統(tǒng)的加鎖/解鎖模式,當一個線程持有一個鎖時,其它所有的線程都會一直阻塞直到持有鎖的線程再次釋放掉這個鎖。如果持有鎖的這個線程被阻塞在某處,這個鎖將很長一段時間不能被釋放,甚至可能一直不能被釋放。

不可替換的數(shù)據(jù)結構

簡單的 CAS 樂觀鎖可以用于共享數(shù)據(jù)結果,這樣一來,整個數(shù)據(jù)結構都可以通過一個單個的 CAS 操作被替換成為一個新的數(shù)據(jù)結構。盡管,使用一個修改后的拷貝來替換真?zhèn)€數(shù)據(jù)結構并不總是可行的。

假設,這個共享數(shù)據(jù)結構是隊列。每當線程嘗試從向隊列中插入或從隊列中取出元素時,都必須拷貝這個隊列然后在拷貝上做出期望的修改。我們可以通過使用一個 AtomicReference 來達到同樣的目的??截愐茫截惡托薷年犃?,嘗試替換在 AtomicReference 中的引用讓它指向新創(chuàng)建的隊列。

然而,一個大的數(shù)據(jù)結構可能會需要大量的內存和 CPU 周期來復制。這會使你的程序占用大量的內存和浪費大量的時間再拷貝操作上。這將會降低你的程序的性能,特別是這個數(shù)據(jù)結構的競用非常高情況下。更進一步說,一個線程花費在拷貝和修改這個數(shù)據(jù)結構上的時間越長,其它線程在此期間修改這個數(shù)據(jù)結構的可能性就越大。如你所知,如果另一個線程修改了這個數(shù)據(jù)結構在它被拷貝后,其它所有的線程都不等不再次執(zhí)行 拷貝-修改 操作。這將會增大性能影響和內存浪費,甚至更多。

接下來的部分將會講解一種實現(xiàn)非阻塞數(shù)據(jù)結構的方法,這種數(shù)據(jù)結構可以被并發(fā)修改,而不僅僅是拷貝和修改。

共享預期的修改

用來替換拷貝和修改整個數(shù)據(jù)結構,一個線程可以共享它們對共享數(shù)據(jù)結構預期的修改。一個線程向對修改某個數(shù)據(jù)結構的過程變成了下面這樣:

  • 檢查是否另一個線程已經(jīng)提交了對這個數(shù)據(jù)結構提交了修改
  • 如果沒有其他線程提交了一個預期的修改,創(chuàng)建一個預期的修改,然后向這個數(shù)據(jù)結構提交預期的修
  • 執(zhí)行對共享數(shù)據(jù)結構的修改
  • 移除對這個預期的修改的引用,向其它線程發(fā)送信號,告訴它們這個預期的修改已經(jīng)被執(zhí)行

如你所見,第二步可以阻塞其他線程提交一個預期的修改。因此,第二步實際的工作是作為這個數(shù)據(jù)結構的一個鎖。如果一個線程已經(jīng)成功提交了一個預期的修改,其他線程就不可以再提交一個預期的修改直到第一個預期的修改執(zhí)行完畢。

如果一個線程提交了一個預期的修改,然后做一些其它的工作時發(fā)生阻塞,這時候,這個共享數(shù)據(jù)結構實際上是被鎖住的。其它線程可以檢測到它們不能夠提交一個預期的修改,然后回去做一些其它的事情。很明顯,我們需要解決這個問題。

可完成的預期修改

為了避免一個已經(jīng)提交的預期修改可以鎖住共享數(shù)據(jù)結構,一個已經(jīng)提交的預期修改必須包含足夠的信息讓其他線程來完成這次修改。因此,如果一個提交了預期修改的線程從未完成這次修改,其他線程可以在它的支持下完成這次修改,保證這個共享數(shù)據(jù)結構對其他線程可用。

下圖說明了上面描述的非阻塞算法的藍圖:

http://wiki.jikexueyuan.com/project/java-concurrent/images/22.png" alt="" />

修改必須被當做一個或多個 CAS 操作來執(zhí)行。因此,如果兩個線程嘗試去完成同一個預期修改,僅有一個線程可以所有的 CAS 操作。一旦一條 CAS 操作完成后,再次企圖完成這個 CAS 操作都不會“得逞”。

A-B-A 問題

上面演示的算法可以稱之為 A-B-A 問題。A-B-A 問題指的是一個變量被從 A 修改到了 B,然后又被修改回 A 的一種情景。其他線程對于這種情況卻一無所知。

如果線程 A 檢查正在進行的數(shù)據(jù)更新,拷貝,被線程調度器掛起,一個線程 B 在此期可能可以訪問這個共享數(shù)據(jù)結構。如果線程對這個數(shù)據(jù)結構執(zhí)行了全部的更新,移除了它的預期修改,這樣看起來,好像線程 A 自從拷貝了這個數(shù)據(jù)結構以來沒有對它做任何的修改。然而,一個修改確實已經(jīng)發(fā)生了。當線程 A 繼續(xù)基于現(xiàn)在已經(jīng)過期的數(shù)據(jù)拷貝執(zhí)行它的更新時,這個數(shù)據(jù)修改已經(jīng)被線程 B 的修改破壞。

下圖說明了上面提到的 A-B-A 問題:

http://wiki.jikexueyuan.com/project/java-concurrent/images/23.png" alt="" />

A-B-A 問題的解決方案

A-B-A 通常的解決方法就是不再僅僅替換指向一個預期修改對象的指針,而是指針結合一個計數(shù)器,然后使用一個單個的 CAS 操作來替換指針 + 計數(shù)器。這在支持指針的語言像 C 和 C++中是可行的。因此,盡管當前修改指針被設置回指向 “不是正在進行的修改”(no ongoing modification),指針 + 計數(shù)器的計數(shù)器部分將會被自增,使修改對其它線程是可見的。

在 Java 中,你不能將一個引用和一個計數(shù)器歸并在一起形成一個單個的變量。不過 Java 提供了 AtomicStampedReference 類,利用這個類可以使用一個 CAS 操作自動的替換一個引用和一個標記(stamp)。

一個非阻塞算法模板

下面的代碼意在在如何實現(xiàn)非阻塞算法上一些啟發(fā)。這個模板基于這篇教程所講的東西。

注意:在非阻塞算法方面,我并不是一位專家,所以,下面的模板可能錯誤。不要基于我提供的模板實現(xiàn)自己的非阻塞算法。這個模板意在給你一個關于非阻塞算法大致是什么樣子的一個 idea。如果,你想實現(xiàn)自己的非阻塞算法,首先學習一些實際的工業(yè)水平的非阻塞算法的時間,在實踐中學習更多關于非阻塞算法實現(xiàn)的知識。

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicStampedReference;

public class NonblockingTemplate{
    public static class IntendedModification{
        public AtomicBoolean completed = new AtomicBoolean(false);
    }

    private AtomicStampedReference<IntendedModification> ongoinMod = new AtomicStampedReference<IntendedModification>(null, 0);
    //declare the state of the data structure here.

    public void modify(){
        while(!attemptModifyASR());
    }

    public boolean attemptModifyASR(){
        boolean modified = false;

        IntendedMOdification currentlyOngoingMod = ongoingMod.getReference();
        int stamp = ongoingMod.getStamp();

        if(currentlyOngoingMod == null){
            //copy data structure - for use
            //in intended modification

            //prepare intended modification
            IntendedModification newMod = new IntendModification();

            boolean modSubmitted = ongoingMod.compareAndSet(null, newMod, stamp, stamp + 1);

            if(modSubmitted){
                 //complete modification via a series of compare-and-swap operations.
                //note: other threads may assist in completing the compare-and-swap
                // operations, so some CAS may fail
                modified = true;
            }
        }else{
             //attempt to complete ongoing modification, so the data structure is freed up
            //to allow access from this thread.
            modified = false;
        }

        return modified;
    }
}

非阻塞算法是不容易實現(xiàn)的

正確的設計和實現(xiàn)非阻塞算法是不容易的。在嘗試設計你的非阻塞算法之前,看一看是否已經(jīng)有人設計了一種非阻塞算法正滿足你的需求。

Java 已經(jīng)提供了一些非阻塞實現(xiàn)(比如 ConcurrentLinkedQueue),相信在 Java 未來的版本中會帶來更多的非阻塞算法的實現(xiàn)。

除了 Java 內置非阻塞數(shù)據(jù)結構還有很多開源的非阻塞數(shù)據(jù)結構可以使用。例如,LAMX Disrupter 和 Cliff Click 實現(xiàn)的非阻塞 HashMap。查看我的 Java concurrency references page 查看更多的資源。

使用非阻塞算法的好處

非阻塞算法和阻塞算法相比有幾個好處。下面讓我們分別看一下:

選擇

非阻塞算法的第一個好處是,給了線程一個選擇當它們請求的動作不能夠被執(zhí)行時做些什么。不再是被阻塞在那,請求線程關于做什么有了一個選擇。有時候,一個線程什么也不能做。在這種情況下,它可以選擇阻塞或自我等待,像這樣把 CPU 的使用權讓給其它的任務。不過至少給了請求線程一個選擇的機會。

在一個單個的 CPU 系統(tǒng)可能會掛起一個不能執(zhí)行請求動作的線程,這樣可以讓其它線程獲得 CPU 的使用權。不過即使在一個單個的 CPU 系統(tǒng)阻塞可能導致死鎖,線程饑餓等并發(fā)問題。

沒有死鎖

非阻塞算法的第二個好處是,一個線程的掛起不能導致其它線程掛起。這也意味著不會發(fā)生死鎖。兩個線程不能互相彼此等待來獲得被對方持有的鎖。因為線程不會阻塞當它們不能執(zhí)行它們的請求動作時,它們不能阻塞互相等待。非阻塞算法任然可能產(chǎn)生活鎖(live lock),兩個線程一直請求一些動作,但一直被告知不能夠被執(zhí)行(因為其他線程的動作)。

沒有線程掛起

掛起和恢復一個線程的代價是昂貴的。沒錯,隨著時間的推移,操作系統(tǒng)和線程庫已經(jīng)越來越高效,線程掛起和恢復的成本也不斷降低。不過,線程的掛起和戶對任然需要付出很高的代價。

無論什么時候,一個線程阻塞,就會被掛起。因此,引起了線程掛起和恢復過載。由于使用非阻塞算法線程不會被掛起,這種過載就不會發(fā)生。這就意味著 CPU 有可能花更多時間在執(zhí)行實際的業(yè)務邏輯上而不是上下文切換。

在一個多個 CPU 的系統(tǒng)上,阻塞算法會對阻塞算法產(chǎn)生重要的影響。運行在 CPUA 上的一個線程阻塞等待運行在 CPU B 上的一個線程。這就降低了程序天生就具備的并行水平。當然,CPU A 可以調度其他線程去運行,但是掛起和激活線程(上下文切換)的代價是昂貴的。需要掛起的線程越少越好。

降低線程延遲

在這里我們提到的延遲指的是一個請求產(chǎn)生到線程實際的執(zhí)行它之間的時間。因為在非阻塞算法中線程不會被掛起,它們就不需要付昂貴的,緩慢的線程激活成本。這就意味著當一個請求執(zhí)行時可以得到更快的響應,減少它們的響應延遲。

非阻塞算法通常忙等待直到請求動作可以被執(zhí)行來降低延遲。當然,在一個非阻塞數(shù)據(jù)數(shù)據(jù)結構有著很高的線程爭用的系統(tǒng)中,CPU 可能在它們忙等待期間停止消耗大量的 CPU 周期。這一點需要牢牢記住。非阻塞算法可能不是最好的選擇如果你的數(shù)據(jù)結構哦有著很高的線程爭用。不過,也常常存在通過重構你的程序來達到更低的線程爭用。

上一篇:重入鎖死下一篇:信號量