深入理解volitale关键字

一、 java 内存模型与多线程编程中的三个感念
1 、原子性
原子性是指一些操作或者全都执行,要么或者全都不执行,整个操作作为一个整体是不可分割的,例如,一个银行中有两个账户 A B ,现在要从 A 账户中转账 500 元到 B 账户,那么一共可以分为两个步骤:
1 、从 A 账户取出 500 元: A = A - 500
2 、向 B 账户存入 500 元: B = B + 500
这两个步骤作为一个整体,要么全部执行要么全部都不执行,如果只执行步骤一,那么 A 账户就会莫名其妙的丢失 500 元, B 账户却什么都没有收到。
Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

例如:

i = 3;
i = i+3;
i++;
i = j;
// 线程 2
j=i;
java 中每个线程都有自己的线程栈,当一个线程执行需要数据时,会到内存中将需要的数据复制到自己的线程栈中,然后对线程栈中的副本进行操作,再操作完成后再将数据写回到内存中。
例如:线程 1 i 的值读到自己的线程栈中,然后对 i 进行了加 3 操作,但是这一操作并没有被及时的写回到内存中,所以线程 2 在执行时看到的 i 的值仍然是 3 ,这就是可见性的问题。
java 提供的 volitale 关键字可以保证数据的可见性。
3 、有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。我们写代码会有一个先后的顺序,但是那仅仅是我们看到的顺序,但是当编译器编译时会进行指令重排,于是代码的执行顺序有可能和我们想的不一样。例如:
int i = 0;             
boolean flag = false; //语句3
i = 1;                //语句1 
flag = true;          //语句2



语句 1 和语句 2 的执行顺序改变一下对程序的结果并没有什么影响,所以这时可能会改变这两条指令的顺序。那么语句 2 会不会在语句 3 之前执行呢,答案是不会呢,因为语句 2 用到了语句 3 声明的变量,这时编译器会限制语句的执行顺序来保证程序的正确性。
在单线程中,改变指令的顺序可能不会产生不良后果,但是在多线程中就不一定了。例如:
//线程1:
context = loadContext();   // 语句1
inited = true;             // 语句2

//线程2:while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

由于语句 1 和语句 2 没有数据依赖性,所以编译器可能会将两条指令重新排序,如果先执行语句 2 ,这时线程 1 被阻塞,然后线程 2 while 循环条件不满足,接着往下执行,但是由于 context 没有赋值,于是会产生错误。
二、 volitale 关键字的作用
volitale 关键字保证了可见性和一定程度上的有序性,但是不能保证原子性。
1 volitale 关键字保证可见性
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:
   1 )保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
   2 )禁止进行指令重排序。
  先看一段代码,假如线程 1 先执行,线程 2 后执行:

//线程1boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程 1 在运行的时候,会将 stop 变量的值拷贝一份放在自己的工作内存当中。
  那么当线程 2 更改了 stop 变量的值之后,但是还没来得及写入主存当中,线程 2 转去做其他事情了,那么线程 1 由于不知道线程 2 stop 变量的更改,因此还会一直循环下去。
  但是用 volatile 修饰之后就变得不一样了:
  第一:使用 volatile 关键字会强制将修改的值立即写入主存;
  第二:使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存行无效;
  第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取。
  那么在线程 2 修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工作内存中的值,然后将修改后的值写入内存),会使得线程 1 的工作内存中缓存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
  那么线程 1 读取到的就是最新的正确的值。
2 volitale 关键字不能保证原子性
从上面知道 volatile 关键字保证了操作的可见性,但是 volatile 能保证对变量的操作是原子性吗?
  下面看一个例子:
public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  // 保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}


大家想一下这段程序的输出结果是多少?也许有些朋友认为是 10000 。但是事实上运行它会发现每次运行结果都不一致,都是一个小于 10000 的数字。
  可能有的朋友就会有疑问,不对啊,上面是对变量 inc 进行自增操作,由于 volatile 保证了可见性,那么在每个线程中对 inc 自增完之后,在其他线程中都能看到修改后的值啊,所以有 10 个线程分别进行了 1000 次操作,那么最终 inc 的值应该是 1000*10=10000
  这里面就有一个误区了, volatile 关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是 volatile 没办法保证对变量的操作的原子性。
  在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加 1 操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
  假如某个时刻变量 inc 的值为 10
  线程 1 对变量进行自增操作,线程 1 先读取了变量 inc 的原始值,然后线程 1 被阻塞了;
  然后线程 2 对变量进行自增操作,线程 2 也去读取变量 inc 的原始值,由于线程 1 只是对变量 inc 进行读取操作,而没有对变量进行修改操作,所以不会导致线程 2 的工作内存中缓存变量 inc 的缓存行无效,所以线程 2 会直接去主存读取 inc 的值,发现 inc 的值时 10 ,然后进行加 1 操作,并把 11 写入工作内存,最后写入主存。
  然后线程 1 接着进行加 1 操作,由于已经读取了 inc 的值,注意此时在线程 1 的工作内存中 inc 的值仍然为 10 ,所以线程 1 inc 进行加 1 操作后 inc 的值为 11 ,然后将 11 写入工作内存,最后写入主存。
  那么两个线程分别进行了一次自增操作后, inc 只增加了 1
  解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改 volatile 变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的 happens-before 规则中的 volatile 变量规则,但是要注意,线程 1 对变量进行读取操作之后,被阻塞了的话,并没有对 inc 值进行修改。然后虽然 volatile 能保证线程 2 对变量 inc 的值读取是从内存中读取的,但是线程 1 没有进行修改,所以线程 2 根本就不会看到修改的值。
  根源就在这里,自增操作不是原子性操作,而且 volatile 也无法保证对变量的任何操作都是原子性的。
3 volitale 关键字在一定程度上保证有序性
在前面提到 volatile 关键字能禁止指令重排序,所以 volatile 能在一定程度上保证有序性。
   volatile 关键字禁止指令重排序有两层意思:
   1 )当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
   2 )在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
  可能上面说的比较绕,举个简单的例子:
//x y 为非 volatile 变量 //flag volatile 变量
x = 2;        // 语句1
y = 0;        // 语句2
flag = true;  // 语句3
x = 4;         // 语句4
y = -1;       // 语句5

由于 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1 、语句 2 前面,也不会讲语句 3 放到语句 4 、语句 5 后面。但是要注意语句 1 和语句 2 的顺序、语句 4 和语句 5 的顺序是不作任何保证的。
  并且 volatile 关键字能保证,执行到语句 3 时,语句 1 和语句 2 必定是执行完毕了的,且语句 1 和语句 2 的执行结果对语句 3 、语句 4 、语句 5 是可见的。
  那么我们回到前面举的一个例子:
// 线程 1:
context = loadContext();   // 语句 1
inited = true;             // 语句 2

// 线程 2:while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

       

  前面举这个例子的时候,提到有可能语句 2 会在语句 1 之前执行,那么久可能导致 context 还没被初始化,而线程 2 中就使用未初始化的 context 去进行操作,导致程序出错。
  这里如果用 volatile 关键字对 inited 变量进行修饰,就不会出现这种问题了,因为当执行到语句 2 时,必定能保证 context 已经初始化完毕。

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值