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

          這些線程安全的坑,你在工作中踩了么?

          共 8518字,需瀏覽 18分鐘

           ·

          2021-04-22 20:53

          來源 | 月伴飛魚

          我們知道多線程能并發(fā)的處理多個任務,有效地提高復雜應用程序的性能,在實際開發(fā)中扮演著十分重要的角色

          但是使用多線程也帶來了很多風險,并且由線程引起的問題往往在測試中難以發(fā)現(xiàn),到了線上就會造成重大的故障和損失

          下面我會結(jié)合幾個實際案例,幫助大家在工作做規(guī)避這些問題

          多線程問題

          首先介紹下使用的多線程會有哪些問題

          使用多線程的問題很大程度上源于多個線程對同一變量的操作權,以及不同線程之間執(zhí)行順序的不確定性

          《Java并發(fā)編程實戰(zhàn)》這本書中提到了三種多線程的問題:安全性問題、活躍性問題和性能問題

          安全性問題

          例如有一段很簡單的扣庫存功能操作,如下:

          public int decrement(){
           return --count;//count初始庫存為10
          }

          在單線程環(huán)境下,這個方法能正確工作,但在多線程環(huán)境下,就會導致錯誤的結(jié)果

          --count看上去是一個操作,但實際上它包含三步(讀取-修改-寫入):

          • 讀取count的值
          • 將值減一
          • 最后把計算結(jié)果賦值給count

          如下圖展示了一種錯誤的執(zhí)行過程,當有兩個線程1、2同時執(zhí)行該方法時,它們讀取到count的值都是10,最后返回結(jié)果都是9;意味著可能有兩個人購買了商品,但庫存卻只減了1,這對于真實的生產(chǎn)環(huán)境是不可接受的

          像上面例子這樣由于不恰當?shù)膱?zhí)行時序?qū)е虏徽_結(jié)果的情況,是一種很常見的并發(fā)安全問題,被稱為競態(tài)條件

          decrement()方法這個導致發(fā)生競態(tài)條件的代碼區(qū)被稱為臨界區(qū)

          避免這種問題,需要保證讀取-修改-寫入這樣復合操作的原子性

          在Java中,有很多方式可以實現(xiàn),比如使用synchronize內(nèi)置鎖或ReentrantLock顯式鎖的加鎖機制、使用線程安全的原子類、以及采用CAS的方式等

          活躍性問題

          活躍性問題指的是,某個操作因為阻塞或循環(huán),無法繼續(xù)執(zhí)行下去

          最典型的有三種,分別為死鎖、活鎖和饑餓

          死鎖

          最常見的活躍性問題是死鎖

          死鎖是指多個線程之間相互等待獲取對方的鎖,又不會釋放自己占有的鎖,而導致阻塞使得這些線程無法運行下去就是死鎖,它往往是不正確的使用加鎖機制以及線程間執(zhí)行順序的不可預料性引起的

          如何預防死鎖

          1.盡量保證加鎖順序是一樣的

          例如有A,B,C三把鎖。

          • Thread 1的加鎖順序為A、B、C這樣的。

          • Thread 2的加鎖順序為A、C,這樣就不會死鎖。

          如果Thread2的加鎖順序為B、A或者C、A這樣順序就不一致了,就會出現(xiàn)死鎖問題。

          2.盡量用超時放棄機制

          Lock接口提供了tryLock(long time, TimeUnit unit)方法,該方法可以按照固定時長等待鎖,因此線程可以在獲取鎖超時以后,主動釋放之前已經(jīng)獲得的所有的鎖??梢员苊馑梨i問題

          活鎖

          活鎖與死鎖非常相似,也是程序一直等不到結(jié)果,但對比于死鎖,活鎖是活的,什么意思呢?因為正在運行的線程并沒有阻塞,它始終在運行中,卻一直得不到結(jié)果

          饑餓

          饑餓是指線程需要某些資源時始終得不到,尤其是CPU 資源,就會導致線程一直不能運行而產(chǎn)生的問題。

          在 Java 中有線程優(yōu)先級的概念,Java 中優(yōu)先級分為 1 到 10,1 最低,10 最高。

          如果我們把某個線程的優(yōu)先級設置為 1,這是最低的優(yōu)先級,在這種情況下,這個線程就有可能始終分配不到 CPU 資源,而導致長時間無法運行。

          性能問題

          線程本身的創(chuàng)建、以及線程之間的切換都要消耗資源,如果頻繁的創(chuàng)建線程或者CPU在線程調(diào)度花費的時間遠大于線程運行的時間,使用線程反而得不償失,甚至造成CPU負載過高或者OOM的后果

          舉例說明

          線程不安全類

          案例1

          使用線程不安全集合(ArrayList、HashMap等)要進行同步,最好使用線程安全的并發(fā)集合

          在多線程環(huán)境下,對線程不安全的集合遍歷進行操作時,可能會拋出ConcurrentModificationException的異常,也就是常說的fail-fast機制

          下面例子模擬了多個線程同時對ArrayList操作,線程t1遍歷list并打印,線程t2向list添加元素

          List<Integer> list = new ArrayList<>();
          list.add(0); 
          list.add(1); 
          list.add(2);  //list: [0,1,2]
          System.out.println(list);

          //線程t1遍歷打印list
          Thread t1 = new Thread(() -> {
            for(int i : list){
              System.out.println(i);
            }
          });  

          //線程t2向list添加元素
          Thread t2 = new Thread(() -> {
            for(int i = 3; i < 6; i++){
              list.add(i);
            }
          });

          t1.start();
          t2.start();

          進到拋異常的ArrayList源碼中,可以看到遍歷ArrayList是通過內(nèi)部實現(xiàn)的迭代器完成的

          調(diào)用迭代器的next()方法獲取下一個元素時,會先通過checkForComodification()方法檢查modCountexpectedModCount是否相等,若不相等則拋出ConcurrentModificationException

          modCount是ArrayList的屬性,表示集合結(jié)構被修改的次數(shù)(列表長度發(fā)生變化的次數(shù)),每次調(diào)用add或remove等方法都會使modCount加1

          expectedModCount是迭代器的屬性,在迭代器實例創(chuàng)建時被賦與和遍歷前modCount相等的值(expectedModCount=modCount

          所以當有其他線程添加或刪除集合元素時,modCount會增加,然后集合遍歷時expectedModCount不等于modCount,就會拋出異常

          使用加鎖機制操作線程不安全的集合類

          List<Integer> list = new ArrayList<>();
          list.add(0); 
          list.add(1); 
          list.add(2);
          System.out.println(list);

          //線程t1遍歷打印list
          Thread t1 = new Thread(() -> {
            synchronized (list){   //使用synchronized關鍵字
              for(int i : list){
                System.out.println(i);
              }
            }
          });  

          //線程t2向list添加元素
          Thread t2 = new Thread(() -> {
            synchronized (list){
              for(int i = 3; i < 6; i++){
                list.add(i);
                System.out.println(list);
              }
            }
          });  

          t1.start();
          t2.start();

          如上面代碼,用synchronized關鍵字鎖住對list的操作,就不會拋出異常。不過用synchronized相當于把鎖住的代碼塊串行化,性能上是不占優(yōu)勢的

          推薦使用線程安全的并發(fā)工具類

          JDK1.5加入了很多線程安全的工具類供使用,如CopyOnWriteArrayList、ConcurrentHashMap等并發(fā)容器

          日常開發(fā)中推薦使用這些工具類來實現(xiàn)多線程編程

          案例2

          不要將SimpleDateFormat作為全局變量使用

          SimpleDateFormat實際上是一個線程不安全的類,其根本原因是SimpleDateFormat的內(nèi)部實現(xiàn)對一些共享變量的操作沒有進行同步

          public static final SimpleDateFormat SDF_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

          public static void main(String[] args) {
            //兩個線程同時調(diào)用SimpleDateFormat.parse方法
            Thread t1 = new Thread(() -> {
              try {
                Date date1 = SDF_FORMAT.parse("2019-12-09 17:04:32");
              } catch (ParseException e) {
                e.printStackTrace();
              }
            });

            Thread t2 = new Thread(() -> {
              try {
                Date date2 = SDF_FORMAT.parse("2019-12-09 17:43:32");
              } catch (ParseException e) {
                e.printStackTrace();
              }
            });

            t1.start();
            t2.start();
          }

          建議將SimpleDateFormat作為局部變量使用,或者配合ThreadLocal使用

          最簡單的做法是將SimpleDateFormat作為局部變量使用即可

          但如果是在for循環(huán)中使用,會創(chuàng)建很多實例,可以優(yōu)化下配合ThreadLocal使用

          //初始化
          public static final ThreadLocal<SimpleDateFormat> SDF_FORMAT = new ThreadLocal<SimpleDateFormat>(){
            @Override
            protected SimpleDateFormat initialValue() {
              return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            }
          };
          //調(diào)用
          Date date = SDF_FORMAT.get().parse(wedDate);

          推薦使用Java8的LocalDateTime和DateTimeFormatter

          LocalDateTime和DateTimeFormatter是Java 8引入的新特性,它們不僅是線程安全的,而且使用更方便

          推薦在實際開發(fā)中用LocalDateTime和DateTimeFormatter替代Calendar和SimpleDateFormat

          DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
          LocalDateTime time = LocalDateTime.now();
          System.out.println(formatter.format(time));

          鎖的正確釋放

          假設有這樣一段偽代碼:

          Lock lock = new ReentrantLock();
          ...  
          try{
            lock.tryLock(timeout, TimeUnit.MILLISECONDS)
            //業(yè)務邏輯
          }
          catch (Exception e){
            //錯誤日志
            //拋出異?;蛑苯臃祷?/span>
          }
          finally {
            //業(yè)務邏輯
            lock.unlock();
          }
          ...

          這段代碼中在finally代碼塊釋放鎖之前,執(zhí)行了一段業(yè)務邏輯

          假如不巧這段邏輯中依賴服務不可用導致占用鎖的線程不能成功釋放鎖,會造成其他線程因無法獲取鎖而阻塞,最終線程池被打滿的問題

          所以在釋放鎖之前;finally子句中應該只有對當前線程占有的資源(如鎖、IO流等)進行釋放的一些處理

          還有就是獲取鎖時設置合理的超時時間

          為了避免線程因獲取不到鎖而一直阻塞,可以設置一個超時時間,當獲取鎖超時后,線程可以拋出異?;蚍祷匾粋€錯誤的狀態(tài)碼。其中超時時間的設置也要合理,不應過長,并且應該大于鎖住的業(yè)務邏輯的執(zhí)行時間。

          正確使用線程池

          案例1

          不要將線程池作為局部變量使用

          public void request(List<Id> ids) {
            for (int i = 0; i < ids.size(); i++) {
               ExecutorService threadPool = Executors.newSingleThreadExecutor();
            }
          }

          在for循環(huán)中創(chuàng)建線程池,那么每次執(zhí)行該方法時,入?yún)⒌膌ist長度有多大就會創(chuàng)建多少個線程池,并且方法執(zhí)行完后也沒有及時調(diào)用shutdown()方法將線程池銷毀

          這樣的話,隨著不斷有請求進來,線程池占用的內(nèi)存會越來越多,就會導致頻繁fullGC甚至OOM。每次方法調(diào)用都創(chuàng)建線程池是很不合理的,因為這和自己頻繁創(chuàng)建、銷毀線程沒有區(qū)別,不僅沒有利用線程池的優(yōu)勢,反而還會耗費線程池所需的更多資源

          所以盡量將線程池作為全局變量使用

          案例2

          謹慎使用默認的線程池靜態(tài)方法

          Executors.newFixedThreadPool(int);     //創(chuàng)建固定容量大小的線程池
          Executors.newSingleThreadExecutor();   //創(chuàng)建容量為1的線程池
          Executors.newCachedThreadPool();       //創(chuàng)建一個線程池,線程池容量大小為Integer.MAX_VALUE

          上述三個默認線程池的風險點:

          newFixedThreadPool創(chuàng)建的線程池corePoolSize和maximumPoolSize值是相等的,使用的阻塞隊列是LinkedBlockingQueue。

          newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置為1,也使用的LinkedBlockingQueue

          LinkedBlockingQueue默認容量為Integer.MAX_VALUE=2147483647,對于真正的機器來說,可以被認為是無界隊列

          • newFixedThreadPool和newSingleThreadExecutor在運行的線程數(shù)超過corePoolSize時,后來的請求會都被放到阻塞隊列中等待,因為阻塞隊列設置的過大,后來請求不能快速失敗而長時間阻塞,就可能造成請求端的線程池被打滿,拖垮整個服務。

          newCachedThreadPool將corePoolSize設置為0,將maximumPoolSize設置為Integer.MAX_VALUE,阻塞隊列使用的SynchronousQueue,SynchronousQueue不會保存等待執(zhí)行的任務

          • 所以newCachedThreadPool是來了任務就創(chuàng)建線程運行,而maximumPoolSize相當于無限的設置,使得創(chuàng)建的線程數(shù)可能會將機器內(nèi)存占滿。

          所以需要根據(jù)自身業(yè)務和硬件配置創(chuàng)建自定義線程池

          線程數(shù)建議

          線程池corePoolSize數(shù)量設置建議

          1.CPU密集型應用

          CPU密集的意思是任務需要進行大量復雜的運算,幾乎沒有阻塞,需要CPU長時間高速運行。

          一般公式:corePoolSize=CPU核數(shù)+1個線程。JVM可運行的CPU核數(shù)可以通過Runtime.getRuntime().availableProcessors()查看。

          2.IO密集型應用

          IO密集型任務會涉及到很多的磁盤讀寫或網(wǎng)絡傳輸,線程花費更多的時間在IO阻塞上,而不是CPU運算。一般的業(yè)務應用都屬于IO密集型。

          參考公式:最佳線程數(shù)=CPU數(shù)/(1-阻塞系數(shù));   阻塞系數(shù)=線程等待時間/(線程等待時間+CPU處理時間) 。

          IO密集型任務的CPU處理時間往往遠小于線程等待時間,所以阻塞系數(shù)一般認為在0.8-0.9之間,以4核單槽CPU為例,corePoolSize可設置為 4/(1-0.9)=40。當然具體的設置還是要根據(jù)機器實際運行中的各項指標而定


          1、全網(wǎng)最全 Java 日志框架適配方案!還有誰不會?
          2、Chrome瀏覽器最新高危漏洞曝光!升級最新版也沒用~
          3、Spring中毒太深,離開Spring我居然連最基本的接口都不會寫了
          4、黑客用GitHub服務器挖礦,三天跑了3萬個任務,代碼驚現(xiàn)中文
          5、瀏覽器輸入「xxxxhub」的背后.....
          6、Gradle真能干掉Maven?今天體驗了一把,賊爽!
          7、如何重構千行“又臭又長”的類?IntelliJ IDEA 幾分鐘就搞定!

          點分享

          點收藏

          點點贊

          點在看

          瀏覽 27
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  在线观看欧美日韩 | 嗯啊啊网站 | 国产欧美久久精品 | 日倝黄色视屏 | 黄色在线观看网 |