日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
學(xué)妹問我,并發(fā)問題的根源到底是什么?

并發(fā)編程是 java 高級(jí)程序員的必備的基礎(chǔ)技能之一。但是想要寫好并發(fā)程序并非易事。

我們提供的服務(wù)有:網(wǎng)站設(shè)計(jì)、做網(wǎng)站、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、天峨ssl等。為成百上千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的天峨網(wǎng)站制作公司

那究竟是什么原因?qū)е麓蟀训摹案褡由馈迸笥褵o法寫出優(yōu)質(zhì)和性能穩(wěn)定的并發(fā)程序呢?根本原因就是大家對(duì)并發(fā)編程的核心理論的模糊和不理解。想要運(yùn)用好一項(xiàng)技術(shù)。理論知識(shí)和核心概念是一定要理解透徹的。

今天我們就來一起看下并發(fā)編程三大核心基礎(chǔ)理論:原子性、可見性、有序性

1、原子性

先來看下什么叫原子性

第一種理解:原子(atomic)本意是“不能被進(jìn)一步分割的最小粒子”,而原子操作(atomic operation)意 為“不可被中斷的一個(gè)或一系列操作”

第二種理解:原子性,即一個(gè)操作或多個(gè)操作,要么全部執(zhí)行并且在執(zhí)行的過程中不被打斷,要么全部不執(zhí)行。(提供了互斥訪問,在同一時(shí)刻只有一個(gè)線程進(jìn)行訪問)

原子,在物理學(xué)中定義是組成物體的不可分割的最小的單位。在 java 并發(fā)編程中我們可以將其理解為:一組要么成功要么失敗的操作。

1.1、原子性問題的產(chǎn)生的原因

原子性問題產(chǎn)生的根本原因是什么?我們只要知道了癥狀才能準(zhǔn)確的對(duì)癥下藥,本小節(jié),我們就來一起探討下原子性問題的由來。

我們都知道,程序在執(zhí)行的時(shí)候,一定是以線程為單位在執(zhí)行的,因?yàn)榫€程是 CPU 進(jìn)行任務(wù)調(diào)度的基本單位。

電腦的 CPU 會(huì)根據(jù)不同的任務(wù)調(diào)度算法去執(zhí)行線程的調(diào)度,將時(shí)間分片并派分給各個(gè)線程。

當(dāng)某個(gè)線程獲得CPU的時(shí)間片之后就獲取了CPU的執(zhí)行權(quán),就可以執(zhí)行任務(wù),當(dāng)時(shí)間片耗盡之后,就會(huì)失去CPU使用權(quán)。

進(jìn)而本任務(wù)會(huì)暫時(shí)的停止執(zhí)行。多線程場景下,由于時(shí)間片在線程間輪換,就會(huì)發(fā)生原子性問題。

看完理論似乎并不能直觀的理解原子性問題。下面我們就通過代碼的方式來具體闡述下原子性問題的產(chǎn)生原因。

1.2、案例分析

我們以常見的 i++ 為例,這是一個(gè)老生常談的原子性問題了,先來看下代碼

 
 
 
 
  1. public class AtomicDemo { 
  2.  
  3.     private int count = 0; 
  4.  
  5.     public void add() { 
  6.  
  7.         count++; 
  8.  
  9.     } 
  10.  
  11.     public int get() { 
  12.  
  13.         return count; 
  14.  
  15.     } 
  16.  
  17.     public static void main(String[] args) throws InterruptedException { 
  18.  
  19.         CountDownLatch countDownLatch = new CountDownLatch(100); 
  20.  
  21.         AtomicDemo atomicDemo = new AtomicDemo(); 
  22.  
  23.         IntStream.rangeClosed(0, 100).forEach(item -> { 
  24.  
  25.             new Thread(() -> { 
  26.  
  27.                 IntStream.rangeClosed(1, 100).forEach(i -> { 
  28.  
  29.                     atomicDemo.add(); 
  30.  
  31.                 }); 
  32.  
  33.             }).start(); 
  34.  
  35.             countDownLatch.countDown(); 
  36.  
  37.         }); 
  38.  
  39.         countDownLatch.await(); 
  40.  
  41.         System.out.println(atomicDemo.get()); 
  42.  
  43.     } 
  44.  

上面 代碼的作用是將初始值為0的 count 變量,通過100線程每個(gè)線程累加100次的方式來累加。想要得到一個(gè)結(jié)果為 10000 的值。但是實(shí)際上結(jié)果很難達(dá)到10000。

產(chǎn)生這個(gè)問題的原因:

count++ 的執(zhí)行實(shí)際上這個(gè)操作不是原子性的,因?yàn)?count++ 會(huì)被拆分成以下三個(gè)步驟執(zhí)行(這樣的步驟不是虛擬的,而是真實(shí)情況就是這么執(zhí)行的)

第一步:讀取 count 的值;

第二步:計(jì)算 +1 的結(jié)果;

第三步:將 +1 的結(jié)果賦值給 count變量

那問題又來了。分三步又咋樣?讓他執(zhí)行完不就行了?

理論上是這樣子的,大家都很友好,你執(zhí)行完我執(zhí)行,我執(zhí)行完你繼續(xù)。你想象的可能是這樣的”烏托邦圖“

image-20210430131612018

但是實(shí)際上這些線程已經(jīng)”黑化”了。他們絕不可能互相謙讓。CPU或者是程序的世界觀里面。大家做任何事情都是在”爭搶“。我們來看下面這張圖:

上圖詳細(xì)分析:

第一步:A線程從主內(nèi)存中讀取 count 的值 0;

第二步:A線程開始對(duì) count 值進(jìn)行累加;

第三步:B線程從主內(nèi)存中讀取 count 的值 0(PS:具體第三步從哪里開始都不是重點(diǎn),重點(diǎn)是:A線程將 count 值寫入主內(nèi)存之前 B 線程就開始讀取 count 并執(zhí)行。此時(shí) B線程 讀取到的 count 值依舊是還未被操作過的原始值);

第四步:(PS:到這里其實(shí)已經(jīng)不重要了。因?yàn)椴还?A線程和B線程現(xiàn)在怎么操作。結(jié)果已經(jīng)不可逆轉(zhuǎn),已經(jīng)錯(cuò)了)B線程開始對(duì) count 值進(jìn)行累加;

第五步:A 線程將累加后的結(jié)果賦值給 count 結(jié)果為 1;

第六步:B 線程將累加后的結(jié)果賦值給 count 結(jié)果為 1;

第七步:A 線程將結(jié)果 count =1 刷回到主內(nèi)存;

第八步:B 線程將結(jié)果 count =1 刷回到主內(nèi)存;

相信大家此時(shí)已經(jīng)非常清晰地分析出了原子性產(chǎn)生的根本原因了。

至于解決方案可以通過鎖或者是 CAS 的方式。具體方案就不再這里贅述了。

2、可見性

萬丈高樓平地起,再復(fù)雜的技術(shù)我們也需要從基本的概念看起來:

可見性:一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到,我們稱為可見性。

2.1、可見性問題產(chǎn)生的原因

在很多年前,那個(gè)嫁妝只需要一個(gè)手電筒的年代你或許還不會(huì)出現(xiàn)可見性這樣的問題,因?yàn)榇蠹叶际菃魏颂幚砥?,不存在并發(fā)的情況。

而對(duì)于現(xiàn)在“視金錢如糞土”的年代。多核處理器已經(jīng)是現(xiàn)代超級(jí)計(jì)算機(jī)的基礎(chǔ)硬件。高速的CPU處理器和緩慢的內(nèi)存之前數(shù)據(jù)的通信成了矛盾。

所以為了解決和緩和這樣的情況,每個(gè)CPU和線程都有自己的本地緩存,所謂本地緩存即該緩存僅僅對(duì)它所在的處理器可見,CPU緩存與內(nèi)存的數(shù)據(jù)不容易保證一致。

為了避免這種因?yàn)閷憯?shù)據(jù)速度不一致而導(dǎo)致 CPU 的性能浪費(fèi)的情況,處理器通過使用寫緩沖區(qū)來臨時(shí)保存待寫入主內(nèi)存的數(shù)據(jù)。寫緩沖區(qū)合并對(duì)同一內(nèi)存地址的多次寫,并以批處理的方式刷新,也就是說寫緩沖區(qū)不會(huì)立即將數(shù)據(jù)刷新到主內(nèi)存中。

緩存不能及時(shí)刷新到主內(nèi)存就是導(dǎo)致可見性問題產(chǎn)生的根本原因。

2.2、案例分析

 
 
 
 
  1. public class AtomicDemo { 
  2.  
  3.     private int count = 0; 
  4.  
  5.     public void add() { 
  6.  
  7.         count++; 
  8.  
  9.     } 
  10.  
  11.     public int get() { 
  12.  
  13.         return count; 
  14.  
  15.     } 
  16.  
  17.     public static void main(String[] args) throws InterruptedException { 
  18.  
  19.         CountDownLatch countDownLatch = new CountDownLatch(100); 
  20.  
  21.         AtomicDemo atomicDemo = new AtomicDemo(); 
  22.  
  23.         IntStream.rangeClosed(0, 100).forEach(item -> { 
  24.  
  25.             new Thread(() -> { 
  26.  
  27.                 IntStream.rangeClosed(1, 100).forEach(i -> { 
  28.  
  29.                     atomicDemo.add(); 
  30.  
  31.                 }); 
  32.  
  33.             }).start(); 
  34.  
  35.             countDownLatch.countDown(); 
  36.  
  37.         }); 
  38.  
  39.         countDownLatch.await(); 
  40.  
  41.         System.out.println(atomicDemo.get()); 
  42.  
  43.     } 
  44.  

“what * *”,怎么和上面代碼一樣。。。結(jié)果就不截圖了,必然不是10000。

我們來看下執(zhí)行的流程圖(PS:不要糾結(jié)于為什么和上面的不一樣,特定問題特定分析。在闡述一種問題的時(shí)候,一定會(huì)在某些層面上屏蔽另外一種問題的干擾)

假設(shè) A 線程和 B 線程同時(shí)開始執(zhí)行,首先 A 線程和 B 線程會(huì)將主內(nèi)存中的 count 的值加載/緩存到自己的本地內(nèi)存中。然后會(huì)讀取各自的內(nèi)存中的值去執(zhí)行操作,也就是說此時(shí) A 線程和 B 線程就好像是兩個(gè)世界的人,彼此不會(huì)產(chǎn)生任何關(guān)聯(lián)。

操作完之后 A 線程將結(jié)果寫回到自己的本地內(nèi)存中,同樣 B 線程將結(jié)果寫回到自己的本地內(nèi)存中。然后回來某個(gè)時(shí)機(jī)各自將結(jié)果刷回到主內(nèi)存。那最終必然是一方的數(shù)據(jù)被另一方覆蓋。這就是緩存的可見性問題。

3、有序性

不積跬步無以至千里,我們還是先來看概念

有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

這有啥的,程序老老實(shí)實(shí)按照程序員寫的代碼執(zhí)行就完事了,這還會(huì)有什么問題嗎?

3.1、有序性問題產(chǎn)生的原因

實(shí)際上編譯器為了提高程序執(zhí)行的性能。會(huì)改變我們代碼的執(zhí)行順序的。即你寫在前面的代碼不一定是先被執(zhí)行完的。

例如:int a = 1;int b =4;從表面和常規(guī)角度來看,程序的執(zhí)行應(yīng)該是先初始化 a ,然后初始化 b 。但是實(shí)際上非常有可能是先初始化 b,然后初始化 a。因?yàn)樵诰幾g器看了來,先初始化誰對(duì)這兩個(gè)變量不會(huì)有任何影響。即這兩個(gè)變量之間沒有任何的數(shù)據(jù)依賴。

指令重排序有三種類型,分別為:

① 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。

② 指令級(jí)并行的重排序?,F(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng) 機(jī)器指令的執(zhí)行順序。

③ 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上 去可能是在亂序執(zhí)行。

3.2、案例分析

有序性的案例最常見的就是 DCL了(double check lock)就是單例模式中的雙重檢查鎖功能。先來看下代碼

 
 
 
 
  1. public class SingletonDclDemo { 
  2.  
  3.     private SingletonDclDemo(){} 
  4.  
  5.     private static SingletonDclDemo instance; 
  6.  
  7.     public static SingletonDclDemo getInstance(){ 
  8.  
  9.         if (Objects.isNull(instance)) { 
  10.  
  11.             synchronized (SingletonDclDemo.class) { 
  12.  
  13.                 if (Objects.isNull(instance)) { 
  14.  
  15.                     instance = new SingletonDclDemo(); 
  16.  
  17.                 } 
  18.  
  19.             } 
  20.  
  21.         } 
  22.  
  23.         return instance; 
  24.  
  25.     } 
  26.  
  27.     public static void main(String[] args) { 
  28.  
  29.         IntStream.rangeClosed(0,100).forEach(item->{ 
  30.  
  31.             new Thread(SingletonDclDemo::getInstance).start(); 
  32.  
  33.         }); 
  34.  
  35.     } 
  36.  

這個(gè)代碼還是比較簡單的。

在獲取對(duì)象實(shí)例的方法中,程序首先判斷 instance 對(duì)象是否為空,如果為空,則鎖定SingletonDclDemo.class 并再次檢查instance是否為空,如果還為空則創(chuàng)建 Singleton的一個(gè)實(shí)例??此坪芡昝?,既保證了線程完全的初始化單例,又經(jīng)過判斷 instance 為 null 時(shí)再用 synchronized 同步加鎖。但是還有問題!

instance = new SingletonDclDemo(); 創(chuàng)建對(duì)象的代碼,分為三步:① 分配內(nèi)存空間;② 初始化對(duì)象SingletonDclDemo;③ 將內(nèi)存空間的地址賦值給instance;

但是這三步經(jīng)過重排之后:① 分配內(nèi)存空間 ② 將內(nèi)存空間的地址賦值給instance ③ 初始化對(duì)象SingletonDclDemo

會(huì)導(dǎo)致什么結(jié)果呢?

線程 A 先執(zhí)行 getInstance() 方法,當(dāng)執(zhí)行完指令②時(shí)恰好發(fā)生了線程切換,切換到了線程B上;如果此時(shí)線程B也執(zhí)行 getInstance() 方法,那么線程B在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn)instance!=null,所以直接返回instance,而此時(shí)的instance是沒有初始化過的,如果我們這個(gè)時(shí)候訪問instance的成員變量就可能觸發(fā)空指針異常。

繼續(xù)來張圖來更直觀的理解下:

具體的執(zhí)行流程在上面已經(jīng)分析了。相信這張圖片一定能讓你徹底理解。

4、本文小結(jié)

并發(fā)編程的學(xué)習(xí)和使用并非一朝一夕的事情,也并非會(huì)幾個(gè)理論就能寫好優(yōu)質(zhì)的并發(fā)程序。這需要長時(shí)間的實(shí)踐和總結(jié)。好的代碼很少是寫出來的,都是迭代和優(yōu)化的。


網(wǎng)站欄目:學(xué)妹問我,并發(fā)問題的根源到底是什么?
文章網(wǎng)址:http://www.dlmjj.cn/article/cdhghdi.html