Java-JUC(一):volatile引入

问题背景:

volatile是为了解决内存可见性而生的,什么是内存不可见性呢?

以下边的代码为例:

package com.dx.juc;

public class VoltileTest {
    public static void main(String[] args) {
        MyThread thread=new MyThread();
        thread.start();

        while (true){
            if(thread.flag){
                System.out.println("thread flag is true");
                break;
            }
        }

        System.out.println("complete");
    }
}

class MyThread extends Thread {
    public  boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
            flag = true;
            System.out.println("flag is changed;" + flag);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}

在线程thread开始执行的过程中会吧thread.flag属性值修改为true,一般情况下来说,main线程在while(true)循环内部是可以检测到thread.flag被修改了,而且我们希望是这样子。但是程序运行起来的时候会发现flag在线程thread中被修改后,main线程并不能读取到被修改的值。

输出结果为:

此时,就是main一直在执行while(true)循环操作。

出现问题的原因?

 

原因:

1)thread线程修改flag值时间晚于main获取(拷贝)flag值(到main缓存)的时间;

2)同时main线程中读取flag数据是从main线程的缓存中读取,而不是直接从主存中读取。

或用更简洁的话来描述:

两个线程操作共享数据时,彼此不可见,线程可见性导致的问题。

那么为什么要使用缓存?

  • Register

寄存器是CPU的内部组成单元,是CPU运算时取指令和数据的地方,速度很快,寄存器可以用来暂存指令、数据和地址。在CPU中,通常有通用寄存器,如指令寄存器IR;特殊功能寄存器,如程序计数器PC、sp等

  • 寄存器的工作方式很简单,只有两步:(1)找到相关的位,(2)读取这些位。
  • Cache

缓存即就是用于暂时存放内存中的数据,若果寄存器要取内存中的一部分数据时,可直接从缓存中取到,这样可以调高速度。高速缓存是内存的部分拷贝。

  • 内存的工作方式就要复杂得多:

(1)找到数据的指针。(指针可能存放在寄存器内,所以这一步就已经包括寄存器的全部工作了。)

(2)将指针送往内存管理单元(MMU),由MMU将虚拟的内存地址翻译成实际的物理地址。

(3)将物理地址送往内存控制器(memory controller),由内存控制器找出该地址在哪一根内存插槽(bank)上。

(4)确定数据在哪一个内存块(chunk)上,从该块读取数据。

(5)数据先送回内存控制器,再送回CPU,然后开始使用。

内存的工作流程比寄存器多出许多步。每一步都会产生延迟,累积起来就使得内存比寄存器慢得多。

为了缓解寄存器与内存之间的巨大速度差异,硬件设计师做出了许多努力,包括在CPU内部设置缓存、优化CPU工作方式,尽量一次性从内存读取指令所要用到的全部数据等等。

 如何解决内存不可见的问题?使用volatile

 使用java中的volatile实现内存可见性

package com.dx.juc;

public class VoltileTest {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();

        while (true) {
            if (thread.isFlag()) {
                System.out.println("thread flag is true");
                break;
            }
        }

        System.out.println("complete");
    }
}

class MyThread extends Thread {
    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(200);
            setFlag(true) ;
            System.out.println("flag is changed;" + isFlag());
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

 使用了volatile后确实可以保证了内存可见性,当thread线程修改了flag的值时,会先把thread线程缓存中的值修改,立即把thread线程缓存中修改的值刷新到主存中,同时引用了被volatile修饰的变量所在的线程对应缓存(清空)失效,此时main线程在while循环处理是检测到缓存中无该变量值,则要从主存中获取flag对象,因此,确保了数据的可见性。

用volatile修饰之后带来的影响:

第一:使用volatile关键字会强制将修改的值立即写入主存;

第二:使用volatile关键字的话,当线程thread进行修改时,会导致线程main的工作内存中缓存变量flag的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:由于线程main的工作内存中缓存变量flag的缓存行无效,所以线程main再次读取变量flag的值时会去主存读取。

那么在线程thread修改flag值时(当然这里包括2个操作,修改线程thread工作内存中的值,然后将修改后的值写入内存),会使得线程main的工作内存中缓存变量flag的缓存行无效,然后线程main读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程main读取到的就是最新的正确的值。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值