美文网首页
并发 - volatile(一)

并发 - volatile(一)

作者: sunyelw | 来源:发表于2019-12-21 12:04 被阅读0次

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

看一个IDEA自带的提示

code
Non-atomic operations on volatile fields

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

  • 字段countvolatile 修饰的
  • count++ 是非原子操作

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

  • volatile不能保证原子性

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


volatile 的两个重点

  • 可见性
  • 禁止指令重排

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

  • volatile修饰的共享变量对其他线程具有可见性

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

  • 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));
    }
}
  • VolatileDemo类中有一个静态字段count
  • 启动一个缓存线程池进行对这个字段进行自增
  • 100次自增

看看输出结果

end...
count:96
cost:114914100

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

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

这里我们可以知道

  • static修饰的变量确实可以在线程间通信, 对各个线程都是可见的
  • 这种可见不能保证线程安全

二、volatile的可见性

  • Java虚拟机规范中

先来看下硬件的存储层次


缓存与主存

再看下执行时间


执行时间
  • 寄存器的速度可以理解为CPU的速度, 它是离CPU最近的存储容器

网上找了几张经典的图


Java内存模型 并发访问

下面是一些我自己的理解

  • 存储大体上分两种, 主存是堆内存, 工作内存是栈内存, 属于线程私有
  • 对字段操作都需要先从主存读取数据加载进工作内存, 工作内存对这个副本数据进行操作
  • volatile修饰的变量, 等于在堆中这个变量的内存区域上打了个标, 所有操作都必须从主存中读取, 由MESI<缓存一致性协议>实现
  • MESI规定了四种值类型
状态 描述
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));
    }
}
  • count加了个volatile修饰

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

end...
count:99
cost:116959800

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

Non-atomic operations on volatile fields
  • volatile 不能保证原子性

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

  • i = 1是原子操作
  • i++不是, i++可以分为三步
    • 从主存读取 i
    • 在寄存器中进行加一运算, 自增操作, 此时已经修改工作内存中的值
    • 将计算后的值赋给 i, 也就是刷回主存

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

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

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

那咋保证原子性呢?

  • synchronized关键字
  • atomic原子类
  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++;
}

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


总结

  • static修饰的变量是静态变量, 表示不管对象有多少实例, 只有这一个变量, 强调变量的唯一性
  • volatile是基于JMMMESI提出的一种内存一致性解决方案, 强调的是对共享变量的修改是可见的, 变量值是唯一的
  • volatile 可以保证对其所修饰的共享变量的原子性操作是线程安全的

相关文章

网友评论

      本文标题:并发 - volatile(一)

      本文链接:https://www.haomeiwen.com/subject/sezwnctx.html