欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java中JMM與volatile關(guān)鍵字的學(xué)習(xí)

 更新時間:2021年09月24日 09:45:03   作者:BeiChen_103  
這篇文章主要介紹了通過實例解析JMM和Volatile關(guān)鍵字的學(xué)習(xí),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下

JMM

JMM是指Java內(nèi)存模型,不是Java內(nèi)存布局,不是所謂的棧、堆、方法區(qū)。

每個Java線程都有自己的工作內(nèi)存。操作數(shù)據(jù),首先從主內(nèi)存中讀,得到一份拷貝,操作完畢后再寫回到主內(nèi)存。

在這里插入圖片描述

JMM可能帶來可見性、原子性和有序性問題。

1.可見性:指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對于串行程序來說,可見性問題 是不存在。因為你在任何一個操作步驟中修改某個變量,那么在后續(xù)的步驟中,讀取這個變量的值,一定是修改后的新值。但是這個問題在并行程序中就不見得了。如果一個線程修改了某一個全局變量,那么其他線程未必可以馬上知道這個改動。

2.原子性:指一個操作是不可中斷的,即使是多個線程一起執(zhí)行的時候,一個線程操作一旦開始,就不會被其他線程干擾比如,對于一個靜態(tài)全局變量int i,兩個線程同時對它賦值,線程A 給他賦值 1,線程 B 給它賦值為 -1,。那么不管這兩個線程以何種方式,何種步調(diào)工作,i的值要么是1,要么是-1,線程A和線程B之間是沒有干擾的。這就是原子性的一個特點,不可被中斷。

3.有序性:對于一個線程的執(zhí)行代碼而言,我們總是習(xí)慣地認為代碼的執(zhí)行時從先往后,依次執(zhí)行的。這樣的理解也不能說完全錯誤,因為就一個線程而言,確實會這樣。但是在并發(fā)時,程序的執(zhí)行可能就會出現(xiàn)亂序。給人直觀的感覺就是:寫在前面的代碼,會在后面執(zhí)行。有序性問題的原因是因為程序在執(zhí)行時,可能會進行指令重排,重排后的指令與原指令的順序未必一致。

volatile關(guān)鍵字

volatile關(guān)鍵字是Java提供的一種輕量級同步機制。它能夠保證可見性和有序性,但是不能保證原子性。

可見性與原子性測試

class MyData{
    int number=0;
    //volatile int number=0;
    AtomicInteger atomicInteger=new AtomicInteger();
    public void setTo60(){
        this.number=60;
    }
    //此時number前面已經(jīng)加了volatile,但是不保證原子性
    public void addPlusPlus(){
        number++;
    }
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}
//volatile可以保證可見性,及時通知其它線程主物理內(nèi)存的值已被修改
private static void volatileVisibilityDemo() {
    System.out.println("可見性測試");
    MyData myData=new MyData();//資源類
    //啟動一個線程操作共享數(shù)據(jù)
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+"\t come in");
        try {TimeUnit.SECONDS.sleep(3);myData.setTo60();
        System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();}
    },"AAA").start();
    while (myData.number==0){
     //main線程持有共享數(shù)據(jù)的拷貝,一直為0
    }
    System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number);
}

可見性:

MyData類是資源類,一開始number變量沒有用volatile修飾,所以程序運行的結(jié)果是:

可見性測試
AAA come in
AAA update number value: 60

雖然"AAA"線程把number修改成了60,但是main線程持有的仍然是最開始的0,所以一直循環(huán),程序不會結(jié)束。

如果對number添加了volatile修飾,運行結(jié)果是:

AAA come in
AAA update number value: 60
main mission is over. main get number value: 60

可見某個線程對number的修改,會立刻反映到主內(nèi)存上。

原子性:

volatile并不能保證操作的原子性。這是因為,比如一條number++的操作,底層會形成3條指令。

getfield        //讀
iconst_1	//++常量1
iadd		//加操作
putfield	//寫操作

假設(shè)有3個線程,分別執(zhí)行number++,都先從主內(nèi)存中拿到最開始的值,number=0,然后三個線程分別進行操作。假設(shè)線程A執(zhí)行完畢,number=1,也立刻通知到了其它線程,但是此時線程B、C已經(jīng)拿到了number=0,所以結(jié)果就是寫覆蓋,線程B、C將number變成1。

解決的辦法就是:

  • addPlusPlus()方法加鎖。
  • 使用java.util.concurrent.AtomicInteger類。
private static void atomicDemo() {
    System.out.println("原子性測試");
    MyData myData=new MyData();
    for (int i = 1; i <= 20; i++) {
        new Thread(()->{
            for (int j = 0; j <1000 ; j++) {
                myData.addPlusPlus();
                myData.addAtomic();
            }
        },String.valueOf(i)).start();
    }
    while (Thread.activeCount()>2){
        Thread.yield();
    }
    System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);
    System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger);
}

結(jié)果:可見,由于volatile不能保證原子性,出現(xiàn)了線程重復(fù)寫的問題,最終結(jié)果比20000小。而AtomicInteger可以保證原子性。

原子性測試
main	 int type finally number value: 17542
main	 AtomicInteger type finally number value: 20000

有序性:

volatile可以保證有序性,也就是防止指令重排序。所謂指令重排序,就是出于優(yōu)化考慮,CPU執(zhí)行指令的順序跟程序員自己編寫的順序不一致。就好比一份試卷,題號是老師規(guī)定的(代碼是程序員規(guī)定的),但是考生(CPU)可以先做選擇題,也可以先做填空題。

但是有時候這種情況就會出現(xiàn)問題:

int x = 11; //語句1
int y = 12; //語句2
x = x + 5;  //語句3
y = x * x;  //語句4

以上例子,可能出現(xiàn)的執(zhí)行順序有1234、2134、1342,這三個都沒有問題,最終結(jié)果都是x = 16,y=256。但是如果是4開頭,就有問題了,y=0。這個時候就不需要指令重排序。

哪些地方用到過volatile?

單例模式的安全問題

常見的DCL(Double Check Lock)模式雖然加了同步,但是在多線程下依然會有線程安全問題。

public class SingletonDemo {
    private static SingletonDemo singletonDemo=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是構(gòu)造方法");
    }
    //DCL模式 Double Check Lock 雙端檢索機制:在加鎖前后都進行判斷
    public static SingletonDemo getInstance(){
        if (singletonDemo==null){
            synchronized (SingletonDemo.class){
                 if (singletonDemo==null){
                     singletonDemo=new SingletonDemo();
                 }
            }
        }
        return singletonDemo;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i+1)).start();
        }
    }
}

這個漏洞比較tricky,很難捕捉,但是是存在的。instance=new SingletonDemo();可以大致分為三步:

memory = allocate();     //1.分配內(nèi)存
instance(memory);	 //2.初始化對象
instance = memory;	 //3.設(shè)置引用地址

由于Java編譯器允許處理器亂序執(zhí)行,以及JDK1.5之前JMM(Java Memory Medel,即Java內(nèi)存模型)中Cache、寄存器到主內(nèi)存回寫順序的規(guī)定,上面的第二點和第三點的順序是無法保證的,也就是說,執(zhí)行順序可能是1-2-3也可能是1-3-2,如果是后者,并且在3執(zhí)行完畢、2未執(zhí)行之前,被切換到線程B上,這時候instance因為已經(jīng)在線程A內(nèi)執(zhí)行過了第三點,instance已經(jīng)是非空了,所以線程B直接拿走instance,然后使用,然后順理成章地報錯,而且這種難以跟蹤難以重現(xiàn)的錯誤很可能會隱藏很久。

解決的方法就是對singletondemo對象添加上volatile關(guān)鍵字,禁止指令重排。

你知道CAS嗎?

CAS是指Compare And Swap,比較并交換,是一種很重要的同步思想。如果主內(nèi)存的值跟期望值一樣,那么就進行修改,否則一直重試,直到一致為止。

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2021)+"\t current data : "+ atomicInteger.get());
        //修改失敗
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get());
    }
}

第一次修改,期望值為5,主內(nèi)存也為5,主內(nèi)存的值修改成功,為2021;第二次修改,期望值為5,主內(nèi)存實際值為2021,修改失敗。

CAS底層原理

public final int getAndIncrement(){
    return unsafe.getAndAddInt(this,valueOffset,1);
}

查看AtomicInteger.getAndIncrement()方法,發(fā)現(xiàn)其沒有加synchronized也實現(xiàn)了同步。這是為什么?

AtomicInteger內(nèi)部維護了volatile int valueprivate static final Unsafe unsafe兩個比較重要的參數(shù)。

AtomicInteger.getAndIncrement()調(diào)用了Unsafe.getAndAddInt()方法。Unsafe類的大部分方法都是native的,用來像C語言一樣從底層操作內(nèi)存。

public final int getAnddAddInt(Object var1,long var2,int var4){
    int var5;
    do{
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

這個方法的var1和var2,就是根據(jù)對象和偏移量得到在主內(nèi)存的快照值var5。然后compareAndSwapInt方法通過var1和var2得到當前主內(nèi)存的實際值。如果這個實際值跟快照值相等,那么就更新主內(nèi)存的值為var5+var4。如果不等,那么就一直循環(huán),一直獲取快照,一直對比,直到實際值和快照值相等為止。

比如有A、B兩個線程,一開始都從主內(nèi)存中拷貝了原值為3,A線程執(zhí)行到var5=this.getIntVolatile,即var5=3。此時A線程掛起,B修改原值為4,B線程執(zhí)行完畢,由于加了volatile,所以這個修改是立即可見的。A線程被喚醒,執(zhí)行this.compareAndSwapInt()方法,發(fā)現(xiàn)這個時候主內(nèi)存的值不等于快照值3,所以繼續(xù)循環(huán),重新從主內(nèi)存獲取。

CAS缺點

CAS實際上是一種自旋鎖

  • 一直循環(huán)等待,開銷比較大。
  • 只能保證一個變量的原子操作,多個變量依然要加鎖。
  • 引出ABA問題。

ABA問題

所謂的ABA問題,就是比較并交換的循環(huán),存在一個時間差,而這個時間差可能帶來意想不到的問題。比如線程T1將一個值從A改為B,然后又從B改為A。當線程T2訪問時,看到的就是A,但是卻不知道這個A其實發(fā)生了更改。盡管線程T2 CAS操作成功,但是不代表就沒有問題。

有的需求,比如CAS,只注重頭尾(只看期望值和實際值),只要首尾一致就接受。但是有的需求,還看重過程,中間不能發(fā)生任何修改,這就引出了AtomicReference:原子引用。

AtomicReference

AtomicInteger對整數(shù)進行原子操作,但是如果對象是一個POJO呢?我們這時就可以使用AtomicReference來包裝這個POJO,使其操作原子化。

User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false

AtomicStampedReferenceABA問題的解決

使用AtomicStampedReference類可以解決ABA問題。這個類維護了一個版本號Stamp,在進行CAS操作的時候,不僅要比較當前值,還要比較版本號。只有兩者都相等,才能執(zhí)行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

使用實例:

package thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    public static void main(String[] args) {
        System.out.println("======ABA問題的產(chǎn)生======");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get().toString());
        }, "t2").start();
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
        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(100,101,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第二次版本號: " + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,
                    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);
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result=atomicStampedReference.compareAndSet(100,2019,
                    stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t修改成功與否:"+result+"  當前最新版本號"+atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName()+"\t當前實際值:"+atomicStampedReference.getReference());
        }, "t4").start();
    }
}

總結(jié)

總結(jié)來源于GitHub,內(nèi)部帶有源碼和用例圖。

本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!

相關(guān)文章

  • 詳解Java 包掃描實現(xiàn)和應(yīng)用(Jar篇)

    詳解Java 包掃描實現(xiàn)和應(yīng)用(Jar篇)

    這篇文章主要介紹了詳解Java 包掃描實現(xiàn)和應(yīng)用(Jar篇),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-07-07
  • 淺談spring容器中bean的初始化

    淺談spring容器中bean的初始化

    下面小編就為大家?guī)硪黄獪\談spring容器中bean的初始化。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-06-06
  • SpringBoot如何導(dǎo)出Jar包并測試(使用IDEA)

    SpringBoot如何導(dǎo)出Jar包并測試(使用IDEA)

    這篇文章主要介紹了SpringBoot如何導(dǎo)出Jar包并測試(使用IDEA),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • SpringBoot 如何使用Dataway配置數(shù)據(jù)查詢接口

    SpringBoot 如何使用Dataway配置數(shù)據(jù)查詢接口

    這篇文章主要介紹了SpringBoot 如何使用Dataway配置數(shù)據(jù)查詢接口,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • 三分鐘讀懂mybatis中resultMap和resultType區(qū)別

    三分鐘讀懂mybatis中resultMap和resultType區(qū)別

    這篇文章主要給大家介紹了mybatis中resultMap和resultType區(qū)別的相關(guān)資料,resultType和resultMap都是mybatis進行數(shù)據(jù)庫連接操作處理返回結(jié)果的,需要的朋友可以參考下
    2023-07-07
  • SpringCloud 服務(wù)注冊和消費實現(xiàn)過程

    SpringCloud 服務(wù)注冊和消費實現(xiàn)過程

    這篇文章主要介紹了SpringCloud 服務(wù)注冊和消費實現(xiàn)過程,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • 頁面的緩存與不緩存設(shè)置及html頁面中meta的作用

    頁面的緩存與不緩存設(shè)置及html頁面中meta的作用

    這篇文章主要介紹了頁面的緩存與不緩存設(shè)置及html頁面中meta的作用的相關(guān)資料,需要的朋友可以參考下
    2016-05-05
  • 淺析JAVA常用JDBC連接數(shù)據(jù)庫的方法總結(jié)

    淺析JAVA常用JDBC連接數(shù)據(jù)庫的方法總結(jié)

    本篇文章是對在JAVA中常用JDBC連接數(shù)據(jù)庫的方法進行了詳細的總結(jié)分析,需要的朋友參考下
    2013-07-07
  • Java實現(xiàn)簡單計算器小程序

    Java實現(xiàn)簡單計算器小程序

    這篇文章主要為大家詳細介紹了Java實現(xiàn)簡單計算器小程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-07-07
  • 深入了解Java.Util.Date詳情

    深入了解Java.Util.Date詳情

    這篇文章主要介紹了Java.Util.Date,很少有類能像java.util.Date那樣在堆棧溢出方面引起如此多的類似問題,關(guān)于具體原因下文內(nèi)容詳細介紹,需要的朋友可以參考一下
    2022-06-06

最新評論