java

位置:IT落伍者 >> java >> 浏览文章

解析Java的多线程机制


发布日期:2023年02月03日
 
解析Java的多线程机制

进程与应用程序的区别

进程(Process)是最初定义在Unix等多用户多任务操作系统环境下用于表示应用程序在内存环境中基本执行单元的概念以Unix操作系统为例进程是Unix操作系统环境中的基本成分是系统资源分配的基本单位Unix操作系统中完成的几乎所有用户管理和资源分配等工作都是通过操作系统对应用程序进程的控制来实现的

CC++Java等语言编写的源程序经相应的编译器编译成可执行文件后提交给计算机处理器运行这时处在可执行状态中的应用程序称为进程从用户角度来看进程是应用程序的一个执行过程从操作系统核心角度来看进程代表的是操作系统分配的内存CPU时间片等资源的基本单位是为正在运行的程序提供的运行环境进程与应用程序的区别在于应用程序作为一个静态文件存储在计算机系统的硬盘等存储空间中而进程则是处于动态条件下由操作系统维护的系统资源管理实体多任务环境下应用程序进程的主要特点包括

进程在执行过程中有内存单元的初始入口点并且进程存活过程中始终拥有独立的内存地址空间

进程的生存期状态包括创建就绪运行阻塞和死亡等类型

从应用程序进程在执行过程中向CPU发出的运行指令形式不同可以将进程的状态分为用户态和核心态处于用户态下的进程执行的是应用程序指令处于核心态下的应用程序进程执行的是操作系统指令

在Unix操作系统启动过程中系统自动创建swapperinit等系统进程用于管理内存资源以及对用户进程进行调度等在Unix环境下无论是由操作系统创建的进程还要由应用程序执行创建的进程均拥有唯一的进程标识(PID)

进程与Java线程的区别

应用程序在执行过程中存在一个内存空间的初始入口点地址一个程序执行过程中的代码执行序列以及用于标识进程结束的内存出口点地址在进程执行过程中的每一时间点均有唯一的处理器指令与内存单元地址相对应

Java语言中定义的线程(Thread)同样包括一个内存入口点地址一个出口点地址以及能够顺序执行的代码序列但是进程与线程的重要区别在于线程不能够单独执行它必须运行在处于活动状态的应用程序进程中因此可以定义线程是程序内部的具有并发性的顺序代码流

Unix操作系统和Microsoft Windows操作系统支持多用户多进程的并发执行而Java语言支持应用程序进程内部的多个执行线程的并发执行多线程的意义在于一个应用程序的多个逻辑单元可以并发地执行但是多线程并不意味着多个用户进程在执行操作系统也不把每个线程作为独立的进程来分配独立的系统资源进程可以创建其子进程子进程与父进程拥有不同的可执行代码和数据内存空间而在用于代表应用程序的进程中多个线程共享数据内存空间但保持每个线程拥有独立的执行堆栈和程序执行上下文(Context)

基于上述区别线程也可以称为轻型进程 (Light Weight ProcessLWP)不同线程间允许任务协作和数据交换使得在计算机系统资源消耗等方面非常廉价

线程需要操作系统的支持不是所有类型的计算机都支持多线程应用程序Java程序设计语言将线程支持与语言运行环境结合在一起提供了多任务并发执行的能力这就好比一个人在处理家务的过程中将衣服放到洗衣机中自动洗涤后将大米放在电饭锅里然后开始做菜等菜做好了饭熟了同时衣服也洗好了

需要注意的是在应用程序中使用多线程不会增加 CPU 的数据处理能力只有在多CPU 的计算机或者在网络计算体系结构下将Java程序划分为多个并发执行线程后同时启动多个线程运行使不同的线程运行在基于不同处理器的Java虚拟机中才能提高应用程序的执行效率

另外如果应用程序必须等待网络连接或数据库连接等数据吞吐速度相对较慢的资源时多线程应用程序是非常有利的基于Internet的应用程序有必要是多线程类型的例如当开发要支持大量客户机的服务器端应用程序时可以将应用程序创建成多线程形式来响应客户端的连接请求使每个连接用户独占一个客户端连接线程这样用户感觉服务器只为连接用户自己服务从而缩短了服务器的客户端响应时间

Java语言的多线程程序设计方法

利用Java语言实现多线程应用程序的方法很简单根据多线程应用程序继承或实现对象的不同可以采用两种方式一种是应用程序的并发运行对象直接继承Java的线程类Thread另外一种方式是定义并发执行对象实现Runnable接口

继承Thread类的多线程程序设计方法

Thread 类是JDK中定义的用于控制线程对象的类在该类中封装了用于进行线程控制的方法见下面的示例代码

  1. //Consumerjava

    import javautil*;

    class Consumer extends Thread

    {

    int nTime;

    String strConsumer;

    public Consumer(int nTime String strConsumer)

    {

    thisnTime = nTime;

    thisstrConsumer = strConsumer;

    }

    public void run()

    {

    while(true)

    {

    try

    {

    Systemoutprintln(Consumer name:+strConsumer+\n);

    Threadsleep(nTime);

    }

    catch(Exception e)

    {

    eprintStackTrace();

    }

    }

    }

    static public void main(String args[])

    {

    Consumer aConsumer = new Consumer ( aConsumer);

    aConsumerstart();

    Consumer bConsumer = new Consumer ( bConsumer);

    bConsumerstart();

    Consumer cConsumer = new Consumer ( cConsumer );

    cConsumerstart();

    }

    }

从上面的程序代码可以看出多线程执行地下Consumer继承Java语言中的线程类Thread并且在main方法中创建了三个Consumer对象的实例当调用对象实例的start方法时自动调用Consumer类中定义的run方法启动对象线程运行线程运行的结果是每间隔nTime时间打印出对象实例中的字符串成员变量strConsumer的内容

可以总结出继承Thread类的多线程程序设计方法是使应用程序类继承Thread类并且在该类的run方法中实现并发性处理过程

实现Runnable接口的多线程程序设计方法

Java语言中提供的另外一种实现多线程应用程序的方法是多线程对象实现Runnable接口并且在该类中定义用于启动线程的run方法这种定义方式的好处在于多线程应用对象可以继承其它对象而不是必须继承Thread类从而能够增加类定义的逻辑性

实现Runnable接口的多线程应用程序框架代码如下所示

file://Consumerjava

import javautil*;

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( aConsumer));

aConsumerstart();

file://其它对象实例的运行线程

file://… …

}

}

从上述代码可以看出该类实现了Runnable接口并且在该类中定义了run方法这种多线程应用程序的实现方式与继承Thread类的多线程应用程序的重要区别在于启动多线程对象的方法设计方法不同在上述代码中通过创建Thread对象实例并且将应用对象作为创建Thread类实例的参数

线程间的同步

Java应用程序的多个线程共享同一进程的数据资源多个用户线程在并发运行过程中可能同时访问具有敏感性的内容在Java中定义了线程同步的概念实现对共享资源的一致性维护下面以笔者最近开发的移动通信计费系统中线程间同步控制方法说明Java语言中多线程同步方式的实现过程

在没有多线程同步控制策略条件下的客户账户类定义框架代码如下所示

public class RegisterAccount

{

float fBalance;

file://客户缴费方法

public void deposit(float fFees){ fBalance += fFees; }

file://通话计费方法

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将线程的优先级分为个等级分别用之间的数字表示数字越大表明线程的级别越高相应地在Thread类中定义了表示线程最低最高和普通优先级的成员变量MIN_PRIORITYMAX_PRIORITY和NORMAL_PRIORITY代表的优先级等级分别为当一个线程对象被创建时其默认的线程优先级是

为了控制线程的运行策略Java定义了线程调度器来监控系统中处于就绪状态的所有线程线程调度器按照线程的优先级决定那个线程投入处理器运行在多个线程处于就绪状态的条件下具有高优先级的线程会在低优先级线程之前得到执行线程调度器同样采用抢占式策略来调度线程执行即当前线程执行过程中有较高优先级的线程进入就绪状态则高优先级的线程立即被调度执行具有相同优先级的所有线程采用轮转的方式来共同分配CPU时间片

在应用程序中设置线程优先级的方法很简单在创建线程对象之后可以调用线程对象的setPriority方法改变该线程的运行优先级同样可以调用getPriority方法获取当前线程的优先级

在Java中比较特殊的线程是被称为守护(Daemon)线程的低级别线程这个线程具有最低的优先级用于为系统中的其它对象和线程提供服务将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon方法典型的守护线程例子是JVM中的系统资源自动回收线程它始终在低级别的状态中运行用于实时监控和管理系统中的可回收资源

线程分组管理

Java定义了在多线程运行系统中的线程组(ThreadGroup)对象用于实现按照特定功能对线程进行集中式分组管理用户创建的每个线程均属于某线程组这个线程组可以在线程创建时指定也可以不指定线程组以使该线程处于默认的线程组之中但是一旦线程加入某线程组该线程就一直存在于该线程组中直至线程死亡不能在中途改变线程所属的线程组

当Java的Application应用程序运行时JVM创建名称为main的线程组除非单独指定在该应用程序中创建的线程均属于main线程组在main线程组中可以创建其它名称的线程组并将其它线程加入到该线程组中依此类推构成线程和线程组之间的树型管理和继承关系

与线程类似可以针对线程组对象进行线程组的调度状态管理以及优先级设置等在对线程组进行管理过程中加入到某线程组中的所有线程均被看作统一的对象

小结

本文针对Java平台中线程的性质和应用程序的多线程策略进行了分析和讲解

与其它操作系统环境不同Java运行环境中的线程类似于多用户多任务操作系统环境下的进程但在进程和线程的运行及创建方式等方面进程与Java线程具有明显区别

Unix操作系统环境下应用程序可以利用fork函数创建子进程但子进程与该应用程序进程拥有独立的地址空间系统资源和代码执行单元并且进程的调度是由操作系统来完成的使得在应用进程之间进行通信和线程协调相对复杂而Java应用程序中的多线程则是共享同一应用系统资源的多个并行代码执行体线程之间的通信和协调方法相对简单

可以说Java语言对应用程序多线程能力的支持增强了Java作为网络程序设计语言的优势为实现分布式应用系统中多客户端的并发访问以及提高服务器的响应效率奠定坚实基础

               

上一篇:Java 6中的线程优化真的有效么?[4]

下一篇:Java程序中的多线程