苹果的成长日记

我还是个青苹果呀!

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  57 随笔 :: 0 文章 :: 74 评论 :: 0 Trackbacks
JAVA教程:解析Java的多线程机制
http://dev.21tx.com 2005年05月03日 赛迪网

一、进程与应用程序的区别
  
  进程(Process)是最初定义在Unix等多用户、多任务操作系统环境下用于表示应用程序在内存环境中基本执行单元的概念。以Unix操作系统为例,进程是Unix操作系统环境中的基本成分、是系统资源分配的基本单位。Unix操作系统中完成的几乎所有用户管理和资源分配等工作都是通过操作系统对应用程序进程的控制来实现的。
  
  C、C++、Java等语言编写的源程序经相应的编译器编译成可执行文件后,提交给计算机处理器运行。这时,处在可执行状态中的应用程序称为进程。从用户角度来看,进程是应用程序的一个执行过程。从操作系统核心角度来看,进程代表的是操作系统分配的内存、CPU时间片等资源的基本单位,是为正在运行的程序提供的运行环境。进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中,而进程则是处于动态条件下由操作系统维护的系统资源管理实体。多任务环境下应用程序进程的主要特点包括:
  
  ●进程在执行过程中有内存单元的初始入口点,并且进程存活过程中始终拥有独立的内存地址空间;
  
  ●进程的生存期状态包括创建、就绪、运行、阻塞和死亡等类型;
  
  ●从应用程序进程在执行过程中向CPU发出的运行指令形式不同,可以将进程的状态分为用户态和核心态。处于用户态下的进程执行的是应用程序指令、处于核心态下的应用程序进程执行的是操作系统指令。
  
  在Unix操作系统启动过程中,系统自动创建swapper、init等系统进程,用于管理内存资源以及对用户进程进行调度等。在Unix环境下无论是由操作系统创建的进程还要由应用程序执行创建的进程,均拥有唯一的进程标识(PID)。

二、进程与Java线程的区别
  
  
  应用程序在执行过程中存在一个内存空间的初始入口点地址、一个程序执行过程中的代码执行序列以及用于标识进程结束的内存出口点地址,在进程执行过程中的每一时间点均有唯一的处理器指令与内存单元地址相对应。
  
  Java语言中定义的线程(Thread)同样包括一个内存入口点地址、一个出口点地址以及能够顺序执行的代码序列。但是进程与线程的重要区别在于线程不能够单独执行,它必须运行在处于活动状态的应用程序进程中,因此可以定义线程是程序内部的具有并发性的顺序代码流。
  
  Unix操作系统和Microsoft Windows操作系统支持多用户、多进程的并发执行,而Java语言支持应用程序进程内部的多个执行线程的并发执行。多线程的意义在于一个应用程序的多个逻辑单元可以并发地执行。但是多线程并不意味着多个用户进程在执行,操作系统也不把每个线程作为独立的进程来分配独立的系统资源。进程可以创建其子进程,子进程与父进程拥有不同的可执行代码和数据内存空间。而在用于代表应用程序的进程中多个线程共享数据内存空间,但保持每个线程拥有独立的执行堆栈和程序执行上下文(Context)。
  
  基于上述区别,线程也可以称为轻型进程 (Light Weight Process,LWP)。不同线程间允许任务协作和数据交换,使得在计算机系统资源消耗等方面非常廉价。
  
  线程需要操作系统的支持,不是所有类型的计算机都支持多线程应用程序。Java程序设计语言将线程支持与语言运行环境结合在一起,提供了多任务并发执行的能力。这就好比一个人在处理家务的过程中,将衣服放到洗衣机中自动洗涤后将大米放在电饭锅里,然后开始做菜。等菜做好了,饭熟了同时衣服也洗好了。
  
  需要注意的是:在应用程序中使用多线程不会增加 CPU 的数据处理能力。只有在多CPU 的计算机或者在网络计算体系结构下,将Java程序划分为多个并发执行线程后,同时启动多个线程运行,使不同的线程运行在基于不同处理器的Java虚拟机中,才能提高应用程序的执行效率。

另外,如果应用程序必须等待网络连接或数据库连接等数据吞吐速度相对较慢的资源时,多线程应用程序是非常有利的。基于Internet的应用程序有必要是多线程类型的,例如,当开发要支持大量客户机的服务器端应用程序时,可以将应用程序创建成多线程形式来响应客户端的连接请求,使每个连接用户独占一个客户端连接线程。这样,用户感觉服务器只为连接用户自己服务,从而缩短了服务器的客户端响应时间。
  
  
三、Java语言的多线程程序设计方法
  
  
  利用Java语言实现多线程应用程序的方法很简单。根据多线程应用程序继承或实现对象的不同可以采用两种方式:一种是应用程序的并发运行对象直接继承Java的线程类Thread;另外一种方式是定义并发执行对象实现Runnable接口。
  
  继承Thread类的多线程程序设计方法
  
  Thread 类是JDK中定义的用于控制线程对象的类,在该类中封装了用于进行线程控制的方法。见下面的示例代码:
  
  [code]//Consumer.java
  import java.util.*;
  class Consumer extends Thread
  {
   int nTime;
   String strConsumer;
   public Consumer(int nTime, String strConsumer)
   {
   this.nTime = nTime;
   this.strConsumer = strConsumer;
   }
   public void run()
   {
  while(true)
  {
   try
  {
   System.out.println("Consumer name:"+strConsumer+"\n");
   Thread.sleep(nTime);
   }
  catch(Exception e)
  {
   e.printStackTrace();
   }
  }
   }
  static public void main(String args[])
  {
   Consumer aConsumer = new Consumer (1000, "aConsumer");
   aConsumer.start();
   Consumer bConsumer = new Consumer (2000, "bConsumer");
   bConsumer.start();
   Consumer cConsumer = new Consumer (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
  import java.util.*;
  class Consumer implements Runnable
  {
   … …
  public Consumer(int nTime, String strConsumer){… …}
  public void run(){… …}
  static public void main(String args[])
  {
  Thread aConsumer = new Thread(new Consumer(1000, "aConsumer"));
  aConsumer.start();
  //其它对象实例的运行线程
   //… …
   }
  }
  
  从上述代码可以看出:该类实现了Runnable接口并且在该类中定义了run方法。这种多线程应用程序的实现方式与继承Thread类的多线程应用程序的重要区别在于启动多线程对象的方法设计方法不同。在上述代码中,通过创建Thread对象实例并且将应用对象作为创建Thread类实例的参数。

四、线程间的同步
  
  Java应用程序的多个线程共享同一进程的数据资源,多个用户线程在并发运行过程中可能同时访问具有敏感性的内容。在Java中定义了线程同步的概念,实现对共享资源的一致性维护。下面以笔者最近开发的移动通信计费系统中线程间同步控制方法,说明Java语言中多线程同步方式的实现过程。
  
  在没有多线程同步控制策略条件下的客户账户类定义框架代码如下所示:
  
  public class RegisterAccount
  {
  float fBalance;
  //客户缴费方法
  public void deposit(float fFees){ fBalance += fFees; }
  //通话计费方法
  public void withdraw(float fFees){ fBalance -= fFees; }
  … …
  }

  
  
  
  
  读者也许会认为:上述程序代码完全能够满足计费系统实际的需要。确实,在单线程环境下该程序确实是可靠的。但是,多进程并发运行的情况是怎样的呢?假设发生这种情况:客户在客户服务中心进行缴费的同时正在利用移动通信设备进行通话,客户通话结束时计费系统启动计费进程,而同时服务中心的工作人员也提交缴费进程运行。读者可以看到如果发生这种情况,对客户账户的处理是不严肃的。
  
  如何解决这种问题呢?很简单,在RegisterAccount类方法定义中加上用于标识同步方法的关键字synchronized。这样,在同步方法执行过程中该方法涉及的共享资源(在上述代码中为fBalance成员变量)将被加上共享锁以确保在方法运行期间只有该方法能够对共享资源进行访问,直到该方法的线程运行结束打开共享锁,其它线程才能够访问这些共享资源。在共享锁没有打开的时候其它访问共享资源的线程处于阻塞状态。
  
  进行线程同步策略控制后的RegisterAccount类定义如下面代码所示:
  
  public class RegisterAccount
  {
  float fBalance;
  public synchronized void deposit(float fFees){ fBalance += fFees; }
  public synchronized void withdraw(float fFees){ fBalance -= fFees; }
  … …
  }

  
  从经过线程同步机制定义后的代码形式可以看出:在对共享资源进行访问的方法访问属性关键字(public)后附加同步定义关键字synchronized,使得同步方法在对共享资源访问的时候,为这些敏感资源附加共享锁来控制方法执行期间的资源独占性,实现了应用系统数据资源的一致性管理和维护。


五、 Java线程的管理
  
  
  线程的状态控制
  
  在这里需要明确的是:无论采用继承Thread类还是实现Runnable接口来实现应用程序的多线程能力,都需要在该类中定义用于完成实际功能的run方法,这个run方法称为线程体(Thread Body)。按照线程体在计算机系统内存中的状态不同,可以将线程分为创建、就绪、运行、睡眠、挂起和死亡等类型。这些线程状态类型下线程的特征为:
  
  创建状态:当利用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作为网络程序设计语言的优势,为实现分布式应用系统中多客户端的并发访问以及提高服务器的响应效率奠定坚实基础。
posted on 2005-06-24 09:43 苹果 阅读(491) 评论(1)  编辑  收藏 所属分类: J2EE/JAVA学习

评论

# re: 【转载至赛迪网】解析java的多线程机制 2005-06-24 09:50 苹果
一般来说,我们把正在计算机中执行的程序叫做"进程"(Process) ,而不将其
称为程序(Program)。所谓"线程"(Thread),是"进程"中某个单一顺序的控制流。
新兴的操作系统,如Mac,Windows NT,Windows 95等,大多采用多线程的概念,把线
程视为基本执行单位。线程也是Java中的相当重要的组成部分之一。

  甚至最简单的Applet也是由多个线程来完成的。在Java中,任何一个Applet的
paint()和update()方法都是由AWT(Abstract Window Toolkit)绘图与事件处理线
程调用的,而Applet 主要的里程碑方法——init(),start(),stop()和destory()
——是由执行该Applet的应用调用的。

  单线程的概念没有什么新的地方,真正有趣的是在一个程序中同时使用多个线
程来完成不同的任务。某些地方用轻量进程(Lightweig ht Process)来代替线程
,线程与真正进程的相似性在于它们都是单一顺序控制流。然而线程被认为轻量是
由于它运行于整个程序的上下文内,能使用整个程序共有的资源和程序环境。

  作为单一顺序控制流,在运行的程序内线程必须拥有一些资源作为必要的开销
。例如,必须有执行堆栈和程序计数器。在线程内执行的代码只在它的上下文中起
作用,因此某些地方用"执行上下文"来代替"线程"。

  2.线程属性

  为了正确有效地使用线程,必须理解线程的各个方面并了解Java 实时系统。
必须知道如何提供线程体、线程的生命周期、实时系统如 何调度线程、线程组、
什么是幽灵线程(Demo nThread)。

  (1)线程体
  所有的操作都发生在线程体中,在Java中线程体是从Thread类继承的run()方
法,或实现Runnable接口的类中的run()方法。当线程产生并初始化后,实时系统调
用它的run()方法。run()方法内的代码实现所产生线程的行为,它是线程的主要部
分。

  (2)线程状态
  附图表示了线程在它的生命周期内的任何时刻所能处的状态以及引起状态改
变的方法。这图并不是完整的有限状态图,但基本概括了线程中比较感兴趣和普遍
的方面。以下讨论有关线程生命周期以此为据。


  ●新线程态(New Thread)
  产生一个Thread对象就生成一个新线程。当线程处于"新线程"状态时,仅仅是
一个空线程对象,它还没有分配到系统资源。因此只能启动或终止它。任何其他操
作都会引发异常。
  ●可运行态(Runnable)
  start()方法产生运行线程所必须的资源,调度线程执行,并且调用线程的run
()方法。在这时线程处于可运行态。该状态不称为运行态是因为这时的线程并不
总是一直占用处理机。特别是对于只有一个处理机的PC而言,任何时刻只能有一个
处于可运行态的线程占用处理 机。Java通过调度来实现多线程对处理机的共享。

  ●非运行态(Not Runnable)
  当以下事件发生时,线程进入非运行态。
  ①suspend()方法被调用;
  ②sleep()方法被调用;
  ③线程使用wait()来等待条件变量;
  ④线程处于I/O等待。
  ●死亡态(Dead)
  当run()方法返回,或别的线程调用stop()方法,线程进入死亡态 。通常Appl
et使用它的stop()方法来终止它产生的所有线程。

  (3)线程优先级
  虽然我们说线程是并发运行的。然而事实常常并非如此。正如前面谈到的,当
系统中只有一个CPU时,以某种顺序在单CPU情况下执行多线程被称为调度(schedu
ling)。Java采用的是一种简单、固定的调度法,即固定优先级调度。这种算法是
根据处于可运行态线程的相对优先级来实行调度。当线程产生时,它继承原线程的
优先级。在需要时可对优先级进行修改。在任何时刻,如果有多条线程等待运行,
系统选择优先级最高的可运行线程运行。只有当它停止、自动放弃、或由于某种
原因成为非运行态低优先级的线程才能运行。如果两个线程具有相同的优先级,它
们将被交替地运行。
  Java实时系统的线程调度算法还是强制性的,在任何时刻,如果一个比其他线
程优先级都高的线程的状态变为可运行态,实时系统将选择该线程来运行。

  (4)幽灵线程
  任何一个Java线程都能成为幽灵线程。它是作为运行于同一个进程内的对象
和线程的服务提供者。例如,HotJava浏览器有一个称为" 后台图片阅读器"的幽灵
线程,它为需要图片的对象和线程从文件系统或网络读入图片。
  幽灵线程是应用中典型的独立线程。它为同一应用中的其他对象和线程提供
服务。幽灵线程的run()方法一般都是无限循环,等待服务请求。

  (5)线程组
  每个Java线程都是某个线程组的成员。线程组提供一种机制,使得多个线程集
于一个对象内,能对它们实行整体操作。譬如,你能用一个方法调用来启动或挂起
组内的所有线程。Java线程组由ThreadGroup类实现。
  当线程产生时,可以指定线程组或由实时系统将其放入某个缺省的线程组内。
线程只能属于一个线程组,并且当线程产生后不能改变它所属的线程组。

  3.多线程程序

  对于多线程的好处这就不多说了。但是,它同样也带来了某些新的麻烦。只要
在设计程序时特别小心留意,克服这些麻烦并不算太困难。

  (1)同步线程
  许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态。这就
需要同步机制。在Java中每个对象都有一把锁与之对应。但Java不提供单独的lo
ck和unlock操作。它由高层的结构隐式实现, 来保证操作的对应。(然而,我们注
意到Java虚拟机提供单独的monito renter和monitorexit指令来实现lock和unlo
ck操作。)
  synchronized语句计算一个对象引用,试图对该对象完成锁操作, 并且在完成
锁操作前停止处理。当锁操作完成synchronized语句体得到执行。当语句体执行
完毕(无论正常或异常),解锁操作自动完成。作为面向对象的语言,synchronized
经常与方法连用。一种比较好的办法是,如果某个变量由一个线程赋值并由别的线
程引用或赋值,那么所有对该变量的访问都必须在某个synchromized语句或synch
ronized方法内。
  现在假设一种情况:线程1与线程2都要访问某个数据区,并且要求线程1的访
问先于线程2, 则这时仅用synchronized是不能解决问题的。这在Unix或Windows
NT中可用Simaphore来实现。而Java并不提供。在Java中提供的是wait()和noti
fy()机制。使用如下:
  synchronized method-1(…){ call by thread 1.
  ∥access data area;
  available=true;
  notify()
  }
  synchronized method-2(…){∥call by thread 2.
  while(!available)
  try{
  wait();∥wait for notify().
  }catch (Interrupted Exception e){
  }
  ∥access data area
  }
  其中available是类成员变量,置初值为false。
  如果在method-2中检查available为假,则调用wait()。wait()的作用是使线
程2进入非运行态,并且解锁。在这种情况下,method-1可以被线程1调用。当执行
notify()后。线程2由非运行态转变为可运行态。当method-1调用返回后。线程2
可重新对该对象加锁,加锁成功后执行wait()返回后的指令。这种机制也能适用于
其他更复杂的情况。

  (2)死锁
  如果程序中有几个竞争资源的并发线程,那么保证均衡是很重要的。系统均衡
是指每个线程在执行过程中都能充分访问有限的资源。系统中没有饿死和死锁的
线程。Java并不提供对死锁的检测机制。对大多数的Java程序员来说防止死锁是
一种较好的选择。最简单的防止死锁的方法是对竞争的资源引入序号,如果一个线
程需要几个资源,那么它必须先得到小序号的资源,再申请大序号的资源。

  4.小结

  线程是Java中的重要内容,多线程是Java的一个特点。虽然Java的同步互斥不
如某些系统那么丰富,但适当地使用它们也能收到满意的效果。   回复  更多评论
  


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


网站导航: