Picses' sky

Picses' sky
posts - 43, comments - 29, trackbacks - 0, articles - 24

用Java线程获取优异性能(II)1

Posted on 2007-07-18 13:11 Matthew Chen 阅读(224) 评论(0)  编辑  收藏 所属分类: Java MultiThread
摘要
开发者有时创建的多线程程序会生成错误值或产生其它奇怪的行为。古怪行为一般出现在一个多线程程序没使用同步连载线程访问关键代码部份的时候。同步连载线程访问关键代码部份是什么意思呢?在这篇文章中解释了同步,Java的同步机制,以及当开发者没有正确使用这个机制时出现的两个问题。一旦你看完这篇文章,你就可以避免在你的多线程Java程序中因缺乏同步而产生的奇怪行为。
创建多线程Java程序难吗?仅从《用Java线程获取优异性能(I)》中获得的信息你就可以回答,不。毕竟,我已经向你显示了如何轻松地创建线程对象,通过调用Thread的start()方法起动与这些对象相关的线程,以及通过调用其它Thread方法,比如三个重载的join()方法执行简单的线程操作。至今仍有许多开发者在开发一些多线程程序时面临困难境遇。他们的程序经常功能不稳定或产生错误值。例如,一个多线程程序可能将不正确的雇员资料存贮在数据库中,比如姓名和地址。姓名可能属于一个雇员的,而地址却属于另一个的。是什么引起这种奇怪行为的呢? 是缺乏同步:连载行为,或在同一时间排序,线程访问那些让多重线程操作的类和字段变量实例的代码序列,以及其他共享资源。我称这些代码序列为关键代码部份。
注意:不象类和实例字段变量,线程不能共享本地变量和参数。原因是:本地变量和参数在一个线程方法中分配——叫堆栈。结果,每一个线程都收到它自己对那些变量的拷贝。相反,线程能够共享类字段和实例字段因为那些变量在一个线程方法(叫堆栈)中没有被分配。取而代之,它们作为类(类字段)或对象(实例字段)的一部份在共享内存堆中被分配。
这篇文章将教你如何使用同步连载线程访问关键代码部份。我用一个说明为什么一些多线程程序必须使用同步的例子作为开始。我接下来就监视器和锁探讨Java的同步机制和synchronized 关键字。我通过研究由这样的错用产生的两个问题判定常常因为不正确的使用同步机制而否认了它的好处。
阅读关于线程程序的整个系列:
· 第I部份:介绍线程、线程类及Runnable
· 第II部份:使用同步连载线程访问关键代码部份
对于同步的需要
为什么我们需要同步呢?一种回答,考虑这个例子:你写一个使用一对线程模拟取款/存款金融事务的Java程序。在那个程序中,一个线程处理存款,同时其它线程正处理取款。每一个线程操作一对共享变量、类及实例字段变量,这些用来标识金融事务的姓名和账号。对于一个正确的金融事务,每一个线程必须在其它线程开始给name和amount赋值前(并且同时打印那些值)给name和amount变量赋值(并打印那些值,模拟存贮事务)。其源代码如下:
列表1. NeedForSynchronizationDemo.java
// NeedForSynchronizationDemo.java
class NeedForSynchronizationDemo
{
public static void main (String [] args)
{
FinTrans ft = new FinTrans ();
TransThread tt1 = new TransThread (ft, "Deposit Thread");
TransThread tt2 = new TransThread (ft, "Withdrawal Thread");
tt1.start ();
tt2.start ();
}
}
class FinTrans
{
public static String transName;
public static double amount;
}
class TransThread extends Thread
{
private FinTrans ft;
TransThread (FinTrans ft, String name)
{
super (name); //保存线程名称
this.ft = ft; //保存对金融事务对象的引用
}
public void run ()
{
for (int i = 0; i < 100; i++)
{
if (getName ().equals ("Deposit Thread"))
{
//存款线程关键代码部份的开始
ft.transName = "Deposit";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 2000.0;
System.out.println (ft.transName + " " + ft.amount);
//存款线程关键代码部份的结束
}
else
{
//取款线程关键代码部份的开始
ft.transName = "Withdrawal";
try
{
Thread.sleep ((int) (Math.random () * 1000));
}
catch (InterruptedException e)
{
}
ft.amount = 250.0;
System.out.println (ft.transName + " " + ft.amount);
//取款线程关键代码部份的结束
}
}
}
}
NeedForSynchronizationDemo的源代码有两个关键代码部份:一个可理解为存款线程,另一个可理解为取款线程。在存款线程关键代码部份中,线程分配Deposit String对象的引用给共享变量transName及分配2000.0 给共享变量amount。同样,在取款关键代码部份,线程分配Withdrawal String对象的引用给transName及分配250.0给amount。在每个线程的分配之后打印那些变量的内容。当你运行NeedForSynchronizationDemo时,你可能期望输出类似于Withdrawal 250.0 和Deposit 2000.0两行组成的列表。相反,你收到的输出如下所示:
Withdrawal 250.0
Withdrawal 2000.0
Deposit 2000.0
Deposit 2000.0
Deposit 250.0
程序明显有问题。取款线程不应该模拟$2,000的取款,存款线程不应该模拟$250的存款。每一个线程产生不一致的输出。是什么引起了这些矛盾呢?我们是如下认为的:
· 在一个单处理器机器上,线程共享处理器。结果,一个线程仅能执行一定时间段。在其它时间里, JVM/操作系统暂停那个线程的执行并允许其它线程执行——一种线程时序安排。在一个多处理器机器上,依靠线程和处理器的数目,每一个线程都能拥有它自己的处理器。
· 在一单处理器机器上,一个线程的执行时间段没有足够长到在其它线程开始执行的关键代码部份前完成它自己的关键代码部分。在一个多处理器机器上,线程能够同时执行它们自己的关键代码部份。然而,它们可能在不同的时间进入它们的关键代码部份。
· 无论是单处理器或是多处理器机器,下面的情形都可能发生:线程A在它的关键代码部份分配一个值给共享变量X并决定执行一个要求100毫秒的输入/输出操作。接下来线程B进入它的关键代码部份,分配一个不同的值给X,执行一个50毫秒的输入/输出操作并分配值给共享变量Y 和Z。线程A的输入/输出操作完成,并分配它自己的值给Y和Z。因为X包含一个B分配的值,然而Y和Z包含A分配的值,这是一个矛盾的结果。
这个矛盾是怎样在NeedForSynchronizationDemo中产生的呢?假设存款线程执行ft.transName = "Deposit"并且接下来调用Thread.sleep()。在那一点,存款线程交出处理器控制一段时间进行休眠,让取款线程执行。假定存款线程休眠500毫秒(感谢Math.random()从0到999毫秒范围随机选取一个值)。在存款线程休眠期间,取款线程执行ft.transName = "Withdrawal",休眠50毫秒 (取款线程随机选取休眠值),醒后执行ft.amount = 250.0并执行System.out.println (ft.transName + " " + ft.amount)—所有都在存款线程醒来之前。结果,取款线程打印Withdrawal 250.0,那是正确的。当存款线程醒来执行ft.amount = 2000.0,接下来执行System.out.println (ft.transName + " " + ft.amount)。这个时间Withdrawal 2000.0 打印,那是不正确的。虽然存款线程先前分配"Deposit"的引用给transName,但这个引用随后会在取款线程分配”Withdrawal”引用给那个共享变量时消失。当存款线程醒来时,它就不能存贮正确的引用到transName,但通过分配2000.0给amount继续它的执行。虽然两个变量都不会有无效的值,但它们的结合值却是矛盾的。假如这样的话,它们的值显示企图取款$2,000。
很久以前,计算机科学家发明了描述导致矛盾的多线程组合行为的一个术语。术语是竞态条件(race condition)—每一个线程竞相在其它线程进入同一关键代码部份前完成它自己的关键代码部份的行为。作为NeedForSynchronizationDemo示范,线程的执行顺序是不可知的。这里不能保证一个线程能够在其它线程进入关键代码部份前完成它自己的关键代码部份。因此,我们会有竞态条件引起不一致。要阻止竞态条件,每一个线程必须在其它线程进入同一关键代码部份或其它操作同一共享变量或资源的相关关键代码部份前完成它自己的关键代码部份。对于一个关键代码部份没有连载访问方法(即是在一个时间只允许访问一个线程),你就不能阻止竞态条件或不一致的出现。幸运的是,Java提供了连载线程访问的方法:通过它的同步机制。
注意:对于Java的类型,只有长整型和双精度浮点型变量倾向于不一致。为什么?一个32位JVM一般用两个临近32位步长访问一个64位的长整型变量或一个64位双精度浮点型变量。一个线程可能在完成第一步后等待其它线程执行所有的两步。接下来,第一个线程可能醒来并完成第二步,产生一个值既不同于第一个线程也不同于第二线程的值的变量。结果,如果至少一个线程能够修改一个长整型变量或一个双精度浮点型变量,那些读取和(或)修改那个变量的所有线程就必须使用同步连载访问。

只有注册用户登录后才能发表评论。


网站导航: