從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小于進程。另外,多核 CPU 時代意味著多個線程可以同時運行,這減少了線程上下文切換的開銷。
從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的并發量,而多線程并發編程正是開發高并發系統的基礎,利用好多線程機制可以大大提高系統整體的并發能力以及性能。
總結:并發編程的目的就是為了能提高程序的執行效率提高程序運行速度,充分的利用多核CPU資源。
原子性(Atomicity):在一次或多次操作中,要么所有的操作都執行并且不會受其他因素干擾而中斷,要么所有的操作都不執行
可見性:一個線程對共享變量的修改,其他線程能夠立刻看到。(synchronized,volatile)
有序性:程序執行的順序按照代碼的先后順序執行。(指令重排:處理器為了提高程序運行效率,處理器根據指令之間的數據依賴性,可能會對指令進行重排序,單線程下可以保證程序最終執行結果和代碼順序執行的結果是一致的,但是多線程下有可能出現問題)。
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
舉個簡單的例子,看下面這段代碼:
//線程1執行的代碼
int i = 0;
i = 10;
//線程2執行的代碼
j = i;
假若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i = 10這句時,會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當中i的值變為10了,卻沒有立即寫入到主存當中。此時線程2執行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當中,注意此時內存當中i的值還是0,那么就會使得j的值為0,而不是10。線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。在多線程環境下,一個線程對共享變量的操作對其他線程是不可見的。這就是可見性問題。
對于可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。而普通的共享變量不能保證可見性,因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
即程序執行的順序按照代碼的先后順序執行。
舉個簡單的例子,看下面這段代碼:
int i = 0; ? ? ? ? ? ? ?
boolean flag = false;
i = 1; ? ? ? ? ? ? ? ?//語句1 ?
flag = true; ? ? ? ? ?//語句2
上面代碼定義了一個int型變量,定義了一個boolean類型變量,然后分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,為什么呢?這里可能會發生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序,一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果并沒有影響,那么就有可能在執行過程中,語句2先執行而語句1后執行。那么它靠什么保證的呢?進行重排序時是會考慮指令之間的數據依賴性。雖然重排序不會影響單個線程內程序執行的結果,但是多線程呢?下面看一個例子:
//線程1:
context = loadContext(); ? //語句1
inited = true; ? ? ? ? ? ? //語句2
//線程2:
while(!inited ){
? ? sleep()
}
doSomethingwithconfig(context);
上面代碼中,由于語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以為初始化工作已經完成,那么就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context并沒有被初始化,就會導致程序出錯。
從上面可以看出,在Java內存模型中,允許編譯器和處理器對指令進行重排序,
但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發也不支持并行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續吃飯,這說明你支持并發。 (不一定是同時的)
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。
并發的關鍵是你有處理多個任務的能力,不一定要同時。
并行的關鍵是你有同時處理多個任務的能力。
1)Java 中的線程對應是操作系統級別的線程,線程數量控制不好,頻繁的創建、銷毀線程和線程間的切換,比較消耗內存和時間。
2)容易帶來線程安全問題。如線程的可見性、有序性、原子性問題,會導致程序出現的結果與預期結果不一致。
3)多線程容易造成死鎖、活鎖、線程饑餓等問題。此類問題往往只能通過手動停止線程、甚至是進程才能解決,影響嚴重。
4)對編程人員的技術要求較高,編寫出正確的并發程序并不容易。
5)并發程序易出問題,且難調試和排查;問題常常詭異地出現,又詭異地消失。
Java 5.0 提供了java.util.concurrent(簡稱JUC)包,在此包中增加了在并發編程中很常見的實用工具類,用于定義類似于編程的自定義子系統,包括線程池、異步IO和輕量級任務框架。提供可調的、靈活的線程池。還提供了設計用于多線程上下文的Collection實現等。
JMM其實并不像JVM內存模型一樣是真實存在的,它只是一個抽象的規范。在不同的硬件或者操作系統下,對內存的訪問邏輯都有一定的差異,而這種差異會導致同一套代碼在不同操作系統或者硬件下,得到了不同的結果,而JMM的存在就是為了解決這個問題,通過JMM的規范,保證Java程序在各種平臺下對內存的訪問都能得到一致的效果。
計算機在執行程序的時候,每條指令都是在 CPU 中執行的,而執行的時候,又免不了和數據打交道,而計算機上面的數據,是存放在計算機的物理內存上的。當內存的讀取速度和CPU的執行速度相比差別不大的時候,這樣的機制是沒有任何問題的,可是隨著CPU的技術的發展,CPU的執行速度和內存的讀取速度差距越來越大,導致CPU每次操作內存都要耗費很多等待時間。
為了解決這個問題,初代程序員大佬們想到了一個的辦法,就是在CPU和物理內存上新增高速緩存,這樣程序在運行過程中,會將運算所需要的數據從主內存復制一份到CPU的高速緩存中,當CPU進行計算時就可以直接從高速緩存中讀數據和寫數據了,當運算結束再將數據刷新到主內存就可以了。
隨著時代的變遷,CPU開始出現了多核的概念,每個核都有一套自己的緩存,并且隨著計算機能力不斷提升,還開始支持多線程,最終演變成多個線程訪問進程中的某個共享內存,且這多個線程分別在不同的核心上執行,則每個核心都會在各自的 Cache 中保留一份共享內存的緩沖,我們知道多核是可以并行的,這樣就會出現多個線程同時寫各自的緩存的情況,導致各自的 Cache 之間的數據可能不同。
總結下來就是:在多核 CPU 中,每個核的自己的緩存,關于同一個數據的緩存內容可能不一致。
重排序指的是在執行程序時,為了提高性能,從源代碼到最終執行指令的過程中,編譯器和處理器會對指令進行重排的一種手段。
下圖為從源代碼到最終指令示意圖
重排序的分為3種
1)編譯器優化的重排序:編譯器在不改變單線程程序語義(as-if-serial)的的前提下,可以重新安排語句的執行順序。
2)指令級并行的重排序:現在處理器采用指令級并行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
3)內存系統的重排序:由于處理器使用了存儲和讀寫緩沖區,這使得加載和存儲操作看上去亂序執行。
1.編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
2.指令級并行的重排序。現代處理器采用了指令級并行技術(ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
3.內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
4.這些重排序對于單線程沒問題,但是多線程都可能會導致多線程程序出現內存可見性問題。
數據依賴性:編譯器和處理器在重排序時,針對單個處理器中執行的指令序列和單個線程中執行的操作會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
遵守as-if-serial 語義:不管編譯器和處理器為了提高并行度怎么重排序,(單線程)程序的執行結果不能被改變。
區別:
as-if-serial定義:無論編譯器和處理器如何進行重排序,單線程程序的執行結果不會改變。
happens-before定義:一個操作happens-before另一個操作,表示第一個的操作結果對第二個操作可見,并且第一個操作的執行順序也在第二個操作之前。但這并不意味著Java虛擬機必須按照這個順序來執行程序。如果重排序的后的執行結果與按happens-before關系執行的結果一致,Java虛擬機也會允許重排序的發生。
happens-before關系保證了同步的多線程程序的執行結果不被改變,as-if-serial保證了單線程內程序的執行結果不被改變。
相同點:happens-before和as-if-serial的作用都是在不改變程序執行結果的前提下,提高程序執行的并行度。
不可變對象即對象一旦被創建,它的狀態(對象屬性值)就不能改變。
不可變對象的類即為不可變類。Java 平臺類庫中包含許多不可變類,如 String、基本類型的包裝類、BigInteger 和 BigDecimal 等。
不可變對象保證了對象的內存可見性,對不可變對象的讀取不需要進行額外的同步手段,提升了代碼執行效率。
1.保證變量寫操作的可見性;
2.保證變量前后代碼的執行順序;
不能。volatile不能保證原子性,只能保證線程可見性,可見性表現在當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。
被volatile修飾的變量被修改時,會將修改后的變量直接寫入主存中,并且將其他線程中該變量的緩存置為無效,從而讓其它線程對該變量的引用直接從主存中獲取數據,這樣就保證了變量的可見性。
但是volatile修飾的變量在自增時由于該操作分為讀寫兩個步驟,所以當一個線程的讀操作被阻塞時,另一個線程同時也進行了自增操作,此時由于第一個線程的寫操作沒有進行所以主存中仍舊是之前的原數據,所以當兩個線程自增完成后,該變量可能只加了1。因而volatile是無法保證對變量的任何操作都是原子性的。
能,Java 中可以創建 volatile 類型數組,但如果多個線程改變引用指向的數組,將會受到 volatile 的保護,如果多個線程改變數組的元素內容,volatile 標示符就不能起到之前的保護作用了。
volatile變量可以確保先行關系,即寫操作會發生在后續的讀操作之前,但它并不能保證原子性。例如用volatile修飾count變量那么count++操作就不是原子性的。
而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement( )方法會原子性的進行增量操作把當前值加- ,其它數據類型和引用變量也可以進行相似操作。
原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分,將整個操作視作一個整體是原子性的核心特征。
而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執行添加、遞增、遞減等操作。比如之前多線程下的線程不安全的 i++ 問題,到了原子類這里,就可以用功能相同且線程安全的 getAndIncrement 方法來優雅地解決。
原子類的作用和鎖有類似之處,是為了保證并發情況下線程安全。不過原子類相比于鎖,有一定的優勢:
粒度更細:原子變量可以把競爭范圍縮小到變量級別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會比使用同步互斥鎖的效率更高,因為原子類底層利用了 CAS 操作,不會阻塞線程。原子類的作用和鎖有類似之處,是為了保證并發情況下線程安全。不過原子類相比于鎖,有一定的優勢:
粒度更細:原子變量可以把競爭范圍縮小到變量級別,通常情況下,鎖的粒度都要大于原子變量的粒度。
效率更高:除了高度競爭的情況之外,使用原子類的效率通常會比使用同步互斥鎖的效率更高,因為原子類底層利用了 CAS 操作,不會阻塞線程。
AtomicInteger與AtomicLong:它們的底層實現使用了CAS鎖,不同點在于AtomicInteger包裝了一個Integer型變量,而AtomicLong包裝了一個Long型變量。
LongAdder:它的底層實現是分段鎖+CAS鎖。
atomic代表的是concurrent包下Atomic開頭的類,如AtomicBoolean、AtomicInteger、AtomicLong等都是用原子的方式來實現指定類型的值的更新,它的底層通過CAS原理解決并發情況下原子性的問題,在jdk中CAS是Unsafe類中的api來實現的。
CAS,全稱為Compare and Swap,即比較-替換,實現并發算法時常用到的一種技術。假設有三個操作數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,才會將內存值修改為B并返回true,否則什么都不做并返回false。當然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,否則舊的預期值A對某條線程來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功。
以AtomicInteger為例,說明CAS的使用與原理。首先atomicIngeter初始化為5,調用對象的compareAndSet方法來對比當前值與內存中的值,是否相等,相等則更新為2019,不相等則不會更新,compareAndSet方法返回的是boolean類型。
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,2019)+" \t current "+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,2014)+" \t current "+atomicInteger.get());
}
}
分析:第一次調用,內存中的值是5,通過對比相等更新為2019,輸出 true current 2019,第二次調用時,內存重點的值已經更新為2019,不相等不更新內存中的值,輸出 false current 2019。
1)CAS存在一個很明顯的問題,即ABA問題。
如果變量V初次讀取的時候是A,并且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其他線程修改過了嗎?如果在這段期間曾經被改成B,然后又改回A,那CAS操作就會誤認為它從來沒有被修改過。針對這種情況,java并發包中提供了一個帶有標記的原子引用類AtomicStampedReference,它可以通過控制變量值的版本來保證CAS的正確性。
2)只能保證一個共享變量的原子性。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無 法保證操作的原子性,這個時候就可以使用鎖來保證原子性。