程式設計師:結合計算機底層分析「線程安全性的原理分析」

2020-12-17 程序猿的內心獨白

一段代碼引來的思考:為什麼程序一直走不出Thread_One的while循環呢?

public class Test{

public static boolean threadOneFlag = true;

public volatile static boolean threadTwoFlag = true;

public static void main(String[] args) throws InterruptedException {

new Thread(()->{

System.out.println("thread_one_start");

while (threadOneFlag){ }

System.out.println("thread_one_end");

},"Thread_One").start();

System.out.println("thread_two_start");

while (threadTwoFlag){ }

System.out.println("thread_two_end");

},"Thread_Two").start();

Thread.sleep(1000);

//對threadOneFlag變量的修改在線程Thread_One中並不可見

threadOneFlag = false;

threadTwoFlag = false;

}

運行結果:

從硬體層面了解可見性的本質

程序運行時用到的存儲設備有:CPU、內存、磁碟(IO設備),三者有不同的處理速度,而且差異很大。當一個程序運行時如果三者都需要訪問,如果不做任何處理的話,計算效率受限於最慢的設備,計算機硬體對此做了一些優化:

CPU增加了高速緩存

多核CPU並且增加了進程、線程概念,通過時間片切換最大化提升CPU的使用率

編譯器的指令優化,更合理的去利用好CPU的高速緩存

這些優化雖然提升了計算機的計算效率,但是卻帶來的可見性和重排序的問題,下面慢慢講解

CPU高速緩存

存在的意義:絕大多數的運算任務不能僅通過處理器來完成,還需要和內存進行交互。例如:讀取運算數據,存儲運算結果。因為計算機的存儲設備與處理器運算速度差距很大,所以會增加CPU高速緩存作為兩者之間的緩衝:將運算需要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中。

存在的弊端:會帶來緩存一致性的問題

CPU高速緩存的結構:

分為L1,L2,L3三級緩存,L1和L2是CPU私有的,其中L1最小,L1又分為數據緩存和指令緩存

緩存一致性

當高速緩存存在以後,每個CPU獲取/存儲數據直接操作高速緩存,而不是內存,這樣當多個線程運行在不同CPU中時。同一份內存數據就可能會緩存於多個CPU高速緩存中,如不進行限制,就會出現緩存一致性問題

CPU層面提出了兩種解決辦法:1. 總線鎖,2. 緩存鎖

總線鎖和緩存鎖

總線鎖:在多CPU下,當其中一個處理器要對共享內存進行操作的時候,在總線上發出一個LOCK信號,使得其他處理器無法訪問共享數據,開銷很大,如果我們能夠控制鎖的粒度就能減少開銷,從而引入了緩存鎖。

緩存鎖:只要保證多個CPU緩存的同一份數據是一致的就可以了,基於緩存一致性協議來實現的

緩存一致性協議

為了達到數據訪問的一致,需要各個處理器在訪問緩存時遵循一些協議,在讀寫時根據協議來操作,常見的協議有MSI、MESI、MOSI。最常見的是MESI協議。

MESI協議

在MESI協議中,每個緩存的緩存控制器不僅知道自己的讀寫操作,而且也監聽其他Cache的讀寫操作。共有四種狀態,分別是:

M(Modify)表示共享數據只緩存在當前CPU緩存中,並且是被修改的狀態。此時表示當前CPU緩存數據與主內存中不一致,其他CPU緩存中如果緩存了當前數據應是無效狀態,因為該數據已被修改且並未更新到主內存

E(Exclusive)表示緩存的獨佔狀態,數據只緩存在當前CPU緩存中,並且沒有被修改

S(Shared)表示數據可能被多個CPU緩存,並且各個緩存中的數據和主內存中的數據一致

I(Invalid)表示當前緩存已經失效

圖解四種狀態:

對於MESI協議,從CPU讀寫角度來說會遵循一下原則:

CPU讀請求:緩存處於M、E、S狀態都可以被讀取,I狀態CPU只能從主內存中讀取數據

CPU寫請求:緩存處於M、E狀態才可以被寫入主內存中。對於S狀態的寫,需要將其他CPU中緩存行設置為無效才可寫。

使用總線鎖和緩存鎖機制之後,CPU對於內存的操作可以做如下抽象:

MESI協議的不足之處

當一個CPU_0需要將緩存中的數據進行寫入時,首先需要發送失效信息給其他緩存了該數據的CPU,等回執確認之後才會進行寫入。等待回執確認的過程中CPU_0會處於阻塞狀態,為了避免阻塞造成的資源浪費,CPU中引入了Store Bufferes。

引入Sotr Bufferes後,CPU_0在寫入共享數據時,只需將數據寫入store bufferes中,同時向其他緩存了共享數據的CPU發送失效指令就可以做其他操作了。由store bufferes等待回執確認信息,並負責同步到主內存

這種優化方式帶來了兩個現象,引起重排序的問題:

數據什麼時候提交不確定,因為需要等待其他CPU確認回執之後才會提交,這是一個異步操作

引入storebufferes後,處理器會先嘗試從storebuffere中讀取值,如果storebufferes中有數據,則直接從storebuffer中讀取,否則再從緩存行中讀取

重排序

請看如下代碼:假如exeToCPU0和exeToCPU1執行在不同CPU上,當exeToCPU0執行完兩行賦值代碼時,此時exeToCPU1執行if語句時,isFinsh = true,但是可能value並不為10,這就是重排序問題。

原因在於:假設CPU0緩存的兩個變量及狀態為:isFinish(E),value(S),CPU0修改value時只會先將修改結果保存到Store Buffer中,然後繼續執行isFinish=true指令,因為isFinish是(E),所以會直接將修改結果寫入內存中。此時CPU1讀書兩個值時,可能的結果就是:isfinish=true,value=3(不等於10)

為了解決此類問題,CPU層面提出了內存屏障

CPU層面的內存屏障

可以將其粗獷的理解為:將store buffer中的指令寫入到內存,從而使得其他訪問同一共享內存的線程的可見性

X86的 memory barrier的指令包括:讀屏障、寫屏障以及全屏障

寫屏障:告訴處理器在寫屏障之前的所有已經存儲在存儲緩存(store bufferes)中的數據同步到主內存,也就是,寫屏障之前的指令對於屏障之後的讀操作都是可見的。

讀屏障:處理器讀屏障之後的讀操作都在屏障之後執行

全屏障:確保屏障前的內存讀寫操作的結果都對屏障之後的操作可見

這些都不需要我們程式設計師來維護,和我們直接打交道的是JMM

JMM

JMM全稱是Java Memory Model,是隸屬於JVM的,是屬於語言級別的抽象內存模型,可以簡單理解為對硬體模型的抽象,它定義了共享內存中多線程程序讀寫操作的行為規範。JMM並沒有提升或者損失執行性能,也沒有直接限制指令重排序,JMM只是將底層問題抽象到JVM層面,是基於CPU層面提供的內存屏障及限制編譯器的重排序來解決問題的

JMM抽象模型分為主內存和工作內存。主內存是所有線程共享的,工作內存是每個線程獨佔的。線程對變量的所有操作都必須在工作內存中進行,不能直接讀寫主內存中的變量,線程之間共享變量的傳遞都是基於主內存來完成的

JMM體統了一些禁用緩存以及禁止重排序的方法,來解決可見性和有序性問題,例如:volatile、synchronized、final

在JMM中如果一個操作的執行結果必須對另外一個操作可見,兩個操作必須要存在happens-before關係,即happen-before規則(具體參見:happen-before規則)。

相關焦點

  • Spring中獲取Request的幾種方法及其線程安全性分析
    2、線程安全性測試結果:線程安全分析:此時request對象是方法參數,相當於局部變量,毫無疑問是線程安全的。2、線程安全性測試結果:線程安全分析:在Spring中,Controller的scope是singleton(單例),也就是說在整個
  • 深入分析synchronized底層加鎖的原理
    Java對象頭synchronized用的鎖是存在java對象頭裡的,要深入分析synchronized加鎖原理就需要先了解Java對象頭,Java對象頭它裡包含三個內容Mark Word :存儲對象的hashCode、GC標記、鎖信息等
  • Spring中獲取request的幾種方法,及其線程安全性分析
    線程安全性測試結果:線程安全分析:此時request對象是方法參數,相當於局部變量,毫無疑問是線程安全的。(ThreadLocal),因此request對象也是線程局部變量;這就保證了request對象的線程安全性。
  • 高性能內存隊列Disruptor原理分析
    得益於其優秀的設計,和對計算機底層原理的運用,官網說的:mechanical sympathy,我翻譯成硬體偏向或者面向硬體編程。同時它跟我們常見的MQ不一樣,這裡說的線程間其實就是同一個進程內,不同線程間的消息傳遞,跟JDK中的那些阻塞和並發隊列的用法是一樣的,也就是說它們不會誇進程。
  • Linux 進程、線程、文件描述符的底層原理
    說到進程,恐怕面試中最常見的問題就是線程和進程的關係了,那麼先說一下答案:在 Linux 系統中,進程和線程幾乎沒有區別。Linux 中的進程其實就是一個數據結構,順帶可以理解文件描述符、重定向、管道命令的底層工作原理,最後我們從作業系統的角度看看為什麼說線程和進程基本沒有區別。
  • Spring中獲取request的幾種方法,及線程安全性
    線程安全:測試結果:線程安全分析:此時request對象是方法參數,相當於局部變量,毫無疑問是線程安全的。(ThreadLocal),因此request對象也是線程局部變量;這就保證了request對象的線程安全性。
  • 想了解計算機的底層原理?這些資源通俗易懂又有趣
    我在這周更新的文章《學編程,學化學和學開車》中,向大家介紹了和計算機底層運行原理相關的諸多學科
  • 從底層原理深度剖析volatile關鍵字,徹底徵服面試官
    本篇文章從底層原理層面深度剖析volatile關鍵字是如何實現內存可見性的,同時引入了Java內存模型、指令重排序以及內存屏障等知識點作為原理分析的知識支撐。閱讀本文之前,推薦大家先閱讀作者之前的一篇關於happens-before的文章,這樣更有助於大家對volatile關鍵字底層原理的理解。
  • Java多線程:由淺入深看synchronized的底層實現原理
    正文小A:咱們前幾天鋪墊了這麼多內容,今天是不是要好好的深挖一下原理的內容了?MDove:沒錯,接下來。淺聊synchronized的使用MDove:說起synchronized的底層實現原來,咱們先看看synchronized的倆種加鎖方式:1、某個對象實例內此作用域內的synchronized鎖 ,可以防止多個線程同時訪問這個對象的
  • 並發組件CountDownLatch的底層原理
    會快速的分析下他們的原理和基本使用方法,更多的 應用會在之後的JDK第三章實踐篇中提到。 讓我們開始吧! 有了基本的使用經驗後,下面我們逐步分析下它的源碼原理。   2.從創建開始分析CountDownLatch的脈絡和組件還是分析源碼原理前,還是要有先脈絡後細節的思想,簡單看下CountDownLatch的脈絡:
  • 初級程式設計師、中級程式設計師,高級程式設計師是如何定義的?
    >高級程式設計師能夠寫一些框架,甚至一個新語言在具體分析各個級別程式設計師的定義的時候,我們先來想一下,大部分的程式設計師來源於:學校、自學和培訓機構。隨著網際網路高速的發展,企業用人標準越來越高,初級程式設計師很難找到自己心儀的「高薪」工作了。僅僅入門的程式設計師:這個階段算是初級更之前的一個階段,有很多人轉行過來學習編程,或者是很多大學畢業的本科生以為自己會了一些多態多線程就是一個程式設計師了。
  • Java8中Stream原理分析
    Stream API可以極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的代碼。本文會對Stream的實現原理進行剖析。Stream的組成與特點Stream(流)是一個來自數據源的元素隊列並支持聚合操作:元素是特定類型的對象,形成一個隊列。
  • 程式設計師學院每周書單推薦
    書中從並發性和線程安全性的基本概念出發,介紹了如何使用類庫提供的基本並發構建塊,用於避免並發危險、構造線程安全的類及驗證線程安全的規則,如何將小的線程安全類組合成更大的線程安全類,如何利用線程來提高並發應用程式的吞吐量,如何識別可並行執行的任務,如何提高單線程子系統的響應性,如何確保並發程序執行預期任務,如何提高並發代碼的性能和可伸縮性等內容,最後介紹了一些高級主題,如顯式鎖、原子變量、非阻塞算法以及如何開發自定義的同步工具類
  • Java8線程池ThreadPoolExecutor底層原理及其源碼解析
    小侃一下日常開發中, 或許不會直接new線程或線程池, 但這些線程相關的基礎或思想是非常重要的, 參考林迪效應;就算沒有直接用到, 可能間接也用到了類似的思想或原理, 例如tomcat, jetty, 資料庫連接池, MQ;本文不會對線程的基礎知識進行介紹
  • JVM內存分析,程式設計師進階的必經之路
    你說它重要吧,編寫代碼基本用不到它;你說它不重要吧,程式設計師想要進階又必須對底層有一定的了解。最終還是決定更加深入地學習下JVM,同時也用自己的理解詳細地說明Java程序是如何運行的。利用開發工具中的Show in Explorer功能,可以找到計算機裡對應的文件夾文件。out,這個我還不太懂具體是什麼意思,但是.class文件就在這個文件夾裡面。
  • java多線程之Thread構造函數(源碼分析)
    你了解了守護線程的特點之後,就可以運用這個原理做一些意想不到的事,比如說在退出jvm的時候也想讓一些線程跟著退出,就可以把他設置為守護線程。對這個基本的概念了解了之後我們再來看看線程的構造函數。二、線程的構造函數1、構造函數線程Thread得構造函數一共有8個,在這裡我們接觸到了一個新的類ThreadGroup。
  • 培訓班出身的程式設計師,和科班程式設計師有什麼區別?
    對於程式設計師的專業出身,一種是科班程式設計師,也就是上了大學的程式設計師,一種是在培訓機構用三五個月學完了技術,然後出來找工作的程式設計師。一種認為是否為計算機相關專業對於程式設計師的影響不大,畢竟編碼是一個實踐性很強的工作,用的技術與語言需要在實際工作中不斷積累,學校裡學的都是偏理論性的知識,對於實際工作意義不大。另一種認為科班程式設計師和非科班程式設計師即使看似code水平差不多,在初級階段也看不出太大差距。但是一旦進階到中高級,差距就日漸顯現。
  • ConcurrentHashMap實現線程安全的底層原理
    項目中經常會有多個線程要訪問同一個數據,此時比較常用的辦法是用synchronize加鎖,CAS去進行安全的累加,去實現多線程場景下的安全的更新一個數據的效果,HashMap是用的比較多,可能就是多個線程同時讀寫一個HashMap,HashMap是線程不安全的,如果對整個map去synchronized
  • 學習計算機系統必看的5本書
    採用新函數,更新了與協議無關和線程安全的網絡編程。與《深入理解計算機系統》從程式設計師視角來講解計算機系統,內容偏向系統軟體(特別是作業系統)不同,本書在計算機系統結構和作業系統的內容基本平衡,旨在讓讀者了解計算機體系結構和系統軟體之間的關係,為進一步深入學習計算機體系結構、作業系統和網絡的高級課程打下基礎。
  • 專業分析——工學(081201 計算機系統結構)
    計算機系統結構是從外部來研究計算機系統的一門學科,一般說來,凡是計算機系統的使用者(包括一般用戶和系統程式設計師)所能看到的計算機系統的屬性都是計算機系統結構所要研究的對象