一.什么是DLC雙端鎖?有什么用處?
為了解決在多線程模式下,高並發的環境中,唯一確保單例模式只能生成一個實例
多線程環境中,單例模式會因為指令重排和線程競爭的原因會出現多個對象
public class DLCDemo { private static DLCDemo instance = null; private DLCDemo(){ System.out.println(Thread.currentThread().getName() + "\t" + " 線程啟動"); }; public static DLCDemo getInstance(){ if (instance == null){ instance = new DLCDemo(); } return instance; } public static void main(String[] args) { //多線程模式下 for (int i = 1; i <= 10; i++) { new Thread(() -> { DLCDemo.getInstance(); },String.valueOf(i)).start(); } } }
運行結果: 在10個線程下,出現了10個對象,顯然違背了單例模式
改進
public class DLCDemo { /*DLC雙端鎖機制不一定線程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排 * 原因在於某一個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用可能並沒有完成初始化 * instance = new DLCDemo01() 可以分為以下三個步驟 * 1.memory = allocate() 分配對象的內存空間 * 2.instance(memory) 初始化對象 * 3.instance = memory 設置instance指向剛剛分配的內存地址,此時instance != null * 由於步驟2 步驟3不存在數據的依賴關系,而且無論重拍前還是重排后的執行結果在單線程中並沒有發生 * 改變,所以這樣的重排優化是允許的 * 1.memory = allocate() 分配對象的內存空間 * 3.instance = memory 設置instance指向剛剛分配的內存地址,此時instance != null ,但是對象還沒有初始化完成 * 2.instance(memory) 初始化對象 * 所以當一條線程訪問instance不為null時,由於instance實例未必已初始化完成,也就造成了線程安全問題 * */ private static volatile DLCDemo instance = null; private DLCDemo(){ System.out.println(Thread.currentThread().getName() + "\t" + " 線程啟動"); }; // 加入DLC雙端鎖,來保證線程安全 public static DLCDemo getInstance(){ if (instance == null){ synchronized (DLCDemo.class){ if(instance == null){ instance = new DLCDemo(); } } } return instance; } public static void main(String[] args) { //多線程模式下 for (int i = 1; i <= 10; i++) { new Thread(() -> { DLCDemo.getInstance(); },String.valueOf(i)).start(); } } }
運行結果
二.JAVA如何保證原子性?它的底層是如何實現的?
底層通過CAS實現的,CAS比較並交換,是一條CPU並發原語,它的功能是判斷內存某個位置的值是否是預期值,如果是就更改為新值.
CAS並發原語體現在Java語言中就是sun.misc.Unsafe類中的各種方法,調用Unsafe類中的CAS方法,JVM會幫助我們實現CAS匯編指
令,這是一種完全依賴於硬件的功能,通過它實現了原子操作.由於CAS是一種執行原語,屬於操作系統用語范疇,是由若干條指令組成的
它是用於完成某個功能的一個過程,並且原子的執行必須是連續的,在執行過程中不允許被中斷.也就是說,CAS是CPU的原子指令,不會
造成數據不一致的問題.
應用:如果當前線程的期望值和物理內存的實際值是一致的,主內存就會更新為當前線程的新值,否則本次更新無效,需要重新獲取主物理內
存的值.
CAS有3個操作數,內存值V,舊的預期值A,要修改的更新值B,當且僅當預期值A和內存值V相同時,將內存值從A改為B,否則什么都不做.
底層:Unsafe類 +自旋鎖
Unsafe類是CAS的核心,由於Java無法直接訪問底層系統,需要本地(native)方法進行訪問,Unsafe相當於一個后門,基於該類可以直接操作
內存中的數據.Unsafe存在於sun.misc包中,其內部方法可以向C指針一樣直接操作內存,因為Java的CAS執行依賴於Unsafe類的方法.
注:Unsafe類中的所有方法都是native修飾的,也就是說,Unsafe類中的方法都是直接調用操作系統底層資源執行相應的任務.
變量:valueOffset,表示該變量的內存地址偏移值,因為Unsafe類就是根據偏移地址來獲取數據.
變量:value,使用volatile修飾,保證了在多線程下的數據的可見性.
缺點:1.循環時間長,開銷大.2.只能保證一個變量的原子操作.3.會引發ABA問題
public class CASDemo { public static void main(String[] args) { // 原始值 AtomicInteger atomicInteger = new AtomicInteger(3); // 和舊值比較並交換,成功返回true System.out.println(atomicInteger.compareAndSet(3,2019)+"\t" + "the new value is "+ atomicInteger.get()); // 失敗返回false System.out.println(atomicInteger.compareAndSet(3,1024)+"\t" + "the new value is "+ atomicInteger.get()); atomicInteger.getAndIncrement(); } }
運行結果:
三.請你談一談什么是ABA問題,如何解決?
CAS會導致ABA問題,因為CAS算法實現的最重要的前提就是需要取出內存中某個時刻的數據並在當下時刻比較並交換,那么在這個時間差之內,
可能會導致數據發生變化.
比如一個線程T1從內存位置V處取出A,此時另一個線程T2也從內存中取出A,並且把值改為B,然后又把值改回了A,此時T1線程進行CAS操作發現
V處的值依然是A,然后T1線程操作成功,
public static void show1(AtomicReference<Integer> atomicReference){ System.out.println("沒有使用時間戳同步機制,導致ABA問題"); new Thread(() -> { atomicReference.compareAndSet(10,20); atomicReference.compareAndSet(20,10); },"t1").start(); new Thread(() ->{ //線程暫停,保證上面的ABA問題 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } atomicReference.compareAndSet(10,100); System.out.println(atomicReference.get()); },"t2").start(); }
運行結果:
增加版本號控制ABA問題
public static void show2(AtomicStampedReference<Integer> atomicStampedReference){ // 通過增加版本號,來限制數據同步的機制 System.out.println("使用了時間戳同步機制,解決ABA問題"); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+" 第一次版本號:"+"\t"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(10,20, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+ " 第二次版本號:"+"\t"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(20,10, atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+" 第三次版本號:"+"\t"+atomicStampedReference.getStamp()); },"t3").start(); new Thread(() ->{ int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+" 第一次版本號:"+"\t"+stamp); //等待3s,讓t3執行一次ABA操作 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"當前版本號: "+atomicStampedReference.getStamp()); boolean res=atomicStampedReference.compareAndSet(10,1024, stamp,stamp+1); System.out.println(Thread.currentThread().getName()+" 修改是否成功: "+ res + "\t當前實際的版本號: "+ atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName()+"\t當前實際最新值:"+atomicStampedReference.getReference()); },"t4").start(); }
public static void main(String[] args) { AtomicReference<Integer> atomicReference = new AtomicReference<>(10); AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1); //show1(atomicReference); show2(atomicStampedReference); }
運行結果:
解決了ABA問題