<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          你覺得 ThreadLocalRandom 這玩意真的安全嗎?

          共 4925字,需瀏覽 10分鐘

           ·

          2022-01-19 12:38

          前沿技術(shù)早知道,彎道超車有希望?

          積累超車資本,從關(guān)注DD開始

          圖文編輯:xj

          來源:https://zhenbianshu.github.io/2019/12/is_threadlocalrandom_safe.html


          最近在寫一些業(yè)務(wù)代碼時遇到一個需要產(chǎn)生隨機(jī)數(shù)的場景,這時自然想到 JDK 包里的 Random 類。

          但出于對性能的極致追求,就考慮使用 ThreadLocalRandom 類進(jìn)行優(yōu)化,順便查看了一下 ThreadLocalRandom 的實現(xiàn),理解了一下部分源碼。

          在學(xué)習(xí)的過程中也看到了一篇文章,感覺還不錯,分享給大家看看,相互學(xué)習(xí)。

          至于標(biāo)題:你覺得這玩意真的安全嗎?

          先說結(jié)論:是真的安全!

          Random 的性能問題

          使用 Random 類時,為了避免重復(fù)創(chuàng)建的開銷,我們一般將實例化好的 Random 對象設(shè)置為我們所使用服務(wù)對象的屬性或靜態(tài)屬性,這在線程競爭不激烈的情況下沒有問題,但在一個高并發(fā)的 web 服務(wù)內(nèi),使用同一個 Random 對象可能會導(dǎo)致線程阻塞。

          Random 的隨機(jī)原理是對一個”隨機(jī)種子”進(jìn)行固定的算術(shù)和位運(yùn)算,得到隨機(jī)結(jié)果,再使用這個結(jié)果作為下一次隨機(jī)的種子。

          在解決線程安全問題時,Random 使用 CAS 更新下一次隨機(jī)的種子,可以想到,如果多個線程同時使用這個對象,就肯定會有一些線程執(zhí)行 CAS 連續(xù)失敗,進(jìn)而導(dǎo)致線程阻塞。

          ThreadLocalRandom

          jdk 的開發(fā)者自然考慮到了這個問題,在 concurrent 包內(nèi)添加了 ThreadLocalRandom 類,第一次看到這個類名,我以為它是通過 ThreadLocal 實現(xiàn)的,進(jìn)而想到恐怖的內(nèi)存泄漏問題,但點(diǎn)進(jìn)源碼卻沒有 ThreadLocal 的影子,而是存在著大量 Unsafe 相關(guān)的代碼。

          我們來看一下它的核心代碼:

          ?

          UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA);

          翻譯成更直觀的 Java 代碼就像:

          Thread?t?=?Thread.currentThread();
          long?r?=?UNSAFE.getLong(t,?SEED)?+?GAMMA;
          UNSAFE.putLong(t,?SEED,?r);

          看上去非常眼熟,像我們平常往 Map 里 get/set 一樣,以 Thread.currentThread() 獲取到的當(dāng)前對象里 key,以 SEED 隨機(jī)種子作為 value。

          但是以對象作為 key 是可能會造成內(nèi)存泄漏的啊,由于 Thread 對象可能會大量創(chuàng)建,在回收時不 remove Map 里的 value 時會導(dǎo)致 Map 越來越大,最后內(nèi)存溢出。

          如果您正在學(xué)習(xí)Spring Boot,那么推薦一個連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/

          Unsafe

          功能

          不過再仔細(xì)看 ThreadLocalRandom 類的核心代碼,發(fā)現(xiàn)并不是簡單的 Map 操作,它的 getLong() 方法需要傳入兩個參數(shù),而 putLong() 方法需要三個參數(shù),查看源碼發(fā)現(xiàn)它們都是 native 方法,我們看不到具體的實現(xiàn)。

          兩個方法簽名分別是:

          • public native long getLong(Object var1, long var2);
          • public native void putLong(Object var1, long var2, long var4);

          雖然看不到具體實現(xiàn),但我們可以查得到它們的功能,下面是兩個方法的功能介紹:

          • putLong(object, offset, value) 可以將 object 對象內(nèi)存地址偏移 offset 后的位置后四個字節(jié)設(shè)置為 value。
          • getLong(object, offset) 會從 object 對象內(nèi)存地址偏移 offset 后的位置讀取四個字節(jié)作為 long 型返回。

          不安全性

          作為 Unsafe 類內(nèi)的方法,它也透露著一股 “Unsafe” 的氣息,具體表現(xiàn)就是可以直接操作內(nèi)存,而不做任何安全校驗,如果有問題,則會在運(yùn)行時拋出 Fatal Error,導(dǎo)致整個虛擬機(jī)的退出。

          在我們的常識里,get 方法是最容易拋異常的地方,比如空指針、類型轉(zhuǎn)換等,但 Unsafe.getLong() 方法是個非常安全的方法,它從某個內(nèi)存位置開始讀取四個字節(jié),而不管這四個字節(jié)是什么內(nèi)容,總能成功轉(zhuǎn)成 long 型,至于這個 long 型結(jié)果是不是跟業(yè)務(wù)匹配就是另一回事了。

          而 set 方法也是比較安全的,它把某個內(nèi)存位置之后的四個字節(jié)覆蓋成一個 long 型的值,也幾乎不會出錯。

          那么這兩個方法”不安全”在哪呢?

          它們的不安全并不是在這兩個方法執(zhí)行期間報錯,而是未經(jīng)保護(hù)地改變內(nèi)存,會引起別的方法在使用這一段內(nèi)存時報錯。

          public?static?void?main(String[]?args)?throws?NoSuchFieldException,?IllegalAccessException?{
          ????????//?Unsafe?設(shè)置了構(gòu)造方法私有,getUnsafe?獲取實例方法包私有,在包外只能通過反射獲取
          ????????Field?field?=?Unsafe.class.getDeclaredField("theUnsafe");?
          ????????field.setAccessible(true);
          ????????Unsafe?unsafe?=?(Unsafe)?field.get(null);
          ????????//?Test?類是一個隨手寫的測試類,只有一個?String?類型的測試類
          ????????Test?test?=?new?Test();
          ????????test.ttt?=?"12345";
          ????????unsafe.putLong(test,?12L,?2333L);
          ????????System.out.println(test.value);
          }

          運(yùn)行上面的代碼會得到一個 fatal error,報錯信息為

          ?

          “A fatal error has been detected by the Java Runtime Environment: … Process finished with exit code 134 (interrupted by signal 6: SIGABRT)”。

          可以從報錯信息中看到虛擬機(jī)因為這個 fatal error abort 退出了。

          原因也很簡單,我使用 unsafe 將 Test 類 value 屬性的位置設(shè)置成了 long 型值 2333,而當(dāng)我使用 value 屬性時,虛擬機(jī)會將這一塊內(nèi)存解析為 String 對象,原 String 對象對象頭的結(jié)構(gòu)被打亂了,解析對象失敗拋出了錯誤。

          更嚴(yán)重的問題是報錯信息中沒有類名行號等信息,在復(fù)雜項目中排查這種問題真如同大海撈針。

          不過 Unsafe 的其他方法可不一定像這一對方法一樣,使用他們時可能需要注意另外的安全問題,之后有遇到再說。

          ThreadLocalRandom 的實現(xiàn)

          那么 ThreadLocalRandom 是不是安全的呢,再回過頭來看一下它的實現(xiàn)。

          ThreadLocalRandom 的實現(xiàn)需要 Thread 對象的配合,在 Thread 對象內(nèi)存在著一個屬性 threadLocalRandomSeed,它保存著這個線程專屬的隨機(jī)種子,而這個屬性在 Thread 對象的 offset,是在 ThreadLocalRandom 類加載時就確定了的。

          具體方法是:

          SEED = UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("threadLocalRandomSeed"));

          我們知道一個對象所占用的內(nèi)存大小在類被加載后就確定了的,所以使用 Unsafe.objectFieldOffset(class, fieldName) 可以獲取到某個屬性在類中偏移量。

          而在找對了偏移量,又能確定數(shù)據(jù)類型時,使用 ThreadLocalRandom 就是很安全的。

          疑問

          在查找這些問題的過程中,我也產(chǎn)生了兩個疑問點(diǎn)。

          使用場景

          首先就是 ThreadLocalRandom 為什么非要使用 Unsafe 來修改 Thread 對象內(nèi)的隨機(jī)種子呢,在 Thread 對象內(nèi)添加 get/set 方法不是更方便嗎?

          stackOverFlow 上有人跟我同樣的疑問,why is threadlocalrandom implemented so bizarrely:

          ?

          https://stackoverflow.com/questions/40620026/why-is-threadlocalrandom-implemented-so-bizarrely

          被采納的答案里解釋說,對 jdk 開發(fā)者來說 Unsafe 和 get/set 方法都像普通的工具,具體使用哪一個并沒有一個準(zhǔn)則。

          這個答案并沒有說服我,于是我另開了一個問題,里面的一個評論我比較認(rèn)同,大意是 ThreadLocalRandom 和 Thread 不在同一個包下,如果添加 get/set 方法的話,get/set 方法必須設(shè)置為 public,這就有違了類的封閉性原則。

          內(nèi)存布局

          另一個疑問是我看到 Unsafe.objectFieldOffset 可以獲取到屬性在對象內(nèi)存的偏移量后,自己在 IDEA 里使用 main 方法試了上文中提到的 Test 類,發(fā)現(xiàn) Test 類的唯一一個屬性 value 相對對象內(nèi)存的偏移量是 12,于是比較疑惑這 12 個字節(jié)的組成。

          我們知道,Java 對象的對象頭是放在 Java 對象的內(nèi)存起始處的,而一個對象的 MarkWord 在對象頭的起始處,在 32 位系統(tǒng)中,它占用 4 個字節(jié),而在 64 位系統(tǒng)中它占用 8 個字節(jié),我使用的是 64 位系統(tǒng),這毫無疑問會占用 8 個字節(jié)的偏移量。

          緊跟 MarkWord 的應(yīng)該是 Test 類的類指針和數(shù)組對象的長度,數(shù)組長度是 4 字節(jié),但 Test 類并非數(shù)組,也沒有其他屬性,數(shù)據(jù)長度可以排除,但在 64 位系統(tǒng)下指針也應(yīng)該是 8 字節(jié)的啊,為什么只占用了 4 個字節(jié)呢?

          唯一的可能性是虛擬機(jī)啟用了指針壓縮,指針壓縮只能在 64 位系統(tǒng)內(nèi)啟用,啟用后指針類型只需要占用 4 個字節(jié),但我并沒有顯示指定過使用指針壓縮。查了一下,原來在 1.8 以后指針壓縮是默認(rèn)開啟的,在啟用時使用 -XX:-UseCompressedOops 參數(shù)后,value 的偏移量變成了 16。

          最后說一句

          在寫代碼時還是要多注意查看依賴庫的具體實現(xiàn),不然可能踩到意想不到的坑,而且多看看并沒有壞處,仔細(xì)研究一下還能學(xué)到更多。

          好了,看到了這里了,轉(zhuǎn)發(fā)、在看、點(diǎn)贊隨便安排一個吧,要是你都安排上我也不介意。寫文章很累的,需要一點(diǎn)正反饋。給各位讀者朋友們磕一個了:

          對了,我們創(chuàng)建了一個高質(zhì)量的技術(shù)交流群,與優(yōu)秀的人在一起,自己也會優(yōu)秀起來,趕緊點(diǎn)擊加群,享受一起成長的快樂。

          推薦閱讀


          最近兩周DD整理了一波面經(jīng),涵蓋阿里、騰訊、頭條等眾多大廠的真實面經(jīng)分享。最近打算跳槽的小伙伴可以點(diǎn)擊下方,關(guān)注公眾號“SpringForAll社區(qū)”,發(fā)送關(guān)鍵詞“2022Java面經(jīng)”獲取完整PDF哦!

          瀏覽 33
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  囯产精品久久777777换脸 | 91视频直播做爱 | 操逼骚逼网站 | 色se在线| 免费黄色色情成人影片 |