共计 2035 个字符,预计需要花费 6 分钟才能阅读完成。
一、作用
1、保障线程可见性
main 线程和 T 线程共享堆内存的数据;main 和 T 也有本人的工作空间,当要拜访共享内存的数据 flag,会把共享内存的 flag 复制一份到本人的工作空间中。
例如 main 线程对 flag 进行了扭转,首先是在本人的空间进行扭转,批改后的值会马上批改到共享内存;然而 T 线程何时查看共享内存的值有没有被扭转不好管制。
即 main 线程的批改并没有及时的反馈到 T 线程,也就是线程之间不可见。对这个变量加了 volatile 后,可能保障一个线程对这个变量批改后,另一个线程可能马上晓得。
底层是通过 CPU 的缓存一致性协定(MESI)保障的 。
2、禁止指令重排序
指令重排序:CPU 原来执行一条指令时,是一步一步的程序执行;当初的 CPU 为了提高效率,会并发的执行指令,就是第一个指令执行到一半时,第二个指令可能曾经开始执行了,也就是流水线似的执行。这时就要求编译器,要可能对指令重排序产生影响的状况进行相应的解决,此时 volatile 就派上用场了。
拿 DCL 单例 (Double Check Lock)来解释
/**
* @author Java 和算法学习:周一
*/
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {if (INSTANCE == null) {
// 双重查看
synchronized (Singleton.class) {if (INSTANCE == null) {
try {Thread.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}
INSTANCE = new Singleton();}
}
}
return INSTANCE;
}
public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Singleton.getInstance().hashCode());
}).start();}
}
}
INSTANCE = new Singleton();(如果外面有个变量 int a = 100)new 对象的过程分为 3 步
1)申请内存(赋默认值);a=0
2)初始化;a=100
3)赋值,把值给变量;把 a 的值赋值给 INSTANCE,即让 INSTANCE 指向变量 a 的地址
如果外面存在指令重排序的话,就存在还未初始化的变量就进行了赋值操作,即 2、3 两步地位替换了;也就是对象处于半初始化状态就进行了赋值操作。当第一个线程(只管加了锁),执行到 new Singleton() 时,new 了一半;此时第二个线程来了,首先判断 INSTANCE 是否为空,因为 INSTANCE 曾经是半初始化状态,外面曾经有值了,不再是空值了,也就是第二个线程曾经拿到了这个对象了,这个线程就能够间接应用该对象了,很可能就会应用外面的这个值。原本冀望这个值是 100,然而这个值却是 0,如果这个值是订单数的值这时就存在问题。 加了 volatile 后,对这个对象的指令重排序就不容许存在,即必须是在初始化实现之后才会进行赋值操作 。
3、volatile 不能保障原子性
/**
* @author Java 和算法学习:周一
*/
public class T {
public volatile int count = 0;
public synchronized void m() {for (int i = 0; i < 1000; i++) {count++;}
}
public static void main(String[] args) {T t = new T();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 10; i++) {threadList.add(new Thread(t::m));
}
threadList.forEach((o)->{o.start();
});
threadList.forEach((o)->{
try {o.join();
} catch (InterruptedException e) {e.printStackTrace();
}
});
System.out.println(t.count);
}
}
m 办法,如果不加 synchronized,count 始终不会到 10000。起因是,当一个线程把 count 值批改为 1 后,此时进来了第二、第三个线程,都读到 count 为 1,批改后把 count 加 1(即 2)写回去,这时两个线程对 count 批改后,count 的值只从 1 变到了 2,所以最初的后果总是小于 10000。归根结底就是 count 的值是保障了可见性,然而 count++ 自身不是原子性的操作(底层分为几个步骤)。
volatile 能保障线程的可见性,然而并不能代替 synchronized 保障原子性。