美文网首页
【JAVA】 volatile关键字

【JAVA】 volatile关键字

作者: Y了个J | 来源:发表于2018-09-21 20:57 被阅读30次
    屏幕快照 2018-09-21 下午8.08.39.png

    每个线程都有自己的一个工作内存,工作内存中存储着主内存变量的副本。当工作内存的变量发生改变后,会重新写回到主内存。

    内存间的相互操作

    lock 将对象变成线程独占的状态
    unlock 将线程独占状态的对象的锁释放出来
    read 从主内存读数据
    load 将从主内存读取的数据写入工作内存
    use 工作内存使用对象
    assign 对工作内存中的对象进行赋值
    store 将工作内存中的对象传送到主内存当中
    write 将对象写入主内存当中,并覆盖旧值

    Volatile语义

    Volatile的第一个语义就是保证此线程的可见性,一个线程对此变量的更改其他线程是立即可知的。也就是说 assign,store,write这三个操作是原子的,中间不会中断,会马上同步回主存,就好像直接操作主存一样,并通过缓存一致性通知其他缓存中的副本过期。普通变量可能会在assign,store,write之间插入其他操作,导致更改后的数据无法马上同步回主存,其他线程读取的可能是过期的旧数据。

    Cpu与内存数据读取

    在多核cup时代,不同线程可能在不同cup的核心中执行,由于cup处理速度和内存的读取速度大概相差大约一百倍,为了让cpu性能不浪费,cpu中做了一个高速缓存,cpu在处理的时候会把一批可能用到的数据载入到缓存中,等执行完毕再写回内存。
    共享内存的变量与线程栈中的变量副本有可能在主存中,也有可能在cpu缓存中或者cpu寄存器中。

    Q:为什么volatile不是线程安全的?
    public class Counter {
     
        public volatile static int count = 0;
     
        public static void inc() {
            //这里延迟1毫秒,使得结果明显
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            count++;
        }
     
        public static void main(String[] args) {
            //同时启动1000个线程,去进行i++计算,看看实际结果
            for (int i = 0; i < 1000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Counter.inc();
                    }
                }).start();
            }
            //这里每次运行的值都有可能不同,可能为1000
            System.out.println("运行结果:Counter.count=" + Counter.count);
        }
    }
    

    运行结果还是没有我们期望的1000,下面我们分析一下原因

    count++编译后最终并非一个原子操作,它由几个指令一起组合实现。
    count++被分割成5个步骤(当然这个并不是确切的指令执行步骤),这5步不具有原子性,假如在完成过程中,其他线程就去读了主存的count变量,那明显导致了一个脏读现象。

    屏幕快照 2019-01-28 下午1.55.42.png 屏幕快照 2018-09-21 下午8.42.12.png

    read and load 从主存复制变量到当前工作内存
    use and assign 执行代码,改变共享变量值
    store and write 用工作内存数据刷新主存相关内容
    其中use and assign 可以多次出现

    这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

    对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

    例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值

    在线程1对count进行加1之后,会write到主内存中,主内存中的count变量就会变为6

    线程2由于已经进行read,load操作,没有及时更新主内存数据,在进行加1运算之后,也会更新主内存count的变量值为6

    导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。

    导致这个问题的原因其实是因为volatile不具备锁操作,要解决此问题其实不难,就是将这五步变为原子操作,即保证线程一完成之前不能有其他线程读取count变量,对count变量加一个互斥锁即可达到,线程一在执行第①步前对count加锁,其他线程无法对count进行访问,线程一执行完第⑤步后释放锁,此刻开始才允许其他线程获取此变量。


    变量的自增操作 i++,分三个步骤:
    ①从内存中读取出变量 i 的值
    ②将 i 的值加1
    ③将 加1 后的值写回内存
    这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行,写回去也可能把别的线程写入主内存的数据覆盖掉。

    相关文章

      网友评论

          本文标题:【JAVA】 volatile关键字

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