樂觀鎖的一種實現方式——CAS


   在java里面,synchronized關鍵字就是一種悲觀鎖,因為在加上鎖之后,只有當前線程可以操作變量,其他線程只有等待。

  CAS操作是一種樂觀鎖,它假設數據不會產生沖突,而是在提交的時候再進行版本比較。這樣可以減少加鎖的頻率,提高程序的性能。

線程安全

眾所周知,Java是多線程的。但是,Java對多線程的支持其實是一把雙刃劍。一旦涉及到多個線程操作共享資源的情況時,處理不好就可能產生線程安全問題。線程安全性可能是非常復雜的,在沒有充足的同步的情況下,多個線程中的操作執行順序是不可預測的。

   Java里面進行多線程通信的主要方式就是共享內存的方式,共享內存主要的關注點有兩個:可見性和有序性。加上復合操作的原子性,我們可以認為Java的線程安全性問題主要關注點有3個:可見性、有序性和原子性。

   Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題。這里不再詳細介紹JMM及鎖的其他相關知識。但是我們要討論一個問題,那就是鎖到底是不是有利無弊的?  

   鎖存在的問題

   Java在JDK1.5之前都是靠synchronized 關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個線程持有共享變量的鎖,都采用獨占的方式來訪問這些變量。獨占鎖其實就是一種悲觀鎖,所以可以說    synchronized 是悲觀鎖。  

   悲觀鎖機制存在以下問題:

   在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。

     一個線程持有鎖會導致其它所有需要此鎖的線程掛起。如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。與鎖相比,volatile 變量是一個更輕量級的同步機制,因為在使用這些變量時不會發生上下文切換和線程調度等操作,但是 volatile 不能解決原子性問題,因此當一個變量依賴舊值時就不能使用volatile 變量。因此對於同步最終還是要回到鎖機制上來。  

   

樂觀鎖

   樂觀鎖(Optimistic Locking )其實是一種思想。相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。  

   上面提到的樂觀鎖的概念中其實已經闡述了他的具體實現細節:主要就是兩個步驟:沖突檢測和數據更新。其實現方式有一種比較典型的就是Compare and Swap (CAS )。  

 

CAS

   CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

   CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。 ”這其實和樂觀鎖的沖突檢查+數據更新的原理是一樣的。  

    這里再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。

Java對CAS的支持

 

    在JDK1.5 中新增java.util.concurrent就是建立在CAS之上的。相對於對於synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以JUC在性能上有了很大的提升。  

   我們以java.util.concurrent 中的AtomicInteger 為例,看一下在不使用鎖的情況下是如何保證線程安全的。主要理解getAndIncrement 方法,該方法的作用相當於++i 操作。  

 

public class AtomicInteger extends Number implements java.io.Serializable {          private volatile int value;      public final int get() {          return value;      }   public final int getAndIncrement() {    for (;;) {        int current = get();        int next = current + 1;        if (compareAndSet(current, next)){  //該操作先檢查當前數值是否等於current,等於意味着AtomicInteger的值沒有被其他線程修改過,則將AtomicInteger的當前數值更新成next的值,如果不等compareAndSet方法會返回false,程序會進入for循環重新進行compareAndSet操作            return current;    //返回的是當前的值
         }
   } } public final int incrementAndGet() {    for (;;) {        int current = get();        int next = current + 1;        if (compareAndSet(current, next))            return next;          //返回的是加一之后的值    } }    public final boolean compareAndSet(int expect, int update) {          return unsafe.compareAndSwapInt(this, valueOffset, expect, update);      }   }

 

在沒有鎖的機制下需要字段value借助volatile原語,保證線程間的數據是可見的。這樣在獲取變量的值的時候才能直接讀取。然后來看看    ++i 是怎么做到的。  

 

   getAndIncrement 采用了CAS操作,每次從內存中讀取數據然后將此數據和    +1 后的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。而    compareAndSet 利用JNI來完成CPU指令的操作。  

 

 

總結

 

Java中的線程安全問題至關重要,要想保證線程安全,就需要鎖機制。鎖機制包含兩種:樂觀鎖與悲觀鎖。悲觀鎖是獨占鎖,阻塞鎖。樂觀鎖是非獨占鎖,非阻塞鎖。有一種樂觀鎖的實現方式就是CAS ,這種算法在JDK 1.5中引入的    java.util.concurrent 中有廣泛應用。但是值得注意的是這種算法會存在ABA問題。  

 

 

 


免責聲明!

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



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