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

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

          共 8319字,需瀏覽 17分鐘

           ·

          2021-06-23 21:25

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

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

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

           

          多線程問題

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

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

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

          安全性問題

          例如有一段很簡(jiǎn)單的扣庫(kù)存功能操作,如下:

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

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

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

          • 讀取count的值

          • 將值減一

          • 最后把計(jì)算結(jié)果賦值給count

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

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

          decrement()方法這個(gè)導(dǎo)致發(fā)生競(jìng)態(tài)條件的代碼區(qū)被稱為臨界區(qū)

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

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

          活躍性問題

          活躍性問題指的是,某個(gè)操作因?yàn)樽枞蜓h(huán),無法繼續(xù)執(zhí)行下去

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

          死鎖

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

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

          如何預(yù)防死鎖

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

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

          • Thread 1的加鎖順序?yàn)锳、B、C這樣的。
          • Thread 2的加鎖順序?yàn)锳、C,這樣就不會(huì)死鎖。
          如果Thread2的加鎖順序?yàn)锽、A或者C、A這樣順序就不一致了,就會(huì)出現(xiàn)死鎖問題。
          2.盡量用超時(shí)放棄機(jī)制
          Lock接口提供了tryLock(long time, TimeUnit unit)方法,該方法可以按照固定時(shí)長(zhǎng)等待鎖,因此線程可以在獲取鎖超時(shí)以后,主動(dòng)釋放之前已經(jīng)獲得的所有的鎖??梢员苊馑梨i問題
          活鎖
          活鎖與死鎖非常相似,也是程序一直等不到結(jié)果,但對(duì)比于死鎖,活鎖是活的,什么意思呢?因?yàn)檎谶\(yùn)行的線程并沒有阻塞,它始終在運(yùn)行中,卻一直得不到結(jié)果
          饑餓
          饑餓是指線程需要某些資源時(shí)始終得不到,尤其是CPU 資源,就會(huì)導(dǎo)致線程一直不能運(yùn)行而產(chǎn)生的問題。
          在 Java 中有線程優(yōu)先級(jí)的概念,Java 中優(yōu)先級(jí)分為 1 到 10,1 最低,10 最高。
          如果我們把某個(gè)線程的優(yōu)先級(jí)設(shè)置為 1,這是最低的優(yōu)先級(jí),在這種情況下,這個(gè)線程就有可能始終分配不到 CPU 資源,而導(dǎo)致長(zhǎng)時(shí)間無法運(yùn)行。
          性能問題
          線程本身的創(chuàng)建、以及線程之間的切換都要消耗資源,如果頻繁的創(chuàng)建線程或者CPU在線程調(diào)度花費(fèi)的時(shí)間遠(yuǎn)大于線程運(yùn)行的時(shí)間,使用線程反而得不償失,甚至造成CPU負(fù)載過高或者OOM的后果


          舉例說明

          線程不安全類

          案例1

          使用線程不安全集合(ArrayList、HashMap等)要進(jìn)行同步,最好使用線程安全的并發(fā)集合
          在多線程環(huán)境下,對(duì)線程不安全的集合遍歷進(jìn)行操作時(shí),可能會(huì)拋出ConcurrentModificationException的異常,也就是常說的fail-fast機(jī)制
          下面例子模擬了多個(gè)線程同時(shí)對(duì)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();

          進(jìn)到拋異常的ArrayList源碼中,可以看到遍歷ArrayList是通過內(nèi)部實(shí)現(xiàn)的迭代器完成的
          調(diào)用迭代器的next()方法獲取下一個(gè)元素時(shí),會(huì)先通過checkForComodification()方法檢查modCountexpectedModCount是否相等,若不相等則拋出ConcurrentModificationException
          modCount是ArrayList的屬性,表示集合結(jié)構(gòu)被修改的次數(shù)(列表長(zhǎng)度發(fā)生變化的次數(shù)),每次調(diào)用add或remove等方法都會(huì)使modCount加1
          expectedModCount是迭代器的屬性,在迭代器實(shí)例創(chuàng)建時(shí)被賦與和遍歷前modCount相等的值(expectedModCount=modCount
          所以當(dāng)有其他線程添加或刪除集合元素時(shí),modCount會(huì)增加,然后集合遍歷時(shí)expectedModCount不等于modCount,就會(huì)拋出異常
          使用加鎖機(jī)制操作線程不安全的集合類

          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關(guān)鍵字
              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關(guān)鍵字鎖住對(duì)list的操作,就不會(huì)拋出異常。不過用synchronized相當(dāng)于把鎖住的代碼塊串行化,性能上是不占優(yōu)勢(shì)的
          推薦使用線程安全的并發(fā)工具類
          JDK1.5加入了很多線程安全的工具類供使用,如CopyOnWriteArrayList、ConcurrentHashMap等并發(fā)容器
          日常開發(fā)中推薦使用這些工具類來實(shí)現(xiàn)多線程編程

          案例2

          不要將SimpleDateFormat作為全局變量使用
          SimpleDateFormat實(shí)際上是一個(gè)線程不安全的類,其根本原因是SimpleDateFormat的內(nèi)部實(shí)現(xiàn)對(duì)一些共享變量的操作沒有進(jìn)行同步

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

          public static void main(String[] args) {
            //兩個(gè)線程同時(shí)調(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使用
          最簡(jiǎn)單的做法是將SimpleDateFormat作為局部變量使用即可
          但如果是在for循環(huán)中使用,會(huì)創(chuàng)建很多實(shí)例,可以優(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引入的新特性,它們不僅是線程安全的,而且使用更方便
          推薦在實(shí)際開發(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));


          鎖的正確釋放

          假設(shè)有這樣一段偽代碼:

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

          這段代碼中在finally代碼塊釋放鎖之前,執(zhí)行了一段業(yè)務(wù)邏輯
          假如不巧這段邏輯中依賴服務(wù)不可用導(dǎo)致占用鎖的線程不能成功釋放鎖,會(huì)造成其他線程因無法獲取鎖而阻塞,最終線程池被打滿的問題
          所以在釋放鎖之前;finally子句中應(yīng)該只有對(duì)當(dāng)前線程占有的資源(如鎖、IO流等)進(jìn)行釋放的一些處理
          還有就是獲取鎖時(shí)設(shè)置合理的超時(shí)時(shí)間
          為了避免線程因獲取不到鎖而一直阻塞,可以設(shè)置一個(gè)超時(shí)時(shí)間,當(dāng)獲取鎖超時(shí)后,線程可以拋出異?;蚍祷匾粋€(gè)錯(cuò)誤的狀態(tài)碼。其中超時(shí)時(shí)間的設(shè)置也要合理,不應(yīng)過長(zhǎng),并且應(yīng)該大于鎖住的業(yè)務(wù)邏輯的執(zhí)行時(shí)間。


          正確使用線程池

          案例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í)行該方法時(shí),入?yún)⒌膌ist長(zhǎng)度有多大就會(huì)創(chuàng)建多少個(gè)線程池,并且方法執(zhí)行完后也沒有及時(shí)調(diào)用shutdown()方法將線程池銷毀
          這樣的話,隨著不斷有請(qǐng)求進(jìn)來,線程池占用的內(nèi)存會(huì)越來越多,就會(huì)導(dǎo)致頻繁fullGC甚至OOM。每次方法調(diào)用都創(chuàng)建線程池是很不合理的,因?yàn)檫@和自己頻繁創(chuàng)建、銷毀線程沒有區(qū)別,不僅沒有利用線程池的優(yōu)勢(shì),反而還會(huì)耗費(fèi)線程池所需的更多資源
          所以盡量將線程池作為全局變量使用

          案例2

          謹(jǐn)慎使用默認(rèn)的線程池靜態(tài)方法

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

          上述三個(gè)默認(rèn)線程池的風(fēng)險(xiǎn)點(diǎn):
          newFixedThreadPool創(chuàng)建的線程池corePoolSize和maximumPoolSize值是相等的,使用的阻塞隊(duì)列是LinkedBlockingQueue。
          newSingleThreadExecutor將corePoolSize和maximumPoolSize都設(shè)置為1,也使用的LinkedBlockingQueue
          LinkedBlockingQueue默認(rèn)容量為Integer.MAX_VALUE=2147483647,對(duì)于真正的機(jī)器來說,可以被認(rèn)為是無界隊(duì)列
          • newFixedThreadPool和newSingleThreadExecutor在運(yùn)行的線程數(shù)超過corePoolSize時(shí),后來的請(qǐng)求會(huì)都被放到阻塞隊(duì)列中等待,因?yàn)樽枞?duì)列設(shè)置的過大,后來請(qǐng)求不能快速失敗而長(zhǎng)時(shí)間阻塞,就可能造成請(qǐng)求端的線程池被打滿,拖垮整個(gè)服務(wù)。
          newCachedThreadPool將corePoolSize設(shè)置為0,將maximumPoolSize設(shè)置為Integer.MAX_VALUE,阻塞隊(duì)列使用的SynchronousQueue,SynchronousQueue不會(huì)保存等待執(zhí)行的任務(wù)
          • 所以newCachedThreadPool是來了任務(wù)就創(chuàng)建線程運(yùn)行,而maximumPoolSize相當(dāng)于無限的設(shè)置,使得創(chuàng)建的線程數(shù)可能會(huì)將機(jī)器內(nèi)存占滿。
          所以需要根據(jù)自身業(yè)務(wù)和硬件配置創(chuàng)建自定義線程池

          線程數(shù)建議

          線程池corePoolSize數(shù)量設(shè)置建議
          1.CPU密集型應(yīng)用
          CPU密集的意思是任務(wù)需要進(jìn)行大量復(fù)雜的運(yùn)算,幾乎沒有阻塞,需要CPU長(zhǎng)時(shí)間高速運(yùn)行。
          一般公式:corePoolSize=CPU核數(shù)+1個(gè)線程。JVM可運(yùn)行的CPU核數(shù)可以通過Runtime.getRuntime().availableProcessors()查看。
          2.IO密集型應(yīng)用
          IO密集型任務(wù)會(huì)涉及到很多的磁盤讀寫或網(wǎng)絡(luò)傳輸,線程花費(fèi)更多的時(shí)間在IO阻塞上,而不是CPU運(yùn)算。一般的業(yè)務(wù)應(yīng)用都屬于IO密集型。
          參考公式:最佳線程數(shù)=CPU數(shù)/(1-阻塞系數(shù));   阻塞系數(shù)=線程等待時(shí)間/(線程等待時(shí)間+CPU處理時(shí)間) 。
          IO密集型任務(wù)的CPU處理時(shí)間往往遠(yuǎn)小于線程等待時(shí)間,所以阻塞系數(shù)一般認(rèn)為在0.8-0.9之間,以4核單槽CPU為例,corePoolSize可設(shè)置為 4/(1-0.9)=40。當(dāng)然具體的設(shè)置還是要根據(jù)機(jī)器實(shí)際運(yùn)行中的各項(xiàng)指標(biāo)而定

          有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)

          歡迎大家關(guān)注Java之道公眾號(hào)


          好文章,我在看??

          瀏覽 60
          點(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>
                  国内久久 | 神马午夜三级片 | 大香蕉伊人在线视频观看最新 | 痴汉视频网站免费看视频 | 韩国经典一区二区在线 |