JUC之volatile

最近一直在看《Java并发编程的艺术》这本书,看了后有种感觉,网上关于JVM与JUC的绝大部分文章、资料的源头都出自这本书以及《深入理解Java虚拟机》。作为一个Javaer,我应该好好多撸几遍这两本书且多做笔记。下面开始吧~

先看一段代码:

public class TestVolatile {

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo, "Thread-A").start();

        while (true) {
            if (threadDemo.isFlag()) {
                System.out.println(Thread.currentThread().getName() + "=============");
                break;
            }
        }
    }

}

class ThreadDemo implements Runnable {

    private boolean flag = false;

    @Override
    public void run() {

        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag = true;

        System.out.println("flag = " + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

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

输出是什么?为什么?

输入图片说明

发现flag的值已经被改为true了,但是Main线程中的while循环并未结束,这是什么原因导致的?

该问题被称为 内存可见性问题 ,即多个线程操作共享数据时,线程之间不能看到共享变量被修改后的

值,出现的原因就涉及到JVM内存模型(JMM)了:

Java中,所有的共享变量(实例域、静态域和数组)都存在堆内存中,因为堆内存在线程之间是共享的,所以这些变量被称为共享变量,JVM定义了线程与主内存(这里可以理解为就是堆内存)之间的一种抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地工作内存(也可以理解为缓存,主要是为了解决效率问题),本地内存中存储了该线程以读/写共享变量的副本

关于CPU操作主内存、本地工作内存可以类比一下CPU、高速缓存、内存之间的关系

输入图片说明

线程A与线程B之间要通信(相互更改共享变量的值后让对方知道)的话,必须经历下面2个步骤:

  • 线程A把本地工作内存A中更新过的共享变量刷新到主内存中去
  • 线程B到主内存中去读取线程A之前已经更新过的共享变量

上述问题原因分析:

  1. 一开始线程Thread-A与main线程之间都有共享变量副本值flag = false
  2. 线程Thread-A将本地工作内存flag值改为flag = true,此时并不会立即更新到主内存中去
  3. main运行时,一直是用的是最开始从主内存中获取的flag = false,保存在本地工作内存中的, 所以while循环一直未能break

解决办法:

Java提供了volatile关键字,如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

有volatile变量修饰的共享变量进行写操作的时候,JVM执行字节码转化为汇编指令代码时,会多出lock前缀的一行汇编代码,Lock前缀的指令在多和多核处理器下会引发两件事情:

  1. 将当前处理器缓存行(本地工作内存)的数据写回到系统内存(主内存) 对应:当写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内中。

  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效 对应:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内中读取共享变量。

volatile有许多特性:

  1. 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后的写入。什么意思?即当一个线程修改了变量的值,新的值会立即同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。

  2. 原子性。对任意单个volatile变量的读写具有原子性,但类似于volatile++这种复合操作不具有原子性

为什么volatile可以有这样的特性?(先大概了解下,后面可能会详细学习记录happens-before原则)

先行发生原则(happens-before),从JDK1.5开始,Java使用新的JSR-133内存模型,用happens-before原则来阐述线程之间的可见性,在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C ,那么A happens-before C。

了解完上述知识点后,再回到原来的问题,将flag用volatile修饰再运行验证下结论

public class TestVolatile {

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo, "Thread-A").start();

        while (true) {
            if (threadDemo.isFlag()) {
                System.out.println(Thread.currentThread().getName() + "=============");
                break;
            }
        }
    }

}

class ThreadDemo implements Runnable {

    private volatile boolean flag = false;

    @Override
    public void run() {

        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag = true;

        System.out.println("flag = " + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

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

输入图片说明

猜你喜欢

转载自my.oschina.net/hensemlee/blog/1807436