共计 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 属性,同时在对象初始化实现前,不要将此对象的援用写入到其余线程能够拜访到的中央(不要让援用在构造函数中逸出)。