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

          每日一例 | 多線程編程之可重入鎖

          共 6484字,需瀏覽 13分鐘

           ·

          2021-05-09 20:24

          在目前web的開發(fā)大環(huán)境下,高并發(fā),高可用的應用場景越來越普遍,對我們的要求也越來越要求越高了,為了應對這樣超高的要求(比如多線程環(huán)境下的數據共享問題),我們必須掌握很多常用的技術方案,比如鎖(Lock)(就是在某個方法或資源上加鎖,確保同一時間段內只有我們可以訪問該資源),這樣才能寫出更可靠的應用程序,今天我們就一起來看下一個很常用的鎖——可重入鎖(ReentrantLock)。

          在開始今天的內容之前,我們先考慮這樣一個場景:我們有一個審核業(yè)務,同一級的審核人員有兩個,但是業(yè)務只能審核一次,不能重復審核。

          如上圖,如果整個審核方法不加鎖的情況下,很可能發(fā)生同一筆數據審核兩次的情況。因為審核過程會涉及多個步驟,假如第一個人員在查詢未審核數據后,進行業(yè)務審核(處在第三步),但是尚未提交審核結果,這時候第二個人進來,也是查了未審核數據(第二步),由于第一個人員未提交審核結果,這時候數據依然是未審核,然后第二個人開始審核,這時候第一個人提交了審核結果,然后緊接著第二個人提交審核結果。最后,審核結果就會變成兩條。

          接下來,我們講的內容,就是為了解決這樣的額應用場景。

          一個不加鎖的案例

          在開始可重入鎖的介紹之前,我們先看一個和上面類似的例子,算是簡化版:

          public class Example {
              private static int i;
              public static void main(String[] args) throws InterruptedException {
                  ThreadPoolExecutor executor = new ThreadPoolExecutor(5101, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
                  for (int j = 0; j < 1000; j++) {
                      Thread.sleep(10L);
                      final int finalJ = j;
                      executor.submit(() -> test(finalJ));
                  }
                  executor.shutdown();
              }

              public static void test(int j) {
                  System.out.println("==第" + j + "次調用==start");
                  i ++;
                  Thread.sleep(20L);
                  i ++;
                  System.out.println(i);
                  System.out.println("==第" + j + "次調用==end");
              }

          }

          上面這段代碼其實就是模擬多線程共享數據(就是這里的i),并對數據進行操作的一個示例,運行結果可以很直觀的說明,不加鎖的情況下,在一個線程未執(zhí)行完方法之前,另一個方法也會進入方法執(zhí)行。按照我們代碼的邏輯,應該是先打印start,然后打印i的值,然后再打印end,但是實際情況卻并發(fā)如此,往往可能是這樣的:

          上面的運行結果很直觀的說明,在第1995次未正常運行結束時,第1996次已經開始了,同樣在第1996次未運行完的時候,第1998次都開始了。而且不論你運行多少次,上面的結果都大同小異。

          這時候,如果我們將代碼調整一下,加上鎖,看下會發(fā)生什么:

          public class Example {
              // 可重入鎖
              private static final ReentrantLock mainLock = new ReentrantLock();
              private static int i;
              public static void main(String[] args) throws InterruptedException {
                  ThreadPoolExecutor executor = new ThreadPoolExecutor(5101, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(100));
                  for (int j = 0; j < 1000; j++) {
                      Thread.sleep(10L);
                      final int finalJ = j;
                      executor.submit(() -> testLock(finalJ));
                  }
                  executor.shutdown();
              }

              public static void testLock(int j) {
                  final ReentrantLock reentrantLock = mainLock;
                  // 如果被其它線程占用鎖,會阻塞在此等待鎖釋放
                  reentrantLock.lock();
                  try {
                      System.out.println("==第" + j + "次調用==start");
                      i ++;
                      Thread.sleep(20L);
                      i ++;
                      System.out.println(i);
                      System.out.println("==第" + j + "次調用==end");
                  } catch (Exception e) {
                      e.printStackTrace();
                  } finally {
                      // 執(zhí)行完之后必須釋放鎖
                      reentrantLock.unlock();
                  }
              }

          }

          然后我們運行一下:

          這時候,你會發(fā)現,無論你運行多少次,都是像上面這樣規(guī)整,也和我們的代碼邏輯是一致的,這其實就是加鎖的作用,目的就是為了控制資源的訪問秩序。

          當然,上面的代碼其實還是存在問題的,因為在循環(huán)中使用線程池本身就是不合理的,當單個線程執(zhí)行時間較長,for中啟動前程前的業(yè)務響應比較快的時候(就是這里的Thread.sleep(10L);),所有的壓力都會到線程池上,會把線程池的資源耗盡,然后報如下錯誤:

          這時候解決方法有兩個,一個就是人為增加線程啟動前的業(yè)務處理時間,這里就是增加睡眠時間,比如調整到Thread.sleep(20L);;另一個是提高線程中的業(yè)務處理效率,只要比前面的業(yè)務處理快就行,但是在實際業(yè)務中,這個是不可能的;最好的解決方法是重構業(yè)務邏輯,想辦法把for循環(huán)放進線程里面,我之前修復的異步線程問題就用的是這個方法。好了,下面開始理論方面的學習。

          什么是可重入鎖

          可重入鎖,顧名思義就是可以重復加鎖的一種鎖,它是指,線程可對同一把鎖進行重復加鎖,而不會被阻塞住,這樣可避免死鎖的產生。

          加鎖的方式

          它的加鎖方式有三種,分別是locktrylocktrylock(long,TimeUnit)。上面我們加鎖的方法只是其中一種,也是最簡單的。

          可以看到ReentrantLock的使用方式比較簡單,創(chuàng)建出一個ReentrantLock對象,通過lock()方法進行加鎖,使用unlock()方法進行釋放鎖操作。

          使用lock來獲取鎖的話,如果鎖被其他線程持有,那么就會處于等待狀態(tài)。同時,需要我們去主動的調用``unlock`方法去釋放鎖,即使發(fā)生異常,它也不會主動釋放鎖,需要我們顯式的釋放。

          使用trylock方法獲取鎖,是有返回值的,獲取成功返回true,獲取失敗返回false,不會一直處于等待狀態(tài)。

          使用trylock(long,TimeUnit)指定時間參數來獲取鎖,在等待時間內獲取到鎖返回true,超時返回false。還可以調用lockInterruptibly方法去中斷鎖,如果線程正在等待獲取鎖,可以中斷線程的等待狀態(tài)。

          總結

          關于鎖這一塊,其實內容比較多,涉及的知識也比較雜,不僅包括javasynchronized、原子類、鎖等這些線程安全的知識,還包括數據的行級鎖、表級鎖等內容,如果是分布式應用,還需要考慮分布式鎖的實現,這里面還涉及了redis的知識,想要完全掌握還是難度很大的,但是隨著我們一點點的學習和應用,你慢慢會掌握很多常用的技術和解決方案,你會更清楚各種鎖和技術的應用場景,你會涉及出更優(yōu)秀的高并發(fā)高可用的系統(tǒng),為了實現這個目標,讓我們一起學習,一起遇見更好的自己,加油吧!

          項目路徑:

          https://github.com/Syske/example-everyday

          本項目會每日更新,讓我們一起學習,一起進步,遇見更好的自己,加油呀

          - END -


          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产在线视频卡一卡二 | 亚洲日韩一区二区三区四区丨高清 | 欧美精品一二三区 | 色婷婷视频在线 | 男人天堂v在线 |