<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>

          使用了synchronized,竟然還有線程安全問(wèn)題!

          共 8431字,需瀏覽 17分鐘

           ·

          2021-07-27 11:42

          實(shí)戰(zhàn)中受過(guò)的傷,才能領(lǐng)悟的更透徹,二師兄帶你分析實(shí)戰(zhàn)案例。

          線程安全問(wèn)題一直是系統(tǒng)亙古不變的痛點(diǎn)。這不,最近在項(xiàng)目中發(fā)了一個(gè)錯(cuò)誤使用線程同步的案例。表面上看已經(jīng)使用了同步機(jī)制,一切歲月靜好,但實(shí)際上線程同步卻毫無(wú)作用。

          關(guān)于線程安全的問(wèn)題,基本上就是在挖坑與填坑之間博弈,這也是為什么面試中線程安全必不可少的原因。下面,就來(lái)給大家分析一下這個(gè)案例。

          有隱患的代碼

          先看一個(gè)脫敏的代碼實(shí)例。代碼要處理的業(yè)務(wù)邏輯很簡(jiǎn)單,就是多線程訪問(wèn)一個(gè)單例對(duì)象的成員變量,對(duì)其進(jìn)行自增處理。

          SyncTest類實(shí)現(xiàn)了Runnable接口,run方法中處理業(yè)務(wù)邏輯。在run方法中通過(guò)synchronized來(lái)保證線程安全問(wèn)題,在main方法中創(chuàng)建一個(gè)SyncTest類的對(duì)象,兩個(gè)線程同時(shí)操作這一個(gè)對(duì)象。

          public class SyncTest implements Runnable {

              private Integer count = 0;

              @Override
              public void run() {
                  synchronized (count) {
                      System.out.println(new Date() + " 開始休眠" + Thread.currentThread().getName());
                      count++;
                      try {
                          Thread.sleep(10000);
                          System.out.println(new Date() + " 結(jié)束休眠" + Thread.currentThread().getName());
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }

              public static void main(String[] args) throws InterruptedException {
                  SyncTest test = new SyncTest();
                  new Thread(test).start();
                  Thread.sleep(100);
                  new Thread(test).start();
              }
          }

          在上述代碼中,兩個(gè)線程訪問(wèn)SyncTest的同一個(gè)對(duì)象,并對(duì)該對(duì)象的count屬性進(jìn)行自增操作。由于是多線程,那就要保證count++的線程安全。

          代碼中使用了synchronized來(lái)鎖定代碼塊,進(jìn)行同步處理。為了演示效果,在處理完業(yè)務(wù)邏輯對(duì)線程進(jìn)行睡眠。

          理想的狀況是第一個(gè)線程執(zhí)行完畢,然后第二個(gè)線程才能進(jìn)入并執(zhí)行。

          表面上看,一切都很完美,下面我們來(lái)執(zhí)行一下程序看看結(jié)果。

          執(zhí)行驗(yàn)證

          執(zhí)行main方法打印結(jié)果如下:

          Fri Jul 23 22:10:34 CST 2021 開始休眠Thread-0
          Fri Jul 23 22:10:34 CST 2021 開始休眠Thread-1
          Fri Jul 23 22:10:44 CST 2021 結(jié)束休眠Thread-0
          Fri Jul 23 22:10:45 CST 2021 結(jié)束休眠Thread-1

          正常來(lái)說(shuō),由于使用了synchronized來(lái)進(jìn)行同步處理,那么第一個(gè)線程進(jìn)入run方法之后,會(huì)進(jìn)行鎖定。先執(zhí)行“開始休眠”,然后再執(zhí)行“結(jié)束休眠”,最后釋放鎖之后,第二個(gè)線程才能夠進(jìn)入。

          但分析上面的日志,會(huì)發(fā)現(xiàn)兩個(gè)線程同時(shí)進(jìn)入了“開始休眠”狀態(tài),也就是說(shuō)鎖并未起效,線程安全依舊存在問(wèn)題。下面我們就針對(duì)synchronized失效原因進(jìn)行逐步分析。

          synchronized知識(shí)回顧

          在分析原因之前,我們先來(lái)回顧一下synchronized關(guān)鍵字的使用。

          synchronized關(guān)鍵字解決并發(fā)問(wèn)題時(shí)通常有三種使用方式:

          • 同步普通方法,鎖的是當(dāng)前對(duì)象;
          • 同步靜態(tài)方法,鎖的是當(dāng)前Class對(duì)象;
          • 同步塊,鎖的是()中的對(duì)象;

          很顯然,上面的場(chǎng)景中,使用的是第三種方式進(jìn)行鎖定處理。

          synchronized實(shí)現(xiàn)同步的過(guò)程是:JVM通過(guò)進(jìn)入、退出對(duì)象監(jiān)視器(Monitor)來(lái)實(shí)現(xiàn)對(duì)方法、同步塊的同步的。

          代碼在編譯時(shí),編譯器會(huì)在同步方法調(diào)用前加入一個(gè)monitor.enter指令,在退出方法和異常處插入monitor.exit的指令。其本質(zhì)就是對(duì)一個(gè)對(duì)象監(jiān)視器(Monitor)進(jìn)行獲取,而這個(gè)獲取過(guò)程具有排他性從而達(dá)到了同一時(shí)刻只能一個(gè)線程訪問(wèn)的目的。

          原因分析

          經(jīng)過(guò)上面基礎(chǔ)知識(shí)的鋪墊,我們就來(lái)排查分析一下上述代碼的問(wèn)題。其實(shí),對(duì)于這個(gè)問(wèn)題,IDE已經(jīng)能夠給出提示了。

          如果你使用的IDE帶有代碼檢查的插件,synchronized (count)的count上會(huì)有如下提示:

          Synchronization on a non-final field 'xxx' Inspection info: Reports synchronized statements where the lock expression is a reference to a non-final field. Such statements are unlikely to have useful semantics, as different threads may be locking on different objects even when operating on the same object.

          很多人可能會(huì)忽視掉這個(gè)提示,但它已經(jīng)明確指出此處代碼有線程安全問(wèn)題。提示的核心是“同步處理應(yīng)用在了非final修飾的變量上”。

          對(duì)于synchronized關(guān)鍵字來(lái)說(shuō),如果加鎖的對(duì)象是一個(gè)可變的對(duì)象,那么當(dāng)這個(gè)變量的引用發(fā)生了改變,不同的線程可能鎖定不同的對(duì)象,進(jìn)而都會(huì)成功獲得各自的鎖。

          用一個(gè)圖來(lái)回顧一下上述過(guò)程:


          在上圖中,Thread0在①處進(jìn)行了鎖定,但鎖定的對(duì)象是Integer(0);Thread1中②處也進(jìn)行鎖定,但此時(shí)count已經(jīng)進(jìn)行自增,導(dǎo)致Thread1鎖定的是對(duì)象Integer(1);也就是說(shuō),兩個(gè)線程鎖定的對(duì)象不是同一個(gè),也就無(wú)法保證線程安全了。

          解決方案

          既然找到了問(wèn)題的原因,我們就可以有針對(duì)性的進(jìn)行解決,這里用的count屬性很顯然不可能用final進(jìn)行修飾,不然就無(wú)法進(jìn)行自增處理。這里我們采用對(duì)象鎖的方式來(lái)進(jìn)行處理,也就鎖對(duì)象為當(dāng)前this或者說(shuō)是當(dāng)前類的實(shí)例對(duì)象。修改之后的代碼如下:

          public class SyncTest implements Runnable {

              private Integer count = 0;

              @Override
              public void run() {
                  synchronized (this) {
                      System.out.println(new Date() + " 開始休眠" + Thread.currentThread().getName());
                      count++;
                      try {
                          Thread.sleep(10000);
                          System.out.println(new Date() + " 結(jié)束休眠" + Thread.currentThread().getName());
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
              // ...
          }

          在上述代碼中鎖定了當(dāng)前對(duì)象,而當(dāng)前對(duì)象在這個(gè)示例中是同一個(gè)SyncTest的對(duì)象。

          再次執(zhí)行main方法,打印日志如下:

          Fri Jul 23 23:13:55 CST 2021 開始休眠Thread-0
          Fri Jul 23 23:14:05 CST 2021 結(jié)束休眠Thread-0
          Fri Jul 23 23:14:05 CST 2021 開始休眠Thread-1
          Fri Jul 23 23:14:15 CST 2021 結(jié)束休眠Thread-1

          可以看到,第一個(gè)線程完全執(zhí)行完畢之后,第二個(gè)線程才進(jìn)行執(zhí)行,達(dá)到預(yù)期的同步處理目標(biāo)。

          上面鎖定當(dāng)前對(duì)象還是有一個(gè)小缺點(diǎn),大家在使用時(shí)需要注意:比如該類有其他方法也使用了synchronized (this),那么由于兩個(gè)方法鎖定的都是當(dāng)前對(duì)象,其他方法也會(huì)進(jìn)行阻塞。所以通常情況下,建議每個(gè)方法鎖定各自定義的對(duì)象。

          比如,單獨(dú)定義一個(gè)private的變量,然后進(jìn)行鎖定:

          public class SyncTest implements Runnable {

              private Integer count = 0;

              private final Object locker = new Object();

              @Override
              public void run() {
                  synchronized (locker) {
                      System.out.println(new Date() + " 開始休眠" + Thread.currentThread().getName());
                      count++;
                      try {
                          Thread.sleep(10000);
                          System.out.println(new Date() + " 結(jié)束休眠" + Thread.currentThread().getName());
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          synchronized使用小常識(shí)

          在使用synchronized時(shí),我們首先要搞清楚它鎖定的是哪個(gè)對(duì)象,這能幫助我們?cè)O(shè)計(jì)更安全的多線程程式。

          在使用和設(shè)計(jì)鎖時(shí),我們還要了解一下知識(shí)點(diǎn):

          • 對(duì)象建議定義為private的,然后通過(guò)getter方法訪問(wèn)。而不是定義為public/protected,否則外界能夠繞過(guò)同步方法的控制而直接取得對(duì)象并改變它。這也是JavaBean的標(biāo)準(zhǔn)實(shí)現(xiàn)方式之一。
          • 當(dāng)鎖定對(duì)象為數(shù)組或ArrayList等類型時(shí),getter方法獲得的對(duì)象仍可以被改變,這時(shí)就需要將get方法也加上synchronized同步,并且只返回這個(gè)private對(duì)象的clone()。這樣,調(diào)用端得到的就是對(duì)象副本的引用了。
          • 無(wú)論synchronized關(guān)鍵字加在方法上還是對(duì)象上,取得的鎖都是對(duì)象,而不是把一段代碼或函數(shù)當(dāng)作鎖。同步方法很可能還會(huì)被其他線程的對(duì)象訪問(wèn);
          • 每個(gè)對(duì)象只有一個(gè)鎖(lock)和之相關(guān)聯(lián);
          • 實(shí)現(xiàn)同步是要很大的系統(tǒng)開銷作為代價(jià)的,甚至可能造成死鎖,所以盡量避免無(wú)謂的同步控制;

          小結(jié)

          通過(guò)本文的實(shí)踐案例主要為大家輸出兩個(gè)關(guān)鍵點(diǎn):第一,不要忽視IDE對(duì)代碼的提示信息,某些提示真的很有用,如果深挖還能發(fā)現(xiàn)很多性能問(wèn)題或代碼bug;第二,對(duì)于多線程的運(yùn)用,不僅要全面了解相關(guān)的基礎(chǔ)知識(shí)點(diǎn),還需要盡可能的進(jìn)行壓測(cè),這樣才能讓問(wèn)題事先暴露出來(lái)。


          往期推薦

          《跟二師兄學(xué)Nacos吧》EXT-03篇 Nacos中此處為什么采用反射機(jī)制?

          《跟二師兄學(xué)Nacos吧》EXT-02篇 面試官問(wèn)工廠模式,你理解的對(duì)嗎?

          《跟二師兄學(xué)Nacos吧》EXT-01篇 看看Nacos是怎么活學(xué)活用簡(jiǎn)單工廠模式的!

          《跟二師兄學(xué)Nacos吧》第1篇 Nacos客戶端服務(wù)注冊(cè)源碼分析

          Spring Boot Actuator的端點(diǎn)都怎么用?咱用事實(shí)說(shuō)話!

          Spring Boot Actuator集成,難的是靈活運(yùn)用!



          如果你覺得這篇文章不錯(cuò),那么,下篇通常會(huì)更好。添加微信好友,可備注“加群”(微信號(hào):zhuan2quan)

          一篇文章就看透技術(shù)本質(zhì)的人,
            和花一輩子都看不清的人,
            注定是截然不同的搬磚生涯。
          ▲ 長(zhǎng)按關(guān)注”程序新視界“,洞察技術(shù)內(nèi)幕


          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  二区在线999 | 精品麻豆 | 免费黄色视频网站 | AV无码免费电影 | 操屄网站 |