首页 > 资讯 > 常见问题
写多线程Java应用程序常见问题
发布时间:2008-05-22   浏览次数:1151647
写多线程Java应用程序常见问题 在本文中,我们将探讨使用多线程时遇到的问题,并提出那些常见陷阱的解决方案。   线程是什么?   一个程序或进程能够包含多个线程,这些线程可以根据程序的代码执行相应的指令。多线程看上去似乎在并行执行它们各自的工作,像在一台计算机上运行着多个处理机一样。在多处理机计算机上实现多线程时,它们确实可以并行工作。和进程不同的是,线程共享地址空间。也是说,多个线程能够读写相同的变量或数据结构。   编写多线程程序时,你必须注意每个线程是否干扰了其他线程的工作。可以将程序看作一个办公室,如果不需要共享办公室资源或与其他人交流,所有职员会独立并行地工作。某个职员若要和其他人交谈,当且仅当该职员在“听”且他们两说同样的语言。此外,只有在复印机空闲且处于可用状态(没有仅完成一半的复印工作,没有纸张阻塞等问题)时,职员才能够使用它。在这篇文章中你将看到,在 Java 程序中互相协作的线程好像是在一个组织良好的机构中工作的职员。   在多线程程序中,线程可以从准备绪队列中得到,并在可获得的系统 CPU 上运行。操作系统可以将线程从处理器移到准备绪队列或阻塞队列中,这种情况可以认为是处理器“挂起”了该线程。同样,Java 虚拟机 (JVM) 也可以控制线程的移动 在协作或抢先模型中 从准备绪队列中将进程移到处理器中,于是该线程可以开始执行它的程序代码。   协作式线程模型允许线程自己决定什么时候放弃处理器来等待其他的线程。程序开发员可以地决定某个线程何时会被其他线程挂起,允许它们与对方有效地合作。缺点在于某些恶意或是写得不好的线程会消耗所有可获得的 CPU 时间,导致其他线程“饥饿”。   在抢占式线程模型中,操作系统可以在任何时候打断线程。通常会在它运行了一段时间(是所谓的一个时间片)后才打断它。这样的结果自然是没有线程能够不公平地长时间霸占处理器。然而,随时可能打断线程会给程序开发员带来其他麻烦。同样使用办公室的例子,假设某个职员抢在另一人前使用复印机,但打印工作在未完成的时候离开了,另一人接着使用复印机时,该复印机上可能还有先前那名职员留下来的资料。抢占式线程模型要求线程正确共享资源,协作式模型却要求线程共享执行时间。由于 JVM 规范并没有特别规定线程模型,Java 开发员必须编写可在两种模型上正确运行的程序。在了解线程以及线程间通讯的一些方面之后,我们可以看到如何为这两种模型设计程序。   线程和 Java 语言   为了使用 Java 语言创建线程,你可以生成一个 Thread 类(或其子类)的对象,并给这个对象发送 start() 消息。(程序可以向任何一个派生自 Runnable 接口的类对象发送 start() 消息。)每个线程动作的定义包含在该线程对象的 run() 方法中。run 方法相当于传统程序中的 main() 方法;线程会持续运行,直到 run() 返回为止,此时该线程便死了。   上锁   大多数应用程序要求线程互相通信来同步它们的动作。在 Java 程序中最简单实现同步的方法是上锁。为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。假想给复印机上锁,任一时刻只有一个职员拥有钥匙。若没有钥匙不能使用复印机。给共享变量上锁使得 Java 线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,可以知道没有其他线程能够访问该对象。即使在抢占式模型中,其他线程也不能够访问此对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线程开锁。一旦锁被打开,这些睡眠进程会被唤醒并移到准备绪队列中。   在 Java 编程中,所有的对象都有锁。线程可以使用 synchronized 关键字来获得锁。在任一时刻对于给定的类的实例,方法或同步的代码块只能被一个线程执行。这是因为代码在执行之前要求获得对象的锁。继续我们关于复印机的比喻,为了避免复印冲突,我们可以简单地对复印资源实行同步。如同下列的代码例子,任一时刻只允许一位职员使用复印资源。通过使用方法(在 Copier 对象中)来修改复印机状态。这个方法是同步方法。只有一个线程能够执行一个 Copier 对象中同步代码,因此那些需要使用 Copier 对象的职员必须排队等候。    class CopyMachine {   public synchronized void makeCopies(Document d, int nCopies) {   // only one thread executes this at a time   }   public void loadPaper() {   // multiple threads could access this at once!   synchronized(this) {   // only one thread accesses this at a time   // feel free to use shared resources, overwrite members, etc.   }   }   }正如前面所提到的,在抢占式模型中线程可以在代码的任何一个部分的中间被打断,除非那是一个原子操作代码块。原子操作代码块中的代码段一旦开始执行,要在该线程被换出处理器之前执行完毕。在 Java 编程中,分配一个小于 32 位的变量空间是一种原子操作,而此外象 double 和 long 这两个 64 位数据类型的分配不是原子的。使用锁来正确同步共享资源的访问,足以保证一个多线程程序在抢占式模型下正确工作。   而在协作式模型中,是否能保证线程正常放弃处理器,不掠夺其他线程的执行时间,则完全取决于程序员。调用 yield() 方法能够将当前的线程从处理器中移出到准备绪队列中。另一个方法则是调用 sleep() 方法,使线程放弃处理器,并且在 sleep 方法中指定的时间间隔内睡眠。   正如你所想的那样,将这些方法随意放在代码的某个地方,并不能够保证正常工作。如果线程正拥有一个锁(因为它在一个同步方法或代码块中),则当它调用 yield() 时不能够释放这个锁。这意味着即使这个线程已经被挂起,等待这个锁释放的其他线程依然不能继续运行。为了缓解这个问题,不在同步方法中调用 yield 方法。将那些需要同步的代码包在一个同步块中,里面不含有非同步的方法,并且在这些同步代码块之外才调用 yield。   另外一个解决方法则是调用 wait() 方法,使处理器放弃它当前拥有的对象的锁。如果对象在方法级别上使同步的,这种方法能够很好的工作。因为它仅仅使用了一个锁。如果它使用 fine-grained 锁,则 wait() 将无法放弃这些锁。此外,一个因为调用 wait() 方法而阻塞的线程,只有当其他线程调用 notifyAll() 时才会被唤醒。   线程和AWT/Swing   在那些使用 Swing 和/或 AWT 包创建 GUI (用户图形界面)的 Java 程序中,AWT 事件句柄在它自己的线程中运行。开发员必须注意避免将这些 GUI 线程与较耗时间的计算工作绑在一起,因为这些线程必须负责处理用户时间并重绘用户图形界面。换句话来说,一旦 GUI 线程处于繁忙,整个程序看起来象无响应状态。Swing 线程通过调用合适方法,通知那些 Swing callback (例如 Mouse Listener 和 Action Listener )。 这种方法意味着 listener 无论要做多少事情,都应当利用 listener callback 方法产生其他线程来完成此项工作。目的便在于让 listener callback 更快速返回,从而允许 Swing 线程响应其他事件。   如果一个 Swing 线程不能够同步运行、响应事件并重绘输出,那怎么能够让其他的线程地修改 Swing 的状态?正如上面提到的,Swing callback 在 Swing 线程中运行。因此他们能修改 Swing 数据并绘到屏幕上。   但是如果不是 Swing callback 产生的变化该怎么办呢?使用一个非 Swing 线程来修改 Swing 数据是不的。Swing 提供了两个方法来解决这个问题:invokeLater() 和 invokeAndWait()。为了修改 Swing 状态,只要简单地调用其中一个方法,让 Runnable 的对象来做这些工作。因为 Runnable 对象通常是它们自身的线程,你可能会认为这些对象会作为线程来执行。但那样做其实也是不的。事实上,Swing 会将这些对象放到队列中,并在将来某个时刻执行它的 run 方法。这样才能够修改 Swing 状态。   总结   Java 语言的设计,使得多线程对几乎所有的 Applet 都是必要的。特别是,IO 和 GUI 编程都需要多线程来为用户提供完美的体验。如果依照本文所提到的若干基本规则,并在开始编程前仔细设计系统 包括它对共享资源的访问等,你可以避免许多常见和难以发觉的线程陷阱
业务咨询

Copyright 2003 - 2023 huinet.cn All Rights Reserved.
慧网公司 版权所有