JAVA教程:解析Java的多线程机制(二)
三、java语言的多线程程序设计方法
利用java语言实现多线程应用程序的方法很简单。根据多线程应用程序继承或实现对象的不同可以采用两种方式:一种是应用程序的并发运行对象直接继承java的线程类thread;另外一种方式是定义并发执行对象实现runnable接口。继承thread类的多线程程序设计方法
thread类是jdk中定义的用于控制线程对象的类,在该类中封装了用于进行线程控制的方法。见下面的示例代码:
[code]//consumer.java
importjava.util.*;
classconsumerextendsthread
{
intntime;
stringstrconsumer;
publicconsumer(intntime,stringstrconsumer)
{
this.ntime=ntime;
this.strconsumer=strconsumer;
}
publicvoidrun()
{
while(true)
{
try
{
system.out.println('consumername:'+strconsumer+'\n');
thread.sleep(ntime);
}
catch(exceptione)
{
e.printstacktrace();
}
}
}
staticpublicvoidmain(stringargs[])
{
consumeraconsumer=newconsumer(1000,'aconsumer');
aconsumer.start();
consumerbconsumer=newconsumer(2000,'bconsumer');
bconsumer.start();
consumercconsumer=newconsumer(3000,'cconsumer');
cconsumer.start();
}
}[/code]
从上面的程序代码可以看出:多线程执行地下consumer继承java语言中的线程类thread并且在main方法中创建了三个consumer对象的实例。当调用对象实例的start方法时,自动调用consumer类中定义的run方法启动对象线程运行。线程运行的结果是每间隔ntime时间打印出对象实例中的字符串成员变量strconsumer的内容。
可以总结出继承thread类的多线程程序设计方法是使应用程序类继承thread类并且在该类的run方法中实现并发性处理过程。
实现runnable接口的多线程程序设计方法
java语言中提供的另外一种实现多线程应用程序的方法是多线程对象实现runnable接口并且在该类中定义用于启动线程的run方法。这种定义方式的好处在于多线程应用对象可以继承其它对象而不是必须继承thread类,从而能够增加类定义的逻辑性。
实现runnable接口的多线程应用程序框架代码如下所示:
//consumer.java
importjava.util.*;
classconsumerimplementsrunnable
{
……
publicconsumer(intntime,stringstrconsumer){……}
publicvoidrun(){……}
staticpublicvoidmain(stringargs[])
{
threadaconsumer=newthread(newconsumer(1000,'aconsumer'));
aconsumer.start();
//其它对象实例的运行线程
//……
}
}
从上述代码可以看出:该类实现了runnable接口并且在该类中定义了run方法。这种多线程应用程序的实现方式与继承thread类的多线程应用程序的重要区别在于启动多线程对象的方法设计方法不同。在上述代码中,通过创建thread对象实例并且将应用对象作为创建thread类实例的参数。
四、线程间的同步
java应用程序的多个线程共享同一进程的数据资源,多个用户线程在并发运行过程中可能同时访问具有敏感性的内容。在java中定义了线程同步的概念,实现对共享资源的一致性维护。下面以笔者最近开发的移动通信计费系统中线程间同步控制方法,说明java语言中多线程同步方式的实现过程。
在没有多线程同步控制策略条件下的客户账户类定义框架代码如下所示:
publicclassregisteraccount
{
floatfbalance;
//客户缴费方法
publicvoiddeposit(floatffees){fbalance+=ffees;}
//通话计费方法
publicvoidwithdraw(floatffees){fbalance-=ffees;}
……
}
读者也许会认为:上述程序代码完全能够满足计费系统实际的需要。确实,在单线程环境下该程序确实是可靠的。但是,多进程并发运行的情况是怎样的呢?假设发生这种情况:客户在客户服务中心进行缴费的同时正在利用移动通信设备仅此通话,客户通话结束时计费系统启动计费进程,而同时服务中心的工作人员也提交缴费进程运行。读者可以看到如果发生这种情况,对客户账户的处理是不严肃的。
如何解决这种问题呢?很简单,在registeraccount类方法定义中加上用于标识同步方法的关键字synchronized。这样,在同步方法执行过程中该方法涉及的共享资源(在上述代码中为fbalance成员变量)将被加上共享锁,以确保在方法运行期间只有该方法能够对共享资源进行访问,直到该方法的线程运行结束打开共享锁,其它线程才能够访问这些共享资源。在共享锁没有打开的时候其它访问共享资源的线程处于阻塞状态。
进行线程同步策略控制后的registeraccount类定义如下面代码所示:
publicclassregisteraccount
{
floatfbalance;
publicsynchronizedvoiddeposit(floatffees){fbalance+=ffees;}
publicsynchronizedvoidwithdraw(floatffees){fbalance-=ffees;}
……
}
从经过线程同步机制定义后的代码形式可以看出:在对共享资源进行访问的方法访问属性关键字(public)后附加同步定义关键字synchronized,使得同步方法在对共享资源访问的时候,为这些敏感资源附加共享锁来控制方法执行期间的资源独占性,实现了应用系统数据资源的一致性管理和维护。
五、java线程的管理
线程的状态控制
在这里需要明确的是:无论采用继承thread类还是实现runnable接口来实现应用程序的多线程能力,都需要在该类中定义用于完成实际功能的run方法,这个run方法称为线程体(threadbody)。按照线程体在计算机系统内存中的状态不同,可以将线程分为创建、就绪、运行、睡眠、挂起和死亡等类型。这些线程状态类型下线程的特征为:
创建状态:当利用new关键字创建线程对象实例后,它仅仅作为一个对象实例存在,jvm没有为其分配cpu时间片等线程运行资源;
就绪状态:在处于创建状态的线程中调用start方法将线程的状态转换为就绪状态。这时,线程已经得到除cpu时间之外的其它系统资源,只等jvm的线程调度器按照线程的优先级对该线程进行调度,从而使该线程拥有能够获得cpu时间片的机会。
睡眠状态:在线程运行过程中可以调用sleep方法并在方法参数中指定线程的睡眠时间将线程状态转换为睡眠状态。这时,该线程在不释放占用资源的情况下停止运行指定的睡眠时间。时间到达后,线程重新由jvm线程调度器进行调度和管理。
挂起状态:可以通过调用suspend方法将线程的状态转换为挂起状态。这时,线程将释放占用的所有资源,由jvm调度转入临时存储空间,直至应用程序调用resume方法恢复线程运行。
死亡状态:当线程体运行结束或者调用线程对象的stop方法后线程将终止运行,由jvm收回线程占用的资源。
在java线程类中分别定义了相应的方法,用于在应用程序中对线程状态进行控制和管理。
线程的调度
线程调用的意义在于jvm应对运行的多个线程进行系统级的协调,以避免多个线程争用有限资源而导致应用系统死机或者崩溃。
为了线程对于操作系统和用户的重要性区分开,java定义了线程的优先级策略。java将线程的优先级分为10个等级,分别用1-10之间的数字表示。数字越大表明线程的级别越高。相应地,在thread类中定义了表示线程最低、最高和普通优先级的成员变量min_priority、max_priority和normal_priority,代表的优先级等级分别为1、10和5。当一个线程对象被创建时,其默认的线程优先级是5。
为了控制线程的运行策略,java定义了线程调度器来监控系统中处于就绪状态的所有线程。线程调度器按照线程的优先级决定那个线程投入处理器运行。在多个线程处于就绪状态的条件下,具有高优先级的线程会在低优先级线程之前得到执行。线程调度器同样采用'抢占式'策略来调度线程执行,即当前线程执行过程中有较高优先级的线程进入就绪状态,则高优先级的线程立即被调度执行。具有相同优先级的所有线程采用轮转的方式来共同分配cpu时间片。
在应用程序中设置线程优先级的方法很简单,在创建线程对象之后可以调用线程对象的setpriority方法改变该线程的运行优先级,同样可以调用getpriority方法获取当前线程的优先级。
在java中比较特殊的线程是被称为守护(daemon)线程的低级别线程。这个线程具有最低的优先级,用于为系统中的其它对象和线程提供服务。将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setdaemon方法。典型的守护线程例子是jvm中的系统资源自动回收线程,它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
线程分组管理
java定义了在多线程运行系统中的线程组(threadgroup)对象,用于实现按照特定功能对线程进行集中式分组管理。用户创建的每个线程均属于某线程组,这个线程组可以在线程创建时指定,也可以不指定线程组以使该线程处于默认的线程组之中。但是,一旦线程加入某线程组,该线程就一直存在于该线程组中直至线程死亡,不能在中途改变线程所属的线程组。
当java的application应用程序运行时,jvm创建名称为main的线程组。除非单独指定,在该应用程序中创建的线程均属于main线程组。在main线程组中可以创建其它名称的线程组并将其它线程加入到该线程组中,依此类推,构成线程和线程组之间的树型管理和继承关系。
与线程类似,可以针对线程组对象进行线程组的调度、状态管理以及优先级设置等。在对线程组进行管理过程中,加入到某线程组中的所有线程均被看作统一的对象。
六、小结:
本文针对java平台中线程的性质和应用程序的多线程策略进行了分析和讲解。
与其它操作系统环境不同,java运行环境中的线程类似于多用户、多任务操作系统环境下的进程,但在进程和线程的运行及创建方式等方面,进程与java线程具有明显区别。
unix操作系统环境下,应用程序可以利用fork函数创建子进程,但子进程与该应用程序进程拥有独立的地址空间、系统资源和代码执行单元,并且进程的调度是由操作系统来完成的,使得在应用进程之间进行通信和线程协调相对复杂。而java应用程序中的多线程则是共享同一应用系统资源的多个并行代码执行体,线程之间的通信和协调方法相对简单。
可以说:java语言对应用程序多线程能力的支持增强了java作为网络程序设计语言的优势,为实现分布式应用系统中多客户端的并发访问以及提高服务器的响应效率奠定坚实基础。