在Java中,線程部分是一個重點,本篇文章說的JUC也是關于線程的。JUC就是java.util .concurrent工具包的簡稱。這是一個處理線程的工具包,JDK 1.5開始出現的。下面一起來看看它怎么使用。
一、volatile關鍵字與內存可見性
1、內存可見性:
先來看看下面的一段代碼:
?
?
?
?
?
上圖中這段代碼很簡單,就是一個ThreadDemo類繼承Runnable創建一個線程。它有一個成員變量flag為false,然后重寫run方法,在run方法里面將flag改為true,同時還有一條輸出語句。然后就是main方法主線程去讀取flag。如果flag為true,就會break掉while循環,否則就是死循環。按道理,下面那個線程將flag改為true了,主線程讀取到的應該也是true,循環應該會結束。運行結果
?
從上圖中可以看到,該程序并沒有結束,也就是死循環。說明主線程讀取到的flag還是false,可是另一個線程明明將flag改為true了,而且打印出來了,這是什么原因呢?這就是內存可見性問題。
- 內存可見性問題:當多個線程操作共享數據時,彼此不可見。
要解決這個問題,可以加鎖。如下:
加了鎖,就可以讓while循環每次都從主存中去讀取數據,這樣就能讀取到true了。但是一加鎖,每次只能有一個線程訪問,當一個線程持有鎖時,其他的就會阻塞,效率就非常低了。不想加鎖,又要解決內存可見性問題,那么就可以使用volatile關鍵字。
?
2、volatile關鍵字:
- 用法:
volatile關鍵字:當多個線程操作共享數據時,可以保證內存中的數據可見。用這個關鍵字修飾共享數據,就會及時的把線程緩存中的數據刷新到主存中去,也可以理解為,就是直接操作主存中的數據。所以在不使用鎖的情況下,可以使用volatile。如下:
這樣就可以解決內存可見性問題了。
- volatile和synchronized的區別:
volatile不具備互斥性(當一個線程持有鎖時,其他線程進不來,這就是互斥性)。
volatile不具備原子性。
?
?
二、原子性
1、理解原子性:
上面說到volatile不具備原子性,那么原子性到底是什么呢?先看如下代碼:
?
?
這段代碼就是在run方法里面讓i++,然后啟動十個線程去訪問。看看結果:
?
?
可以發現,出現了重復數據。明顯產生了多線程安全問題,或者說原子性問題。所謂原子性就是操作不可再細分,而i++操作分為讀改寫三步,如下:
?
所以i++明顯不是原子操作。上面10個線程進行i++時,內存圖解如下:
?
看到這里,好像和上面的內存可見性問題一樣。是不是加個volatile關鍵字就可以了呢?其實不是的,因為加了volatile,只是相當于所有線程都是在主存中操作數據而已,但是不具備互斥性。比如兩個線程同時讀取主存中的0,然后又同時自增,同時寫入主存,結果還是會出現重復數據。
2、原子變量:
JDK 1.5之后,Java提供了原子變量,在java.util.concurrent.atomic包下。原子變量具備如下特點:
- 有volatile保證內存可見性。
- 用CAS算法保證原子性。
3、CAS算法:
CAS算法是計算機硬件對并發操作共享數據的支持,CAS包含3個操作數:
- 內存值V
- 預估值A
- 更新值B
當且僅當V==B時,才會把B的值賦給V,即V = B,否則不做任何操作。就上面的i++問題,CAS算法是這樣處理的:首先V是主存中的值0,然后預估值A也是0,因為此時還沒有任何操作,這時V=B,所以進行自增,同時把主存中的值變為1。如果第二個線程讀取到主存中的還是0也沒關系,因為此時預估值已經變成1,V不等于B,所以不進行任何操作。
4、使用原子變量改進i++問題:
?
- 定義:AtomicInteger是一個提供原子操作的Integer類,通過線程安全的方式操作加減。
- 使用場景?:適合高并發情況下的使用
AtomicInteger是在使用非阻塞算法實現并發控制,在一些高并發程序中非常適合,但并不能每一種場景都適合,不同場景要使用使用不同的數值類。
注意:高并發的情況下,
i++
無法保證原子性,往往會出現問題,所以引入AtomicInteger
類。
原子變量用法和包裝類差不多,如下:
?
?
?