volatile的使用以及引起的問題解決方法


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);
    }
}
View Code

 解決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 }
View Code

 

 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類來處理這種情況,可以將多個變量放到一個對象中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM