在Java多线程编程中,volatile关键字是一种重要的同步机制,可以理解为低配版synchronized,轻量级的同步策略,保证可见性,不保证原子性,禁止指令重排。它用于确保多线程环境下变量的可见性和顺序性。通过使用volatile关键字,可以避免线程之间的竞争条件和数据不一致性问题。本文将详细解释Java中的volatile关键字以及它在多线程编程中的应用。
在Java中,volatile关键字用于确保多线程环境下变量的可见性和顺序性。具体来说,它具有以下两个作用:
在并发环境下,多个线程同时操作共享变量,如果不控制这个共享变量,那么多线程获取这个变量不一定是最新的数据,这就会造成数据不完整的情况。我们可以用代码来验证这种情况,模拟主线程等待子线程处理完之后,子线程更新数据,主线程停止工作。
变量number,先不使用volatile修饰:
class MyData2{
int number = 0;
public void addTo60(){
this.number = 60;
}
}
/**
* 1.验证volatile的可见性
* 1.1 假如 int number = 0; number变量之前没有关键字修饰,没有可见性,数值不会修改到主内存
*/
public class Main {
public static void main(String[] args) {
seeOKByVolatile();
}
/**
*
*/
private static void seeOKByVolatile() {
MyData2 myData = new MyData2();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
// 暂停线程,当前线程挂起,这时候main会抢占资源
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName()+"\t update number "+myData.number);
},"AAA").start();
// 第二个线程就是我们main主线程
while (myData.number == 0){
// 发现myData.number还是0,一直循环,3秒后,myData.number被修改,但是没有通知到主线程,造成可见性问题
}
System.out.println(Thread.currentThread().getName()+"\t main is over ");
}
}可以发现运行结果,主线程一直等待循环,因为子线程修改number之后,主线程不知道,以为还是0,就会一直循环。

使用volatile修饰number变量:
volatile int number = 0;最终运行结果,主线程终止循环

通过代码案例可以看到,使用volatile修饰number变量,修改值之后会通知到主内存。
首先解释一下原子性什么意思?原子性是指不可分割、完整性,也即某个线程正在做某个具体业务时,中间不可以被加载或者分割。需要整体完整,要么同时成功,要么同时失败。比如模拟20个线程,每个线程同时对共享变量加1,最终结果是2000,不能有其他数,其他线程读到的数据是正确的。
class MyData{
volatile int number = 0;
public void addTo60(){
this.number = 60;
}
// 请注意,此时number前面是加了volatile关键字修饰,volatile不保证原子性
public void addPlusPlus(){
number++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
// 需要等待上面所有子线层执行完毕之后,主线程在运行
// 如果当前线程大于2,说明还有子线程
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(myData.number);
}
}运行结果,可以发现结果不一定是2000,说明有其他线程拿到不是完成的数据进行叠加

通过代码验证,这也说明,volatile是轻量级同步锁,想要保证原子性,只能用重量级锁synchronized,或者用JUC包下面的Automic原子类(后续会一篇原子类详讲)。
不理解指令重排的可以看一下这一篇文章:https://cloud.tencent.com/developer/article/2353578
在多线程环境中,如果我们想要想要控制某个对象只能被实例一次,也就是单例的设计模式,就可以使用volatile修饰对象,这样就可以保证创建对象生成的指令不会被重排。

可以看到,new 对象之后,编程的汇编指令有一句: lock addl,可以看出是加锁的意思,这也验证volatile是一种同步机制。
查阅volatile实现在Hotspot里面的c++代码,实现如下:

解释:
is_MP::MUTLI Process ,也就是多个多核时,加汇编指令lock。
lock作用:多核时,执行指令是对共享内存的独占使用。提供有序的指令,无法越过这个内存屏障。所以,volatile修饰对象,jdk底层源码就已经对编译指令进行了加锁处理,避免重排。
volatile关键字是Java多线程编程中一种重要的同步机制,它可以保证变量的可见性和顺序性。但是,在使用时需要注意它的局限性,并且需要根据具体情况选择合适的同步机制来确保程序的正确性和有序性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。