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

          聊聊并發(fā)編程的10個坑

          共 36226字,需瀏覽 73分鐘

           ·

          2022-07-26 18:07

          前言

          對于從事后端開發(fā)的同學(xué)來說,并發(fā)編程肯定再熟悉不過了。

          說實話,在java中并發(fā)編程是一大難點,至少我是這么認(rèn)為的。不光理解起來比較費勁,使用起來更容易踩坑。

          不信,讓繼續(xù)往下面看。

          今天重點跟大家一起聊聊并發(fā)編程的10個坑,希望對你有幫助。

          1. SimpleDateFormat線程不安全

          在java8之前,我們對時間的格式化處理,一般都是用的SimpleDateFormat類實現(xiàn)的。例如:

          @Service
          public class SimpleDateFormatService {

              public Date time(String time) throws ParseException {
                  SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                  return dateFormat.parse(time);
              }
          }

          如果你真的這樣寫,是沒問題的。

          就怕哪天抽風(fēng),你覺得dateFormat是一段固定的代碼,應(yīng)該要把它抽取成常量。

          于是把代碼改成下面的這樣:

          @Service
          public class SimpleDateFormatService {

             private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

              public Date time(String time) throws ParseException {
                  return dateFormat.parse(time);
              }
          }

          dateFormat對象被定義成了靜態(tài)常量,這樣就能被所有對象共用。

          如果只有一個線程調(diào)用time方法,也不會出現(xiàn)問題。

          但Serivce類的方法,往往是被Controller類調(diào)用的,而Controller類的接口方法,則會被tomcat線程池調(diào)用。換句話說,可能會出現(xiàn)多個線程調(diào)用同一個Controller類的同一個方法,也就是會出現(xiàn)多個線程會同時調(diào)用time方法的情況。

          而time方法會調(diào)用SimpleDateFormat類的parse方法:

          @Override
          public Date parse(String text, ParsePosition pos) {
              ...
              Date parsedDate;
              try {
                  parsedDate = calb.establish(calendar).getTime();
                  ...
              } catch (IllegalArgumentException e) {
                  pos.errorIndex = start;
                  pos.index = oldStart;
                  return null;
              }
             return parsedDate;

          該方法會調(diào)用establish方法:

          Calendar establish(Calendar cal) {
              ...
              //1.清空數(shù)據(jù)
              cal.clear();
              //2.設(shè)置時間
              cal.set(...);
              //3.返回
              return cal;
          }

          其中的步驟1、2、3是非原子操作。

          但如果cal對象是局部變量還好,壞就壞在parse方法調(diào)用establish方法時,傳入的calendar是SimpleDateFormat類的父類DateFormat的成員變量:

          public abstract class DateFormat extends Forma {
              ....
              protected Calendar calendar;
              ...
          }

          這樣就可能會出現(xiàn)多個線程,同時修改同一個對象即:dateFormat,他的同一個成員變量即:Calendar值的情況。

          這樣可能會出現(xiàn),某個線程設(shè)置好了時間,又被其他的線程修改了,從而出現(xiàn)時間錯誤的情況。

          那么,如何解決這個問題呢?

          1. SimpleDateFormat類的對象不要定義成靜態(tài)的,可以改成方法的局部變量。
          2. 使用ThreadLocal保存SimpleDateFormat類的數(shù)據(jù)。
          3. 使用java8的DateTimeFormatter類。

          2. 雙重檢查鎖的漏洞

          單例模式無論在實際工作,還是在面試中,都出現(xiàn)得比較多。

          我們都知道,單例模式有:餓漢模式懶漢模式兩種。

          餓漢模式代碼如下:

          public class SimpleSingleton {
              //持有自己類的引用
              private static final SimpleSingleton INSTANCE = new SimpleSingleton();

              //私有的構(gòu)造方法
              private SimpleSingleton() {
              }
              //對外提供獲取實例的靜態(tài)方法
              public static SimpleSingleton getInstance() {
                  return INSTANCE;
              }
          }

          使用餓漢模式的好處是:沒有線程安全的問題,但帶來的壞處也很明顯。

          private static final SimpleSingleton INSTANCE = new SimpleSingleton();

          一開始就實例化對象了,如果實例化過程非常耗時,并且最后這個對象沒有被使用,不是白白造成資源浪費嗎?

          還真是啊。

          這個時候你也許會想到,不用提前實例化對象,在真正使用的時候再實例化不就可以了?

          這就是我接下來要介紹的:懶漢模式

          具體代碼如下:

          public class SimpleSingleton2 {

              private static SimpleSingleton2 INSTANCE;

              private SimpleSingleton2() {
              }

              public static SimpleSingleton2 getInstance() {
                  if (INSTANCE == null) {
                      INSTANCE = new SimpleSingleton2();
                  }
                  return INSTANCE;
              }
          }

          示例中的INSTANCE對象一開始是空的,在調(diào)用getInstance方法才會真正實例化。

          嗯,不錯不錯。但這段代碼還是有問題。

          假如有多個線程中都調(diào)用了getInstance方法,那么都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時默認(rèn)值是null。這樣會導(dǎo)致多個線程中同時創(chuàng)建INSTANCE對象,即INSTANCE對象被創(chuàng)建了多次,違背了只創(chuàng)建一個INSTANCE對象的初衷。

          為了解決餓漢模式懶漢模式各自的問題,于是出現(xiàn)了:雙重檢查鎖

          具體代碼如下:

          public class SimpleSingleton4 {

              private static SimpleSingleton4 INSTANCE;

              private SimpleSingleton4() {
              }

              public static SimpleSingleton4 getInstance() {
                  if (INSTANCE == null) {
                      synchronized (SimpleSingleton4.class{
                          if (INSTANCE == null) {
                              INSTANCE = new SimpleSingleton4();
                          }
                      }
                  }
                  return INSTANCE;
              }
          }

          需要在synchronized前后兩次判空。

          但我要告訴你的是:這段代碼有漏洞的。

          有什么問題?

          public static SimpleSingleton4 getInstance() {
              if (INSTANCE == null) {//1
                  synchronized (SimpleSingleton4.class{//2
                      if (INSTANCE == null) {//3
                          INSTANCE = new SimpleSingleton4();//4
                      }
                  }
              }
              return INSTANCE;//5
          }

          getInstance方法的這段代碼,我是按1、2、3、4、5這種順序?qū)懙模M舶催@個順序執(zhí)行。

          但是java虛擬機(jī)實際上會做一些優(yōu)化,對一些代碼指令進(jìn)行重排。重排之后的順序可能就變成了:1、3、2、4、5,這樣在多線程的情況下同樣會創(chuàng)建多次實例。重排之后的代碼可能如下:

          public static SimpleSingleton4 getInstance() {
              if (INSTANCE == null) {//1
                 if (INSTANCE == null) {//3
                     synchronized (SimpleSingleton4.class{//2
                          INSTANCE = new SimpleSingleton4();//4
                      }
                  }
              }
              return INSTANCE;//5
          }

          原來如此,那有什么辦法可以解決呢?

          答:可以在定義INSTANCE是加上volatile關(guān)鍵字。具體代碼如下:

          public class SimpleSingleton7 {

              private volatile static SimpleSingleton7 INSTANCE;

              private SimpleSingleton7() {
              }

              public static SimpleSingleton7 getInstance() {
                  if (INSTANCE == null) {
                      synchronized (SimpleSingleton7.class{
                          if (INSTANCE == null) {
                              INSTANCE = new SimpleSingleton7();
                          }
                      }
                  }
                  return INSTANCE;
              }
          }

          volatile關(guān)鍵字可以保證多個線程的可見性,但是不能保證原子性。同時它也能禁止指令重排

          雙重檢查鎖的機(jī)制既保證了線程安全,又比直接上鎖提高了執(zhí)行效率,還節(jié)省了內(nèi)存空間。

          3. volatile的原子性

          從前面我們已經(jīng)知道volatile,是一個非常不錯的關(guān)鍵字,它能保證變量在多個線程中的可見性,它也能禁止指令重排,但是不能保證原子性

          使用volatile關(guān)鍵字禁止指令重排,前面已經(jīng)說過了,這里就不聊了。

          可見性主要體現(xiàn)在:一個線程對某個變量修改了,另一個線程每次都能獲取到該變量的最新值。

          先一起看看反例:

          public class VolatileTest extends Thread {

              private  boolean stopFlag = false;

              public boolean isStopFlag() {
                  return stopFlag;
              }

              @Override
              public void run() {
                  try {
                      Thread.sleep(300);
                  } catch (InterruptedException e) {
                      e.printStackTrace();

                  }
                  stopFlag = true;
                  System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);
              }

              public static void main(String[] args) {
                  VolatileTest vt = new VolatileTest();
                  vt.start();

                  while (true) {
                      if (vt.isStopFlag()) {
                          System.out.println("stop");
                          break;
                      }
                  }
              }
          }

          上面這段代碼中,VolatileTest是一個Thread類的子類,它的成員變量stopFlag默認(rèn)是false,在它的run方法中修改成了true。

          然后在main方法的主線程中,用vt.isStopFlag()方法判斷,如果它的值是true時,則打印stop關(guān)鍵字。

          那么,如何才能讓stopFlag的值修改了,在主線程中通過vt.isStopFlag()方法,能夠獲取最新的值呢?

          正例如下:

          public class VolatileTest extends Thread {

              private volatile boolean stopFlag = false;

              public boolean isStopFlag() {
                  return stopFlag;
              }

              @Override
              public void run() {
                  try {
                      Thread.sleep(300);
                  } catch (InterruptedException e) {
                      e.printStackTrace();

                  }
                  stopFlag = true;
                  System.out.println(Thread.currentThread().getName() + " stopFlag = " + stopFlag);
              }

              public static void main(String[] args) {
                  VolatileTest vt = new VolatileTest();
                  vt.start();

                  while (true) {
                      if (vt.isStopFlag()) {
                          System.out.println("stop");
                          break;
                      }
                  }
              }
          }

          volatile關(guān)鍵字修飾stopFlag即可。

          下面重點說說volatile的原子性問題。

          使用多線程給count加1,代碼如下:

          public class VolatileTest {

              public volatile int count = 0;

              public void add() {
                  count++;
              }

              public static void main(String[] args) {
                  final VolatileTest test = new VolatileTest();
                  for (int i = 0; i < 20; i++) {
                      new Thread() {
                          @Override
                          public void run() {
                              for (int j = 0; j < 1000; j++) {
                                  test.add();
                              }
                          }

                          ;
                      }.start();
                  }
                  while (Thread.activeCount() > 2) {
                      //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  }

                  System.out.println(test.count);
              }
          }

          執(zhí)行結(jié)果每次都不一樣,但可以肯定的是count值每次都小于20000,比如:19999。

          這個例子中count是成員變量,雖說被定義成了volatile的,但由于add方法中的count++是非原子操作。在多線程環(huán)境中,count++的數(shù)據(jù)可能會出現(xiàn)問題。

          由此可見,volatile不能保證原子性

          那么,如何解決這個問題呢?

          答:使用synchronized關(guān)鍵字。

          改造后的代碼如下:

          public class VolatileTest {

              public int count = 0;

              public synchronized void add() {
                  count++;
              }

              public static void main(String[] args) {
                  final VolatileTest test = new VolatileTest();
                  for (int i = 0; i < 20; i++) {
                      new Thread() {
                          @Override
                          public void run() {
                              for (int j = 0; j < 1000; j++) {
                                  test.add();
                              }
                          }

                          ;
                      }.start();
                  }
                  while (Thread.activeCount() > 2) {
                      //保證前面的線程都執(zhí)行完
                      Thread.yield();
                  }

                  System.out.println(test.count);
              }
          }

          4. 死鎖

          死鎖可能是大家都不希望遇到的問題,因為一旦程序出現(xiàn)了死鎖,如果沒有外力的作用,程序?qū)恢碧幱谫Y源競爭的假死狀態(tài)中。

          死鎖代碼如下:

          public class DeadLockTest {

              public static String OBJECT_1 = "OBJECT_1";
              public static String OBJECT_2 = "OBJECT_2";

              public static void main(String[] args) {
                  LockA lockA = new LockA();
                  new Thread(lockA).start();

                  LockB lockB = new LockB();
                  new Thread(lockB).start();
              }

          }

          class LockA implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_1) {
                      try {
                          Thread.sleep(500);

                          synchronized (DeadLockTest.OBJECT_2) {
                              System.out.println("LockA");
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          class LockB implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_2) {
                      try {
                          Thread.sleep(500);

                          synchronized (DeadLockTest.OBJECT_1) {
                              System.out.println("LockB");
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          一個線程在獲取OBJECT_1鎖時,沒有釋放鎖,又去申請OBJECT_2鎖。而剛好此時,另一個線程獲取到了OBJECT_2鎖,也沒有釋放鎖,去申請OBJECT_1鎖。由于OBJECT_1和OBJECT_2鎖都沒有釋放,兩個線程將一起請求下去,陷入死循環(huán),即出現(xiàn)死鎖的情況。

          那么如果避免死鎖問題呢?

          4.1 縮小鎖的范圍

          出現(xiàn)死鎖的情況,有可能是像上面那樣,鎖范圍太大了導(dǎo)致的。

          那么解決辦法就是縮小鎖的范圍

          具體代碼如下:

          class LockA implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_1) {
                      try {
                          Thread.sleep(500);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                  synchronized (DeadLockTest.OBJECT_2) {
                       System.out.println("LockA");
                  }
              }
          }

          class LockB implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_2) {
                      try {
                          Thread.sleep(500);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
                  synchronized (DeadLockTest.OBJECT_1) {
                       System.out.println("LockB");
                  }
              }
          }

          在獲取OBJECT_1鎖的代碼塊中,不包含獲取OBJECT_2鎖的代碼。同時在獲取OBJECT_2鎖的代碼塊中,也不包含獲取OBJECT_1鎖的代碼。

          4.2 保證鎖的順序

          出現(xiàn)死鎖的情況說白了是,一個線程獲取鎖的順序是:OBJECT_1和OBJECT_2。而另一個線程獲取鎖的順序剛好相反為:OBJECT_2和OBJECT_1。

          那么,如果我們能保證每次獲取鎖的順序都相同,就不會出現(xiàn)死鎖問題。

          具體代碼如下:

          class LockA implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_1) {
                      try {
                          Thread.sleep(500);

                          synchronized (DeadLockTest.OBJECT_2) {
                              System.out.println("LockA");
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          class LockB implements Runnable {

              @Override
              public void run() {
                  synchronized (DeadLockTest.OBJECT_1) {
                      try {
                          Thread.sleep(500);

                          synchronized (DeadLockTest.OBJECT_2) {
                              System.out.println("LockB");
                          }
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }

          兩個線程,每個線程都是先獲取OBJECT_1鎖,再獲取OBJECT_2鎖。

          5. 沒釋放鎖

          在java中除了使用synchronized關(guān)鍵字,給我們所需要的代碼塊加鎖之外,還能通過Lock關(guān)鍵字加鎖。

          使用synchronized關(guān)鍵字加鎖后,如果程序執(zhí)行完畢,或者程序出現(xiàn)異常時,會自動釋放鎖。

          但如果使用Lock關(guān)鍵字加鎖后,需要開發(fā)人員在代碼中手動釋放鎖。

          例如:

          public class LockTest {

              private final ReentrantLock rLock = new ReentrantLock();

              public void fun() {
                  rLock.lock();

                  try {
                      System.out.println("fun");
                  } finally {
                      rLock.unlock();
                  }
              }
          }

          代碼中先創(chuàng)建一個ReentrantLock類的實例對象rLock,調(diào)用它的lock方法加鎖。然后執(zhí)行業(yè)務(wù)代碼,最后再finally代碼塊中調(diào)用unlock方法。

          但如果你沒有在finally代碼塊中,調(diào)用unlock方法手動釋放鎖,線程持有的鎖將不會得到釋放。

          6. HashMap導(dǎo)致內(nèi)存溢出

          HashMap在實際的工作場景中,使用頻率還是挺高的,比如:接收參數(shù),緩存數(shù)據(jù),匯總數(shù)據(jù)等等。

          但如果你在多線程的環(huán)境中使用HashMap,可能會導(dǎo)致非常嚴(yán)重的后果。

          @Service
          public class HashMapService {

              private Map<Long, Object> hashMap = new HashMap<>();

              public void add(User user) {
                  hashMap.put(user.getId(), user.getName());
              }
          }

          在HashMapService類中定義了一個HashMap的成員變量,在add方法中往HashMap中添加數(shù)據(jù)。在controller層的接口中調(diào)用add方法,會使用tomcat的線程池去處理請求,就相當(dāng)于在多線程的場景下調(diào)用add方法。

          在jdk1.7中,HashMap使用的數(shù)據(jù)結(jié)構(gòu)是:數(shù)組+鏈表。如果在多線程的情況下,不斷往HashMap中添加數(shù)據(jù),它會調(diào)用resize方法進(jìn)行擴(kuò)容。該方法在復(fù)制元素到新數(shù)組時,采用的頭插法,在某些情況下,會導(dǎo)致鏈表會出現(xiàn)死循環(huán)。

          死循環(huán)最終結(jié)果會導(dǎo)致:內(nèi)存溢出

          此外,如果HashMap中數(shù)據(jù)非常多,會導(dǎo)致鏈表很長。當(dāng)查找某個元素時,需要遍歷某個鏈表,查詢效率不太高。

          為此,jdk1.8之后,將HashMap的數(shù)據(jù)結(jié)構(gòu)改成了:數(shù)組+鏈表+紅黑樹

          如果同一個數(shù)組元素中的數(shù)據(jù)項小于8個,則還是用鏈表保存數(shù)據(jù)。如果大于8個,則自動轉(zhuǎn)換成紅黑樹。

          為什么要用紅黑樹?

          答:鏈表的時間復(fù)雜度是O(n),而紅黑樹的時間復(fù)雜度是O(logn),紅黑樹的復(fù)雜度是優(yōu)于鏈表的。

          既然這樣,為什么不直接使用紅黑樹?

          答:樹節(jié)點所占存儲空間是鏈表節(jié)點的兩倍,節(jié)點少的時候,盡管在時間復(fù)雜度上,紅黑樹比鏈表稍微好一些。但是由于紅黑樹所占空間比較大,HashMap綜合考慮之后,認(rèn)為節(jié)點數(shù)量少的時候用占存儲空間更多的紅黑樹不劃算。

          jdk1.8中HashMap就不會出現(xiàn)死循環(huán)?

          答:錯,它在多線程環(huán)境中依然會出現(xiàn)死循環(huán)。在擴(kuò)容的過程中,在鏈表轉(zhuǎn)換為樹的時候,for循環(huán)一直無法跳出,從而導(dǎo)致死循環(huán)。

          那么,如果想多線程環(huán)境中使用HashMap該怎么辦呢?

          答:使用ConcurrentHashMap

          7. 使用默認(rèn)線程池

          我們都知道jdk1.5之后,提供了ThreadPoolExecutor類,用它可以自定義線程池

          線程池的好處有很多,比如:

          1. 降低資源消耗:避免了頻繁的創(chuàng)建線程和銷毀線程,可以直接復(fù)用已有線程。而我們都知道,創(chuàng)建線程是非常耗時的操作。
          2. 提供速度:任務(wù)過來之后,因為線程已存在,可以拿來直接使用。
          3. 提高線程的可管理性:線程是非常寶貴的資源,如果創(chuàng)建過多的線程,不僅會消耗系統(tǒng)資源,甚至?xí)绊懴到y(tǒng)的穩(wěn)定。使用線程池,可以非常方便的創(chuàng)建、管理和監(jiān)控線程。

          當(dāng)然jdk為了我們使用更便捷,專門提供了:Executors類,給我們快速創(chuàng)建線程池。

          該類中包含了很多靜態(tài)方法:

          • newCachedThreadPool:創(chuàng)建一個可緩沖的線程,如果線程池大小超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
          • newFixedThreadPool:創(chuàng)建一個固定大小的線程池,如果任務(wù)數(shù)量超過線程池大小,則將多余的任務(wù)放到隊列中。
          • newScheduledThreadPool:創(chuàng)建一個固定大小,并且能執(zhí)行定時周期任務(wù)的線程池。
          • newSingleThreadExecutor:創(chuàng)建只有一個線程的線程池,保證所有的任務(wù)安裝順序執(zhí)行。

          在高并發(fā)的場景下,如果大家使用這些靜態(tài)方法創(chuàng)建線程池,會有一些問題。

          那么,我們一起看看有哪些問題?

          • newFixedThreadPool:允許請求的隊列長度是Integer.MAX_VALUE,可能會堆積大量的請求,從而導(dǎo)致OOM。
          • newSingleThreadExecutor:允許請求的隊列長度是Integer.MAX_VALUE,可能會堆積大量的請求,從而導(dǎo)致OOM。
          • newCachedThreadPool:允許創(chuàng)建的線程數(shù)是Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導(dǎo)致OOM。

          那我們該怎辦呢?

          優(yōu)先推薦使用ThreadPoolExecutor類,我們自定義線程池。

          具體代碼如下:

          ExecutorService threadPool = new ThreadPoolExecutor(
              8//corePoolSize線程池中核心線程數(shù)
              10//maximumPoolSize 線程池中最大線程數(shù)
              60//線程池中線程的最大空閑時間,超過這個時間空閑線程將被回收
              TimeUnit.SECONDS,//時間單位
              new ArrayBlockingQueue(500), //隊列
              new ThreadPoolExecutor.CallerRunsPolicy()); //拒絕策略

          順便說一下,如果是一些低并發(fā)場景,使用Executors類創(chuàng)建線程池也未嘗不可,也不能完全一棍子打死。在這些低并發(fā)場景下,很難出現(xiàn)OOM問題,所以我們需要根據(jù)實際業(yè)務(wù)場景選擇。

          8. @Async注解的陷阱

          之前在java并發(fā)編程中實現(xiàn)異步功能,一般是需要使用線程或者線程池

          線程池的底層也是用的線程。

          而實現(xiàn)一個線程,要么繼承Thread類,要么實現(xiàn)Runnable接口,然后在run方法中寫具體的業(yè)務(wù)邏輯代碼。

          開發(fā)spring的大神們,為了簡化這類異步操作,已經(jīng)幫我們把異步功能封裝好了。spring中提供了@Async注解,我們可以通過它即可開啟異步功能,使用起來非常方便。

          具體做法如下:

          1.在springboot的啟動類上面加上@EnableAsync注解。

          @EnableAsync
          @SpringBootApplication
          public class Application {

              public static void main(String[] args) {
                  SpringApplication.run(Application.classargs);
              }
          }

          2.在需要執(zhí)行異步調(diào)用的業(yè)務(wù)方法加上@Async注解。

          @Service
          public class CategoryService {

               @Async
               public void add(Category category) {
                  //添加分類
               }
          }

          3.在controller方法中調(diào)用這個業(yè)務(wù)方法。

          @RestController
          @RequestMapping("/category")
          public class CategoryController {

               @Autowired
               private CategoryService categoryService;
            
               @PostMapping("/add")
               public void add(@RequestBody category) {
                  categoryService.add(category);
               }
          }

          這樣就能開啟異步功能了。

          是不是很easy?

          但有個壞消息是:用@Async注解開啟的異步功能,會調(diào)用AsyncExecutionAspectSupport類的doSubmit方法。

          默認(rèn)情況會走else邏輯。

          而else的邏輯最終會調(diào)用doExecute方法:

          protected void doExecute(Runnable task) {
            Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
            thread.start();
          }

          我去,這不是每次都會創(chuàng)建一個新線程嗎?

          沒錯,使用@Async注解開啟的異步功能,默認(rèn)情況下,每次都會創(chuàng)建一個新線程。

          如果在高并發(fā)的場景下,可能會產(chǎn)生大量的線程,從而導(dǎo)致OOM問題。

          建議大家在@Async注解開啟的異步功能時,請別忘了定義一個線程池

          9. 自旋鎖浪費cpu資源

          在并發(fā)編程中,自旋鎖想必大家都已經(jīng)耳熟能詳了。

          自旋鎖有個非常經(jīng)典的使用場景就是:CAS(即比較和交換),它是一種無鎖化思想(說白了用了一個死循環(huán)),用來解決高并發(fā)場景下,更新數(shù)據(jù)的問題。

          而atomic包下的很多類,比如:AtomicInteger、AtomicLong、AtomicBoolean等,都是用CAS實現(xiàn)的。

          我們以AtomicInteger類為例,它的incrementAndGet沒有每次都給變量加1。

          public final int incrementAndGet() {
              return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
          }

          它的底層就是用的自旋鎖實現(xiàn)的:

          public final int getAndAddInt(Object var1, long var2, int var4) {
            int var5;
            do {
                var5 = this.getIntVolatile(var1, var2);
            } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

              return var5;
          }

          在do...while死循環(huán)中,不停進(jìn)行數(shù)據(jù)的比較和交換,如果一直失敗,則一直循環(huán)重試。

          如果在高并發(fā)的情況下,compareAndSwapInt會很大概率失敗,因此導(dǎo)致了此處cpu不斷的自旋,這樣會嚴(yán)重浪費cpu資源。

          那么,如果解決這個問題呢?

          答:使用LockSupport類的parkNanos方法。

          具體代碼如下:

          private boolean compareAndSwapInt2(Object var1, long var2, int var4, int var5) {
               if(this.compareAndSwapInt(var1,var2,var4, var5)) {
                    return true;
                } else {
                    LockSupport.parkNanos(10);
                    return false;
                }
           }

          當(dāng)cas失敗之后,調(diào)用LockSupport類的parkNanos方法休眠一下,相當(dāng)于調(diào)用了Thread.Sleep方法。這樣能夠有效的減少頻繁自旋導(dǎo)致cpu資源過度浪費的問題。

          10. ThreadLocal用完沒清空

          在java中保證線程安全的技術(shù)有很多,可以使用synchroized、Lock等關(guān)鍵字給代碼塊加鎖。

          但是它們有個共同的特點,就是加鎖會對代碼的性能有一定的損耗。

          其實,在jdk中還提供了另外一種思想即:用空間換時間

          沒錯,使用ThreadLocal類就是對這種思想的一種具體體現(xiàn)。

          ThreadLocal為每個使用變量的線程提供了一個獨立的變量副本,這樣每一個線程都能獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。

          ThreadLocal的用法大致是這樣的:

          1. 先創(chuàng)建一個CurrentUser類,其中包含了ThreadLocal的邏輯。
          public class CurrentUser {
              private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
              
              public static void set(UserInfo userInfo) {
                  THREA_LOCAL.set(userInfo);
              }
              
              public static UserInfo get() {
                 THREA_LOCAL.get();
              }
              
              public static void remove() {
                 THREA_LOCAL.remove();
              }
          }
          1. 在業(yè)務(wù)代碼中調(diào)用CurrentUser類。
          public void doSamething(UserDto userDto) {
             UserInfo userInfo = convert(userDto);
             CurrentUser.set(userInfo);
             ...

             //業(yè)務(wù)代碼
             UserInfo userInfo = CurrentUser.get();
             ...
          }

          在業(yè)務(wù)代碼的第一行,將userInfo對象設(shè)置到CurrentUser,這樣在業(yè)務(wù)代碼中,就能通過CurrentUser.get()獲取到剛剛設(shè)置的userInfo對象。特別是對業(yè)務(wù)代碼調(diào)用層級比較深的情況,這種用法非常有用,可以減少很多不必要傳參。

          但在高并發(fā)的場景下,這段代碼有問題,只往ThreadLocal存數(shù)據(jù),數(shù)據(jù)用完之后并沒有及時清理。

          ThreadLocal即使使用了WeakReference(弱引用)也可能會存在內(nèi)存泄露問題,因為 entry對象中只把key(即threadLocal對象)設(shè)置成了弱引用,但是value值沒有。

          那么,如何解決這個問題呢?

          public void doSamething(UserDto userDto) {
             UserInfo userInfo = convert(userDto);
             
             try{
               CurrentUser.set(userInfo);
               ...
               
               //業(yè)務(wù)代碼
               UserInfo userInfo = CurrentUser.get();
               ...
             } finally {
                CurrentUser.remove();
             }
          }

          需要在finally代碼塊中,調(diào)用remove方法清理沒用的數(shù)據(jù)。

          以上就是關(guān)于并發(fā)編程中的10個坑介紹以及解決方案的全部內(nèi)容,希望本篇文章能給大家提供幫助,也希望大家多多支持一下。

          w3cschool編程獅


          專門學(xué)習(xí)編程的學(xué)習(xí)網(wǎng)站


          同時,也歡迎各位朋友們添加我們學(xué)習(xí)顧問的微信 ↓,可以領(lǐng)取10G免費學(xué)習(xí)資料包,也可以申請加入學(xué)習(xí)交流群(包括前端、Python、Java、Php、C、小程序、數(shù)據(jù)庫等)。


          編程獅學(xué)習(xí)顧問-七七

          添加時請備注(咨詢

          如果您覺得本篇文章還不錯
          點贊、在看、分享三連支持一下
          瀏覽 21
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  日韩在线播放欧美字幕 | 91人妻人人澡人人精品 | 老色鬼久久综合 | 国产一级二级三级在线观看 | 精品久久久久久久久久久久久久 |