日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java并发编程之美-阅读记录2

發(fā)布時間:2024/9/15 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java并发编程之美-阅读记录2 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

2.1什么是多線程并發(fā)編程

  并發(fā):是指在同一時間段內(nèi),多個任務(wù)同時在執(zhí)行,并且執(zhí)行沒有結(jié)束(同一時間段又包括多個單位時間,也就是說一個cpu執(zhí)行多個任務(wù))

  并行:是指在單位時間內(nèi)多個任務(wù)在同時執(zhí)行(也就是多個cpu同時執(zhí)行任務(wù))

  

  而在多線程編程實(shí)踐中,線程的個數(shù)一般是多于cpu的個數(shù)的

2.2為什么要多線程并發(fā)編程

  多個cpu同時執(zhí)行多個任務(wù),減少了線程上下文切換的開銷

2.3線程安全問題

  共享資源:就是說該資源可以被多個線程持有,或者說能夠被多個線程訪問。

?  對共享資源的修改會造成線程安全問題。

2.4共享變量的內(nèi)存可見性問題

  java內(nèi)存模型(JMM)規(guī)定,所有的變量都存儲在主內(nèi)存中,當(dāng)線程使用變量時,會將主內(nèi)存中的變量復(fù)制一份到自己的工作內(nèi)存,之后線程操作的變量都是自己工作內(nèi)存(L1緩存或者L2緩存或者寄存器)中的變量。

  這樣對于內(nèi)存不可見(沒有使用volatile修改的變量)的變量來說,在不同線程中就可能存在不同的值。就那下圖一個雙核cpu系統(tǒng)來說,當(dāng)操作一個共享變量X時,線程A就會獲取當(dāng)前內(nèi)存中的變量X,由于線程A是第一次操作,當(dāng)前工作內(nèi)存中沒有該變量,此時,線程A就會將主內(nèi)存中的變量X復(fù)制一份到自己的工作內(nèi)存(L1/L2緩存),線程A給變量X重新賦值(假設(shè)主內(nèi)存中默認(rèn)值為1,線程A修改為2),修改后,線程A會將修改后的值重新刷會主內(nèi)存,此時線程A是正常工作的。然后線程B也要操作變量X,同樣的也會將主內(nèi)存中的變量X復(fù)制一份到自己的工作內(nèi)存(此時變量X的值為2),此時獲取的變量X(值為2)就是線程A操作后的值,那么線程B同樣修改該變量,改為3,修改后線程B也會將變量重新刷回到主內(nèi)存,此時,主內(nèi)存中的變量X的值為3,線程A緩存中的值為2,線程B緩存中的值為3,那么線程A再要操作變量X的時候,就會直接操作緩存中的數(shù)據(jù)2,此時該值就不是正確的值了,出現(xiàn)內(nèi)存可見性的問題。

  解決內(nèi)存可見性,就是講共享變量X使用volatile或synchronized關(guān)鍵字。

  

2.5synchronized關(guān)鍵字

  synchronized也能夠解決共享變量的內(nèi)存可見性問題,通常是用來解決原子性問題。

  synchronized內(nèi)存語義:就是在進(jìn)入synchrinize代碼塊時,把塊內(nèi)使用到的變量從線程的工作內(nèi)存中清除,直接使用主內(nèi)存中的變量數(shù)據(jù),同樣在退出synchronize塊時,將對共享變量的操作刷新到主內(nèi)存。(也是枷鎖和解鎖的語義,加鎖是清空線程工作緩存中共享變量的值,在使用的時候直接加載主內(nèi)存中的數(shù)據(jù),釋放鎖的時候,將線程內(nèi)共享變量的數(shù)據(jù)刷回到主內(nèi)存中)

2.6volatile關(guān)鍵字

  volatile可以保證在對共享變量操作時對其他線程是可見的。volatile能夠保證可見性,但是不能保證原子性(synchronized能夠保證可見性和原子性)

2.7原子操作

  原子操作:指的就是一系列操作要么都執(zhí)行成功,要么都失敗。

  例如程序計數(shù)器 ++count; 操作就不是一個原子操作,因?yàn)樗鼉?nèi)部設(shè)計到讀-改-寫三個操作。

2.8CAS操作

  CAS即campare and swap操作是jdk提供的非阻塞的原子操作,它通過硬件來保證“比較-更新”操作的原子性。

  CAS中的一個經(jīng)典問題ABA問題,該問題產(chǎn)生的原因就是變量產(chǎn)生了環(huán)形轉(zhuǎn)換,也就是變量A->B->A

  AtomicStampedReference能夠解決ABA問題(通過給每一個變量加了一個時間戳)

2.9Unsafe類

  提供了硬件級別的原子操作方法(不建議在代碼中使用該類)。

  unsafe.objectFieldOffset(Field field):返回偏移量,理解為內(nèi)存里java對象的各個部分放在內(nèi)存的不同位置,而該方法則會返回指定字段相對于java對象的“起始地址”的偏移量,后續(xù)可以通過unsafe的getint、getlong等方法,通過偏移量直接獲取java對象的某個字段

2.10指令重拍

  java內(nèi)存模型允許編譯器和處理器對指令進(jìn)行重排序以提供性能,并且只會對不存在數(shù)據(jù)依賴行的指令重排序。

  在單線程下指令重排序?qū)ψ罱K結(jié)果沒有影響,但是在多線程下就會存在問題。

2.11偽共享

要理解偽共享需要先理解cpu緩存(1級緩存,2級緩存,3級緩存)、緩存行等

CPU 是計算機(jī)的心臟,所有運(yùn)算和程序最終都要由它來執(zhí)行。為了解決cpu和主內(nèi)存運(yùn)行速度差的問題,會在CPU 和主內(nèi)存之間設(shè)置好幾級緩存,因?yàn)榧词怪苯釉L問主內(nèi)存也是非常慢的。

如果對一塊數(shù)據(jù)做相同的運(yùn)算多次,那么在執(zhí)行運(yùn)算的時候把它加載到離 CPU 很近的地方就有意義了(離cpu遠(yuǎn)近的緩存處理速度越快,其大小也就越小),比如一個循環(huán)計數(shù),你不想每次循環(huán)都跑到主內(nèi)存去取這個數(shù)據(jù)來增長它吧。

?

越靠近 CPU 的緩存越快也越小,所以 L1 緩存很小但很快,并且緊靠著在使用它的 CPU 內(nèi)核。

L2 大一些,也慢一些,并且仍然只能被一個單獨(dú)的 CPU 核使用。L3 在現(xiàn)代多核機(jī)器中更普遍,仍然更大,更慢,并且被單個插槽上的所有 CPU 核共享。

最后,主存保存著程序運(yùn)行的所有數(shù)據(jù),它更大,更慢,由全部插槽上的所有 CPU 核共享。

當(dāng) CPU 執(zhí)行運(yùn)算的時候,它先去 L1 查找所需的數(shù)據(jù),再去 L2,然后是 L3,最后如果這些緩存中都沒有,所需的數(shù)據(jù)就要去主內(nèi)存拿,走得越遠(yuǎn),運(yùn)算耗費(fèi)的時間就越長,所以如果進(jìn)行一些很頻繁的運(yùn)算,要確保數(shù)據(jù)在 L1 緩存中。

cpu緩存行

緩存是由緩存行組成的,通常是 2的冪次數(shù)字節(jié),例如64 字節(jié)(常用處理器的緩存行是 64 字節(jié)的,比較舊的處理器緩存行是 32 字節(jié)),并且它有效地引用主內(nèi)存中的一塊地址。

一個 Java 的 long 類型是 8 字節(jié),因此在一個緩存行中可以存 8 個 long 類型的變量。

在程序運(yùn)行的過程中,緩存每次更新都從主內(nèi)存中加載連續(xù)的 64 個字節(jié)。因此,如果訪問一個 long 類型的數(shù)組時,當(dāng)數(shù)組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中。

但是,如果使用的數(shù)據(jù)結(jié)構(gòu)中的項(xiàng)在內(nèi)存中不是彼此相鄰的,比如鏈表,那么將得不到免費(fèi)緩存加載帶來的好處。

不過,這種免費(fèi)加載也有一個壞處。設(shè)想如果我們有個 long 類型的變量 a,它不是數(shù)組的一部分,而是一個單獨(dú)的變量,并且還有另外一個 long 類型的變量 b 緊挨著它,那么當(dāng)加載 a 的時候?qū)⒚赓M(fèi)加載 b(前提這兩個變量都是volatile修飾的)。

看起來似乎沒有什么毛病,但是如果一個 CPU 核心的線程在對 a 進(jìn)行修改,另一個 CPU 核心的線程卻在對 b 進(jìn)行讀取。

當(dāng)前者修改 a 時,會把 a 和 b 同時加載到前者核心的緩存行中,更新完 a 后其它所有包含 a 的緩存行都將失效,因?yàn)槠渌彺嬷械?a 不是最新值了。而當(dāng)后者讀取 b 時,發(fā)現(xiàn)這個緩存行已經(jīng)失效了,需要從主內(nèi)存中重新加載。

請記住,我們的緩存都是以緩存行作為一個單位來處理的,所以失效 a 的緩存的同時,也會把 b 失效,反之亦然。

?

這樣就出現(xiàn)了一個問題,b 和 a 完全不相干,每次卻要因?yàn)?a 的更新需要從主內(nèi)存重新讀取,它被緩存未命中給拖慢了。這就是偽共享。

當(dāng)多線程修改互相獨(dú)立的變量時,如果這些變量共享同一個緩存行,就會無意中影響彼此的性能,這就是偽共享。

我們來看看下面這個例子,充分說明了偽共享是怎么回事。

public class FalseSharingTest {public static void main(String[] args) throws InterruptedException {testPointer(new Pointer());}private static void testPointer(Pointer pointer) throws InterruptedException {long start = System.currentTimeMillis();Thread t1 = new Thread(() -> {for (int i = 0; i < 100000000; i++) {pointer.x++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100000000; i++) {pointer.y++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(System.currentTimeMillis() - start);System.out.println(pointer);} }class Pointer {volatile long x;volatile long y; }

這個例子中,我們聲明了一個 Pointer 的類,它包含 x 和 y 兩個變量(必須聲明為volatile,保證可見性,關(guān)于內(nèi)存屏障的東西我們后面再講),一個線程對 x 進(jìn)行自增1億次,一個線程對 y 進(jìn)行自增1億次。

可以看到,x 和 y 完全沒有任何關(guān)系,但是更新 x 的時候會把其它包含 x 的緩存行失效,同時也就失效了 y,運(yùn)行這段程序輸出的時間為3890ms。

偽共享的原理我們知道了,一個緩存行是 64 個字節(jié),一個 long 類型是 8 個字節(jié),所以避免偽共享也很簡單,大概有以下三種方式:

(1)在兩個 long 類型的變量之間再加 7 個 long 類型

我們把上面的Pointer改成下面這個結(jié)構(gòu):

class Pointer {volatile long x;long p1, p2, p3, p4, p5, p6, p7; // 添加這7個變量的原因就是讓x和y不在一個緩存行中,這樣修改x的時候,就不會影響到對y的操作volatile long y; }

再次運(yùn)行程序,會發(fā)現(xiàn)輸出時間神奇的縮短為了695ms。

(2)重新創(chuàng)建自己的 long 類型,而不是 java 自帶的 long

修改Pointer如下:

class Pointer {MyLong x = new MyLong();MyLong y = new MyLong(); }class MyLong {volatile long value;long p1, p2, p3, p4, p5, p6, p7; // 同樣是占用緩沖行的位置 }

同時把 pointer.x++; 修改為 pointer.x.value++;,把 pointer.y++; 修改為 pointer.y.value++;,再次運(yùn)行程序發(fā)現(xiàn)時間是724ms。

(3)使用 @sun.misc.Contended 注解(java8)

修改 MyLong 如下:

@sun.misc.Contended class MyLong {volatile long value; }

默認(rèn)使用這個注解是無效的,需要在JVM啟動參數(shù)加上-XX:-RestrictContended才會生效,,再次運(yùn)行程序發(fā)現(xiàn)時間是718ms。

注意,以上三種方式中的前兩種是通過加字段的形式實(shí)現(xiàn)的,加的字段又沒有地方使用,可能會被jvm優(yōu)化掉,所以建議使用第三種方式。

(1)CPU具有多級緩存,越接近CPU的緩存越小也越快;

(2)CPU緩存中的數(shù)據(jù)是以緩存行為單位處理的;

(3)CPU緩存行能帶來免費(fèi)加載數(shù)據(jù)的好處,所以處理數(shù)組性能非常高;

(4)CPU緩存行也帶來了弊端,多線程處理不相干的變量時會相互影響,也就是偽共享;

(5)避免偽共享的主要思路就是讓不相干的變量不要出現(xiàn)在同一個緩存行中;

(6)一是每兩個變量之間加七個 long 類型;

(7)二是創(chuàng)建自己的 long 類型,而不是用原生的;

(8)三是使用 java8 提供的注解;

?

2.12鎖

  樂觀鎖和悲觀鎖:事務(wù)性

  公平鎖和非公平鎖:獲取所的機(jī)制(先到先得就是公平,隨機(jī)搶占就是非公平的)

  獨(dú)占所和共享鎖:能夠被多個線程共同持有

  可重入鎖:持有鎖的對象是自己時,不會被阻塞

  自旋鎖:當(dāng)線程在獲取鎖的時候,發(fā)現(xiàn)鎖已被其他線程占用,此時該線程并不會立刻阻塞,而是循環(huán)多次獲取(默認(rèn)次數(shù)為10次),扔獲取不到時,才會阻塞線程。其中阻塞此時可以設(shè)置-XX:PreBlockSpinsh

?

?

?

參考:偽共享相關(guān):https://www.jianshu.com/p/7758bb277985

與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖

總結(jié)

以上是生活随笔為你收集整理的java并发编程之美-阅读记录2的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。