天天看點

剖析Disruptor:為什麼會這麼快?(三)揭秘記憶體屏障

譯者:杜建雄     校對:歐振聰

<b>主題是什麼?</b>

<b>什麼是記憶體屏障?</b>

它是一個cpu指令。沒錯,又一次,我們在讨論cpu級别的東西,以便獲得我們想要的性能(martin著名的mechanical sympathy理論)。基本上,它是這樣一條指令: a)確定一些特定操作執行的順序; b)影響一些資料的可見性(可能是某些指令執行後的結果)。

編譯器和cpu可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個記憶體屏障,相當于告訴cpu和編譯器先于這個指令的必須先執行,後于這個指令的必須後執行。正如去拉斯維加斯旅途中各個站點的先後順序在你心中都一清二楚。

剖析Disruptor:為什麼會這麼快?(三)揭秘記憶體屏障

記憶體屏障另一個作用是強制更新一次不同cpu的緩存。例如,一個寫屏障會把這個屏障前寫入的資料重新整理到緩存,這樣任何試圖讀取該資料的線程将得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆cpu執行的。

<b>和java有什麼關系?</b>

現在我知道你在想什麼——這不是彙程式設計式。它是java。

這裡有個神奇咒語叫volatile(我覺得這個詞在java規範中從未被解釋清楚)。如果你的字段是volatile,java記憶體模型将在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。

剖析Disruptor:為什麼會這麼快?(三)揭秘記憶體屏障

這意味着如果你對一個volatile字段進行寫操作,你必須知道:

1、一旦你完成寫入,任何通路這個字段的線程将會得到最新的值。

2、在你寫入前,會保證所有之前發生的事已經發生,并且任何更新過的資料值也是可見的,因為記憶體屏障會把之前的寫入值都重新整理到緩存。

<b>舉個例子呗!</b>

很高興你這樣說了。又是時候讓我來畫幾個甜甜圈了。

剖析Disruptor:為什麼會這麼快?(三)揭秘記憶體屏障

這時候,消費者們能獲得最新的序列号碼(8),并且因為記憶體屏障保證了它之前執行的指令的順序,消費者們可以确信生産者對7号entry所作的改動已經可用。

<b>…</b><b>那麼消費者那邊會發生什麼?</b>

剖析Disruptor:為什麼會這麼快?(三)揭秘記憶體屏障

是以,如果你的下遊消費者(c2)看見前一個消費者(c1)在消費号碼為12的entry,當c2的讀取也到了12,它在更新序列号前将可以獲得c1對該entry的所作的更新。

基本來說就是,c1更新序列号前對ring buffer的所有操作(如上圖黑色所示),必須先發生,待c2拿到c1更新過的序列号之後,c2才可以為所欲為(如上圖藍色所示)。

<b>對性能的影響</b>

<a href="http://ifeve.com/disruptor-memory-barrier/#viewsource">檢視源代碼</a>

<code>01</code>

<code>   </code><code>long</code> <code>nextsequence = sequence +</code><code>1</code><code>;</code>

<code>02</code>

<code>   </code><code>while</code> <code>(running)</code>

<code>03</code>

<code>   </code><code>{</code>

<code>04</code>

<code>       </code><code>try</code>

<code>05</code>

<code>       </code><code>{</code>

<code>06</code>

<code>           </code><code>final</code> <code>long</code> <code>availablesequence = consumerbarrier.waitfor(nextsequence);</code>

<code>07</code>

<code>           </code><code>while</code> <code>(nextsequence &lt;= availablesequence)</code>

<code>08</code>

<code>           </code><code>{</code>

<code>09</code>

<code>               </code><code>entry = consumerbarrier.getentry(nextsequence);</code>

<code>10</code>

<code>               </code><code>handler.onavailable(entry);</code>

<code>11</code>

<code>               </code><code>nextsequence++;</code>

<code>12</code>

<code>           </code><code>}</code>

<code>13</code>

<code>           </code><code>handler.onendofbatch();</code>

<code>14</code>

<code>           </code><code>sequence = entry.getsequence();</code>

<code>15</code>

<code>       </code><code>}</code>

<code>16</code>

<code>       </code><code>…</code>

<code>17</code>

<code>       </code><code>catch</code> <code>(</code><code>final</code> <code>exception ex)</code>

<code>18</code>

<code>19</code>

<code>           </code><code>exceptionhandler.handle(ex, entry);</code>

<code>20</code>

<code>21</code>

<code>           </code><code>nextsequence = entry.getsequence() +</code><code>1</code><code>;</code>

<code>22</code>

<code>23</code>

<code>   </code><code>}</code>

(你會注意到,這是個舊式的代碼和命名習慣,因為這是摘自我以前的部落格文章,我認為如果直接轉換為新式的代碼和命名習慣會讓人有點混亂)

在上面的代碼中,我們在消費者處理entries的循環中用一個局部變量(nextsequence)來遞增。這表明我們想盡可能地減少對volatile類型的序列号的進行讀寫。

<b>總結</b>

記憶體屏障是cpu指令,它允許你對資料什麼時候對其他程序可見作出假設。在java裡,你使用volatile關鍵字來實作記憶體屏障。使用volatile意味着你不用被迫選擇加鎖,并且還能讓你獲得性能的提升。

但是,你需要對你的設計進行一些更細緻的思考,特别是你對volatile字段的使用有多頻繁,以及對它們的讀寫有多頻繁。