自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

學(xué)妹問(wèn)我,并發(fā)問(wèn)題的根源到底是什么?

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

[[408736]]

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

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

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

1、原子性

先來(lái)看下什么叫原子性

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

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

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

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

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

我們都知道,程序在執(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í)行。多線程場(chǎng)景下,由于時(shí)間片在線程間輪換,就會(huì)發(fā)生原子性問(wèn)題。

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

1.2、案例分析

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

  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 變量,通過(guò)100線程每個(gè)線程累加100次的方式來(lái)累加。想要得到一個(gè)結(jié)果為 10000 的值。但是實(shí)際上結(jié)果很難達(dá)到10000。

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

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

第一步:讀取 count 的值;

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

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

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

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

image-20210430131612018

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

上圖詳細(xì)分析:

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

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

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

第四步:(PS:到這里其實(shí)已經(jīng)不重要了。因?yàn)椴还?A線程和B線程現(xiàn)在怎么操作。結(jié)果已經(jīng)不可逆轉(zhuǎn),已經(jīng)錯(cuò)了)B線程開(kāi)始對(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)生的根本原因了。

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

2、可見(jiàn)性

萬(wàn)丈高樓平地起,再?gòu)?fù)雜的技術(shù)我們也需要從基本的概念看起來(lái):

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

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

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

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

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

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

緩存不能及時(shí)刷新到主內(nèi)存就是導(dǎo)致可見(jiàn)性問(wèn)題產(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。

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

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

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

3、有序性

不積跬步無(wú)以至千里,我們還是先來(lái)看概念

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

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

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

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

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

指令重排序有三種類(lèi)型,分別為:

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

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

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

3.2、案例分析

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

  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è)代碼還是比較簡(jiǎn)單的。

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

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

但是這三步經(jīng)過(guò)重排之后:① 分配內(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是沒(méi)有初始化過(guò)的,如果我們這個(gè)時(shí)候訪問(wèn)instance的成員變量就可能觸發(fā)空指針異常。

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

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

4、本文小結(jié)

 

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

 

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專(zhuān)欄
相關(guān)推薦

2020-03-05 10:28:19

MySQLMRR磁盤(pán)讀

2022-10-08 00:00:00

Spring數(shù)據(jù)庫(kù)項(xiàng)目

2020-10-14 06:22:14

UWB技術(shù)感知

2010-11-01 01:25:36

Windows NT

2020-09-22 08:22:28

快充

2020-09-27 06:53:57

MavenCDNwrapper

2011-04-27 09:30:48

企業(yè)架構(gòu)

2009-06-09 22:11:44

JavaScriptObject

2023-10-11 08:29:54

volatileJava原子性

2019-10-30 10:13:15

區(qū)塊鏈技術(shù)支付寶

2020-08-04 14:20:20

數(shù)據(jù)湖Hadoop數(shù)據(jù)倉(cāng)庫(kù)

2013-06-09 09:47:31

.NetPDBPDB文件

2021-09-03 09:12:09

Linux中斷軟件

2010-04-22 14:14:29

Live-USB

2021-01-21 21:24:34

DevOps開(kāi)發(fā)工具

2023-07-12 15:32:49

人工智能AI

2021-07-07 05:07:15

JDKIterator迭代器

2021-09-01 23:29:37

Golang語(yǔ)言gRPC

2021-02-05 10:03:31

區(qū)塊鏈技術(shù)智能

2024-02-04 00:01:00

云原生技術(shù)容器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)