最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java多線程基本概念以及避坑指南

 更新時間:2021年09月17日 10:33:03   作者:小姐姐味道  
多線程之于進程的理解,可以類比多進程之于操作系統(tǒng),多線程指在單個程序中可以同時運行多個不同的線程執(zhí)行不同的任務(wù),這篇文章主要給大家介紹了關(guān)于Java多線程基本概念以及避坑指南的相關(guān)資料,需要的朋友可以參考下

前言

多核的機器,現(xiàn)在已經(jīng)非常常見了。即使是一塊手機,也都配備了強勁的多核處理器。通過多進程和多線程的手段,就可以讓多個CPU同時工作,來加快任務(wù)的執(zhí)行。

多線程,是編程中一個比較高級的話題。由于它涉及到共享資源的操作,所以在編碼時非常容易出現(xiàn)問題。Java的concurrent包,提供了非常多的工具,來幫助我們簡化這些變量的同步,但學(xué)習(xí)應(yīng)用之路依然充滿了曲折。

本篇文章,將簡單的介紹一下Java中多線程的基本知識。然后著重介紹一下初學(xué)者在多線程編程中一些最容易出現(xiàn)問題的地方,很多都是血淚經(jīng)驗。規(guī)避了這些坑,就相當(dāng)于規(guī)避了90%兇殘的多線程bug。

1. 多線程基本概念

1.1 輕量級進程

在JVM中,一個線程,其實是一個輕量級進程(LWP)。所謂的輕量級進程,其實是用戶進程調(diào)用系統(tǒng)內(nèi)核,所提供的一套接口。實際上,它還要調(diào)用更加底層的內(nèi)核線程(KLT)。

實際上,JVM的線程創(chuàng)建銷毀以及調(diào)度等,都是依賴于操作系統(tǒng)的。如果你看一下Thread類里面的多個函數(shù),你會發(fā)現(xiàn)很多都是native的,直接調(diào)用了底層操作系統(tǒng)的函數(shù)。

下圖是JVM在Linux上簡單的線程模型。

可以看到,不同的線程在進行切換的時候,會頻繁在用戶態(tài)和內(nèi)核態(tài)進行狀態(tài)轉(zhuǎn)換。這種切換的代價是比較大的,也就是我們平常所說的上下文切換(Context Switch)。

1.2 JMM

在介紹線程同步之前,我們有必要介紹一個新的名詞,那就是JVM的內(nèi)存模型JMM。

JMM并不是說堆、metaspace這種內(nèi)存的劃分,它是一個完全不同的概念,指的是與線程相關(guān)的Java運行時線程內(nèi)存模型。

由于Java代碼在執(zhí)行的時候,很多指令都不是原子的,如果這些值的執(zhí)行順序發(fā)生了錯位,就會獲得不同的結(jié)果。比如,i++的動作就可以翻譯成以下的字節(jié)碼。

getfield      // Field value:I
iconst_1
iadd
putfield      // Field value:I

這還只是代碼層面的。如果再加上CPU每核的各級緩存,這個執(zhí)行過程會變得更加細膩。如果我們希望執(zhí)行完i++之后,再執(zhí)行i--,僅靠初級的字節(jié)碼指令,是無法完成的。我們需要一些同步手段。

上圖就是JMM的內(nèi)存模型,它分為主存儲器(Main Memory)和工作存儲器(Working Memory)兩種。我們平常在Thread中操作這些變量,其實是操作的主存儲器的一個副本。當(dāng)修改完之后,還需要重新刷到主存儲器上,其他的線程才能夠知道這些變化。

1.3 Java中常見的線程同步方式

為了完成JMM的操作,完成線程之間的變量同步,Java提供了非常多的同步手段。

  1. Java的基類Object中,提供了wait和notify的原語,來完成monitor之間的同步。不過這種操作我們在業(yè)務(wù)編程中很少遇見
  2. 使用synchronized對方法進行同步,或者鎖住某個對象以完成代碼塊的同步
  3. 使用concurrent包里面的可重入鎖。這套鎖是建立在AQS之上的
  4. 使用volatile輕量級同步關(guān)鍵字,實現(xiàn)變量的實時可見性
  5. 使用Atomic系列,完成自增自減
  6. 使用ThreadLocal線程局部變量,實現(xiàn)線程封閉
  7. 使用concurrent包提供的各種工具,比如LinkedBlockingQueue來實現(xiàn)生產(chǎn)者消費者。本質(zhì)還是AQS
  8. 使用Thread的join,以及各種await方法,完成并發(fā)任務(wù)的順序執(zhí)行

從上面的描述可以看出,多線程編程要學(xué)的東西可實在太多了。幸運的是,同步方式雖然千變?nèi)f化,但我們創(chuàng)建線程的方式卻沒幾種。

第一類就是Thread類。大家都知道有兩種實現(xiàn)方式。第一可以繼承Thread覆蓋它的run方法;第二種是實現(xiàn)Runnable接口,實現(xiàn)它的run方法;而第三種創(chuàng)建線程的方法,就是通過線程池。

其實,到最后,就只有一種啟動方式,那就是Thread。線程池和Runnable,不過是一種封裝好的快捷方式罷了。

多線程這么復(fù)雜,這么容易出問題,那常見的都有那些問題,我們又該如何避免呢?下面,我將介紹10個高頻出現(xiàn)的坑,并給出解決方案。

2. 避坑指南

2.1. 線程池打爆機器

首先,我們聊一個非常非常低級,但又產(chǎn)生了嚴重后果的多線程錯誤。

通常,我們創(chuàng)建線程的方式有Thread,Runnable和線程池三種。隨著Java1.8的普及,現(xiàn)在最常用的就是線程池方式。

有一次,我們線上的服務(wù)器出現(xiàn)了僵死,就連遠程ssh,都登錄不上,只能無奈的重啟。大家發(fā)現(xiàn),只要啟動某個應(yīng)用,過不了幾分鐘,就會出現(xiàn)這種情況。最終定位到了幾行讓人啼笑皆非的代碼。

有位對多線程不太熟悉的同學(xué),使用了線程池去異步處理消息。通常,我們都會把線程池作為類的靜態(tài)變量,或者是成員變量。但是這位同學(xué),卻將它放在了方法內(nèi)部。也就是說,每當(dāng)有一個請求到來的時候,都會創(chuàng)建一個新的線程池。當(dāng)請求量一增加,系統(tǒng)資源就被耗盡,最終造成整個機器的僵死。

void realJob(){
    ThreadPoolExecutor exe = new ThreadPoolExecutor(...);
    exe.submit(new Runnable(){...})
}

這種問題如何去避免?只能通過代碼review。所以多線程相關(guān)的代碼,哪怕是非常簡單的同步關(guān)鍵字,都要交給有經(jīng)驗的人去寫。即使沒有這種條件,也要非常仔細的對這些代碼進行review。

2.2. 鎖要關(guān)閉

相比較synchronized關(guān)鍵字加的獨占鎖,concurrent包里面的Lock提供了更多的靈活性??梢愿鶕?jù)需要,選擇公平鎖與非公平鎖、讀鎖與寫鎖。

但Lock用完之后是要關(guān)閉的,也就是lock和unlock要成對出現(xiàn),否則就容易出現(xiàn)鎖泄露,造成了其他的線程永遠了拿不到這個鎖。

如下面的代碼,我們在調(diào)用lock之后,發(fā)生了異常,try中的執(zhí)行邏輯將被中斷,unlock將永遠沒有機會執(zhí)行。在這種情況下,線程獲取的鎖資源,將永遠無法釋放。

private final Lock lock = new ReentrantLock();
void doJob(){
    try{
        lock.lock();
        //發(fā)生了異常
        lock.unlock();
    }catch(Exception e){
    }
}

正確的做法,就是將unlock函數(shù),放到finally塊中,確保它總是能夠執(zhí)行。

由于lock也是一個普通的對象,是可以作為函數(shù)的參數(shù)的。如果你把lock在函數(shù)之間傳來傳去的,同樣會有時序邏輯混亂的情況。在平時的編碼中,也要避免這種把lock當(dāng)參數(shù)的情況。

2.3. wait要包兩層

Object作為Java的基類,提供了四個方法wait wait(timeout) notify notifyAll ,用來處理線程同步問題,可以看出wait等函數(shù)的地位是多么的高大。在平常的工作中,寫業(yè)務(wù)代碼的同學(xué)使用這些函數(shù)的機率是比較小的,所以一旦用到很容易出問題。

但使用這些函數(shù)有一個非常大的前提,那就是必須使用synchronized進行包裹,否則會拋出IllegalMonitorStateException。比如下面的代碼,在執(zhí)行的時候就會報錯。

final Object condition = new Object();
public void func(){
 condition.wait();
}

類似的方法,還有concurrent包里的Condition對象,使用的時候也必須出現(xiàn)在lock和unlock函數(shù)之間。

為什么在wait之前,需要先同步這個對象呢?因為JVM要求,在執(zhí)行wait之時,線程需要持有這個對象的monitor,顯然同步關(guān)鍵字能夠完成這個功能。

但是,僅僅這么做,還是不夠的,wait函數(shù)通常要放在while循環(huán)里才行,JDK在代碼里做了明確的注釋。

重點:這是因為,wait的意思,是在notify的時候,能夠向下執(zhí)行邏輯。但在notify的時候,這個wait的條件可能已經(jīng)是不成立的了,因為在等待的這段時間里條件條件可能發(fā)生了變化,需要再進行一次判斷,所以寫在while循環(huán)里是一種簡單的寫法。

final Object condition = new Object();
public void func(){
 synchronized(condition){
  while(<條件成立>){
   condition.wait();
  }
 }
}

帶if條件的wait和notify要包兩層,一層synchronized,一層while,這就是wait等函數(shù)的正確用法。

2.4. 不要覆蓋鎖對象

使用synchronized關(guān)鍵字時,如果是加在普通方法上的,那么鎖的就是this對象;如果是加載static方法上的,那鎖的就是class。除了用在方法上,synchronized還可以直接指定要鎖定的對象,鎖代碼塊,達到細粒度的鎖控制。

如果這個鎖的對象,被覆蓋了會怎么樣?比如下面這個。

List listeners = new ArrayList();

void add(Listener listener, boolean upsert){
    synchronized(listeners){
        List results = new ArrayList();
        for(Listener ler:listeners){
        ...
        }
        listeners = results;
    }
}

上面的代碼,由于在邏輯中,強行給鎖listeners對象進行了重新賦值,會造成鎖的錯亂或者失效。

為了保險起見,我們通常把鎖對象聲明成final類型的。

final List listeners = new ArrayList();

或者直接聲明專用的鎖對象,定義成普通的Object對象即可。

final Object listenersLock = new Object();

2.5. 處理循環(huán)中的異常

在異步線程里處理一些定時任務(wù),或者執(zhí)行時間非常長的批量處理,是經(jīng)常遇到的需求。我就不止一次看到小伙伴們的程序執(zhí)行了一部分就停止的情況。

排查到這些中止的根本原因,就是其中的某行數(shù)據(jù)發(fā)生了問題,造成了整個線程的死亡。

我們還是來看一下代碼的模板。

volatile boolean run = true;
void loop(){
    while(run){
     for(Task task: taskList){
            //do . sth
            int a = 1/0;
     }
    }
}

在loop函數(shù)中,執(zhí)行我們真正的業(yè)務(wù)邏輯。當(dāng)執(zhí)行到某個task的時候,發(fā)生了異常。這個時候,線程并不會繼續(xù)運行下去,而是會拋出異常直接中止。在寫普通函數(shù)的時候,我們都知道程序的這種行為,但一旦到了多線程,很多同學(xué)都會忘了這一環(huán)。

值得注意的是,即使是非捕獲類型的NullPointerException,也會引起線程的中止。所以,時刻把要執(zhí)行的邏輯,放在try catch中,是個非常好的習(xí)慣。

volatile boolean run = true;
void loop(){
    while(run){
     for(Task task: taskList){
      try{
                //do . sth
                int a = 1/0;
      }catch(Exception ex){
       //log
      }
     }
    }
}

2.6. HashMap正確用法

HashMap在多線程環(huán)境下,會產(chǎn)生死循環(huán)問題。這個問題已經(jīng)得到了廣泛的普及,因為它會產(chǎn)生非常嚴重的后果:CPU跑滿,代碼無法執(zhí)行,jstack查看時阻塞在get方法上。

至于怎么提高HashMap效率,什么時候轉(zhuǎn)紅黑樹轉(zhuǎn)列表,這是陽春白雪的八股界話題,我們下里巴人只關(guān)注怎么不出問題。

網(wǎng)絡(luò)上有詳細的文章描述死循環(huán)問題產(chǎn)生的場景,大體因為HashMap在進行rehash時,會形成環(huán)形鏈。某些get請求會走到這個環(huán)上。JDK并不認為這是個bug,雖然它的影響比較惡劣。

如果你判斷你的集合類會被多線程使用,那就可以使用線程安全的ConcurrentHashMap來替代它。

HashMap還有一個安全刪除的問題,和多線程關(guān)系不大,但它拋出的是ConcurrentModificationException,看起來像是多線程的問題。我們一塊來看看它。

Map<String, String> map = new HashMap<>();
map.put("xjjdog0", "狗1");
map.put("xjjdog1", "狗2");
 
for (Map.Entry<String, String> entry : map.entrySet()) {
    String key = entry.getKey();
    if ("xjjdog0".equals(key)) {
       map.remove(key);
    }
}

上面的代碼會拋出異常,這是由于HashMap的Fail-Fast機制。如果我們想要安全的刪除某些元素,應(yīng)該使用迭代器。

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
   Map.Entry<String, String> entry = iterator.next();
   String key = entry.getKey();
   if ("xjjdog0".equals(key)) {
       iterator.remove();
   }
}

2.7. 線程安全的保護范圍

使用了線程安全的類,寫出來的代碼就一定是線程安全的么?答案是否定的。

線程安全的類,只負責(zé)它內(nèi)部的方法是線程安全的。如我我們在外面把它包了一層,那么它是否能達到線程安全的效果,就需要重新探討。

比如下面這種情況,我們使用了線程安全的ConcurrentHashMap來存儲計數(shù)。雖然ConcurrentHashMap本身是線程安全的,不會再出現(xiàn)死循環(huán)的問題。但addCounter函數(shù),明顯是不正確的,它需要使用synchronized函數(shù)包裹才行。

private final ConcurrentHashMap<String,Integer> counter;
public int addCounter(String name) {
    Integer current = counter.get(name);
    int newValue = ++current;
    counter.put(name,newValue);
    return newValue;
}

這是開發(fā)人員常踩的坑之一。要達到線程安全,需要看一下線程安全的作用范圍。如果更大維度的邏輯存在同步問題,那么即使使用了線程安全的集合,也達不到想要的效果。

2.8. volatile作用有限

volatile關(guān)鍵字,解決了變量的可見性問題,可以讓你的修改,立馬讓其他線程給讀到。

雖然這個東西在面試的時候問的挺多的,包括ConcurrentHashMap中隊volatile的那些優(yōu)化。但在平常的使用中,你真的可能只會接觸到boolean變量的值修改。

volatile boolean closed;  
  
public void shutdown() {   
    closed = true;   
}  

千萬不要把它用在計數(shù)或者線程同步上,比如下面這樣。

volatile count = 0;
void add(){
    ++count;
}

這段代碼在多線程環(huán)境下,是不準確的。這是因為volatile只保證可見性,不保證原子性,多線程操作并不能保證其正確性。

直接用Atomic類或者同步關(guān)鍵字多好,你真的在乎這納秒級別的差異么?

2.9. 日期處理要小心

很多時候,日期處理也會出問題。這是因為使用了全局的Calendar,SimpleDateFormat等。當(dāng)多個線程同時執(zhí)行format函數(shù)的時候,就會出現(xiàn)數(shù)據(jù)錯亂。

SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

Date getDate(String str){
    return format(str);
}

為了改進,我們通常將SimpleDateFormat放在ThreadLocal中,每個線程一份拷貝,這樣可以避免一些問題。當(dāng)然,現(xiàn)在我們可以使用線程安全的DateTimeFormatter了。

static DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
public static void main(String[] args) {
    ZonedDateTime zdt = ZonedDateTime.now();
    System.out.println(FOMATTER.format(zdt));
}

2.10. 不要在構(gòu)造函數(shù)中啟動線程

在構(gòu)造函數(shù),或者static代碼塊中啟動新的線程,并沒有什么錯誤。但是,強烈不推薦你這么做。

因為Java是有繼承的,如果你在構(gòu)造函數(shù)中做了這種事,那么子類的行為將變得非常魔幻。另外,this對象可能在構(gòu)造完畢之前,出遞到另外一個地方被使用,造成一些不可預(yù)料的行為。

所以把線程的啟動,放在一個普通方法,比如start中,是更好的選擇。它可以減少bug發(fā)生的機率。

End

wait和notify是非常容易出問題的地方,

編碼格式要求非常嚴格。synchronized關(guān)鍵字相對來說比較簡單,但同步代碼塊的時候依然有許多要注意的點。這些經(jīng)驗,在concurrent包所提供的各種API中依然實用。我們還要處理多線程邏輯中遇到的各種異常問題,避免中斷,避免死鎖。規(guī)避了這些坑,基本上多線程代碼寫起來就算是入門了。

許多java開發(fā),都是剛剛接觸多線程開發(fā),在平常的工作中應(yīng)用也不是很多。如果你做的是crud的業(yè)務(wù)系統(tǒng),那么寫一些多線程代碼的時候就更少了。但總有例外,你的程序變得很慢,或者排查某個問題,你會直接參與到多線程的編碼中來。

我們的各種工具軟件,也在大量使用多線程。從Tomcat,到各種中間件,再到各種數(shù)據(jù)庫連接池緩存等,每個地方都充斥著多線程的代碼。

即使是有經(jīng)驗的開發(fā),也會陷入很多多線程的陷阱。因為異步會造成時序的混亂,必須要通過強制的手段達到數(shù)據(jù)的同步。多線程運行,首先要保證準確性,使用線程安全的集合進行數(shù)據(jù)存儲;還要保證效率,畢竟使用多線程的目標就是如此。

希望本文中的這些實際案例,讓你對多線程的理解,更上一層樓。

到此這篇關(guān)于Java多線程基本概念以及避坑指南的文章就介紹到這了,更多相關(guān)Java多線程概念及避坑內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • springmvc請求轉(zhuǎn)發(fā)和重定向問題(攜帶參數(shù)和不攜帶參數(shù))

    springmvc請求轉(zhuǎn)發(fā)和重定向問題(攜帶參數(shù)和不攜帶參數(shù))

    這篇文章主要介紹了springmvc請求轉(zhuǎn)發(fā)和重定向問題(攜帶參數(shù)和不攜帶參數(shù)),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-10-10
  • java 實現(xiàn)計數(shù)排序和桶排序?qū)嵗a

    java 實現(xiàn)計數(shù)排序和桶排序?qū)嵗a

    這篇文章主要介紹了java 實現(xiàn)計數(shù)排序和桶排序?qū)嵗a的相關(guān)資料,需要的朋友可以參考下
    2017-02-02
  • Java 輸入流中的read(byte[] b)方法詳解

    Java 輸入流中的read(byte[] b)方法詳解

    這篇文章主要介紹了Java 輸入流中的read(byte[] b)方法詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • 最新評論

    永寿县| 巴中市| 莱阳市| 门头沟区| 永清县| 平湖市| 扶沟县| 安岳县| 巴彦淖尔市| 措勤县| 明溪县| 龙川县| 龙陵县| 鸡东县| 延长县| 石嘴山市| 休宁县| 黔西| 彰化市| 西乌珠穆沁旗| 舞阳县| 海原县| 濮阳市| 喀什市| 雅安市| 深州市| 新巴尔虎左旗| 策勒县| 五常市| 若羌县| 新田县| 太和县| 井研县| 天柱县| 南雄市| 九江县| 玛纳斯县| 栾川县| 民县| 高雄县| 林甸县|