并发 - volatile(一)

2019-12-21  本文已影响0人  sunyelw

问题是最好的导师, 细心是最棒的品质

看一个IDEA自带的提示

code
Non-atomic operations on volatile fields

volatile 字段上进行了非原子类操作,有两个信息

这句话都是告诉我们一个事实

为什么呢?常见的可见性又是什么?还有其他特性吗?


volatile 的两个重点

volatile 的可见性一句话描述就是

而我们熟悉的static关键字的作用是什么?

是不是感觉特别像?

一、static 的可见性

看一个例子

public class VolatileDemo {

    private static int count = 0;

    private static void count() {
        count++;
    }

    public static void main(String[] args){

        ExecutorService es = Executors.newCachedThreadPool();
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            es.execute(VolatileDemo::count);
        }
        es.shutdown();
        while (true) {
            if (es.isTerminated()) {
                System.out.println("end...");
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.nanoTime();

        System.out.println("count:" + count);
        System.out.println("cost:" + (end - start));
    }
}

看看输出结果

end...
count:96
cost:114914100

多运行几次,会发现结果都不一样,都是100100以下

count = 0时, t1线程与 t2线程读取 count 值, 然后同步修改为 1, 再写回内存, 写了两遍 1, 所以此时count值不是预期中2而是1

这里我们可以知道

二、volatile的可见性

先来看下硬件的存储层次


缓存与主存

再看下执行时间


执行时间

网上找了几张经典的图


Java内存模型 并发访问

下面是一些我自己的理解

状态 描述
M<Modified> 该缓存行只被缓存在该CPU的缓存中且被修改过, 需要写回主存, 成功写回后变为E, 失败则为I
E<Exclusive> 该缓存行只被缓存在该CPU的缓存中且未被修改过, 如果被改动了为M, 其他CPU从主存读取了此缓存行为S
S<Shared> 该缓存行被多个CPU所读取, 且数据一致. 如果有改动, 改动的那个线程中的值为M, 其他的仍S, 当改动后的数据被刷入主存中时, 其他所有CPU中的值均为I
I<Invalid> 该缓存行已经被其他CPU成功修改主存, 需要重新读取

我们稍微修改下这个例子

也就是说每次修改volatile变量都需要重新读取数据, 相比于static的线程不安全, 那volatile完全可以保障线程安全啊, 因为你在修改count值时需要重新读取数据, 就不会发生重复赋值吧.

让我们修改下上面的例子来验证下

public class VolatileDemo {

    private volatile static int count = 0;

    private static void count() {
        count++;
    }

    public static void main(String[] args){

        ExecutorService es = Executors.newCachedThreadPool();
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            es.execute(VolatileDemo::count);
        }
        es.shutdown();
        while (true) {
            if (es.isTerminated()) {
                System.out.println("end...");
                break;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long end = System.nanoTime();

        System.out.println("count:" + count);
        System.out.println("cost:" + (end - start));
    }
}

看看执行结果,发现还是有不为100的情况

end...
count:99
cost:116959800

这就很难受了....
回到开篇提出的问题, IDEA给出的提示

Non-atomic operations on volatile fields

什么是原子性? 就是不可分割

重点就在于计算结果已经有了, 只差一步赋值, 哪怕此时重新读取主存数据也不会再计算一遍

这里的原子性的问题就暴露出来了, 试想一种场景

t1线程与t2线程同时从主存中读取了count = 1,自增, 这时候两个线程的寄存器中存的计算后的值都是 2, 然后要写回count的主存, 假设这时 t1成功了, 那么主存中的count就是2, 然后根据MESI协议, t2需要重新从主存读取count值, 得到的是2, 再将寄存器中的计算结果2 赋值给count, 刷回主存, 此时主存中的count值还是2, 而不是期望中的3

这就是volatile的非原子性

拓展: 分析一下上述场景中两个线程的MESI状态

操作 t1 t2 主存中count
读取count=1 S S 1
自增 M M 1
t1成功刷回主存 E I 2
t2从主存重新读取 S M 2
t2刷回主存 S S 2

那咋保证原子性呢?

  1. atomic+volatile
private volatile static AtomicInteger count = new AtomicInteger(0);

private static void count() {
    count.incrementAndGet();
}

输出结果会发现全都是100, 线程安全

end...
count:100
cost:120775900
  1. synchronized + static
private static int count = 0;

private synchronized static void count() {
    count++;
}

输出结果也是线程安全的.


总结

上一篇 下一篇

猜你喜欢

热点阅读