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

          7張圖帶你輕松理解Java 線(xiàn)程安全

          共 1628字,需瀏覽 4分鐘

           ·

          2020-11-11 17:03

          鏈接:juejin.im/post/5d2c97bff265da1bc552954b

          ??? ?

          ? ?正文 ??


          /? ?什么是線(xiàn)程? ?/


          按操作系統(tǒng)中的描述,線(xiàn)程是 CPU 調(diào)度的最小單元,直觀(guān)來(lái)說(shuō)線(xiàn)程就是代碼按順序執(zhí)行下來(lái),執(zhí)行完畢就結(jié)束的一條線(xiàn)。


          舉個(gè) ?,富土康的一個(gè)組裝車(chē)間相當(dāng)于 CPU ,而線(xiàn)程就是當(dāng)前車(chē)間里的一條條作業(yè)流水線(xiàn)。為了提高產(chǎn)能和效率,車(chē)間里一般都會(huì)有多條流水線(xiàn)同時(shí)作業(yè)。同樣在我們 Android 開(kāi)發(fā)中多線(xiàn)程可以說(shuō)是隨處可見(jiàn)了,如執(zhí)行耗時(shí)操作,網(wǎng)絡(luò)請(qǐng)求、文件讀寫(xiě)、數(shù)據(jù)庫(kù)讀寫(xiě)等等都會(huì)開(kāi)單獨(dú)的子線(xiàn)程來(lái)執(zhí)行。


          那么你的線(xiàn)程是安全的嗎?線(xiàn)程安全的原理又是什么呢?(本文內(nèi)容是個(gè)人學(xué)習(xí)總結(jié)淺見(jiàn),如有錯(cuò)誤的地方,望大佬們輕拍指正)


          /? ?線(xiàn)程安全? ?/


          了解線(xiàn)程安全的之前先來(lái)了解一下 Java 的內(nèi)存模型,先搞清楚線(xiàn)程是怎么工作的。


          ?Java 內(nèi)存模型 - JMM


          什么是 JMM


          JMM(Java Memory Model),是一種基于計(jì)算機(jī)內(nèi)存模型(定義了共享內(nèi)存系統(tǒng)中多線(xiàn)程程序讀寫(xiě)操作行為的規(guī)范),屏蔽了各種硬件和操作系統(tǒng)的訪(fǎng)問(wèn)差異的,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪(fǎng)問(wèn)都能保證效果一致的機(jī)制及規(guī)范。保證共享內(nèi)存的原子性、可見(jiàn)性、有序性。


          能用圖的地方盡量不廢話(huà),先來(lái)看一張圖:



          上圖描述了一個(gè)多線(xiàn)程執(zhí)行場(chǎng)景。線(xiàn)程 A 和線(xiàn)程 B 分別對(duì)主內(nèi)存的變量進(jìn)行讀寫(xiě)操作。其中主內(nèi)存中的變量為共享變量,也就是說(shuō)此變量只此一份,多個(gè)線(xiàn)程間共享。但是線(xiàn)程不能直接讀寫(xiě)主內(nèi)存的共享變量,每個(gè)線(xiàn)程都有自己的工作內(nèi)存,線(xiàn)程需要讀寫(xiě)主內(nèi)存的共享變量時(shí)需要先將該變量拷貝一份副本到自己的工作內(nèi)存,然后在自己的工作內(nèi)存中對(duì)該變量進(jìn)行所有操作,線(xiàn)程工作內(nèi)存對(duì)變量副本完成操作之后需要將結(jié)果同步至主內(nèi)存。


          線(xiàn)程的工作內(nèi)存是線(xiàn)程私有內(nèi)存,線(xiàn)程間無(wú)法互相訪(fǎng)問(wèn)對(duì)方的工作內(nèi)存。


          為了便于理解,用圖來(lái)描述一下線(xiàn)程對(duì)變量賦值的流程。



          那么問(wèn)題來(lái)了,線(xiàn)程工作內(nèi)存怎么知道什么時(shí)候又是怎樣將數(shù)據(jù)同步到主內(nèi)存呢?這里就輪到 JMM 出場(chǎng)了。JMM 規(guī)定了何時(shí)以及如何做線(xiàn)程工作內(nèi)存與主內(nèi)存之間的數(shù)據(jù)同步。


          對(duì) JMM 有了初步的了解,簡(jiǎn)單總結(jié)一下原子性、可見(jiàn)性、有序性。


          原子性:對(duì)共享內(nèi)存的操作必須是要么全部執(zhí)行直到執(zhí)行結(jié)束,且中間過(guò)程不能被任何外部因素打斷,要么就不執(zhí)行。


          可見(jiàn)性:多線(xiàn)程操作共享內(nèi)存時(shí),執(zhí)行結(jié)果能夠及時(shí)的同步到共享內(nèi)存,確保其他線(xiàn)程對(duì)此結(jié)果及時(shí)可見(jiàn)。


          有序性:程序的執(zhí)行順序按照代碼順序執(zhí)行,在單線(xiàn)程環(huán)境下,程序的執(zhí)行都是有序的,但是在多線(xiàn)程環(huán)境下,JMM 為了性能優(yōu)化,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排,程序的執(zhí)行會(huì)變成無(wú)序。


          到這里,我們可以引出本文的主題了 --【線(xiàn)程安全】。


          線(xiàn)程安全的本質(zhì)


          其實(shí)第一張圖的例子是有問(wèn)題的,主內(nèi)存中的變量是共享的,所有線(xiàn)程都可以訪(fǎng)問(wèn)讀寫(xiě),而線(xiàn)程工作內(nèi)存又是線(xiàn)程私有的,線(xiàn)程間不可互相訪(fǎng)問(wèn)。那在多線(xiàn)程場(chǎng)景下,圖上的線(xiàn)程 A 和線(xiàn)程 B 同時(shí)來(lái)操做共享內(nèi)存里的同一個(gè)變量,那么主內(nèi)存內(nèi)的此變量數(shù)據(jù)就會(huì)被破壞。也就是說(shuō)主內(nèi)存內(nèi)的此變量不是線(xiàn)程安全的。我們來(lái)看個(gè)代碼小例子幫助理解。


          public?class?ThreadDemo?{
          ????private?int?x?=?0;

          ????private?void?count()?{
          ????????x++;
          ????}

          ????public?void?runTest()?{
          ????????new?Thread()?{
          ????????????@Override
          ????????????public?void?run()?
          {
          ????????????????for?(int?i?=?0;?i?1
          _000_000;?i++)?{
          ????????????????????count();
          ????????????????}
          ????????????????System.out.println("final?x?from?1:?"?+?x);
          ????????????}
          ????????}.start();
          ????????new?Thread()?{
          ????????????@Override
          ????????????public?void?run()?
          {
          ????????????????for?(int?i?=?0;?i?1_000_000;?i++)?{
          ????????????????????count();
          ????????????????}
          ????????????????System.out.println("final?x?from?2:?"?+?x);
          ????????????}
          ????????}.start();
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????new?ThreadDemo().runTest();
          ????}
          }


          示例代碼中 runTest 方法2個(gè)線(xiàn)程分別執(zhí)行 1_000_000 次 count() 方法, count() 方法中只執(zhí)行簡(jiǎn)單的 x++ 操作,理論上每次執(zhí)行 runTest 方法應(yīng)該有一個(gè)線(xiàn)程輸出的 x 結(jié)果應(yīng)該是2_000_000。但實(shí)際的運(yùn)行結(jié)果并非我們所想:


          final?x?from?1:?989840
          final?x?from?2:?1872479


          我運(yùn)行了10次,其中一個(gè)線(xiàn)程輸出 x 的值為 2_000_000 只出現(xiàn)了2次。


          final?x?from?1:?1000000
          final?x?from?2:?2000000


          出現(xiàn)這樣的結(jié)果的原因也就是我們上面所說(shuō)的,在多線(xiàn)程環(huán)境下,我們主內(nèi)存的 x 變量的數(shù)據(jù)被破壞了。我們都知道完成一次 i++ 相當(dāng)于執(zhí)行了:


          int?tmp?=?x?+?1;
          x?=?tmp;


          在多線(xiàn)程環(huán)境下就會(huì)出現(xiàn)在執(zhí)行完 int tmp = x + 1; 這行代碼時(shí)就發(fā)生了線(xiàn)程切換,當(dāng)線(xiàn)程再次切回來(lái)的時(shí)候,x 就會(huì)被重復(fù)賦值,導(dǎo)致出現(xiàn)上面的運(yùn)行結(jié)果,2個(gè)線(xiàn)程都無(wú)法輸出 2_000_000。


          下圖描述了示例代碼的執(zhí)行時(shí)序:



          那么 Java 是如何來(lái)解決上述問(wèn)題來(lái)保證線(xiàn)程安全,保證共享內(nèi)存的原子性、可見(jiàn)性、有序性的呢?


          /? ?線(xiàn)程同步? ?/


          Java 提供了一系列的關(guān)鍵字和類(lèi)來(lái)保證線(xiàn)程安全。


          Synchronized 關(guān)鍵字


          Synchronized 作用


          保證方法或代碼塊操作的原子性


          Synchronized 保證?法內(nèi)部或代碼塊內(nèi)部資源(數(shù)據(jù))的互斥訪(fǎng)問(wèn)。即同?時(shí)間、由同?個(gè) Monitor(監(jiān)視鎖) 監(jiān)視的代碼,最多只能有?個(gè)線(xiàn)程在訪(fǎng)問(wèn)。


          話(huà)不多說(shuō)來(lái)張動(dòng)圖描述一下 Monitor 工作機(jī)制:




          被 Synchronized 關(guān)鍵字描述的方法或代碼塊在多線(xiàn)程環(huán)境下同一時(shí)間只能由一個(gè)線(xiàn)程進(jìn)行訪(fǎng)問(wèn),在持有當(dāng)前 Monitor 的線(xiàn)程執(zhí)行完成之前,其他線(xiàn)程想要調(diào)用相關(guān)方法就必須進(jìn)行排隊(duì),知道持有持有當(dāng)前 Monitor 的線(xiàn)程執(zhí)行結(jié)束,釋放 Monitor ,下一個(gè)線(xiàn)程才可獲取 Monitor 執(zhí)行。


          如果存在多個(gè) Monitor 的情況時(shí),多個(gè) Monitor 之間是不互斥的。


          多個(gè) Monitor 的情況出現(xiàn)在自定義多個(gè)鎖分別來(lái)描述不同的方法或代碼塊,Synchronized 在描述代碼塊時(shí)可以指定自定義 Monitor ,默認(rèn)為 this 即當(dāng)前類(lèi)。




          保證監(jiān)視資源的可見(jiàn)性


          保證多線(xiàn)程環(huán)境下對(duì)監(jiān)視資源的數(shù)據(jù)同步。即任何線(xiàn)程在獲取到 Monitor 后的第?時(shí) 間,會(huì)先將共享內(nèi)存中的數(shù)據(jù)復(fù)制到??的緩存中;任何線(xiàn)程在釋放 Monitor 的第? 時(shí)間,會(huì)先將緩存中的數(shù)據(jù)復(fù)制到共享內(nèi)存中。


          保證線(xiàn)程間操作的有序性


          Synchronized 的原子性保證了由其描述的方法或代碼操作具有有序性,同一時(shí)間只能由最多只能有一個(gè)線(xiàn)程訪(fǎng)問(wèn),不會(huì)觸發(fā) JMM 指令重排機(jī)制。


          Volatile 關(guān)鍵字


          Volatile 作用


          保證被 Volatile 關(guān)鍵字描述變量的操作具有可見(jiàn)性和有序性(禁止指令重排)。


          注意:


          1. Volatile 只對(duì)基本類(lèi)型 (byte、char、short、int、long、float、double、boolean) 的賦值 操作和對(duì)象的引?賦值操作有效。

          2. 對(duì)于 i++ 此類(lèi)復(fù)合操作, Volatile 無(wú)法保證其有序性和原子性。

          3. 相對(duì) Synchronized 來(lái)說(shuō) Volatile 更加輕量一些。


          java.util.concurrent.atomic


          包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等類(lèi)。使用這些類(lèi)來(lái)聲明變量可以保證對(duì)其操作具有原子性來(lái)保證線(xiàn)程安全。


          實(shí)現(xiàn)原理上與 Synchronized 使用 Monitor(監(jiān)視鎖)保證資源在多線(xiàn)程環(huán)境下阻塞互斥訪(fǎng)問(wèn)不同,java.util.concurrent.atomic 包下的各原子類(lèi)基于 CAS(CompareAndSwap) 操作原理實(shí)現(xiàn)。?


          CAS 又稱(chēng)無(wú)鎖操作,一種樂(lè)觀(guān)鎖策略,原理就是多線(xiàn)程環(huán)境下各線(xiàn)程訪(fǎng)問(wèn)共享變量不會(huì)加鎖阻塞排隊(duì),線(xiàn)程不會(huì)被掛起。通俗來(lái)講就是一直循環(huán)對(duì)比,如果有訪(fǎng)問(wèn)沖突則重試,直到?jīng)]有沖突為止。


          Lock


          Lock 也是 java.util.concurrent 包下的一個(gè)接口,定義了一系列的鎖操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 實(shí)現(xiàn)類(lèi)。與 Synchronized 不同是 Lock 提供了獲取鎖和釋放鎖等相關(guān)接口,使得使用上更加靈活,同時(shí)也可以做更加復(fù)雜的操作,如:


          ReentrantReadWriteLock?lock?=?new?ReentrantReadWriteLock();
          Lock?readLock?=?lock.readLock();
          Lock?writeLock?=?lock.writeLock();
          private?int?x?=?0;
          private?void?count()?{
          ????writeLock.lock();
          ????try?{
          ????????x++;
          ????}?finally?{
          ????????writeLock.unlock();
          ????}
          }
          private?void?print(int?time)?{
          ????readLock.lock();
          ????try?{
          ????????for?(int?i?=?0;?i?????????????System.out.print(x?+?"?");
          ????????}
          ????????System.out.println();
          ????}?finally?{
          ????????readLock.unlock();
          ????}
          }


          /? ?總結(jié)? ?/


          出現(xiàn)線(xiàn)程安全問(wèn)題的原因:


          在多個(gè)線(xiàn)程并發(fā)環(huán)境下,多個(gè)線(xiàn)程共同訪(fǎng)問(wèn)同一共享內(nèi)存資源時(shí),其中一個(gè)線(xiàn)程對(duì)資源進(jìn)行寫(xiě)操作的中途(寫(xiě)?入已經(jīng)開(kāi)始,但還沒(méi) 結(jié)束),其他線(xiàn)程對(duì)這個(gè)寫(xiě)了一半的資源進(jìn)?了讀操作,或者對(duì)這個(gè)寫(xiě)了一半的資源進(jìn)?了寫(xiě)操作,導(dǎo)致此資源出現(xiàn)數(shù)據(jù)錯(cuò)誤。


          如何避免線(xiàn)程安全問(wèn)題?


          • 保證共享資源在同一時(shí)間只能由一個(gè)線(xiàn)程進(jìn)行操作(原子性,有序性)。

          • 將線(xiàn)程操作的結(jié)果及時(shí)刷新,保證其他線(xiàn)程可以立即獲取到修改后的最新數(shù)據(jù)(可見(jiàn)性)。

          瀏覽 30
          點(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>
                  丁香激情五月天 | 先锋AV网址 | av手机久久久久久 | 99草在线观看 | 含羞草一区二区三区 |