Java中的变量分为两类局部变量和类变量局部变量是指在方法内定义的变量如在run方法中定义的变量对于这些变量来说并不存在线程之间共享的问题因此它们不需要进行数据同步类变量是在类中定义的变量作用域是整个类这类变量可以被多个线程共享因此我们需要对这类变量进行数据同步
数据同步就是指在同一时间只能由一个线程来访问被同步的类变量当前线程访问完这些变量后其他线程才能继续访问这里说的访问是指有写操作的访问如果所有访问类变量的线程都是读操作一般是不需要数据同步的
那么如果不对共享的类变量进行数据同步会发生什么情况呢?让我们先看看下面的代码会发生什么样的事情
packagetest;
publicclassMyThreadextendsThread
{
publicstaticintn=;
publicvoidrun()
{
intm=n;
yield();
m++;
n=m;
}
publicstaticvoidmain(String[]args)throwsException
{
MyThreadmyThread=newMyThread();
Threadthreads[]=newThread[];
for(inti=;i<threadslength;i++)
threads[i]=newThread(myThread);
for(inti=;i<threadslength;i++)
threads[i]start();
for(inti=;i<threadslength;i++)
threads[i]join();
Systemoutprintln(n=+MyThreadn);
}
}
在执行上面代码的可能结果如下
n=
看到这个结果可能很多读者会感到奇怪这个程序明明是启动了个线程然后每个线程将静态变量n加最后使用join方法使这个线程都运行完后再输出这个n值按正常来讲结果应该是n = 可偏偏结果小于
其实产生这种结果的罪魁祸首就是我们经常提到的髒数据而run方法中的yield()语句就是产生髒数据的始作俑者(不加yield语句也可能会产生髒数据但不会这么明显只有将改成更大的数才会经常产生髒数据在本例中调用yield就是为了放大髒数据的效果)yield方法的作用是使线程暂停也就是使调用yield方法的线程暂时放弃CPU资源使CPU有机会来执行其他的线程为了说明这个程序如何产生髒数据我们假设只创建了两个线程thread和thread由于先调用了thread的start方法因此thread的run方法一般会先运行当thread的run方法运行到第一行(int m = n)时将n的值赋给m当执行到第二行的yield方法后thread就会暂时停止执行而当thread暂停时thread获得了CPU资源后开始运行(之前thread一直处于就绪状态)当thread执行到第一行(int m = n)时由于thread在执行到yield时n仍然是因此thread中的m获得的值也是这样就造成了thread和thread的m获得的都是在它们执行完yield方法后都是从开始加因此无论谁先执行完最后n的值都是只是这个n被thread和thread各赋了一遍值这个过程如下图如示
也许有人会问如果只有n++会产生髒数据吗?答案是肯定的那么n++只是一条语句又如何在执行过程中将CPU交给其他的线程呢?其实这只是表面现象n++在被Java编译器编译成中间语言(也叫做字节码)后并不是一条语言让我们看看下面的Java代码将会被编译成什么样的Java中间语言
Java源代码
publicvoidrun()
{
n++;
}
被编译后的中间语言代码
publicvoidrun()
{
aload_
dup
getfield
iconst_
iadd
putfield
return
}
大家可以看到在run方法中只有n++一条语句而在编译后却有条中间语言语句我们并不需要知道这些语句的功能是什么只看一下第和行语句在行是getfield根据它的英文含义可知是要得到某个值因为这里只有一个n所以毫无疑问是要得到n的值而在行的iadd也不难猜测是将这个得到的n值加在行的putfield的含义我想大家可能已经猜出来了它负责将这个加后的n再更新回类变量n说到这可能大家还有一个疑惑执行n++时直接将n加不就行了为什么要如此费周折其实这里涉及到一个Java内存模型的问题
Java的内存模型分为主存储区和工作存储区主存储区保存了Java中所有的实例也就是说在我们使用new来建立一个对象后这个对象及它内部的方法变量等都保存在这一区域在MyThread类中的n就保存在这个区域主存储区可以被所有线程共享而工作存储区就是我们前面所讲的线程栈在这个区域里保存了在run方法以及run方法所调用的方法中定义的变量也就是方法变量在线程要修改主存储区中的变量时并不是直接修改这些变量而是将它们先复制到当前线程的工作存储区在修改完后再将这个变量值覆盖主存储区的相应的变量值
在了解了Java的内存模型后就不难理解为什么n++也不是原子操作了它必须经过一个拷贝加和覆盖的过程这个过程和在MyThread类中模拟的过程类似大家可以想象如果在执行到getfield时thread由于某种原因被中断那么就会发生和MyThread类的执行结果类似的情况要想彻底解决这个问题就必须使用某种方法对n进行同步也就是在同一时间只能有一个线程操作n这也称为对n的原子操作