Java
中的变量分为两类:局部变量和类变量。局部变量是指在方法内定义的变量,如在run
方法中定义的变量。对于这些变量来说,并不存在线程之间共享的问题。因此,它们不需要进行数据同步。类变量是在类中定义的变量,作用域是整个类。这类变量可以被多个线程共享。因此,我们需要对这类变量进行数据同步。
数据同步就是指在同一时间,只能由一个线程来访问被同步的类变量,当前线程访问完这些变量后,其他线程才能继续访问。这里说的访问是指有写操作的访问,如果所有访问类变量的线程都是读操作,一般是不需要数据同步的。
那么如果不对共享的类变量进行数据同步,会发生什么情况呢?让我们先看看下面的代码会发生什么样的事情:
package
test;
public class MyThread extends Thread
{
public static int n = 0 ;
public void run()
{
int m = n;
yield();
m ++ ;
n = m;
}
public static void main(String[] args) throws Exception
{
MyThread myThread = new MyThread ();
Thread threads[] = new Thread[ 100 ];
for ( int i = 0 ; i < threads.length; i ++ )
threads[i] = new Thread(myThread);
for ( int i = 0 ; i < threads.length; i ++ )
threads[i].start();
for ( int i = 0 ; i < threads.length; i ++ )
threads[i].join();
System.out.println( " n = " + MyThread.n);
}
}
public class MyThread extends Thread
{
public static int n = 0 ;
public void run()
{
int m = n;
yield();
m ++ ;
n = m;
}
public static void main(String[] args) throws Exception
{
MyThread myThread = new MyThread ();
Thread threads[] = new Thread[ 100 ];
for ( int i = 0 ; i < threads.length; i ++ )
threads[i] = new Thread(myThread);
for ( int i = 0 ; i < threads.length; i ++ )
threads[i].start();
for ( int i = 0 ; i < threads.length; i ++ )
threads[i].join();
System.out.println( " n = " + MyThread.n);
}
}
在执行上面代码的可能结果如下:
n
=
59
看到这个结果,可能很多读者会感到奇怪。这个程序明明是启动了100
个线程,然后每个线程将静态变量n
加1
。最后使用join
方法使这100
个线程都运行完后,再输出这个n
值。按正常来讲,结果应该是n = 100
。可偏偏结果小于100
。
其实产生这种结果的罪魁祸首就是我们经常提到的“脏数据”。而run方法中的yield()
语句就是产生“脏数据”的始作俑者(不加yield语句也可能会产生“脏数据”,但不会这么明显,只有将100改成更大的数,才会经常产生“脏数据”,在本例中调用yield就是为了放大“脏数据”的效果)。yield
方法的作用是使线程暂停,也就是使调用yield
方法的线程暂时放弃CPU
资源,使CPU
有机会来执行其他的线程。为了说明这个程序如何产生“脏数据”,我们假设只创建了两个线程:thread1
和thread2
。由于先调用了thread1
的start
方法,因此,thread1
的run
方法一般会先运行。当thread1
的run
方法运行到第一行(int m = n;
)时,将n
的值赋给m
。当执行到第二行的yield
方法后,thread1
就会暂时停止执行,而当thread1
暂停时,thread2
获得了CPU
资源后开始运行(之前thread2
一直处于就绪状态),当thread2
执行到第一行(int m = n;)
时,由于thread1
在执行到yield
时n
仍然是0
,因此,thread2
中的m
获得的值也是0
。这样就造成了thread1
和thread2
的m
获得的都是0
。在它们执行完yield
方法后,都是从0
开始加1
,因此,无论谁先执行完,最后n
的值都是1
,只是这个n
被thread1
和thread2
各赋了一遍值。这个过程如下图如示:
也许有人会问,如果只有n++,会产生“脏数据”吗?答案是肯定的。那么n++
只是一条语句,又如何在执行过程中将CPU
交给其他的线程呢?其实这只是表面现象,n++
在被Java
编译器编译成中间语言(也叫做字节码)后,并不是一条语言。让我们看看下面的Java
代码将会被编译成什么样的Java
中间语言。
Java
源代码
public
void
run()
{
n ++ ;
}
{
n ++ ;
}
被编译后的中间语言代码
001
public
void
run()
002 {
003 aload_0
004 dup
005 getfield
006 iconst_1
007 iadd
008 putfield
009 return
010 }
002 {
003 aload_0
004 dup
005 getfield
006 iconst_1
007 iadd
008 putfield
009 return
010 }
大家可以看到在run
方法中只有n++
一条语句,而在编译后,却有7
条中间语言语句。我们并不需要知道这些语句的功能是什么,只看一下第005
、007
和008
行语句。在005
行是getfield
,根据它的英文含义可知是要得到某个值,因为这里只有一个n
,所以毫无疑问,是要得到n
的值。而在007
行的iadd
也不难猜测是将这个得到的n
值加1
。在008
行的putfield
的含义我想大家可能已经猜出来了,它负责将这个加1
后的n
再更新回类变量n
。说到这,可能大家还有一个疑惑,执行n++
时直接将n
加1
不就行了,为什么要如此费周折。其实这里涉及到一个Java
内存模型的问题。
Java
的内存模型分为主存储区和工作存储区。主存储区保存了Java
中所有的实例。也就是说,在我们使用new
来建立一个对象后,这个对象及它内部的方法、变量等都保存在这一区域,在MyThread类中
的n
就保存在这个区域。主存储区可以被所有线程共享。而工作存储区就是我们前面所讲的线程栈,在这个区域里保存了在run
方法以及run
方法所调用的方法中定义的变量,也就是方法变量。在线程要修改主存储区中的变量时,并不是直接修改这些变量,而是将它们先复制到当前线程的工作存储区,在修改完后,再将这个变量值覆盖主存储区的相应的变量值。
在了解了Java
的内存模型后,就不难理解为什么n++
也不是原子操作了。它必须经过一个拷贝、加1
和覆盖的过程。这个过程和在MyThread类
中模拟的过程类似。大家可以想象,如果在执行到getfield
时,thread1
由于某种原因被中断,那么就会发生和MyThread类的执行结果
类似的情况。要想彻底解决这个问题,就必须使用某种方法对n进行同步,也就是在同一时间只能有一个线程操作n,这也称为对n的原子操作。
本文转自 androidguy 51CTO博客,原文链接:http://blog.51cto.com/androidguy/214828,如需转载请自行联系原作者