Java并发编程之volatile关键字

本文深入探讨了计算机内存模型,特别是CPU高速缓存的作用及其与主存之间的交互方式。通过具体示例,阐述了Java内存模型中volatile关键字如何确保变量的可见性,但同时也揭示了volatile无法保证操作原子性的问题。

内存模型

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
就像我们Zookeeper实现分布式应用系统服务器上下线动态感知中serverList是被多个线程访问的变量为共享变量,是有可能出现每个线程的工作空间(CPU的高速缓存)的serverList副本不一致。

volatile的工作机制

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

使用volatile关键字修饰的变量,当一个线程对变量进行写操作,如果发现操作的变量是共享变量,即在其他线程中也存在该变量的副本,会发出信号通知其他线程将该变量的缓存行置为无效状态,因此当其他线程需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。这样看起来就相当于这些线程不拷贝副本到自己的线程工作空间中,而是直接操作volatile关键字修饰的变量。

那这么说volatile关键字修饰的变量就一定是线程安全的嘛?

volatile修饰的变量非线程安全

我们可以来写一个代码,就可以知道volatile的变量是不是一定是线程安全的。

package cn.itcast_01_mythread.volatiletest;

public class TestVolatile {

    public static volatile int numb = 0;

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

        for (int i = 0; i < 100; i++) {

            new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        numb++;
                    }
                }
            }).start();

        }

        Thread.sleep(2000);
        System.out.println(numb);
    }

}

上面代码中我们有一个静态的共享变量numb ,并且我们用了volatile 修饰。
然后我们开启100个线程,然后每个线程中都进行1000次numb++的自增操作。如果numb是线程安全的话,最后numb的值应该是10000。
现在我们开始测试。
这里写图片描述
结果发现numb的值小于10000,说明volatile关键字修饰的变量不一定是线程安全的。

上面代码中为什么会出现numb的值小于10000?

这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性
那什么是原子性操作
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
那我们来看看下面的操作哪些是原子性操作

x=10;    //语句1
y=x;    //语句2
x++;    //语句3
x= x + 1;    //语句4

这里面只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

所以现在我们可以解释一下我们的代码中为numb的值会小于10000了。
自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
假如某个时刻变量numb的值为10,

线程1对变量进行自增操作,线程1先读取了变量numb的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量numb的原始值,由于线程1只是对变量numb进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量numb的缓存行无效,所以线程2会直接去主存读取numb的值,发现numb的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了numb的值,注意此时在线程1的工作内存中numb的值仍然为10,所以线程1对numb进行加1操作后numb的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,numb只增加了1。

解释到这里,可能有人有疑问,不对啊,前面不是保证一个线程在修改volatile变量时,会让缓存行无效吗?然后其他线程需要读取变量的时候就会读到新的值。是的,没错是这样。volatile变量规则的直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

但是这里要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对numb值进行修改。然后虽然volatile能保证线程2对变量numb的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。

所以根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。*所以volatile关键字修饰的变量不一定是线程安全的*

synchronized关键字与volatile关键字

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值