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

          日常工作中最容易犯的幾個(gè)并發(fā)錯(cuò)誤

          共 6515字,需瀏覽 14分鐘

           ·

          2021-04-25 21:54

          走過路過不要錯(cuò)過

          點(diǎn)擊藍(lán)字關(guān)注我們


          前言

          列舉大家平時(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)真看完這篇文章


          END


          關(guān)注作者微信公眾號(hào) —《JAVA爛豬皮》


          了解更多java后端架構(gòu)知識(shí)以及最新面試寶典


          你點(diǎn)的每個(gè)好看,我都認(rèn)真當(dāng)成了


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

          瀏覽 45
          點(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>
                  一道夲一二三区区 | 色之综合天天综合色天天棕色 | 做爱视频网 | 国产亚洲本日 | 日产一区二区三区视频 |