在前面的文章中,我们一起深入分析了volatile关键字的效果原理《如何彻底理解volatile关键字?》。在文章的末尾,给大家留了一个问题:“如果volatile的修饰的是一个引用类型的对象变量,那么对象中定义的一些普通全局变量是否会受到volatile关键字的效果影响呢?”
接下来,我们就一起来分析下这个问题!让我们先通过一个例子来回顾下volatile关键字的作用!
public class VolatitleFoo {
//类变量
final static int max = ;
static int init_value = ;
public static void main(String args[]) {
//启动一个线程,当发现local_value与init_value不同时,则输出init_value被修改的值
new Thread(() -> {
int localValue = init_value;
while (localValue < max) {
if (init_value != localValue) {
System.out.printf("The init_value is update ot [%d]\n", init_value);
//对localValue进行重新赋值
localValue = init_value;
}
}
}, "Reader").start();
//启动updater线程,主要用于对init_value的修改,当local_value=5的时候退出生命周期
new Thread(() -> {
int localValue = init_value;
while (localValue < max) {
//修改init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
init_value = localValue;
try {
TimeUnit.SECONDS.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
在上面的代码示例中,我们定义了两个类变量max、init_value,然后在主线程中分别启动一个Reader线程,一个Updater线程。Updater线程做的事情就是在值小于max的值时以每两毫秒的速度进行自增。而Reader线程则是在感知init_value值发生变化的情况下进行读取操作。
期望的效果是线程Updater更新init_value值之后,可以立刻被线程Reader感知到,从而进行输出显示。实际运行效果如下:
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
实际的运行效果是在Updater修改类变量init_value后,Reader线程并没有立马感知到变化,所以没有进行相应的显示输出。而原因就在于共享类变量init_value在被线程Updater拷贝到该线程的工作内存中后,Updater对变量init_value的修改都是在工作内存中进行的,完成操作后没有立刻同步回主内存,所以Reader线程对其改变并不可见。
为了解决线程间对类变量init_value的可见性问题,我们将类变量init_value用volatile关键字进行下修饰,如下:
static volatile int init_value = ;
然后我们再运行下代码,看看结果:
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
此时线程Updater对类变量init_value的修改,立马就能被Reader线程感知到了,这就是volatile关键字的效果,可以让共享变量在线程间实现可见,原因就在于在JVM的语义层面要求被volatile修饰的共享变量,在工作内存中的修改要立刻同步回主内存,并且读取也需要每次都重新从主内存中刷新一份到工作内存中后才可以操作。
关于以上适用volatile关键字修饰基本类型的类变量、实例变量的场景,相信大家会比较好理解。接下来,我们来继续改造下代码:
public class VolatileEntity {
//使用volatile修饰共享资源i
//类变量
final static int max = ;
int init_value = ;
public static int getMax() {
return max;
}
public int getInit_value() {
return init_value;
}
public void setInit_value(int init_value) {
this.init_value = init_value;
}
private static class VolatileEntityHolder {
private static VolatileEntity instance = new VolatileEntity();
}
public static VolatileEntity getInstance() {
return VolatileEntityHolder.instance;
}
}
我们将之前代码中的类变量init_value放到实体类VolatileEntity中,并将其设计为一个实例变量,为了便于理解,我们将实体类VolatileEntity设计为单例模式,确保两个线程操作的是同一个共享堆内存对象。如下:
public class VolatileEntityTest {
//使用volatile修饰共享资源
private static VolatileEntity volatileEntity = VolatileEntity.getInstance();
private static final CountDownLatch latch = new CountDownLatch();
public static void main(String args[]) throws InterruptedException {
//启动一个线程,当发现local_value与init_value不同时,则输出init_value被修改的值
new Thread(() -> {
int localValue = volatileEntity.init_value;
while (localValue < VolatileEntity.max) {
if (volatileEntity.init_value != localValue) {
System.out.printf("The init_value is update ot [%d]\n", volatileEntity.init_value);
//对localValue进行重新赋值
localValue = volatileEntity.init_value;
}
}
}, "Reader").start();
//启动updater线程,主要用于对init_value的修改,当local_value=5的时候退出生命周期
new Thread(() -> {
int localValue = volatileEntity.init_value;
while (localValue < VolatileEntity.max) {
//修改init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
volatileEntity.init_value = localValue;
try {
TimeUnit.SECONDS.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
在上述代码中线程Updater和Reader此时操作的是类变量VolatileEntity对象中的普通实例变量init_value。在VolatileEntity对象没被volatile关键字修饰之前,我们看下运行效果:
The init_value will be changed to [1]
The init_value will be changed to [2]
The init_value will be changed to [3]
The init_value will be changed to [4]
The init_value will be changed to [5]
与未被volatile修饰的int类型的类变量效果一样,线程Updater对VolatileEntity对象中init_value变量的操作也不能立马被线程Reader可见。如果此时我们不VolatileEntity类中单独用volatile关键字修饰init_value变量,而是直接VolatileEntity对象用volatile关键字修饰,效果会如何呢?
private static volatile VolatileEntity volatileEntity = VolatileEntity.getInstance();
此时VolatileEntity对象的引用变量被volatile关键字修饰了,然而其中的普通实例变量init_value并没有直接被volatile关键字修饰,然后我们在运行下代码看看效果:
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
The init_value will be changed to []
The init_value is update ot []
从实际的运行效果上看,虽然我们没有直接用volatile关键字修饰对象中的类变量init_value,而是修改了对象的引用,但是我们看到对象中的普通实例变量仍然实行了线程间的可见性,也就是说间接也相当于被volatile关键字修饰了。所以,在这里问题也就基本上有了答案,那就是:“被volatile关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被volatile关键字修饰了”。
这个问题主要是考查大家对volatile关键字的理解是否深入,另外也是对Java数据存储结构的考查,虽然可能大家对volatile关键字的作用会有了解,但是如果突然被问到这样的问题,如果不加以思考,在面试中也是很容易被问懵的!希望本文能够对你所有帮助,如果觉得还可以,欢迎转发+点击在看哦!