原创文章,转载请注明原文章地址,谢谢!
volatile关键字理解
volatile是Java提供的轻量级的同步机制,其主要有三个特性
- 保证内存可见性
- 不保证原子性
- 禁止指令重排序
保证内存可见性
当某个线程在自己的工作内存中将主内存中共享数据的副本,修改并刷新到主内存后,其它线程能够立即感知到该共享数据发生变化。public class VolatileDemo {
private int num;
//private volatile int num;
private void add() {
num = 10;
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
demo.add();
System.out.println("num值发生变化");
}).start();
while (demo.num == 0) {
}
System.out.println("main线程感知到了num值发生变化");
}
}
运行结果
程序一直处于运行中,并未停止,这是因为程序一直处于while循环中,main线程并未感知到num值的变化。而当num使用volatile修饰的时候,程序将正常执行运行结束,因为线程在修改num值的时候,这时候对main线程是可见的,这样就会跳出while循环,结束。
不保证原子性
不保证原子性正是volatile轻量级的体现,多个线程对volatile修饰的变量进行操作时,会出现容易出现写覆盖的情况。
public class VolatileDemo {
private volatile int num;
private void add() {
num++;
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
demo.add();
}).start();
}
System.out.println("main线程执行结束,num值为:" + demo.num);
}
}
运行结果
100个线程分别执行num++操作,理论上结果应该是100,但是实际结果是小于100,这是因为num++不是原子操作,volatile不保证原子性。解决方法是使用java.util.concurrent.atomic包下的原子类AtomicInteger。
public class VolatileDemo {
private static AtomicInteger num = new AtomicInteger(0);
private static void add() {
num.incrementAndGet();
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
add();
}).start();
}
while (100 != num.get()) {
}
System.out.println("main线程执行结束,num值为:" + num);
}
}
禁止指令重排序
计算机执行程序是为了提高性能,编译器和处理器常常会进行指令重排。单线程环境下程序最终执行结果和执行顺序一致,多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程使用的变量一致性无法保证。处理器在进行指令重排的时候必须考虑指令之间的数据依赖性。volatile实现禁止指令重排的原理
-
内存屏障是一个CPU指令。由于编译器和CPU都能够执行指令重排,如果在指令间插入一条内存屏障,则会告诉编译器和CPU,任何指令都不能和该条内存屏障指令进行重排序,即通过插入内存屏障指令能够禁止在内存屏障前后的指令执行重排序优化。
-
内存屏障的另外一个作用是强制刷新各种CPU的缓存数据,因此任何CPU上的线程都能够读取到这些数据的最新版本。
- 对volatile变量进行写操作时,会在写操作之后加入一条store屏障指令,将工作内存中的共享变量copy刷新回主内存中。
- 对volatile变量进行读操作时,会在读操作之前加入一条load的屏障指令,从主内存中读取共享变量。
volatile的实际应用
- 并发环境下使用双重检索方式的单例模式。
- java.util.concurrent.atomic包下的原子类。
单例模式为什么需要volatile关键字修饰?
public class SingletonDemo {
private static volatile SingletonDemo singletonDemo;
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if (singletonDemo == null) { //1
synchronized (SingletonDemo.class) { //2
if (singletonDemo == null) { //3
singletonDemo = new SingletonDemo(); //4
}
}
}
return singletonDemo;
}
}
以上单例模式使用的是双检索方式,且是懒汉模式。先判断singleton是否已经初始化,如果初始化了就直接返回,如果没有初始化,则创建对象。
在多线程情况下,只有1,没有2、3,就可能导致创建多个实例。比如,线程A和线程B都调用getInstance方法,线程A判断了代码1,然后切换到了线程B,线程B判断代码1,然后创建了singleton实例,然后切换到了线程A,此时线程A又创建了singleton实例,这样就创建了多个singleton实例。
加入synchronized保证同一时刻只有一个线程进入临界区。首先假设没有代码3,考虑这样的场景。此时假设线程A和线程B都判断了代码1,进入代码2,线程A先进入临界区,线程B发现线程A在临界区,便在队列中等待。线程A继续执行,创建了一个singleton实例,退出临界区。然后线程B进入临界区,又创建了singleton实例,结果又是两个singleton实例。所以代码3的作用是必须的。
如果代码2和代码3都存在,那么当线程A创建了singleton实例后,线程B判断singleton不为null,就不会再创建实例了。这样一来的话,实际上代码1和代码3的作用似乎一样。但是考虑这样的一个场景,假设没有代码1,通过线程A和线程B,singleton已经创建好了,此时来了个线程C,直接进入临界区加锁,然后判断singleton实例不为null,跳出。但是这样一上来就加锁,是较消耗资源的,所以代码1的作用就不言而喻了。
最后singleton变量为什么要用volatile修饰呢?分析一下new一个对象,需要有几个步骤
- 该Class对象是否已经加载,如果没有就先加载Class对象。
- 分配内存空间,初始化实例。
- 调用构造函数。
- 返回引用地址给变量。
上述步骤中,cpu为了优化程序,可能会进行指令重排序,打乱第3、4步骤,导致实例还没创建,引用指向的变量就被使用了。比如,线程A执行到new Singleton(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。这时时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量。加了volatile之后,就保证new不会被指令重排序。
博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!
网友评论