Volatile的内存可见性

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/HNUST_LIZEMING/article/details/88353907

内存模型
什么是JAVA 内存模型?

Java Memory Model (JAVA 内存模型)是描述线程之间如何通过内存(memory)来进行交互。 具体说来, JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。  简称JMM。每一个CPU中都有一个缓存,这个缓存一般存储线程中的变量,这样就影响了线程之间的可见性(降低了耦合)。

  但是很多的时候我们要确保多处理器开发中共享变量的可见性。从硬件层次上分析如何实现Volatile。

通过Lock前缀指令

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存操作会使在其他CPU中缓存了该内存地址的数据无效(通过嗅探在总线上传播的数据来检查自己缓存值是否过期)。

那么当写两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,Thread-A写了变量i,那么:

Thread-A发出LOCK#指令
发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
Thread-A向主存回写最新修改的i
Thread-B读取变量i,那么:

Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值。

Java内存模型的抽象示意图如下: 

扫描二维码关注公众号,回复: 5477857 查看本文章

è¿éåå¾çæè¿°
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

1、线程A把本地内存A中更新过的共享变量刷新到主内存中去。 
2、线程B到主内存中去读取线程A之前已更新过的共享变量。

说明白了内存模型,我们看一看什么是内存可见性?

内存可见性
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。

由于线程之间的交互都发生在主内存中,但对于变量的修改又发生在自己的工作内存中,经常会造成读写共享变量的错误,我们也叫可见性错误。

可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。

解决方案
我们可以通过同步来保证对象被安全地发布。除此之外我们也可以使用一种更加轻量级的volatile变量,还可以使用ReentrantLock,CAS等等。

synchronized关键字
public class TestSynchronized {

    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true) {
            synchronized (td) {
                if (td.getFlag()) {
                    System.out.println("主线程flag:" + td.getFlag());
                    break;
                }
            }
        }
    }
}

class ThreadDemo implements Runnable {

    //共享变量
    private boolean flag = false;

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

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (Exception e) {
        }

        flag = true;

        System.out.println("其他线程flag=" + getFlag());
    }
}

同步锁方案:会带来性能问题,效率特别低,造成线程阻塞。

volatile关键字
java 提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当多个线程进行操作共享数据时,可以保证内存中的数据可见。 相较于synchronized是一种较为轻量级的同步策略。

public class TestVolatile {

    public static void main(String[] args) {

        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while(true){
            if(td.getFlag()){
                System.out.println("主线程flag:" + td.getFlag());
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable{
    //共享变量
    private volatile boolean  flag = false;

    public boolean getFlag() {
        return flag;
    }

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

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (Exception e) {
        }

        flag = true;

        System.out.println("其他线程flag=" + getFlag());
    }
}

volatile的读写操作的过程: 
(1)线程写volatile变量的过程: 
1、改变线程工作内存中volatile变量的副本的值 
2、将改变后的副本的值从工作内存刷新到主内存 
(2)线程读volatile变量的过程: 
1、从主内存中读取volatile变量的最新值到线程的工作内存中 
2、从工作内存中读取volatile变量的副本

volatile方案: 
1、能够保证volatile变量的可见性 
2、不能保证变量状态的”原子性操作(Atomic operations)”

虽然我们对Volatile的内存可见性有了一定理解,我们还需要对它有更加深刻的认识,不再多写了:面试时被问到了volatile ,找个文章总结一下(早点看到就好了)

注意的是:

读写volatile变量是原子的,包括64位的long和double。

锁总线和锁内存的区别是:

锁总线的话,所有处理器都无法执行读写内存的操作。消耗比较大。

锁缓存的话,就是无发对指定内存进行读写操作,对于其他地址内存而言都是所有处理器都是可以进行访问的。

原子操作:不可以中断的一个或者是一系列的操作。

延申可以查看:https://blog.csdn.net/HNUST_LIZEMING/article/details/88355963

猜你喜欢

转载自blog.csdn.net/HNUST_LIZEMING/article/details/88353907