java 併發

併發概覽
併發編程背景概述[建議改成“併發編程背景概述”或者“CPU和內存發展概述”。]
在早期,計算機採用的都是馮諾依曼架構,即CPU-內存架構。運行在計算機上的程序也是順序執行的,當一個程序執行完,才能執行另一個。但很快發現順序執行效率非常低下,當一個程序被阻塞時,整個計算機都被阻塞。隨着計算機技術的發展,於是出現了多道程序設計,允許一個計算機可以同時運行多個進程,當一個進程被阻塞時可以讓出CPU允許其他進程執行。但是隨着CPU和內存的發展,CPU和內存之間的速度差越來越大,CPU等待內存I/O成爲了主要瓶頸。[s]於是在CPU和內存之間增加了高速緩存,高速緩存是和CPU綁定的。但隨着單個CPU的性能已經到達了物理極限,於是出現了多CPU架構,每個CPU都有自己的緩存。在多CPU架構中,每個CPU上都可以運行一個進程。相對來看CPU上進行進程切換是比較耗時的,因爲不同的進程對計算機資源的需求(內存,緩存,寄存器)差異很大,導致每次切換進程需要更換大量的計算機資源。於是出現了線程,線程屬於進程。在進程內部可以劃分出多個線程,當一個線程被阻塞時,可以切換到其他線程。[描述突兀。可以和進程進行對比描述。]因爲線程屬於進程,而且比進程更加輕量級,線程之間切換更加高效。
高速緩存
緩存:這裏的緩存指的是物理硬件緩存,並不是軟件組件緩存。緩存會把經常讀取或將要讀取的數據存儲起來,當CPU讀取這些數據時可以從緩存裏進行讀取,當CPU寫數據時,也不會直接對內存進行寫入,而是將數據寫入緩存,再從緩存寫入到內存。CPU對緩存訪問要比對內存訪問快的多。
進程概念
進程:一個正在運行的程序就是一個進程[請解釋的再具體一些。],例如運行一個hello word就是一個進程,同時運行兩個hello word,就是兩個進程。
線程概念
線程:CPU調度的最小單位。當一個進程被阻塞時,可以不切換進程,而是讓該進程的線程進行切換。
併發概念
併發分爲進程併發和線程併發。進程併發指的是多個進程都在運行,線程併發指的是多個線程都在運行。併發是有一個閾值的,當併發數小於閾值時併發可以提高CPU執行效率,當併發數超過閾值時,更多的併發並不能提高CPU效率,反而會浪費計算機資源。
併發問題
進程併發不會導致數據錯誤(進程之間基本不會共享數據),但線程併發如果不對共享資源進行訪問控制可能會引發數據錯誤。假設這樣一種場景, foo=0,線程A和線程B要對foo執行foo++,線程A先執行,但線程A還沒有執行完畢,線程B也開始執行,因此線程B讀到的值也是foo=0,線程A執行完畢foo=1, 線程B執行完畢foo=1.這樣就產生了髒數據,但期望值是foo=2.
Java內存模型
JMM
java是運行在java虛擬機(JVM)上的,但本質上還是運行在操作系統和硬件上的。Java早期的一個宣傳口號就是:”Write Once, Run Anywhere”。那如何實現呢?通過JVM對操作系統和硬件進行封裝,對計算機資源做了進一步的抽象和統一。在JVM中,就包含了對內存的抽象,也就是java內存模型JMM。JMM的作用就是屏蔽掉操作系統和硬件所導致的內存訪問的差異性。JMM定義了主存(Main Memory)和工作內存(Working Memory),每條線程都有自己的工作內存。主存保存了各個線程的共享變量,工作內存存儲了對應線程所使用的共享變量的副本和本地變量。線程訪問共享變量時,是不能直接訪問主存的,線程只能訪問工作內存中的數據。工作內存可以訪問主存中的數據,工作內存可以類比爲計算機中的緩存(cache)。
圖2-1java內存模型

原子操作
原子在古希臘中指的是不可分割的最小物質,強調的是不可分割。原子操作指的是不可分割的操作,其他操作無法讀取到原子操作的中間狀態。
讀取數據
CPU訪問數據需要通過一系列的CPU指令。在JMM中訪問數據類似,也必須通過一系列的操作。線程訪問數據時需要通過以下8中操作,lock, unlock,read,load,use,assign,store,write每一種操作都是原子操作。例如線程A讀取數據,需要執行read,load兩個原子操作,但是在這兩個原子操作中間還可以發生其他原子操作。因此線程A讀取數據不是原子的。同樣進行寫數據,需要進行store和write操作,因此寫操作也不是原子操作。
指令重排序
指令流水
爲了提高CPU執行效率,將指令執行拆分爲了流水線結構。指令的執行分爲以下幾個步驟:指令讀取,指令編譯,讀取操作數,指令執行,寫結果。[可以具體說明一下。]當一條指令執行完指令讀取,然後執行指令編譯,另一條指令就可以執行編譯了。CPU將時間量化,將讀取一個指令字的最短時間規定爲CPU週期。假設CPU執行指令讀取,指令編譯,讀取操作數,指令執行,寫結果幾個操作分別需要一個CPU週期,那麼執行完這條執行需要佔用5個CPU週期。採用流水線結構之後,CPU可以一次讀入5條指令執行,相當於只佔用了一個CPU週期。
多級指令流水
CPU爲了進一步提高指令執行效率,採用了多級指令流水,即CPU內有多條指令流水線,每條指令流水線都可以執行多條指令。
多級指令流水3-1

亂序執行
採用多級指令流水雖然可以高效利地用CPU,但是也會導致一些問題。程序是順序執行的,一條指令執行完畢,纔可以執行另一條,但由於指令分佈在多條流水線上,後面的指令有可能先執行完畢。爲了保證程序的正確性,編譯器會禁止對某些指令進行亂序執行,比如邏輯結構,if,switch等等,因此在單線程的情況下亂序執行能夠保證正確,並提高效率。
但是在多線程的環境下,指令亂序執行仍然會導致錯誤。因爲在多線程情況下,一個線程的執行需要依‘觀察’另一個線程,考慮以下情況:
變量x=0,變量f=0,線程A中當f=0時,一直進行循環,f!=0是會print x,線程B中對x進行賦值,x=42,對f進行賦值,f=1。
線程A:
while (f==0);
// 插入內存屏障
print x;
線程B:
x = 42;
// 插入內存屏障
f = 1;
對於這段程序,期望值是x=42,但有可能輸出的不是42。線程B進行亂序執行,f=1先執行,然後線程A執行,檢測f!=0,print x=0;同樣線程A可能進行亂序執行,print x先執行,然後再執行f的檢查操作。
內存屏障
內存屏障(memory barrier):該指令之前的操作執行完畢之後,纔可以執行該指令之後的操作。在單個CPU的情況內存屏障沒有什麼意義,但是在多CPU的情況下,就可以避免指令亂序執行導致的錯誤。上例中,可以在註釋處插入內存屏障來保證程序的正確性。內存屏障屬於CPU指令級別的操作,在高級程序語言中是無法直接使用,但高級語言會通過一些特性,進行隱式的調用,在java中就是volatile關鍵字。
Volatile
Volatile語義
Volatile關鍵字使用的非常少,很多開放人員都無法正確的使用它。線程訪問volatile修飾的變量時會遵循以下規則:
1進行多操作時,會把主存中的數據更新到工作內存中,然後從工作內存中讀取數據。
2進行寫操作時,會把數據寫入到工作內存中,然後從工作內存中再同步到主存中。
因此被volatile修飾的變量每次都可以讀取到最新的數據。但是並不能保證在多線程的情況下的正確性。考慮以下情況,foo=0, 線程A讀取foo=0, 線程B讀取foo=0,線程A執行foo++寫入工作內存,並刷新主存foo=1,線程B執行foo++寫入工作內存foo=1,並刷新主存foo=1,這樣仍然會產生髒數據。volatile本質上是實現了happens-before約束。
happens-before
happens-before:如果操作A先發生,然後操作B發生,操作B是會被操作A影響到的。這看起來和自然界的公理一樣。爲什麼說volatile實現了happens-before呢?雖然讀操作和寫操作是原子操作,但是JMM中有一個類似於緩存的概念—工作內存。工作內存並不會把數據實時的刷新到主存中,也不會從主存中實時的獲取數據。通過volatile的工作內存和主存之間的數據同步策略,可以實現happens-before。
volatile應用場景
volatile適用於這樣的多線程場景,至多隻有一個線程可以對volatile變量進行寫操作,其他線程只有讀操作。比如有一個多線程應用,在應用啓動的時候需要做大量的初始化和檢查操作,當這些操作執行完畢之後纔可以提供服務,可以稱之爲開關服務。
開關服務僞代碼
volatile switch = false;
public void run(){
while(!switch){
//do something
}
}
public void start(){
switch = true;
}
[缺少資源引用]結束語
計算機技術在不斷地發展,從經典的內存–CPU架構到內存–多級緩存–CPU架構,從單核到多核,從單指令執行到多級指令流水。每一次技術的升級都會極大的提升計算機性能,但對軟件設計也 要求更加苛刻。java是一門流行的高級程序語言,通過虛擬機的形式屏蔽掉了計算機硬件和操作系統的差異,然後提出了統一的主存–工作內存–線程模型。在java中會碰到擁有奇怪特性的volatile關鍵字。通過了解硬件特性和JMM,volatile關鍵字並沒有那麼詭異

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