業務背景
在內存中,對mq消息進行分類計數。
問題描述
生產環境,運行一段時間後,發現消息隊列有大量堆積。如果把計數邏輯註釋掉,只接收用戶訪問消息而不進行處理,則mq隊列無堆積。mq棧dump信息如下:
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進行賦值操作。
心得
編寫高併發代碼的時候,除了要格外細心,還需要儘可能的模擬真實環境的數據進行併發測試,找有經驗的同事進行代碼審查也是非常有必要的。