在java中,執行下面這個語句
1
|
int i = 12 ; |
執行線程必須先在自己的工作線程中對變量i所在的緩存行進行賦值操作,然后再寫入主存當中。而不是直接將數值10寫入主存(物理內存)當中。
1 原子性
定義:即一個操作或者多個操作 要么全部執行并且執行的過程不會被任何因素打斷,要么就都不執行。
舉個最簡單的例子,大家想一下假如為一個32位的變量賦值過程不具備原子性的話,會發生什么后果?
int i =12;
假若一個線程執行到這個語句時,暫且假設為一個32位的變量賦值包括兩個過程:為低16位賦值,為高16位賦值。
那么就可能發生一種情況:當將低16位數值寫入之后,突然被中斷,而此時又有一個線程去讀取i的值,那么讀取到的就是錯誤的數據。
1.1 java中的原子性操作
在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要么執行,要么不執行。
例如:
1
2
3
4
|
int x = 10 ; //語句1 int y = x; //語句2 x++; //語句3 x = x + 1 ; //語句4 |
語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工作內存中,所以是原子性操作。
語句2實際上包含2個操作,它先要去讀取x的值,再將y的值寫入主存,雖然讀取x的值以及 將y的值寫入主存 這2個操作都是原子性操作,但是合起來就不是原子性操作了。
語句3 語句4 同理,先將x的值讀取到高速緩存中,然后+1賦值后,再寫入到主存中。
也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。
2 可見性
定義:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
2.1 可見性問題
例如:
1
2
3
4
5
6
|
//線程1 int i = 12 ; i= 13 ; //線程2 int j=i; |
假若執行線程1的是CPU1
,執行線程2的是CPU2
。當線程1執行 i =13
這句時,會先把i的初始值加載到CPU1
的高速緩存中,然后賦值為13,那么在CPU1的高速緩存當中i的值變為13了,卻沒有立即寫入到主存當中。
此時線程2執行 j = i
,它會先去主存讀取i的值并加載到CPU2
的緩存當中,注意此時內存當中i的值還是12,那么就會使得j的值為12,而不是13。
這就是可見性問題,也就是說 i 的值在線程一中修改了,沒有通知其他線程更新而導致的數據錯亂。
2.2 解決可見性問題
Java
提供了volatile
關鍵字來保證可見性。
也就是說當一個共享變量被volatile
修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
3 有序性
定義:即程序執行的順序按照代碼的先后順序執行。
3.1 單個線程內程序的指令重排序
例如:
1
2
3
4
|
int i = 0 ; boolean flag = false ; i = 1 ; //語句1 flag = true ; //語句2 |
按照我們日常的思維,程序的執行過程是從上至下一行一行執行的,就是說按照代碼的順序來執行,那么JVM在實際中一定會這樣嗎??? 答案是否定的,這里可能會發生指令重排序(Instruction Reorder
)。
指令重排序(Instruction Reorder
) 是指: 處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。
在Java
內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
比如上面的代碼中,語句1和語句2誰先執行對最終的程序結果并沒有影響,那么就有可能在執行過程中,語句2先執行而語句1后執行。
需要注意的是:處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令Instruction 2
必須用到Instruction
1的結果,那么處理器會保證Instruction 1
會在Instruction 2
之前執行。
3.2 多線程內程序的指令重排序
重排序不會影響單個線程內程序執行的結果,但是多線程就不一定了。
1
2
3
4
5
6
7
8
9
|
//線程1: context = loadContext(); //語句1 inited = true ; //語句2 //線程2: while (!inited ){ sleep() } doSomethingwithconfig(context); |
上面代碼中,由于語句1和語句2沒有數據依賴性,因此可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以為初始化工作已經完成,那么就會跳出while
循環,去執行doSomethingwithconfig(context)
方法,而此時context
并沒有被初始化,就會導致程序出錯。
3.3 保證有序性的解決方法
在Java里面,可以通過volatile
關鍵字來保證一定的“有序性”。
當然可以通過synchronized
和Lock
來保證有序性,很顯然,synchronized
和Lock
保證每個時刻是有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼,自然就保證了有序性。
3.4 volatile 保證有序性的原理
volatile
關鍵字能禁止指令重排序,所以volatile
能在一定程度上保證有序性,也就是說:
當程序執行到volatile
變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;
在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行。
4 實例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class Test { public volatile int inc = 0 ; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for ( int i= 0 ;i< 10 ;i++){ new Thread(){ public void run() { for ( int j= 0 ;j< 1000 ;j++) test.increase(); }; }.start(); } while (Thread.activeCount()> 1 ) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } } |
一般說來 有10個線程分別進行了1000次操作,那么最終inc的值應該是1000*10=10000。但實際中并不是這樣,進行過測試后會發現,每次執行結束后,得到的都是一個比10000要小的值。
4.1 原理分析
自增操作是不具備原子性的,它包括讀取變量的原始值到高速緩存中、進行加1操作、寫入主存中這三個過程。
也就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
假如某個時刻變量inc的值為10,
線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了;
然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,
此時 變量inc的值還沒有任何改變,此時線程2拿到的值也為10,然后進行加1操作,然后將值11寫入到主存中,
然后線程1繼續進行加1操作 這里線程1中 inc的值依然為10,進行加1操作,然后將值11寫入到主存中
那么兩個線程分別進行了一次自增操作后,inc只增加了1。
4.2 synchronized 結合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class Test { public int inc = 0 ; public synchronized void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for ( int i= 0 ;i< 10 ;i++){ new Thread(){ public void run() { for ( int j= 0 ;j< 1000 ;j++) test.increase(); }; }.start(); } while (Thread.activeCount()> 1 ) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } } |
4.3 Lock 結合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public class Test { public int inc = 0 ; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally { lock.unlock(); } } public static void main(String[] args) { final Test test = new Test(); for ( int i= 0 ;i< 10 ;i++){ new Thread(){ public void run() { for ( int j= 0 ;j< 1000 ;j++) test.increase(); }; }.start(); } while (Thread.activeCount()> 1 ) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } } |
4.4 使用AtomicInteger替換int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class Test { public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } public static void main(String[] args) { final Test test = new Test(); for ( int i= 0 ;i< 10 ;i++){ new Thread(){ public void run() { for ( int j= 0 ;j< 1000 ;j++) test.increase(); }; }.start(); } while (Thread.activeCount()> 1 ) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } } |
到此這篇關于java并發編程之原子性、可見性、有序性 的文章就介紹到這了,更多相關java并發編程之原子性、可見性、有序性 內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.51cto.com/928343994/2841441