遇到的坑 :
今天在线上突然出现了一个问题,查了半天原因就是我使用的成员变量是一个bean,但是我在发生特殊情况的时候会重置这个bean,即使我加了volatile或者是AtomicReference,然而在并发环境下,其他线程还是使用了bean中变量的老引用导致出现问题.
由此衍生出了我对volatile和AtomicReference的研究.
1:volatile的作用
volatile关键字的主要作用有两个:
- 防止指令重排序 : 讲人话就是防止编译后java会按照一定规则和把指令重新排序优化执行
- 强制读主存 : 讲人话就是jvm虚拟机会有线程内存副本,线程间的共享变量可能无法察觉对方的变化.
【注意点】:volatile虽然可以保证线程间的可见性但是无法提供原子性操作,也就是需要加锁或者cas来保证原子性 这时候可以使用synchronized加锁操作或者juc的Atomic来代替
下面的代码我们就来测试一下volatile的线程可见性:
/***
** 这是一个测试的对象
/**
public class AtomicReferenceExample {
private static final ExecutorService POOL = Executors.newCachedThreadPool();
private static final int THREAD_SIZE = 5;
private int nInt = 0;
private volatile int vInt = 0;
private volatile Integer vInteger = 0;
private AtomicInteger atomicInteger = new AtomicInteger(0);
private void nIntIncr() {
nInt++;
}
private void vIntIncr() {
vInt++;
}
}
/**
* 测试volatile的可见性
*/
private static void testVolatileView() {
AtomicReferenceExample example = new AtomicReferenceExample();
//设置nint监听线程
for (int i = 0; i < THREAD_SIZE; i++) {
POOL.execute(() -> {
boolean flag = true;
while (flag) {
if (example.nInt > 0) {
System.out.println("监听到nint值改变 time : " + System.currentTimeMillis());
flag = false;
}
}
});
//设置vint监听线程
POOL.execute(() -> {
boolean flag = true;
while (flag) {
if (example.vInt > 0) {
System.out.println("监听到vint值改变 time : " + System.currentTimeMillis());
flag = false;
}
}
});
}
System.out.println("提交更改");
example.vIntIncr();
example.nIntIncr();
System.out.println("执行更改值完成 time : System.currentTimeMillis() +" nint = " + example.nInt + " vint = " + example.vInt);
System.out.println("提交执行完毕");
}
执行结果我们是只会监听到vint的改变,如果给nint也加上volatile,那么nint也会被监听到
下面的代码我们就来测试一下volatile无法保证原子性:
private static void testVolatilAtomic() throws InterruptedException {
int threadSize = 30;
AtomicReferenceExample example = new AtomicReferenceExample();
CountDownLatch countDownLatch = new CountDownLatch(threadSize * 2);
for (int i = 0; i < threadSize; i++) {
POOL.execute(() -> {
for (int j = 0; j < 5000; j++) {
example.vIntIncr();
}
countDownLatch.countDown();
});
POOL.execute(() -> {
for (int j = 0; j < 5000; j++) {
example.getAtomicInteger().incrementAndGet();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("最终结果 vint = " + example.getVInt());
System.out.println("最终结果 atomicInt = " + example.getAtomicInteger().get());
POOL.shutdown();
}
执行结果我们可以看到两个结果不一致,就是因为没有实现原子性,具体原理百度都能搜到,就是主ram和线程ram的各种读啊什么的问题了,有兴趣可以自己查下,这边就不多做阐述了
2:AtomicReference的作用
AtomicReference的源码很简单
- 用volatile修改他的成员变量v,作用如上volatile的作用
- 用cas去更新他的成员变量v,保证他的引用更新是原子性的
3:volatile修饰引用变量的疑问
其实AtomicReference内部的v也是用了volatile修饰的,就像我开头描述的问题那样,当volatile修饰引用类型的时候,到底能不能保证bean内部的成员变量也可以拥有可见性呢?
这个问题可以用下面的代码测试:
public class TestVolatile implements Runnable{
class Foo {
boolean flag = true;
}
private volatile Foo foo = new Foo();
public void stop(){
foo.flag = false;
}
@Override
public void run() {
while (foo.flag){}
}
public static void main(String[] args) throws InterruptedException {
TestVolatile test = new TestVolatile();
Thread t = new Thread(test);
t.start();
Thread.sleep(1000);
test.stop();
}
}
然而问题来了,这段代码在网上有些人测试会一直执行,为变量flag加上volatile才会马上退出,但是有的人说不管加没加都会马上退出,好像是因为不同版本不同厂商的虚拟机,执行策略都不一样.
我最上面写的那个问题也是基于这个疑惑,是否volatile修饰引用变量的时候,引用内部的变量是否能够保证绝对的可见性? 不知道有没有大佬能够指教回答!!
【最后】: 所以其实在使用的时候,我建议修饰基本类型变量的时候使用volatile,修饰引用类型的时候使用AtomicReference.