volatile是JVM提供的一个轻量级的同步机制
volatile修饰的变量可以对所有线程立即可见
由JMM相关知识可知,当给变量加了volatile关键字后,当主内存中的变量被修改后,其他拷贝主内存中变量的线程会将之前拷贝的变量置为无效,重新从主内存中读取更新后的变量。
java中,并非所有的语句都是原子性的(并非写成一行的语句就会原子执行)
JVM会对编写的代码进行一些额外的优化,但是原则是:不影响单线程程序的执行结果,即最后的执行结果肯定是保证相同的。
从一个单例模式开始说起
public class Singleton {
private static Singleton instance = null;//多个线程共享instance
private Singleton() {}
public static Singleton getInstance() {
if (instance == null){
synchronized(Singleton.class){
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
ps:这段代码有问题,要不然不会总结volatile关键字了
instance = new Singleton();
上面这行代码不是一个原子性的操作,JVM在执行时大致拆分为以下三步:
由于重排序,可能是1、3、2,单线程情况下并不会出异常,但是多线程:
线程X:执行到3(已被赋值了,不为null但是还未实例化),与此同时
线程Y:执行到
if (instance == null){
}
return instance;
发现instance不为null ,直接将该未实例化的对象返回,后续如果使用的话会造成未知的错误
======================================================================================
所以为了避免上述的问题,我们给instance加上volatile关键字,禁止JVM重排序
private volatile static Singleton instance = null;
这样子,这个单例模式才算是完整
注意:此处解决的问题为禁止重排序,但是volatile并不保证原子性,所以并不能保证线程安全
内存屏障
在volatile写操作前,插入一个StoreStore屏障
在volatile写操作后,插入一个StoreLoad屏障
在volatile读操作前,插入一个LoadLoad屏障
在volatile读操作后,插入一个LoadStore屏障
注意:volatile并不是线程安全的
比如常见的“漏加”例子:
public class TestVolatile_1 {
public static volatile int num = 0;
public static void main(String[] args) throws Exception {
for (int i = 0; i <100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <20000; i++) {
num++;//num++不是一个原子性操作
}
}
}).start();
}
Thread.sleep(3000);//休眠3秒,确保创建的100个线程都已执行完毕
System.out.println(num);
}
}
num++被多个线程同时执行,造成漏加
可以使用JUC中的原子类来保证线程安全
public class TestVolatile_2 {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
for (int i = 0; i <100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <20000; i++) {
num.incrementAndGet() ;// num自增,功能上相当于int类型的num++操作
}
}
}).start();
}
Thread.sleep(3000);//休眠3秒,确保创建的100个线程都已执行完毕
System.out.println(num);
}
}
AtomicInteger实现了CAS算法