日常工作中最容易犯的幾個(gè)并發(fā)錯(cuò)誤
前言
列舉大家平時(shí)在工作中最容易犯的幾個(gè)并發(fā)錯(cuò)誤,都是在實(shí)際項(xiàng)目代碼中看到的鮮活例子,希望對(duì)大家有幫助。
First Blood
線上總是出現(xiàn):ERROR 1062 (23000) Duplicate entry 'xxx' for key 'yyy',我們來看一下有問題的這段代碼:
UserBindInfo info = selectFromDB(userId);if(info == null){info = new UserBindInfo(userId,deviceId);insertIntoDB(info);}else{info.setDeviceId(deviceId);updateDB(info);}
在并發(fā)情況下,第一步判斷都為空,就會(huì)有2個(gè)或者多個(gè)線程進(jìn)入插入數(shù)據(jù)庫操作, 這時(shí)候就出現(xiàn)了同一個(gè)ID插入多次。
正確處理姿勢:
insert into UserBindInfo values(#{userId},#{deviceId}) on duplicate key update deviceId=#{deviceId}多次的情況,導(dǎo)致插入失敗。
一般情況下,可以用insert...on duplicate key update... 解決這個(gè)問題。
注意: 如果UserBindInfo表存在主鍵以及一個(gè)以上的唯一索引,在并發(fā)情況下,使用insert...on duplicate key,可能會(huì)產(chǎn)生死鎖(Mysql5.7),可以這樣處理:
try{UserBindInfoMapper.insertIntoDB(userBindInfo);}catch(DuplicateKeyException ex){UserBindInfoMapper.update(userBindInfo);}
Double Kill
小心你的全局變量,如下面這段代碼:
public class GlobalVariableConcurrentTest {private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1000));while (true){threadPoolExecutor.execute(()->{String dateString = sdf.format(new Date());try {Date parseDate = sdf.parse(dateString);String dateString2 = sdf.format(parseDate);System.out.println(dateString.equals(dateString2));} catch (ParseException e) {e.printStackTrace();}});}}}
可以看到有異常拋出

全局變量的SimpleDateFormat,在并發(fā)情況下,存在安全性問題,阿里Java規(guī)約明確要求謹(jǐn)慎使用它。
除了SimpleDateFormat,其實(shí)很多時(shí)候,面對(duì)全局變量,我們都需要考慮并發(fā)情況是否存在問題,如下
@Componentpublic class Test {public static List<String> desc = new ArrayList<>();public List<String> getDescByUserType(int userType) {if (userType == 1) {desc.add("普通會(huì)員不可以發(fā)送和查看郵件,請購買會(huì)員");return desc;} else if (userType == 2) {desc.add("恭喜你已經(jīng)是VIP會(huì)員,盡情的發(fā)郵件吧");return desc;}else {desc.add("你的身份未知");return desc;}}}
因?yàn)閐esc是全局變量,在并發(fā)情況下,請求getDescByUserType方法,得到的可能并不是你想要的結(jié)果。
Trible Kill
假設(shè)現(xiàn)在有如下業(yè)務(wù):控制同一個(gè)用戶訪問某個(gè)接口的頻率不能小于5秒。一般很容易想到使用redis的 setnx操作來控制并發(fā)訪問,于是有以下代碼:
if(RedisOperation.setnx(userId, 1)){RedisOperation.expire(userId,5,TimeUnit.SECONDS));//執(zhí)行正常業(yè)務(wù)邏輯}else{return “訪問過于頻繁”;}
假設(shè)執(zhí)行完setnx操作,還沒來得及設(shè)置expireTime,機(jī)器重啟或者突然崩潰,將會(huì)發(fā)生死鎖。該用戶id,后面執(zhí)行setnx永遠(yuǎn)將為false,這可能讓你永遠(yuǎn)損失那個(gè)用戶。
那么怎么解決這個(gè)問題呢,可以考慮用SET key value NX EX max-lock-time ,它是一種在 Redis 中實(shí)現(xiàn)鎖的方法,是原子性操作,不會(huì)像以上代碼分兩步執(zhí)行,先set再expire,它是一步到位。
客戶端執(zhí)行以上的命令:
如果服務(wù)器返回 OK ,那么這個(gè)客戶端獲得鎖。
如果服務(wù)器返回 NIL ,那么客戶端獲取鎖失敗,可以在稍后再重試。
設(shè)置的過期時(shí)間到達(dá)之后,鎖將自動(dòng)釋放
Quadra Kill
我們看一下有關(guān)ConcurrentHashMap的一段代碼,如下:
//全局變量Map<String, Integer> map = new ConcurrentHashMap();Integer value = count.get(k);if(value == null){map.put(k,1);}else{map.put(k,value+1);}
假設(shè)兩條線程都進(jìn)入 value==null,這一步,得出的結(jié)果是不是會(huì)變小?OK,客官先稍作休息,閉目養(yǎng)神一會(huì),我們驗(yàn)證一下,請看一個(gè)demo:
public static void main(String[] args) {for (int i = 0; i < 1000; i++) {testConcurrentMap();}}private static void testConcurrentMap() {final Map<String, Integer> count = new ConcurrentHashMap<>();ExecutorService executorService = Executors.newFixedThreadPool(2);final CountDownLatch endLatch = new CountDownLatch(2);Runnable task = ()-> {for (int i = 0; i < 5; i++) {Integer value = count.get("k");if (null == value) {System.out.println(Thread.currentThread().getName());count.put("k", 1);} else {count.put("k", value + 1);}}endLatch.countDown();};executorService.execute(task);executorService.execute(task);try {endLatch.await();if (count.get("k") < 10) {System.out.println(count);}} catch (Exception e) {e.printStackTrace();}
表面看,運(yùn)行結(jié)果應(yīng)該都是10對(duì)吧,好的,我們再看運(yùn)行結(jié)果 :

運(yùn)行結(jié)果出現(xiàn)了5,所以這樣實(shí)現(xiàn)是有并發(fā)問題的,那么正確的實(shí)現(xiàn)姿勢是啥呢?
Map<K,V> map = new ConcurrentHashMap();V v = map.get(k);if(v == null){v = new V();V old = map. putIfAbsent(k,v);if(old != null){v = old;}}
可以考慮使用putIfAbsent解決這個(gè)問題
(1)如果key是新的記錄,那么會(huì)向map中添加該鍵值對(duì),并返回null。
(2)如果key已經(jīng)存在,那么不會(huì)覆蓋已有的值,返回已經(jīng)存在的值
我們再來看看以下代碼以及運(yùn)行結(jié)果:
public static void main(String[] args) {for (int i = 0; i < 1000; i++) {testConcurrentMap();}}private static void testConcurrentMap() {ExecutorService executorService = Executors.newFixedThreadPool(2);final Map<String, AtomicInteger> map = Maps.newConcurrentMap();final CountDownLatch countDownLatch = new CountDownLatch(2);Runnable task = ()-> {AtomicInteger oldValue;for (int i = 0; i < 5; i++) {oldValue = map.get("k");if (null == oldValue) {AtomicInteger initValue = new AtomicInteger(0);oldValue = map.putIfAbsent("k", initValue);if (oldValue == null) {oldValue = initValue;}}oldValue.incrementAndGet();}countDownLatch.countDown();};executorService.execute(task);executorService.execute(task);try {countDownLatch.await();System.out.println(map);} catch (Exception e) {e.printStackTrace();}}

Penta Kill
現(xiàn)有如下業(yè)務(wù)場景:用戶手上有一張現(xiàn)金券,可以兌換相應(yīng)的現(xiàn)金,
錯(cuò)誤示范一
if(isAvailable(ticketId){1、給現(xiàn)金增加操作2、deleteTicketById(ticketId)}else{return “沒有可用現(xiàn)金券”}
解析: 假設(shè)有兩條線程A,B兌換現(xiàn)金,執(zhí)行順序如下:

1.線程A加現(xiàn)金
2.線程B加現(xiàn)金
3.線程A刪除票標(biāo)志
4.線程B刪除票標(biāo)志
顯然,這樣有問題了,已經(jīng)給用戶加了兩次現(xiàn)金了。
錯(cuò)誤示范2
if(isAvailable(ticketId){1、deleteTicketById(ticketId)2、給現(xiàn)金增加操作}else{return “沒有可用現(xiàn)金券”}
并發(fā)情況下,如果一條線程,第一步deleteTicketById刪除失敗了,也會(huì)多添加現(xiàn)金。
正確處理方案
if(deleteAvailableTicketById(ticketId) == 1){1、給現(xiàn)金增加操作}else{return “沒有可用現(xiàn)金券”}

騰訊、阿里、滴滴后臺(tái)面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因?yàn)槟銢]認(rèn)真看完這篇文章

關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識(shí)以及最新面試寶典


看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動(dòng)力
