破除java神话之原子操作都是线程安全的

1/5/2008来源:Java教程人气:9277


  java中原子操作是线程安全的论调经常被提到。根据定义,原子操作是不会被打断地的操作,因此被认为是线程安全的。实际上有一些原子操作不一定是线程安全的。
  
  这个问题出现的原因是尽量减少在代码中同步要害字。同步会损害性能,虽然这个损失因JVM不同而不同。另外,在现代的JVM中,同步的性能正在逐步提高。尽管如此,使用同步仍然是有性能代价的,并且程序员永远会尽力提高他们的代码的效率,因此这个问题就延续了下来。
  
  在java中,32位或者更少位数的赋值是原子的。在一个32位的硬件平台上,除了double和long型的其它原始类型通常都是使用32位进行表示,而double和long通常使用64位表示。另外,对象引用使用本机指针实现,通常也是32位的。对这些32位的类型的操作是原子的。
  
  这些原始类型通常使用32位或者64位表示,这又引入了另一个小小的神话:原始类型的大小是由语言保证的。这是不对的。java语言保证的是原始类型的表数范围而非JVM中的存储大小。因此,int型总是有相同的表数范围。在一个JVM上可能使用32位实现,而在另一个JVM上可能是64位的。在此再次强调:在所有平台上被保证的是表数范围,32位以及更小的值的操作是原子的。
  
  那么,原子操作在什么情况下不是线程安全的?主要的一点是他们也许确实是线程安全的,但是这没有被保证!java线程答应线程在自己的内存区保存变量的副本。答应线程使用本地的私有拷贝进行工作而非每次都使用主存的值是为了提高性能。考虑下面的类:
  
  
  class RealTimeClock
  {
   PRivate int clkID;
   public int clockID()
   {
   return clkID;
   }
   public void setClockID(int id)
   {
   clkID = id;
   }
  //...
  }
  
  现在考虑RealTimeClock的一个实例以及两个线程同时调用setClockID和clockID,并发生以下的事件序列:
  
  T1 调用setClockID(5)
  T1将5放入自己的私有工作内存
  T2调用setClockID(10)
  T2将10放入自己的私有工作内存
  T1调用clockID,它返回5
  5是从T1的私有工作内存返回的
  
  对clockI的调用应该返回10,因为这是被T2设置的,然而返回的是5,因为读写操作是对私有工作内存的而非主存。赋值操作当然是原子的,但是因为JVM答应这种行为,因此线程安全不是一定的,同时,JVM的这种行为也不是被保证的。
  
  两个线程拥有自己的私有拷贝而不和主存一致。假如这种行为出现,那么私有本机变量和主存一致必须在以下两个条件下:
  
  1、变量使用volatile声明
  2、被访问的变量处于同步方法或者同步块中
  
  假如变量被声明为volatile,在每次访问时都会和主存一致。这个一致性是由java语言保证的,并且是原子的,即使是64位的值。(注重很多JVM没有正确的实现volatile要害字。你可以在www.javasoft.com找到更多的信息。)另外,假如变量在同步方法或者同步块中被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁是变量被同步。
  使用任何一种方法都可以保证ClockID返回10,也就是正确的值。变量访问的频度不同则你的选择的性能不同。假如你更新很多变量,那么使用volatile可能比使用同步更慢。记住,假如变量被声明为volatile,那么在每次访问时都会和主存一致。与此对照,使用同步时,变量只在获得锁和释放锁的时候和主存一致。但是同步使得代码有较少的并发性。
  
  假如你更新很多变量并且不想有每次访问都和主存进行同步的损失或者你因为其它的原因想排除并发性时可以考虑使用同步。