Java多线程volatile关键字
一、volatile介绍
1、什么是volatile
volatile是Java提供的一种轻量级的同步机制。Java语言包含两种同步机制:1、同步块(或同步方法) 2、volatile变量。相比于synchronized加锁同步,volatile关键字比较轻量级,开销更低,因为他不会引起线程上下文的切换调度。
2、volatile定义
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
3、volatile关键字的作用
volatile关键字保证了共享变量在多个线程中的可见性。当一个线程修改了共享变量的值时,其他的线程能够看见这个值的改变,并且将重新读取该值。
二、多线程中如何操作共享变量
说明: 共享变量一般是存放在主存中,在多线程中每一个要使用到共享变量的线程都需要将主存中的共享变量拷贝一份放入到自己的工作内存中(线程私有,高速缓存)。每一个线程中都是读取的自己工作内存里共享变量的副本值来进行相关操作,而不会每次都会去访问主存取值(CPU直接访问主存效率低下)。
问题: 如果现在有一个线程(线程A)修改了自己的值。但是别的线程都不知道该值已经改变了,所以还是继续延用他们各自从主存中拷贝过来的共享变量值的副本。这样就会造成一些多线程相关的问题。
三、代码演示
1、示例代码
public class TestVolatitle1 {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
Thread threadA = new Thread(td);
threadA.start();
while (true) {
if (td.isFlag()) {
System.out.println("------------------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {}
flag = true;
System.out.println("flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
代码解释:
1、在以上代码中,一共有两个线程,一个是我们手动开启的线程threadA,一个是主线程。ThreadDemo td = new ThreadDemo();
是创建一个共享变量td。
2、在线程threadA中会执行 run()
方法,先休眠200毫秒,然后将flag的值改变为true。
3、在主线程中while循环会一直不停的判断共享变量的flag值是否为true,若为true则结束程序。
运行结果:
点击运行该程序以后发现,运行结果并不是我们想象的那样。主线程中的while循环一直不会结束。
问题产生原因:
首先线程threadA和主线程都去主存中拷贝一份共享变量的值,放入到自己的工作内存中。线程threadA将自己工作内存中的flag值改变成了true。但是主线程工作内存中的值并没有改变(依然是false),所以while循环就会一直循环不会结束。
问题解决:
当我们给共享变量flag加上volatile
关键字修饰以后,程序正常运行并结束了。
四、volatile关键字详解
1、详解
从以上的代码示例中,我们看见给共享变量添加了volatile关键以后程序就正常运行结束了。那么volatile关键字到底是如何工作的呢?通过上面的代码示例来说明一下加入了volatile关键字以后的程序流程。
1、给共享变量添加了volatile关键字修饰后。首先,线程threadA和主线程都会去主存中拷贝一份共享变量的值。
2、然后在线程A中将flag的值改变了,同时主线程里面的while循环也一直在执行。线程A将共享变量的flag值改变为true了。然后线程A就将flag改变后的值刷写到主存中。
3、当主存*享变量的值更改以后,会导致其他线程中的共享变量副本失效,这时候其他线程需要重新从主存中读取一次共享变量的值到线程的工作内存中。
4、当主线程重新从主存读取共享变量的值以后,读取到的flag的值为true,此时while循环就结束了。
2、volatile底层实现
volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情呢?
当有volatile变量修饰时会出现汇编指令lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
3、小结
所以,当使用了volatile关键字修饰共享变量以后。每一个线程对共享变量做的更改操作都会被重新刷写到主存中,并且当主存*享变量的值改变以后,其他线程中的共享变量副本就失效了,需重新冲主存中读取值。
五、volatile的优缺点
- 优点:轻量级的同步,更少的代码量来实现多线程,而且比较好理解,无须太多的学习成本。
- 缺点:volatile只能保证变量的可见性,无法保证原子性。除此之外,由于volatile屏蔽掉了VM中必要的代码优化,所以在效率上会稍微低点。这是两个缺点。
上一篇: 敏捷开发过程剖析及工具推荐