i++引發的深度思考

i++引發的深度思考

前言

很多小夥伴在面試的時候可能會被面試官問過一個問題:i++操作在多核多線程環境下是安全的嗎?有很大一部分童鞋知道是不安全的,但再深入探討的時候,很多人就是知其然而不知其所以然了。其實,看似簡單的一個操作,實際蘊含著太多基礎的內容,掌握這些基礎,對我們深入學習至關重要,下面我們一起來探討一下吧。

i++

大家都知道,i++的操作實際上是對i變量進行自增1的操作,我相信會有一小部分人認為i++只是一行代碼的事,他就是一個操作而已,在多線程環境下,他就具有原子性。其實不然,

i++的操作,其實分成三個cpu指令週期完成,我們來看看i++在xcode編譯下的彙編代碼:

i++引發的深度思考

i++源碼

對應的彙編如下:

i++引發的深度思考

i++對應的彙編指令

可以看出i++的操作其實是分成三部分的:

  1. 把i變量從內存讀進寄存器
  2. 在寄存器中對變量i進行自增1操作
  3. 把自增後的i值從寄存器寫回到內存

看到這,你還會認為這只是一個操作嗎?

到這,應該比較容易理解i++操作在不做任何同步措施的情況下,在多核環境中,最終i的值會小於或等於正確值。等於的原因是,i++在操作次數不多的情況下,可能並不會調度到多核cpu上,i++的操作次數大,越會小於正確值。

這是因為,同一時刻,相同的值會被多個cpu緩存,這樣分別做i++操作,再同時寫回內存時,就會存在相互覆蓋的情況,這樣最終的值會變小,大家不妨也可以試一試。

解決方案

1、通過循環CAS解決

i++引發的深度思考

循環CAS解決i++問題

使用Java併發包下的AtomicInteger可以解決i++在多線程環境下的問題,底層是通過不斷比較內存中的值是否和期望的值一致,如果不一致,則繼續重複操作,直到一致為止。

這種方式不需要進行線程上下文切換,但會比較容易消耗CPU的資源,因此只適宜做一些簡單快速的業務計算。

2、通過synchronized解決

i++引發的深度思考

通過synchronized解決i++問題

使用synchronized也可樣可以解決線程安全問題,這種方式處理i++這些非常簡單的操作時,可經過jvm優化不會阻塞線程。但如果是其它複雜的操作,在一定程度時會阻塞沒有獲取到鎖的線程,導致其它線程進行等待隊列,在併發量大時,會出現比較大的線程上下文切換。

再談i++

針對上面兩種解決方式,可能會有人問,使用volatile修飾i變量能不能做到線程安全呢?瞭解volatile的人,應該都知道這是不能的。

volatile只能保證內存的可見性,並不能保證操作的原子性。什麼是內容的可見性?簡單可以這樣理解,就是線程A修改了i值,cpu會馬上把這個修改的值刷新回主內存,保證其它線條讀取的時候,都能拿到最新的值。

也就是說上面i++的第一條指令通過volatile能何保證,讀到的是相同的值,但第二個自增操作,因為變量i已經在cpu的寄存器或緩存中了,cpu在這個時刻已經不需要讀取變量i了,所以volatile是無法保證原子操作的。

那很多人可能會問,cpu不是每自增一次就會寫回內存的嗎?其實也不是,現代CPU都是有回寫這個操作的,所謂回寫,為了避免總線上頻繁的數據傳輸,cpu修改的數據不會馬上刷回到內存,而是先寫入到它的緩存。

CPU是不直接和內存連接的,CPU和一級緩存連接,一級緩存和二級緩存連接,二級和三級連接,基本就是這個意思,數據也是逐級傳遞下去的。所以說i++的操作i會在cpu中緩存起來,也就是我們常說的“髒數據”。

所以這個問題在多線程環境下會引起比較大的挑戰,很多時候我們都需要保存多線程環境下數據的一致性。為了解決這個問題,主要有兩個解決方案

  • 鎖定總線

作何數據的寫入讀取都要申請總線的獨佔權,這樣每個指令拿到的數據都是最新的,也就沒有上面所說的問題了。這種方式,簡單粗暴,但性能底下,現在一般都不使用。

  • CPU緩存一致性協議(MESI)

CPU緩存一致性協議的原理是:每個CPU的緩存通過不停地嗅探總線上數據的傳輸,而動態地改變自己的緩存狀態。

緩存一致性協議中數據有四種狀態:

1、失效:數據在cpu緩存中是失效狀態的,這種狀態的數據都會被忽略,相當於從來沒有被加載到緩存中。

2、共享:數據在緩存中和在內存中是保持一致的,這種狀態的數據只能讀取,不能寫入,也就我們常說的只讀模式。所以多個cpu的緩存可以同時擁有相同地址段的內存

3、獨佔:獲取這種狀態前,需要先到總線申請獨佔權,這時會被其它cpu的緩存嗅探到,其它cpu的緩存如果緩存了相當的數據段,由會把其失效。獲取到獨佔權的cpu,才能對數據進行寫入。

4、修改:處理器修改過的數據,回寫到緩存時,會變成修改狀態,處於這種狀態的數據,其它cpu如果緩存了相同的數據,也會被失效。

我們細想一下,CPU緩存一致性協議,很好地解決了內存的可見性單指令的原子性這兩個問題了,但它並不能保證複雜的原子性操作。

其實要保證複雜操作的原子性,也有兩種方法

1、在進入原子操作區域前,在總線上加鎖,此時只有該線程能對內存進行讀寫操作。這種方式同樣時,簡單粗暴,CPU性能開銷大。

2、如果我們原子操作涉及的內存數據可以被CPU緩存,並不會跨越緩存行,則可以通過緩存一致性協議,只鎖定緩存行而不鎖總線,那麼性能會得到很大的改善,因為這不會影響其它cpu通過總線讀取數據。

總結

不管我們上層如何使用各種手段都保證內存可見性也好,原子性也罷,最底層的解決方案也就是那一兩種,萬變不離其宗,從上往上看,你是不是更豁然開朗了呢?喜歡點個關注,謝謝。

i++引發的深度思考

i++引發的深度思考


分享到:


相關文章: