高併發環境下低級死循環bug

業務背景

         在內存中,對mq消息進行分類計數。

問題描述

         生產環境,運行一段時間後,發現消息隊列有大量堆積。如果把計數邏輯註釋掉,只接收用戶訪問消息而不進行處理,則mq隊列無堆積。mqdump信息如下:

ConsumeMessageThread_75    TID: 214 STATE: WAITING

ConsumeMessageThread_75    sun.misc.Unsafe.park(Native Method)

ConsumeMessageThread_75    java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)

ConsumeMessageThread_75   java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)

ConsumeMessageThread_75    java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:867)

ConsumeMessageThread_75   java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1197)

ConsumeMessageThread_75    java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:214)

ConsumeMessageThread_75   java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:290)

ConsumeMessageThread_75   com.youku.paycenter.acl.service.impl.AsyncAclServiceImpl.getAtomicLong(AsyncAclServiceImpl.java:72)

ConsumeMessageThread_75   com.youku.paycenter.acl.service.impl.AsyncAclServiceImpl.count(AsyncAclServiceImpl.java:57)

ConsumeMessageThread_75   com.youku.paycenter.acl.mq.consumer.AclCountConsumer.receive(AclCountConsumer.java:70)

ConsumeMessageThread_75   com.youku.paycenter.mq.rocketmq.RocketMqPushConsumer.syncHandleMessage(RocketMqPushConsumer.java:207)

ConsumeMessageThread_75   com.youku.paycenter.mq.rocketmq.RocketMqPushConsumer.consumeMessage(RocketMqPushConsumer.java:191)

ConsumeMessageThread_75   com.alibaba.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService$ConsumeRequest.run(ConsumeMessageConcurrentlyService.java:142)

ConsumeMessageThread_75   java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)

ConsumeMessageThread_75   java.util.concurrent.FutureTask.run(FutureTask.java:262)

ConsumeMessageThread_75   java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)

ConsumeMessageThread_75    java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)

ConsumeMessageThread_75    java.lang.Thread.run(Thread.java:745)

 

       分析發現有部分消息消費的線程處於等待狀態,代碼指向AsyncAclServiceImpl.getAtomicLong(AsyncAclServiceImpl.java:72)

問題代碼

privatefinal AtomicLong getAtomicLong(Stringkey)
{
   AtomicLong atomicLong =
tempData.get(key); // 註釋1
  
while (null== atomicLong) // 註釋2
   {
     
try
     
{
        
lock.lock(); // 註釋3
        
while (null== tempData.get(key)) // 註釋4
         {
            atomicLong =
new AtomicLong(0);
           
tempData.put(key, atomicLong);
         }
      }
finally
     
{
        
lock.unlock();
      }
   }
  
return atomicLong;
}

問題分析

         看問題代碼,該段代碼的功能是查詢tempData(ConcurrentHashMap)中是否緩存的有計數器,沒有的話創建一個計數器,放入緩存,然後返回。有的話,直接返回。假設併發情況下有2個線程同時執行註釋1,然後依次經過註釋2到達註釋3處的代碼。假設第一個線程獲得了鎖,進入了內層while循環,此時緩存依舊爲空,因此會進行創建然後放入緩存的動作,然後退出,然後釋放鎖。之後,第2個線程在註釋3處被喚醒,再次執行註釋4的時候發現條件已經不成立,於是釋放鎖,進入外層循環,這時候問題應該很明顯了,因爲atomicLong在註釋1處已經執行,只不過當時拿到的是null,於是在最外層,第2個線程進入了死循環。

優化代碼

private final AtomicLong getAtomicLong(String key)
{
   AtomicLong atomicLong =
tempData.get(key);
  
while (null == atomicLong)
   {
     
try
     
{
        
lock.lock();
        
if (null == (atomicLong = tempData.get(key))) // 註釋1
         {
            atomicLong =
new AtomicLong(0);
           
tempData.put(key, atomicLong);
         }
      }
finally
     
{
        
lock.unlock();
      }
   }
  
return atomicLong;
}

         改動點非常簡單,在註釋1處,把內層循環替換爲if,最主要的是判斷的時候同時對atomicLong進行賦值操作。

心得

         編寫高併發代碼的時候,除了要格外細心,還需要儘可能的模擬真實環境的數據進行併發測試,找有經驗的同事進行代碼審查也是非常有必要的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章