Java 如何解决可见性和有序性的问题

Java 内存模型

我们之前说过,导致可见性的的原因是缓存,导致有序性的原因是编译优化,那么如何解决这两个问题呢?当然,最简单暴力的方法就是禁用缓存和编译优化。但是这么做的话,我们为性能所做的努力就都白费了,肯定是行不通的。问题还是要解决的,我们可以按照我们的需要有选择性的禁用缓存和编译优化。那么,问题的关键是:如何禁用?这个时候我们需要 Java 内存模型来帮助我们。

Java 内存模型是个很复杂的规范。Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。这些方法包括:volatile、synchronized、final 三个关键字以及六项 Happens-Before 规则。

volatile 的困惑

volatile 关键字并不是 Java 特有的,古老的 C语言里也有,它最原始的意义就是禁用 CPU 缓存。比如:

volatile int x = 0;

上面这段代码的要表达的是:对 x 这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。看起来很符合我们的需求,但是在 Java 中,1.5版本以后我们才可以放心的这么使用,看下面的代码:

public class VolatileDemo {
    int x = 0;
    volatile boolean v = false;

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                volatileDemo.writer();
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                volatileDemo.read();
            }
        }).start();

    }

    private void writer() {
        v = true;
        x = 2;
    }

    private void read() {
        if (v) {
            System.out.println("Volatile: x = " + x);
        }
    }
}

这段代码的运行结果输出,我们预期的是 x = 2;但是如果在 Java1.5 之前的版本,这个结果是不可预期的,因为有可能是 x = 0;为什么呢?因为在 1.5 版本,Java 内存模型对 volatile 的语义进行了增强,其中一项就是我们要说的 Happens-Before 规则。

Happens-Before 规则

什么是 Happens-Before 规则?简单来说就是:前面一个操作的结果对于后续的操作是可见的。Happens-Before 规则允许编译器的优化行为,但是要求编译优化后要遵守它的规则。它的规则有哪些?

  • 程序的顺序性规则:

在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后面的操作

比如

x = 2;
v = true;

x = 2 Happens-Before 于 v = true;这个就是规则一的含义。这个比较符合单线程的思维:程序前面对某个变量的修改对于后续操作是可见的。

  • volatile 变量规则

对一个 volatile 变量的写操作 Happens-Before 于后续对这个变量的读操作。

看起来就是禁用缓存的意思,我们和规则三联系起来看一下再看我们之前的例子。

  • 传递性

如果 A Happens-Before 于 B,B Happens-Before 于 C,那么 A Happens-Before 于 C。

这个规则应用到我们之前的例子就是:

  1. 首先,x = 2 Happens-Before 于 v = true,这是规则一;
  2. v = true 写操作 Happens-Before 于读变量 v = true,这是规则二;
  3. 根据传递性,x = 2 Happens-Before 于读变量 v = true,这是规则三;
    就是说第一个线程设置的 x = 2 对于第二个线程进行读操作是可见的。如图:

这就是 Java 1.5 版本对于 volatile 语义的增强,1.5版本的并发工具包(java.utile.concurrent)就是靠 volatile 语义来搞定可见性问题的。

  • 管程中锁的规则

对一个锁的解锁 Happens-Before 于后续对于这个锁的加锁。

首先,管程是什么?管程是一种通用的同步原语,直接点,在 Java 中就是指synchronized。如下代码:

public class SynchroziedDemo {
    int x = 10;
    private void change() {
        //此处自动加锁
        synchronized (this) {
            if (this.x < 12) {
                x = 12;
            }
        }
        //此处自动解锁
    }
}

如上代码,我们可以这么理解,线程一执行完代码块后 x = 12;线程二进入代码块后,能看到线程一对于 x 的操作,即知道 x = 12。

  • 线程 start() 规则

主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程之前的操作。

下面示例代码,线程 B 中的输出值应该是:x: 100;

int x = 10;
Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("x: " + x);
            }
});
//此处对共享变量 x 的修改对 B 可见
x = 100;
B.start();
  • 线程 join() 规则

主线程 A 等待线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能看到子线程对于共享变量的操作。

Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                x = 111;//线程B对于共享变量的修改
            }
        });
        B.start();
        B.join();//线程B对于共享变量的修改在B.join()之后皆可见
        System.out.println("x: " + x);// 此处输出:x: 111

以上总结于《Java并发编程实战》:

Java 如何解决可见性和有序性的问题_第1张图片

你可能感兴趣的:(Java多线程)