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

          10張圖告訴你多線程那些破事

          共 3675字,需瀏覽 8分鐘

           ·

          2021-04-17 12:41

          頭發(fā)很多的程序員:『師父,這個批量處理接口太慢了,有什么辦法可以優(yōu)化?』

          架構(gòu)師:『試試使用多線程優(yōu)化』

          第二天

          頭發(fā)很多的程序員:『師父,我已經(jīng)使用了多線程,為什么接口還變慢了?』

          架構(gòu)師:『去給我買杯咖啡,我寫篇文章告訴你』

          ……吭哧吭哧買咖啡去了


          在實際工作中,錯誤使用多線程非但不能提高效率還可能使程序崩潰。以在路上開車為例:

          在一個單向行駛的道路上,每輛汽車都遵守交通規(guī)則,這時候整體通行是正常的。『單向車道』意味著『一個線程』,『多輛車』意味著『多個job任務(wù)』。

          單線程順利同行

          如果需要提升車輛的同行效率,一般的做法就是擴展車道,對應(yīng)程序來說就是『加線程池』,增加線程數(shù)。這樣在同一時間內(nèi),通行的車輛數(shù)遠(yuǎn)遠(yuǎn)大于單車道。

          多線程順利同行

          然而成年人的世界沒有那么完美,車道一旦多起來『加塞』的場景就會越來越多,出現(xiàn)碰撞后也會影響整條馬路的通行效率。這么一對比下來『多車道』確實可能比『單車道』要慢。

          多線程故障

          防止汽車頻繁變道加塞可以采取在車道間增加『護欄』,那在程序的世界該怎么做呢?

          程序世界中多線程遇到的問題歸納起來就是三類:『線程安全問題』『活躍性問題』、『性能問題』,接下來會講解這些問題,以及問題對應(yīng)的解決手段。

          線程安全問題

          有時候我們會發(fā)現(xiàn),明明在單線程環(huán)境中正常運行的代碼,在多線程環(huán)境中可能會出現(xiàn)意料之外的結(jié)果,其實這就是大家常說的『線程不安全』。那到底什么是線程不安全呢?往下看。

          原子性

          舉一個銀行轉(zhuǎn)賬的例子,比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元,兩個操作都成功才意味著一次轉(zhuǎn)賬最終成功。

          試想一下,如果這兩個操作不具備原子性,從A的賬戶扣減了1000元之后,操作突然終止了,賬戶B沒有增加1000元,那問題就大了。

          銀行轉(zhuǎn)賬這個例子有兩個步驟,出現(xiàn)了意外后導(dǎo)致轉(zhuǎn)賬失敗,說明沒有原子性。

          原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

          原子操作:即不會被線程調(diào)度機制打斷的操作,沒有上下文切換。

          在并發(fā)編程中很多操作都不是原子操作,出個小題目:

          i = 0// 操作1
          i++;   // 操作2
          i = j; // 操作3
          i = i + 1// 操作4

          上面這四個操作中有哪些是原子操作,哪些不是的?不熟悉的人可能認(rèn)為這些都是原子操作,其實只有操作1是原子操作。

          • 操作1:對基本數(shù)據(jù)類型變量的賦值是原子操作;
          • 操作2:包含三個操作,讀取i的值,將i加1,將值賦給i;
          • 操作3:讀取j的值,將j的值賦給i;
          • 操作4:包含三個操作,讀取i的值,將i加1,將值賦給i;

          在單線程環(huán)境下上述四個操作都不會出現(xiàn)問題,但是在多線程環(huán)境下,如果不通過加鎖操作,往往可能得到意料之外的值。

          在Java語言中通過可以使用synchronize或者lock來保證原子性。

          可見性

          talk is cheap,先show一段代碼:

          class Test {
            int i = 50;
            int j = 0;
            
            public void update() {
              // 線程1執(zhí)行
              i = 100;
            }
            
            public int get() {
              // 線程2執(zhí)行
              j = i;
              return j;
            }
          }

          線程1執(zhí)行update方法將 i 賦值為100,一般情況下線程1會在自己的工作內(nèi)存中完成賦值操作,卻沒有及時將新值刷新到主內(nèi)存中。

          這個時候線程2執(zhí)行g(shù)et方法,首先會從主內(nèi)存中讀取i的值,然后加載到自己的工作內(nèi)存中,這個時候讀取到i的值是50,再將50賦值給j,最后返回j的值就是50了。原本期望返回100,結(jié)果返回50,這就是可見性問題,線程1對變量i進行了修改,線程2沒有立即看到i的新值。

          可見性:指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

          如上圖每個線程都有屬于自己的工作內(nèi)存,工作內(nèi)存和主內(nèi)存間需要通過store和load等進行交互。

          為了解決多線程可見性問題,Java語言提供了volatile這個關(guān)鍵字。當(dāng)一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當(dāng)有其他線程需要讀取時,它會去內(nèi)存中讀取新值。而普通共享變量不能保證可見性,因為變量被修改后什么時候刷回到主存是不確定的,另外一個線程讀的可能就是舊值。

          當(dāng)然Java的鎖機制如synchronize和lock也是可以保證可見性的,加鎖可以保證在同一時刻只有一個線程在執(zhí)行同步代碼塊,釋放鎖之前會將變量刷回至主存,這樣也就保證了可見性。

          關(guān)于線程不安全的表現(xiàn)還有『有序性』,這個問題會在后面的文章中深入講解。

          活躍性問題

          上面講到為了解決可見性問題,我們可以采取加鎖方式解決,但是如果加鎖使用不當(dāng)也容易引入其他問題,比如『死鎖』。

          在說『死鎖』前我們先引入另外一個概念:活躍性問題。

          活躍性是指某件正確的事情最終會發(fā)生,當(dāng)某個操作無法繼續(xù)下去的時候,就會發(fā)生活躍性問題。

          概念是不是有點拗口,如果看不懂也沒關(guān)系,你可以記住活躍性問題一般有這樣幾類:死鎖,活鎖饑餓問題。

          (1)死鎖

          死鎖是指多個線程因為環(huán)形的等待鎖的關(guān)系而永遠(yuǎn)的阻塞下去。一圖勝千語,不多解釋。

          (2)活鎖

          死鎖是兩個線程都在等待對方釋放鎖導(dǎo)致阻塞。而活鎖的意思是線程沒有阻塞,還活著呢。

          當(dāng)多個線程都在運行并且修改各自的狀態(tài),而其他線程彼此依賴這個狀態(tài),導(dǎo)致任何一個線程都無法繼續(xù)執(zhí)行,只能重復(fù)著自身的動作和修改自身的狀態(tài),這種場景就是發(fā)生了活鎖。

          ![](/Users/ray/Library/Application Support/typora-user-images/image-20210408232019843.png)

          如果大家還有疑惑,那我再舉一個生活中的例子,大家平時在走路的時候,迎面走來一個人,兩個人互相讓路,但是又同時走到了一個方向,如果一直這樣重復(fù)著避讓,這倆人就是發(fā)生了活鎖,學(xué)到了吧,嘿嘿。

          (3)饑餓

          如果一個線程無其他異常卻遲遲不能繼續(xù)運行,那基本是處于饑餓狀態(tài)了。

          常見有幾種場景:

          • 高優(yōu)先級的線程一直在運行消耗CPU,所有的低優(yōu)先級線程一直處于等待;
          • 一些線程被永久堵塞在一個等待進入同步塊的狀態(tài),而其他線程總是能在它之前持續(xù)地對該同步塊進行訪問;

          有一個非常經(jīng)典的饑餓問題就是哲學(xué)家用餐問題,如下圖所示,有五個哲學(xué)家在用餐,每個人必須要同時拿兩把叉子才可以開始就餐,如果哲學(xué)家1和哲學(xué)家3同時開始就餐,那哲學(xué)家2、4、5就得餓肚子等待了。

          性能問題

          前面講到了線程安全和死鎖、活鎖這些問題會影響多線程執(zhí)行過程,如果這些都沒有發(fā)生,多線程并發(fā)一定比單線程串行執(zhí)行快嗎,答案是不一定,因為多線程有創(chuàng)建線程線程上下文切換的開銷。

          創(chuàng)建線程是直接向系統(tǒng)申請資源的,對操作系統(tǒng)來說創(chuàng)建一個線程的代價是十分昂貴的,需要給它分配內(nèi)存、列入調(diào)度等。

          線程創(chuàng)建完之后,還會遇到線程上下文切換。

          CPU是很寶貴的資源速度也非??欤瑸榱吮WC雨露均沾,通常為給不同的線程分配時間片,當(dāng)CPU從執(zhí)行一個線程切換到執(zhí)行另一個線程時,CPU 需要保存當(dāng)前線程的本地數(shù)據(jù),程序指針等狀態(tài),并加載下一個要執(zhí)行的線程的本地數(shù)據(jù),程序指針等,這個開關(guān)被稱為『上下文切換』。

          一般減少上下文切換的方法有:無鎖并發(fā)編程CAS 算法、使用協(xié)程等。

          有態(tài)度的總結(jié)

          多線程用好了可以讓程序的效率成倍提升,用不好可能比單線程還要慢。

          用一張圖總結(jié)一下上面講的:


          -- END --

          雖然講了多線程并發(fā)會遇到的問題,你可能也發(fā)現(xiàn)了,文章中并沒有給出具體的解決方案,因為這些問題在Java語言設(shè)計過程中大神都已經(jīng)為你考慮過了。

          Java并發(fā)編程學(xué)起來有一定難度,但這也是從初級程序員邁向中高級程序員的必經(jīng)道路!加油吧!

          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产人妻人伦精品一区二区网站 | 久操大香蕉在线视频 | 欧美日韩性交 | 人妻夜夜爽天天爽麻豆三区网站 | 日本少妇 ╳乄 黑人 |