指令重排
在我们进行了解之前,我们需要先知道,我们写的程序,并不是直接进行执行的,源代码需要先进行编译器的优化重排,同时指令并行也会重排,内存系统也会重排。举一个例子
int x=1; //1
int x=2; //2
x=x+5; //3
y=x * x; //4
在上面的代码我们理想的情况就是1234进行操作,也有可能变成2134,1324这是可能进行指令重排的,但是绝对不会出现4123。
处理器在进行指令重排的时候是会考虑数据之间的依赖性的!
我们进行下面的操作:
线程 A | 线程 B |
---|---|
x = a |
y = b |
b = 1 |
a = 2 |
按此顺序执行,正常结果:
x = 0
;y = 0
线程 A | 线程 B |
---|---|
b = 1 |
a = 2 |
x = a |
y = b |
当进行指令重排的时候可能会发生上面的情况指令重排导致的结果:x = 2
;y = 1
我们为了防止这样指令重排带来的错误,可以使用voliate来进行防止。它保证了操作的执行顺序,同时保证某些变量的内存可见性。
当我们使用volatile之后就会在操作的时候就会在上下执行之前增加一段内存屏障。所以它可以保证可见性,不保证原子性,通过内存屏障避免指令重排。
单例模式
饿汉式单例模式:饿汉式单例有个很明显的问题就是浪费空间。
public class Hungry {
// 可能会浪费空间
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];
private Hungry() {
}
private static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
懒汉式单例模式
class LazyMan {
// 私有构造方法,防止外部直接实例化
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "ok");
}
// 静态单例对象,未初始化
private static LazyMan lazyMan;
// 双重检测锁模式的单例获取方法
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
// 多线程并发测试
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
LazyMan.getInstance();
}).start();
}
}
}
在懒汉式模式下,我们其实可以知道lazyMan = new LazyMan();
这并不是一个原子性操作,它先机型内存的分配,再执行构造方法初始化空间,然后这个对象指向这个空间,这个时候当多线程的情况下就会出现指令重排的异常操作。所以我们需要增加关键字volatile