<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ## 15 原子性輕量級實現—深入理解Atomic與CAS > 構成我們學習最大障礙的是已知的東西,而不是未知的東西。 > —— 貝爾納 在上一章介紹了并發的三大特性,即原子性、可見性和有序性。從本節起,我們將學習如何在多線程開發中確保這三大特性。首先,最簡單的方式就是使用 synchronized 關鍵字或者其它加鎖。這種方式最大的好處是–簡單!是的,無需動腦子,在需要的地方加鎖就好了。同步方式在并發時包治百病,但治病的手段卻是讓多線程程序轉為串行執行,這相當于自毀武功。如果濫用同步,那么程序就是去了多線程的意義。因此,只有在必要的時候才使用同步。比如對共享資源的訪問。而且盡量控制同步代碼塊的范圍,不需要使用同步的代碼,盡量不要放入同步代碼塊。 那么除了使用 synchronized 實現同步,還有其它手段保證三大特性嗎?答案是肯定的,Java 還提供了輕量級的實現,來解決特定的問題。這些實現方式不像 synchronized 能夠包治百病,但是對癥下藥,療效更好。對于程序來說,在解決問題的同時,還能保證代碼的效率。所以我們需要掌握好 synchronized 同步之外的這些方法,遇到并發問題時,采用更為合適的手段解決問題,而不是一股腦的都用 synchronized 或者其它顯式鎖的方式實現同步。這樣才是一位合格的攻城獅! 本節我們來看看原子性的輕量級實現–Atomic。 ## 1\. Atomic 簡介 Atomic 相關類在 java.util.concurrent.atomic 包中。針對不同的原生類型及引用類型,有 AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference 等。另外還有數組對應類型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。由于 Atomic 提供的功能類似,就不一個個過了。我們以 AtomicInteger 為例,看看 Atomic 類型變量所能提供的功能。 我們先看一個簡單的例子,運算邏輯是對變量 count 的累加。假如 count 為 int 類型,多個線程并發時,可能各自讀取到了同樣的值,也可能 A 線程讀到 2,但由于某種原因更新晚了,count 已經被其它線程更新為了 4,但是線程 A 還是繼續執行了 count+1 的操作,count 反而被更新為更小的值 3。現在的多線程程序是不安全的。要處理此問題,按照我們已經學習過的知識,需要把 count=count+1 放入 synchronized 代碼塊中。這樣做肯定能夠解決問題。但是這種同步操作是悲觀鎖的方式,每次都認為有其它線程在和它并發操作,所以每次都要對資源進行鎖定,而加鎖這個操作自身就有很大消耗。而且不是每一次 count+1 時都有并發發生,無并發發生時的加鎖并無必要。直接用 synchronized 進行同步,效率并不高。 下面我們看看怎么用 AtomicInteger 解決這個問題。使用 AtomicInteger 很簡單,我們在聲明 count 的時候,將其聲明為 AtomicInteger 即可,然后把 count=count+1 的語句改為 count.incrementAndGet ()。問題就完美解決了。 接下來我們看看 Atomic 實現原子操作的原理。我們首先看看 AtomicInteger 的 incrementAndGet 方法注釋: ~~~java /** * Atomically increments by one the current value. * * @return the updated value */ ~~~ 可以看到此方法以原子操作在當前 value 上加 1。count=count+1 這行語句其實隱含了兩步操作,第一步取得 count 的值,第二步為 count 加 1 。而在這兩步操作中間,count 的值可能已經改變了。而 AtomicInteger 提供的 incrementAndGet () 方法,則把這兩步操作作為一個原子性操作來完成,則不會出現線程安全問題。 Atomic 變量的操作是如何保證原子性的呢?其實是使用了 CAS 算法。 ## 2\. CAS 算法分析 CAS 是 Compare and swap 的縮寫,翻譯過來就是比較替換。其實 CAS 是樂觀鎖的一種實現。而 Synchronized 則是悲觀鎖。這里的樂觀和悲觀指的是當前線程對是否有并發的判斷。 悲觀鎖–認為每一次自己的操作大概率會有其它線程在并發,所以自己在操作前都要對資源進行鎖定,這種鎖定是排他的。悲觀鎖的缺點是不但把多線程并行轉化為了串行,而且加鎖和釋放鎖都會有額外的開支。 樂觀鎖–認為每一次操作時大概率不會有其它線程并發,所以操作時并不加鎖,而是在對數據操作時比較數據的版本,和自己更新前取得的版本一致才進行更新。樂觀鎖省掉了加鎖、釋放鎖的資源消耗,而且在并發量并不是很大的時候,很少會發生版本不一致的情況,此時樂觀鎖效率會更高。 Atomic 變量在做原子性操作時,會從內存中取得要被更新的變量值,并且和你期望的值進行比較,期望的值則是你要更新操作的值。如果兩個值相等,那么說明沒有其它線程對其更新,本線程可以繼續執行。如果不等,說明有線程已經先于此線程進行了更新操作。那么則繼續取得該變量的最新值,重復之前的邏輯,直至操作成功。這保證了每個線程對 Atomic 變量操作是線程安全的。 這里舉個例子,我們每天都會向代碼庫提交代碼,不知道你是否遇到過如下場景。你發現代碼中有個 bug,只需要修改一行代碼就可以修復,于是你先 pull,改好這行代碼后立刻 push,但是 git 告訴你由于落后遠程代碼庫的版本,push 失敗了。很不巧,就在你 pull 和 push 之間這短短的幾秒鐘,有其它開發 push 了代碼。那你只能再次 pull,和你這次修改做合并,然后再次 push。仔細想想,這不就是 CAS 嗎?只不過除了數據提交前的版本比較 git 幫你做外,pull、merge、push 需要你手動執行。 ![圖片描述](https://img.mukewang.com/5da542f300018f5810210623.jpg) ## 3\. Atomic 源代碼分析 下面我們看看 AtomicInteger 的源代碼。首先,AtomicInteger 中有 3 個重要的成員變量: ~~~ private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; private volatile int value; ~~~ 第一個 Unsafe 對象,Atomic 中的原子操作都是借助 unsafe 對象所實現的; 第二個是 AtomicInteger 包裝的變量在內存中的地址; 第三個是 AtomicInteger 包裝的變量值,并且用 volatile 修飾,以確保變量的變化能被其它線程看到。 其實 valueOffset 就是 value 的內存地址。 AtomicInteger 中有一段靜態代碼塊如下: ~~~java static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } ~~~ 這段代碼中 unsafe 對象獲取了 AtomicInteger 類中 value 這個字段的 offset。unsafe.objectFieldOffset () 是一個 native 的方法。 AtomicInteger 有一個構造函數如下: ~~~java public AtomicInteger(int initialValue) { value = initialValue; } ~~~ 可以看到對它所包裝的 int 變量 value 進行了賦值。 通過以上分析,我們來總結一下目前對 AtomicInteger 的了解: 1. AtomicInteger 對象包裝了通過構造函數傳入的一個初始 int 值; 2. AtomicInteger 持有這個 int 變量的內存地址; 3. AtomicInteger 還有一個用來做原子性操作的 unsafe 對象。 接下來我們以文章前面提到的 incrementAndGet 方法為例,來看看 Atomic 原子性的實現。代碼如下: ~~~java public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } ~~~ 代碼很簡單,調用了*unsafe*.getAndAddInt(this,*valueOffset*, 1) 后,對其返回 +1,然后 return。 那么原子性實現的秘密就全在*unsafe*.getAndAddInt () 這個方法中了。隨便翻看一下 AtomicInteger 的源代碼,這個方法被各種調用,其實我們搞清楚*unsafe*.getAndAddInt () 的實現,謎底也就揭曉了。我們繼續看*unsafe*.getAndAddInt () 的實現: ~~~java public final int getAndAddInt(Object obj, long valueOffset, int var) { int expect; // 利用循環,直到更新成功才跳出循環。 do { // 獲取value的最新值 expect = this.getIntVolatile(obj, valueOffset); // expect + var表示需要更新的值,如果compareAndSwapInt返回false,說明value值被其他線程更改了。 // 那么就循環重試,再次獲取value最新值expect,然后再計算需要更新的值expect + var。直到更新成功 } while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var)); // 返回當前線程在更改value成功后的,value變量原先值。并不是更改后的值 return expect; } ~~~ 為了幫助理解,我加了一些注釋。三個入參,第一個 obj 傳入的是 AtomicInteger 對象自己,第二個是 value 變量的內存地址,第三個則是要增加的值。 程序體中是一個循環,循環中通過 AtomicInteger 對象和 value 屬性的 offset,取得到當前的 value 值,接下來調用 this.compareAndSwapInt (obj, valueOffset, expect, expect + var)。這個方法名仔細看下,是不是很熟悉?是的,就是 CAS。調用前我們已經獲取到了期望值,所以在這個方法中會把期望值和你要替換掉的值做比較,如果一直則替換,否則重復 while 循環,也就是再此獲取最新的期望值,然后再比較替換,直至替換成功。 你現在一定很好奇 compareAndSwapInt 的方法是如何實現的。我們點開此方法后,可以看到是一個 native 方法,native 方法使用 C 語言編寫。由于 JDK 并未開源,我們只能下載開源版本的 OpenJDK。 可以看到在 compareAndSwapInt 源代碼的最后,調用了 Atomic::cmpxchg (x,addr,e)。這個方法在不同的平臺會有不同的實現。不過總的思想如下: 1. 判斷當前系統是否為多核處理器; 2. 執行 CPU 指令 cmpxchg,如果為多核則在 cmpxchg 加 lock 前綴。 可以看到最終是通過 CPU 指令 cmpxchg 來實現比較交換。那么 Lock 前綴起到什么作用呢?加了 Lock 前綴的操作,在執行期間,所使用的緩存會被鎖定,其他處理器無法讀寫該指令要訪問的內存區域,由此保證了比較替換的原子性。而這個操作過程稱之為緩存鎖定。 ## 4\. CAS 的缺點 CAS 最終通過 CPU 指令實現,把無謂的同步消耗降到最低,但是沒有銀彈,CAS 也有著幾個致命的缺點: 1. 比較替換如果失敗,則會一直循環,直至成功。這在并發量很大的情況下對 CPU 的消耗將會非常大; 2. 只能保證一個變量自身操作的原子性,但多個變量操作要實現原子性,是無法實現的; 3. ABA 問題。 前兩個問題比較簡單,我們重點看一下第三個 ABA 問題。 假如本線程更新前取得期望值為 A,和更新操作之間的這段時間內,其它線程可能把 value 改為了 B 又改回了 A。 而本線程更新時發現 value 和期望值一樣還是 A,認為其沒有變化,則執行了更新操作。但其實此時的 A 已經不是彼時的 A 了。 大多數情況下 ABA 不會造成業務上的問題。但是如果你認為 ABA 問題對你的程序業務有問題,那么就需要解決。 JDK 提供了 AtomicStampedReference 類,通過對 Atomic 包裝的變量增加版本號,來解決 ABA 問題,即使 value 還是 A,但如果版本變化了,也認為比較失敗。 ## 5\. 總結 本節我們學習了輕量級的原子性實現–Atomic。并且以 AtomicInteger 為例進行了源代碼的講解,Atomic 的類很多,但是大同小異,感興趣的話,可以自己讀一下其它 Atomic 類的源代碼。本節最后介紹了 CAS,一定要深入理解,這也是面試中經常會問到的問題之一。我們經過本節的學習,了解了 Atomic 的優點,也知道了它的局限性。在以后的多線程開發中,可以有選擇的使用 Atomic 變量,以使程序達到更好的效率。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看