volatile 和 atomic 原子性的区别和联系


An incorrect piece of lore that is often repeated in Java threading discussions is, "Atomic operations do not need to be synchronized." An atomic operation is one that cannot be interrupted by the thread scheduler; if the operation begins, then it will run to completion before the possibility of a context switch.


Atomicity applies to "simple operations" on primitive types except for longs and doubles. Reading and writing primitive variables other than long and double is guaranteed to go to and from memory as indivisible (atomic) operations. However, the JVM is allowed to
perform reads and writes of 64- bit quantities (long and double variables) as two separate 32-bit operations, raising the possibility that a context switch could happen in the middle of a
read or write, and then different tasks could see incorrect results (this is sometimes called word tearing, because you might see the value after only part of it has been changed). However, you do get atomicity (for simple assignments and returns) if you use the volatile keyword when defining a long or double variable (note that volatile was not working properly before Java SE5). Different JVMs are free to provide stronger guarantees, but you should not rely on platform-specific features.  

The volatile keyword also ensures visibility across the application. If you declare a field to be volatile, this means that as soon as a write occurs for that field, all reads will see the change. This is true even if local caches are involved—volatile fields are immediately written through to main memory, and reads occur from main memory.  

原子操作就是不能被线程调度机制中断的操作。不正确的认识:原子操作不需要进行同步。

在Java 中除了 long 和 double 之外的所有基本类型的读和赋值,都是原子性操作。而64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性,会产生字撕裂问题。但是当你定义long或double变量时,如果使用 volatile关键字,就会获到(简单的赋值与返回操作的)原子性(注意,在Java SE5之前,volatile一直不能正确的工作)。见第四版《Thinking in java》第21章并发。

volatile关键字确保了应用中的可视性。如果你将一个域声明为volatile,那么只要这个域产生了写操作,那么所有的读操作就都可以看到这个修改。

下面来看看volatile 和原子性的区别和联系。我将从下面几个问题进行思考和探索。

第1个问题:如何证明 作者上面所说的long 和 double 的简单操作是非原子性的 - 会产生字撕裂问题,而使用volatile 关键字可以保证 long 或 double 的简单操作具有原子性,以及验证其它的基本类型(如int)的简单操作具有原子性。

我的思路:
       1. 多个任务对同一个 long 变量进行赋值修改,所赋的值为从 1 到64位 仅有1位为1,其余位均为0的数,并所返回赋值完成后的值。如果long 变量不具有原子性,那么很有可能得到一个多个位为1的数或者所有位为0的数,一旦发生,我们输出一条信息,并终止程序。
       2. 如果1出现字撕裂,那么long 变量加上 volatile 限制后,赋值返回的数应该都满足从 1 到64位 仅有1位为1,其余位均为0,即不会出现字撕裂。
       3. 同理,测试int变量,但是由于int 赋值具有原子性,所以即使不加 volatile 限制,赋值返回的数应该都满足从 1 到64位 仅有1位为1,其余位均为0。
      
具体见下面我写的测试代码

// 证明 long 变量简单操作(赋值和返回)不具有原子性,存在字撕裂问题。验证 volatile 可确保
// long 变量简单操作具有原子性。验证 int 变量简单操作(赋值和返回)具有原子性
package concurrency;

import java.util.concurrent.*;

class Operation{
       private int num = 0;
       private long bigNum = 0;

       public int assignInt(int n){
              num = n;
              Thread.yield();
              return num;
       }
       public long assignLong(long n){
              bigNum = n;
              Thread.yield();
              return bigNum;
       }
}

public class AtomicTest{
       static class IntOperationTask implements Runnable{
              private Operation operation;
              public IntOperationTask(Operation op){
                     operation = op;
              }
              public void run() {
                     while(true){
                            int oldNum, newNum;
                            for(int i = 0; i < 32; i++){
                                   oldNum = 1 << i;
                                   newNum = operation.assignInt(oldNum);
                                   if(oldNum != newNum){
                                          int bits = 0;
                                          for(int j = 0; j < 32; j++){
                                                 if(0 != (newNum & (1 << j)))
                                                        bits++;
                                          }
                                         
                                          if(1 != bits){
                                                 System.out.printf("[int TEST] It is no atomic operation." +
                                                               " old:x new:xn",oldNum, newNum);
                                                 System.exit(0);
                                          }
//                                          else
//                                                 System.out.printf("[int TEST] It is no synchronousoperation." +
//                                                               " old:x new:xn",oldNum, newNum);
                                   }
                            }
                     }
              }
       }
      
       static class LongOperationTask implements Runnable{
              private Operation operation;
              public LongOperationTask(Operation op){
                     operation = op;
              }
              public void run() {
                     while(true){
                            long oldNum, newNum;
                            long one = 1;
                            for(int i = 0; i < 64; i++){
                                   oldNum = one << i;
                                   newNum = operation.assignLong(oldNum);
                                   if(oldNum != newNum){
                                          int bits = 0;
                                          for(int j = 0; j < 64; j++){
                                                 if(0 != (newNum & (one << j)))
                                                        bits++;
                                          }
                                         
                                          if(1 != bits){
                                                 System.out.printf("[long TEST] It is no atomic operation. " +
                                                               "old:6x new:6xn",oldNum, newNum);
                                                 System.exit(0);
                                          }
                                   }
                            }
                     }
              }
       }
       public static void main(String[] args){
              Operation op = new Operation();
              ExecutorService service = Executors.newCachedThreadPool();
             
              for(int i = 0; i < 10; i++){
                     //service.execute(new IntOperationTask(op));
                     service.execute(new LongOperationTask(op));
              }
       }
}

测试结果:
       1. 当long 没有使用 volatile 修饰时,不到几秒,就出现了字撕裂:
[long TEST] It is no atomic operation. old:0000010000000000 new:0000002000000001
[long TEST] It is no atomic operation. old:0000000000040000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000080000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000000100000 new:0000000000000000
[long TEST] It is no atomic operation. old:0010000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000000000001 new:0000002000000001
[long TEST] It is no atomic operation. old:0001000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0001000000000000 new:0000000000000000
[long TEST] It is no atomic operation. old:0000000010000000 new:0000000180000000

上面的测试是在公司的电脑上进行的,可是回到家里我使用我自己的笔记本电脑进行测试了1分钟,都没有出现字撕裂!这是怎么回事?它吊起了我的兴趣!两台都 是Win7 64位电脑,都是多核Intel CPU,CPU型号不一样,使用的JRE不一样,一个是JRE6(出现字撕裂),一个是JRE7(运行1分钟仍未出现字撕裂)。我怀疑是JRE问题,把这 台电脑的Eclipse 运行环境换成JRE6,还是没有出现!难道和CPU有关系,这可不好搞,我心里嘀咕着。冷静下来,再分析了下,看了下JRE6的路径是 "C:Program FilesJavajre6" ,我这是Win7 64系统,这意味着我使用的是64位jre环境,会不会我公司用的是32位jre环境?我立即把Eclisep 运行环境换成32位的 jre: "C:Program Files (x86)Javajre6",果然一运行,就出现字撕裂,这次只打印了一条,见下面的打印信息。可以观察到,当使用32位的jre运行 时,javaw.exe 进程是32位进程,但使用64位jre运行时,javaw.exe 进程是64位进程,所以很有可能在64位的jre环境,long double 64位不需要再分离成两个32位来进行操作,即很有可能它们的赋值操作也是原子性的。

[long TEST] It is no atomic operation. old:4000000000000000 new:0000000000000000


       2. 而当long变量使用 volatile 修饰后,程序运行了几分钟,也未出现上面的情况。

       3. int 变量未使用 volatile 修饰,也未出现字撕裂情况。



第2个问题:作者说在java 中 ++ 操作是非原子性操作,那如果使用++递增一个volatile 的int变量,会发生说明,也就是对一个volatile 的变量进行非原子性操作会发生什么,会不会像volatile 限定   long double 变量那样,使得 ++ 变为一个原子性操作呢?

这个问题《Thinking in java》的作者已给出解答和验证代码。当多个任务异步调用 nextSerialNumber 会出现什么问题呢?

//: concurrency/SerialNumberGenerator.java
package concurrency;
public class SerialNumberGenerator {
   private static volatile int serialNumber = 0;
   public static int nextSerialNumber() {
       return serialNumber++; // Not thread-safe
   }
} ///:~

我的思考:

如果 ++ 是原子性操作,那么由于serialNumber 加上了 volatile 限定,所以任何线程对 serialNumber 的修改,在其它线程都可以看到这个修改。并且 return 一个 int 也是原子操作,即不会中断,所以s如果 ++ 是原子性操作,那么serialNumber在内存的值变化一定是递增的(在int 还未溢出为负数时),注意这里并没有说返回的值一定是递增的,因为可能在++ 完成后,任务就被中断,其它任务继续递增了nextSerialNumber 的值,并返回该值,然后之前那个任务才继续返回,这样返回的值就不是递增的了,但是返回的值在一定的区间内肯定是不会出现重复的(在int 还未循环回0时)。

如果 ++ 是非原子性操作,那么有可能有某个任务已经读取 serialNumber到寄存器了,并在在执行++操作时发生中断(这个时候serialNumber值还未完成加1,如果是具有则原子性则不会被中 断),此时另外一个任务也把serialNumber读取到寄存器,并执行完++操作后(虽然具有volatile 的限定,但是前面一个任务已经在此之前读取了serialNumber,所以也就看不到现在serialNumber修改后的值),前面那个任务才继续执 行++操作,那么这两个任务实际上只对serialNumber完成加1的操作,而不是加2的操作,也就是说这两次调用返回的值是一样的!

通过上面的分析,我们可以断定,如果++具有原子性,返回的值在一定的区间内不会发生重复,否则可能会发生重复。

下面是   《Thinking in java 》作者写的代码


//: concurrency/SerialNumberChecker.java
// Operations that may seem safe are not,
// when threads are present.
// {Args: 4}
package concurrency;
import java.util.concurrent.*;

// Reuses storage so we don't run out of memory:
class CircularSet {
   private int[] array;
   private int len;
   private int index = 0;
   public CircularSet(int size) {
       array = new int[size];
       len = size;
       // Initialize to a value not produced
       // by the SerialNumberGenerator:
       for(int i = 0; i < size; i++)
           array[i] = -1;
   }
   public synchronized void add(int i) {
       array[index] = i;
       // Wrap index and write over old elements:
       index = ++index % len;
   }
   public synchronized boolean contains(int val) {
       for(int i = 0; i < len; i++)
           if(array[i] == val) return true;
       return false;
   }
}

public class SerialNumberChecker {
   private static final int SIZE = 10;
   private static CircularSet serials =
       new CircularSet(1000);
   private static ExecutorService exec =
       Executors.newCachedThreadPool();
   static class SerialChecker implements Runnable {
       public void run() {
           while(true) {
               int serial =
                   SerialNumberGenerator.nextSerialNumber();
               if(serials.contains(serial)) {
                   System.out.println("Duplicate: " + serial);
                   System.exit(0);
               }
               serials.add(serial);
           }
       }
   }
   public static void main(String[] args) throws Exception {
       for(int i = 0; i < SIZE; i++)
           exec.execute(new SerialChecker());
       // Stop after n seconds if there's an argument:
       if(args.length > 0) {
           TimeUnit.SECONDS.sleep(new Integer(args[0]));
           System.out.println("No duplicates detected");
           System.exit(0);
       }
   }
} //:~

结论:当你定义long或double变量时,如果使用volatile关键字限定 long 或 double 变量,就会获到(简单的赋值与返回操作的)原子性(注意,在Java SE5之前,volatile一直不能正确的工作),若没有使用volatile关键字限定,那么在32位JRE环境下,肯定是非原子性的,在64位 JRE环境下,很有可能具有原子性(上面我的测试是没有出现字撕裂,呵呵,但我不敢肯定是否一定具有原子性)。但是如果你想使++ 递增操作具有原子性,而仅仅只是同样使用 volatile 进行限定,那么你就会出错!引用《Thinking in java》作者的话:原子操作就是不能被线程调度机制中断的操作。不正确的认识:原子操作不需要进行同步。volatile关键字确保了应用中的可视性。 如果你将一个域声明为volatile,那么只要这个域产生了写操作,那么所有的读操作就都可以看到这个修改。

转自:http://blog.sina.com.cn/s/blog_c00b6f6201016mbk.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值