Java多线程初学者指南(9):为什么要举办数据同步
副标题#e#
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);
}
}
在执行上面代码的大概功效如下:
n = 59
看到这个功效,大概许多读者会感想奇怪。这个措施显着是启动了100个线程,然后每个线程将静态变量n加1。最后利用join要领使这100个线程都运行完后,再输出这个n值。按正常来讲,功效应该是n = 100。可偏偏功效小于100。
#p#副标题#e#
其实发生这种功效的祸首罪魁就是我们常常提到的“脏数据”。而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++;
}
被编译后的中间语言代码
001 public void run()
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内存模子的问题。
#p#分页标题#e#
Java的内存模子分为主存储区和事情存储区。主存储区生存了Java中所有的实例。也就是说,在我们利用new来成立一个工具后,这个工具及它内部的要领、变量等都生存在这一区域,在MyThread类中的n就生存在这个区域。主存储区可以被所有线程共享。而事情存储区就是我们前面所讲的线程栈,在这个区域里生存了在run要领以及run要领所挪用的要领中界说的变量,也就是要领变量。在线程要修改主存储区中的变量时,并不是直接修改这些变量,而是将它们先复制到当前线程的事情存储区,在修改完后,再将这个变量值包围主存储区的相应的变量值。
在相识了Java的内存模子后,就不难领略为什么n++也不是原子操纵了。它必需颠末一个拷贝、加1和包围的进程。这个进程和在MyThread类中模仿的进程雷同。各人可以想象,假如在执行到getfield时,thread1由于某种原因被间断,那么就会产生和MyThread类的执行功效雷同的环境。要想彻底办理这个问题,就必需利用某种要领对n举办同步,也就是在同一时间只能有一个线程操纵n,这也称为对n的原子操纵。