volatile修饰引用变量

JMM可确保声明为volatile的字段,写入操作之后,其值对其他所有线程可见。对于volatile引用变量,虽然可以确保该引用本身将及时对其他线程可见,但对于引用对象的成员变量而言,情况并非如此。如果单独访问,不能保证对象中包含的数据将始终可见。

1、看一个例子:

public class VolatileTest {

    private static volatile Data data;

    public static void setData(int a, int b) {
        data = new Data(a, b);
    }

    private static class Data {
        private int a;
        private int b;

        public Data(int a, int b) {
            this.a = a;
            this.b = b;
        }

        public int getA() {
            return a;
        }

        public int getB() {
            return b;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10000; i++) {
            final int ii = i;
            int a = i;
            int b = i;
            // writer
            Thread writerThread = new Thread(() -> {
                setData(a, b);
            });
            // reader
            Thread readerThread = new Thread(() -> {
                while (data == null) {
                }
                int x = data.getA();
                int y = data.getB();
                if (x != y) {
                    System.out.printf("readerThread2:a = %s, b = %s, i = %s%n", x, y,ii);
                }
            });

            writerThread.start();
            readerThread.start();
            writerThread.join();
            readerThread.join();
        }
        System.out.println("finished");
    }
}

执行后输出:

readerThread2:a = 93964, b = 93965, i = 93965

finished

说明:

虽然Data实例是volatile,单a和b的更新不具有原子性。如果需要多个变量的原子性,我们应该同步我们的代码。原因是因为读线程中获取变量a和b,和写线程之间没有happen-before关系。

解决方法:写入a、b的方法和获取a、b的方法进行同步。在Data类中增加两个方法,这两个方法用sychronized修饰,读写线程分别调用这两个方法

public synchronized void setValues(int a, int b) {
    this.a = a;
    try {
        TimeUnit.MICROSECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.b = b;
}

public synchronized int[] getValues() {
    return new int[]{a, b};
}

注意:对于多变量可见性问题、单变量复合操作问题,只能用“锁”的方式来解决。

2、”双缓冲+引用赋值“的方法:

上述示例很常见,在线业务系统中通常这么做:一个写线程异步更新某个对象,线上web线程实时的访问(读取)该对象。根据前面知识,这种场景下会出现数据争用,又由于是引用类型,所以使用volatile关键字是无法解决的。除了上面加锁的方式解决,通常采用“双缓冲+引用赋值”的方法来解决。

可以看到写线程中调用的setData方法每次都会创建一个新的对象实例,然后赋值给data引用,这种手法就叫“双缓冲”。在读线程中由于data是引用变量,并且和写线程之间没有happen-before关系,所以从data引用中获取各种属性的时候不是原子的操作,这样就会造成一次读取过程中,data所引用的数据不一致。

可以简单在读线程中,加一个临时引用的赋值,来解决这个问题,请看:

// reader
Thread readerThread = new Thread(() -> {
    Data myData = data;
    while (myData == null) {
        myData = data;
    }
    int x = myData.getA();
    int y = myData.getB();
    if (x != y) {
        System.out.printf("a = %s, b = %s, i=%s%n", x, y, ii);
    }
});

说明:在读线程中首先使用临时变量myData来引用data变量的实例,接下来的操作都是对myData变量的操作,因为:

  • 变量的赋值jvm会保证原子性;
  • 写线程使用了双缓冲的方式来改变data变量的引用,即使data引用被改变了也没关系,因为myData变量依然会引用之前的实例;

所以,经过这样处理后,读线程中使用到的Data实例数据是一致的。

3、原子引用:

 

参考:

https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/volatile-ref-object.html

https://blog.csdn.net/u010454030/article/details/80800098

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赶路人儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值