volatile是什么?有什么特性?
1、volatile是java虛擬機提供的輕量級的同步機制
三大特性如下:
1、保證可見性
2、不保證原子性
3、禁止指令重排
JMM內存模型是如何實現可見性?
JVM運行程序實體是線程,線程創建時JVM都會為其開辟一個工作內存,工作內存是每個線程私有的數據區域,而java內存模型中規定所有變量都存在主內存中 (主內存:相當於電腦上的內存條,有8G、16G等...)線程對變量的操作(讀取和賦值)必須在自己的工作內存中進行,
首先要將變量從主內存中拷貝到自己的工作內存空間,然后對變量進行操作,操作完后再將變量寫會主內存。 如下圖:
圖1 圖2
1、由於添加了volatile的緣故,線程A對主內存共享變量的操作線程B是可見的,這就體現了volatile三大特性之一的 “保證可見性”
2、不保證原子性的原因:假如主內存有一個變量值為 age=18, 線程A和線程B分別將主內存的變量拷貝到自己的工作內存中, 線程A將age的值修改成 20,
然后在寫回主內存,此時主內存的值為 20;由於各種原因,線程B網絡故障導致修改時間比線程A慢了十幾秒;而此時線程A又把主內存中的 20 改成 18,這時線程B
通過CAS(ComPareAndSwap):比較並交換)發現主內存中的值還是 18;於是把 18 改成了 22,雖然修改成功了,但並不表示沒有問題了;CAS操作引起了ABA的問題。
3、禁止指令重排:多線程環境下線程交替執行,由於編譯器優先重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測,用volatile修飾變量
可防止指令重排
驗證volatile可見性、解決volatile不保證原子性的case

package com.company; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; class MyData { volatile int number = 0;//沒加volatile的時候不保證可見性 public void addTo() { this.number = 60; } //此時已經添加了volatile,不保證原子性 public void addNumber() { number++; } //AtomicInteger解決原子性問題 AtomicInteger atomicInteger = new AtomicInteger();//不傳參數,底層源碼默認是 0 public void addPlus() { atomicInteger.getAndIncrement();//Atomically increments by one the current value. 源碼注釋每調一次就會自增 1 } } /** * 1.0 驗證 volatile 的可見性 * 1.1 假設int number = 0,number變量不添加volatile關鍵字修飾,沒有可見性 * 1.2 添加了volatile,可以解決可見性問題 * <p> * 2.0 volatile 不保證原子性 */ public class VolatileDemo { public static void main(String[] args) { atomicIntegerSee(); SeeVolatile(); } /** * AtomicInteger保證原子性 */ public static void atomicIntegerSee() { MyData myData = new MyData(); /** * 驗證volatile不保證原子性 * number添加了volatile不保證原子性 * 20加到1000,答案是20000;但volatile不保證原子性 * 如何解決volatile不保證原子性問題? * 方法一:在方法上面添加 synchronized 關鍵字修飾,但每次訪問只能有一個線程;導致並發性下降 * 方法二:使用 AtomicInteger 即可以保證原子性,又不會降低並發量 */ for (int i = 1; i <= 20; i++) { new Thread(() -> { for (int j = 1; j <= 1000; j++) { myData.addNumber();//volatile不保證原子性,所以最終結果不會是20000;會比20000小;有可能僥幸是20000 myData.addPlus();// 20個線程怎么加都是20000.能解決原子性的問題 } }, String.valueOf(i)).start(); } while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + "\t volatile number value is: " + myData.number); System.out.println(Thread.currentThread().getName() + "\t AtomicInteger number value is: " + myData.atomicInteger); } /** * 1.0 驗證volatile的可見性 */ public static void SeeVolatile() { MyData myData = new MyData(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }//線程休眠3秒鍾 myData.addTo();//這時候number已經變成60,但由於沒有添加volition關鍵字,其他線程並不知道number已經變成60 System.out.println(Thread.currentThread().getName() + "\t updata number: " + myData.number);//此時拿到的number已經是修改完后寫回主內存的number }, "AAA").start(); while (myData.number == 0) { //如果取到的number等於0,main線程就會執行里面的內容;但此時main線程已經等於60;所以線程一直處於等待狀態,下面的語句無法輸出 } System.out.println(Thread.currentThread().getName() + "\t main Thread number is fish,main get number: " + myData.number); } }
解決CAS引起的ABA問題

1 package com.company; 2 3 import java.util.concurrent.TimeUnit; 4 import java.util.concurrent.atomic.AtomicReference; 5 import java.util.concurrent.atomic.AtomicStampedReference; 6 7 /** 8 * 版本號的原子引用 9 * 解決ABA問題:AtomicStampedReference 10 */ 11 public class ABADemo { 12 static AtomicReference<Integer> atomicReference = new AtomicReference<>(100); 13 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(100, 1); 14 15 public static void main(String[] args) { 16 System.out.println("=================================以下是ABA問題的產生======================================="); 17 new Thread(() -> { 18 atomicReference.compareAndSet(100, 101); 19 atomicReference.compareAndSet(101, 100); 20 }, "t1").start(); 21 new Thread(() -> { 22 try { 23 TimeUnit.SECONDS.sleep(1); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 System.out.println(atomicReference.compareAndSet(100, 2019) + "\t " + atomicReference.get());//修改成功,但t1中間修改了兩次又改回100,所以存在ABA問題 28 }, "t2").start(); 29 try { 30 TimeUnit.SECONDS.sleep(2); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 }//暫停兩秒鍾,保證上面的t1和t2 線程都操作完 34 System.out.println("=================================以下是ABA問題的解決======================================="); 35 new Thread(() -> { 36 int stamp = atomicStampedReference.getStamp();//獲取當前版本號 37 try { 38 TimeUnit.SECONDS.sleep(1); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 }//休眠1秒,讓t4也獲得當前版本號 42 System.out.println(Thread.currentThread().getName() + "\t 第一版本號為:" + stamp); 43 atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); 44 System.out.println(Thread.currentThread().getName() + "\t 第二版本號為:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference()); 45 atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); 46 System.out.println(Thread.currentThread().getName() + "\t 第三版本號為:" + atomicStampedReference.getStamp() + "\t 當前主內存中的值為:" + atomicStampedReference.getReference()); 47 }, "t3").start(); 48 new Thread(() -> { 49 int stamp = atomicStampedReference.getStamp();//獲取當前版本號 50 System.out.println(Thread.currentThread().getName() + "\t 第一版本號:" + stamp); 51 try { 52 TimeUnit.SECONDS.sleep(3); 53 } catch (InterruptedException e) { 54 e.printStackTrace(); 55 }//休眠3秒,讓t3完成一次ABA的操作 56 boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);//修改失敗,版本號不匹配 57 /*boolean result=atomicStampedReference.compareAndSet(100,2019,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);*/ 58 System.out.println(Thread.currentThread().getName() + "\t 是否修改成功:" + result + "\t 當前最新版本為:" + atomicStampedReference.getStamp()); 59 System.out.println(Thread.currentThread().getName() + "\t 當前主內存的最新值為:" + atomicStampedReference.getReference()); 60 }, "t4").start(); 61 } 62 }
CAS優缺點
優點:
利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。
缺點:
1、ABA問題。當第一個線程執行CAS操作,尚未修改為新值之前,內存中的值已經被其他線程連續修改了兩次,使得變量值經歷 A -> B -> A的過程。
2、循環時間長開銷大。如果有很多個線程並發,CAS自旋可能會長時間不成功,會增大CPU的執行開銷。
3、只能對一個變量進原子操作。JDK1.5之后,新增AtomicReference類來處理這種情況,可以將多個變量放到一個對象中。