Java-多線程三大特性

概述

多任務處理在現代計算機操作系統中幾乎已是一項必備的功能了。在許多情況下,讓計算機同時去做幾件事情,不僅是因爲計算機的運算能力強大了,還有一個很重要的原因是計算機的運算速度與它的存儲和通信子系統速度的差距太大,大量的時間都花費在磁盤I/O、網絡通信或者數據庫訪問上。如果不希望處理器在大部分時間裏都處於等待其他資源的狀態,就必須使用一些手段去把處理器的運算能力“壓榨”出來,否則就會造成很大的浪費,而讓計算機同時處理幾項任務則是最容易想到、也被證明是非常有效的“壓榨”手段。

引用自<深入理解JAVA虛擬機 JVM高級特性與最佳實踐>

CPU緩存(Cache Memory)

CPU緩存是位於CPU與內存之間的臨時存儲器,它的容量比內存小的多但是交換速度卻比內存要快得多。高速緩存的出現主要是爲了解決CPU運算速度與內存讀寫速度不匹配的矛盾,因爲CPU運算速度要比內存讀寫速度快很多,這樣會使CPU花費很長時間等待數據到來或把數據寫入內存。在緩存中的數據是內存中的一小部分,但這一小部分是短時間內CPU即將訪問的,當CPU調用大量數據時,就可先緩存中調用,從而加快讀取速度。

當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。

CPU緩存可以分爲一級緩存,二級緩存,部分高端CPU還具有三級緩存,每一級緩存中所儲存的全部數據都是下一級緩存的一部分,這三種緩存的技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。當CPU要讀取一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找。

cpu緩存帶來的問題-緩存不一致

  • 單線程、cpu
    核心的緩存只被一個線程訪問。緩存獨佔,不會出現訪問衝突等問題。

  • 單核CPU,多線程
    進程中的多個線程會同時訪問進程中的共享數據,CPU將某塊內存加載到緩存後,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即使發生線程的切換,緩存仍然不會失效。但由於任何時刻只能有一個線程在執行,因此不會出現緩存訪問衝突。

  • 多核CPU,多線程。
    每個核都至少有一個L1 緩存。多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的caehe中保留一份共享內存的緩衝。由於多核是可以並行的,可能會出現多個線程同時寫各自的緩存的情況,而各自的cache之間的數據就有可能不同。

在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關於同一個數據的緩存內容可能不一致。

緩存一致性

緩存一致性可以分爲三個層級:

  • 在進行每個寫入運算時都立刻採取措施保證數據一致性
  • 每個獨立的運算,假如它造成數據值的改變,所有進程都可以看到一致的改變結果
  • 在每次運算之後,不同的進程可能會看到不同的值(這也就是沒有一致性的行爲)

爲了解決緩存不一致性問題,通常來說有以下2種解決方法:

  • 通過在總線加LOCK#鎖的方式
  • 通過緩存一致性協議

這2種方式都是硬件層面上提供的方式。

在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。因爲CPU和其他部件進行通信都是通過總線來進行的,如果對總線加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼完全執行完畢之後,其他CPU才能從其內存讀取變量,然後進行相應的操作。這樣就解決了緩存不一致的問題。

但是由於在鎖住總線期間,其他CPU無法訪問內存,會導致效率低下。因此出現了第二種解決方案,通過緩存一致性協議 來解決緩存一致性問題。

最出名的就是Intel的MESI協議,MESI協議保證了每個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信號通知其他CPU將該變量的緩存行置爲無效狀態,因此當其他CPU需要讀取這個變量時,發現自己緩存中緩存該變量的緩存行是無效的,那麼它就會從內存重新讀取。

三個概念

  • 原子性
    原子性,一個或者多個操作一旦開始之後其過程要麼全部成功,要麼全部失敗,中間不會被打斷,所以也不存在中間狀態。
    Java中的原子操作包括:

  • 除 long 和 double 之外的基本類型的賦值操作。
    這是由於 long 和 double 都是64位的數據,在32位JVM中對64位的數據的讀、寫分兩步,每一步讀或者寫32位的數據,這樣就會造成兩個線程對同一個變量的讀寫出現一個線程寫高32位、另一個線程寫入低32位數據。這樣此變量的數據就出現不一致的情況。
    非原子性的讀、寫只是造成long、double類型變量的高、低32位數據不一致。
  • java.concurrent.Atomic.* 包中所有類的一切操作
  • 有序性
    即程序執行的順序按照代碼的先後順序執行。可是在jvm中很多時候程序並不是按照代碼李所寫的順序執行的。

  • 簡單解釋一下:

    我們都知道程序在編寫的時候對於值的變量的定義等都是有一定的編寫順序的。我們期望我們的程序都按照預定的順序運行。然而當代碼實際運行起來的時候就只有cpu能決定運行順序了,當然整個代碼運行的結果肯定是跟預期一樣的,但是其過程就不一定了。這種情況對於單線程來說沒什麼問題,就單線程來說所有數據都是爲自己所用,不論過程如何,只要最終結果是我們預期的就可以,中間經過什麼樣的方式去處理只要能保證效率其實並不重要(但不是說隨意執行,一些必要的順序還是會保持)。但是在多線程的情況下就不一樣,因爲過程不再是自己的過程很有可能也是別的線程的過程。所以過程的不一樣可能會影響到別的線程的過程。

    //在沒有對數據進行引用或者操作之前,賦值無關順序,只要最後每一個值都是對的就可以
    int x = 1int y = 2;
    String hello = "hello";
    • 1
    • 2
    • 3
    • 4

    修改

    //此時 x 的賦值就必須在 y之前 
    int x = 1int y = x;
    String hello = "hello";
    • 1
    • 2
    • 3
    • 4

    就好比早上9點打卡上班,無論是坐公交還是地鐵都無所謂我只要9點打卡上班,但是假如此時你途中要去某個小店遲早點然後騎自行車打卡上班,這個時候上班就不再是地鐵公交無所謂了就需要先選擇一個方式到達小店,再騎車上班打卡。至於一開始是地鐵還是公交雖然還是無所謂但是從到小店開始後面的就必須是固定下來的順序。

    關於指令重排推薦一篇文章淺顯清晰 http://ifeve.com/jvm-memory-reordering/

    在計算機執行指令的順序在經過程序編譯器編譯之後形成的指令序列,一般而言,這個指令序列是會輸出確定的結果;以確保每一次的執行都有確定的結果。但是,一般情況下,CPU和編譯器爲了提升程序執行的效率,會按照一定的規則允許進行指令優化,在某些情況下,這種優化會帶來一些執行的邏輯問題,主要的原因是代碼邏輯之間是存在一定的先後順序,在併發執行情況下,會發生二義性,即按照不同的執行邏輯,會得到不同的結果信息。

    Java內存模型中的程序天然有序性可以總結爲一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現爲串行語義”,後半句是指“指令重排序”現象和“工作內存主主內存同步延遲”現象。
    引用自<深入理解JAVA虛擬機 JVM高級特性與最佳實踐>

    我的理解是就線程內部而言最終結果正確即是合理的操作也就可以說是有序的,相對有序,不是絕對按照程序編寫的順序。但從一個線程觀察另一個線程的時候,另一個線程的操作本會發生重排,並且是可能隨時被打斷(原子操作除外)的,這個時候當線程又相互關係的時候就會發生不可知的運行結果,所以可以說是無序的,無法估計的。

    Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
    引用自<深入理解JAVA虛擬機 JVM高級特性與最佳實踐>

  • 可見性

  • 可見性是指當一個線程修改了線程共享變量的值,其它線程能夠立即得知這個修改。

    Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方法來實現可見性的,無論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是volatile的特殊規則保證了新值能立即同步到主內存,以及每使用前立即從內存刷新。因爲我們可以說volatile保證了線程操作時變量的可見性,而普通變量則不能保證這一點。
    除了volatile之外,Java還有兩個關鍵字能實現可見性,它們是synchronized。同步塊的可見性是由“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store和write操作)”這條規則獲得的,而final關鍵字的可見性是指:被final修飾的字段是構造器一旦初始化完成,並且構造器沒有把“this”引用傳遞出去,那麼在其它線程中就能看見final字段的值。
    引用自<深入理解JAVA虛擬機 JVM高級特性與最佳實踐>

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章