关于java:java并发内存模型的几个显见例子

33次阅读

共计 2592 个字符,预计需要花费 7 分钟才能阅读完成。

并发三问题:重排序,内存可见性,原子性

I、重排序代码示例

import java.util.concurrent.CountDownLatch;

public class RearrangeTest {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;) {
            i ++;
            x = 0; y = 0;
            a = 0; b = 0;

            CountDownLatch latch = new CountDownLatch(1);

            Thread one = new Thread(() -> {
                try {latch.await();
                } catch (InterruptedException e) { }
                a = 1;
                x = b;
            });

            Thread other = new Thread(() -> {
                try {latch.await();
                } catch (InterruptedException e) { }
                b = 1;
                y = a;
            });

            one.start();
            other.start();

            latch.countDown();

            one.join();
            other.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {System.err.println(result);
                break;
            } else {System.out.println(result);
            }
        }
    }
}

运行后果:呈现了 x = 0 且 y = 0 这种反常识后果

II、重排序机制

1、编译器优化:编译器编译时的代码程序重排
如本例中编译器可能变换 a = 1 和 x = b,以及 b = 1 和 y = b 的代码程序。
2、指令重排序:CPU 执行指令时变换代码程序。
3、内存零碎重排序:内存零碎没有重排序,然而因为有缓存的存在,使得程序整体上会体现出乱序的行为。线程 1 批改了 a 的值,然而批改当前,a 的值可能还没有写回到主存中,那么线程 2 可能失去 a = 0。同理,线程 2 对于 b 的赋值操作也可能没有及时刷新到主存中。

III、内存可见性

所有的共享变量存在于主内存中,每个线程有本人的本地内存,线程读写共享数据也是通过本地内存替换的,所以可见性问题仍然是存在的。

IV、原子性

如 long 和 double 类型的值须要占用 64 位的内存空间,Java 对于 64 位的值的写入,能够拆分为两个 32 位的操作进行写入。一个整体的赋值操作,被拆分为低 32 位赋值和高 32 位赋值两个操作,两头如果产生了其余线程对于这个值的读操作,可能会导致原子性问题。

V、Java 并发束缚标准

Synchronization Order
Happens-before Order

VI、synchronized

线程 a 对于进入 synchronized 块之前或在 synchronized 中对于共享变量的操作,对于后续的持有同一个监视器锁(monitor)的线程 b 可见。在进入 synchronized 的时候,并不会保障之前的写操作刷入到主内存中,synchronized 次要是保障退出的时候能将本地内存的数据刷入到主内存。

一个不太正确的单例模式双重查看,代码没有复现多线程问题,得再想想方法:

public class Singleton {
        private static Singleton instance = null;
        private int v;

        public int getV() {return v;}

        public Singleton() {this.v = 1;}

        public static Singleton getInstance() {     // 单例模式双重查看
            if (instance == null) {                 // 操作 1:第一次查看
                synchronized (Singleton.class) {    // 操作 2
                    if (instance == null) {         // 操作 3:第二次查看
                        instance = new Singleton(); // 操作 4}
                }
            }
            return instance;
        }
 }

如例,有两个线程 a 和 b 调用 getInstance() 办法。
假如 a 先走,一路走到 操作 4,即 instance = new Singleton() 这行代码。这行代码首先会申请一段空间,而后将各个属性初始化为零值 (0/null),执行构造方法中的属性赋值[1],将这个对象的援用赋值给 instance[2]。在这个过程中,[1] 和 [2] 可能会产生重排序。
此时,线程 b 刚好执行到 操作 1,就有可能失去 instance 不为 null,而后线程 b 也就不会期待监视器锁,而是间接返回 instance。问题是这个 instance 可能还没执行完构造方法(线程 a 此时还在 4 这一步),所以线程 b 拿到的 instance 是不残缺的,它外面的属性值可能是初始化的零值(0/false/null),而不是线程 a 在构造方法中指定的值。

如果所有的属性都是应用 final 润饰的,其实之前介绍的双重查看是可行的,不须要加 volatile。

VII、volatile:内存可见性和禁止指令重排序

volatile 修饰符实用于以下场景:某个属性被多个线程共享,其中有一个线程批改了此属性,其余线程能够立刻失去批改后的值。在并发包的源码中,它应用得十分多。
volatile 属性的读写操作都是无锁的,它不能代替 synchronized,因为它没有提供原子性和互斥性。因为无锁,不须要破费工夫在获取锁和开释锁上,所以说它是低成本的。
volatile 只能作用于属性,咱们用 volatile 润饰属性,这样 compilers 就不会对这个属性做指令重排序。
volatile 提供了可见性,任何一个线程对其的批改将立马对其余线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
volatile 提供了 happens-before 保障,对 volatile 变量 v 的写入 happens-before 所有其余线程后续对 v 的读操作。
volatile 能够使得 long 和 double 的赋值是原子的。

VIII、final

1、用 final 润饰的类不能够被继承
2、用 final 润饰的办法不能够被覆写
3、用 final 润饰的属性一旦初始化当前不能够被批改。

在对象的构造方法中设置 final 属性,同时在对象初始化实现前,不要将此对象的援用写入到其余线程能够拜访到的中央(不要让援用在构造函数中逸出)。

正文完
 0