随笔 - 303  文章 - 883  trackbacks - 0
<2007年8月>
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

欢迎光临! 
闲聊 QQ:1074961813

随笔分类(357)

我管理的群

公共blog

  • n维空间
  • Email : java3d@126.com 群 : 12999758

参与管理的论坛

好友的blog

我的其他blog

朋友的网站

搜索

  •  

最新评论

编写多线程的 Java 应用程序

如何避免当前编程中最常见的问题

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项



级别: 初级

Alex Roetter (aroetter@CS.Stanford.edu), Teton Data Systems 的软件工程师

2001 年 2 月 01 日

Java Thread API 允许程序员编写具有多处理机制优点的应用程序,在后台处理任务的同时保持用户所需的交互感。Alex Roetter 介绍了 Java Thread API,并概述多线程可能引起的问题以及常见问题的解决方案。

几乎所有使用 AWT 或 Swing 编写的画图程序都需要多线程。但多线程程序会造成许多困难,刚开始编程的开发者常常会发现他们被一些问题所折磨,例如不正确的程序行为或死锁。

在本文中,我们将探讨使用多线程时遇到的问题,并提出那些常见陷阱的解决方案。

线程是什么?

一个程序或进程能够包含多个线程,这些线程可以根据程序的代码执行相应的指令。多线程看上去似乎在并行执行它们各自的工作,就像在一台计算机上运行着多个处理机一样。在多处理机计算机上实现多线程时,它们确实 可以并行工作。和进程不同的是,线程共享地址空间。也就是说,多个线程能够读写相同的变量或数据结构。

编写多线程程序时,你必须注意每个线程是否干扰了其他线程的工作。可以将程序看作一个办公室,如果不需要共享办公室资源或与其他人交流,所有职员就会独立并行地工作。某个职员若要和其他人交谈,当且仅当该职员在“听”且他们两说同样的语言。此外,只有在复印机空闲且处于可用状态(没有仅完成一半的复印工作,没有纸张阻塞等问题)时,职员才能够使用它。在这篇文章中你将看到,在 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.
                        }
                        }
                        }
                        

Fine-grain 锁
在对象级使用锁通常是一种比较粗糙的方法。为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:

class FineGrainLock {
                        MyMemberClass x, y;
                        Object xlock = new Object(), ylock = new Object();
                        public void foo() {
                        synchronized(xlock) {
                        //access x here
                        }
                        //do something here - but don't use shared resources
                        synchronized(ylock) {
                        //access y here
                        }
                        }
                        public void bar() {
                        synchronized(this) {
                        //access both x and y here
                        }
                        //do something here - but don't use shared resources
                        }
                        }
                        

若为了在方法级上同步,不能将整个方法声明为 synchronized 关键字。它们使用的是成员锁,而不是 synchronized 方法能够获得的对象级锁。





回页首


信号量

通常情况下,可能有多个线程需要访问数目很少的资源。假想在服务器上运行着若干个回答客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。你要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程?一种控制访问一组资源的方法(除了简单地上锁之外),就是使用众所周知的信号量计数 (counting semaphore)。 信号量计数将一组可获得资源的管理封装起来。信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器。例如我们可以将一个信号量初始化为可获得的数据库连接个数。一旦某个线程获得了信号量,可获得的数据库连接数减一。线程消耗完资源并释放该资源时,计数器就会加一。当信号量控制的所有资源都已被占用时,若有线程试图访问此信号量,则会进入阻塞状态,直到有可用资源被释放。

信号量最常见的用法是解决“消费者-生产者问题”。当一个线程进行工作时,若另外一个线程访问同一共享变量,就可能产生此问题。消费者线程只能在生产者线程完成生产后才能够访问数据。使用信号量来解决这个问题,就需要创建一个初始化为零的信号量,从而让消费者线程访问此信号量时发生阻塞。每当完成单位工作时,生产者线程就会向该信号量发信号(释放资源)。每当消费者线程消费了单位生产结果并需要新的数据单元时,它就会试图再次获取信号量。因此信号量的值就总是等于生产完毕可供消费的数据单元数。这种方法比采用消费者线程不停检查是否有可用数据单元的方法要高效得多。因为消费者线程醒来后,倘若没有找到可用的数据单元,就会再度进入睡眠状态,这样的操作系统开销是非常昂贵的。

尽管信号量并未直接被 Java 语言所支持,却很容易在给对象上锁的基础上实现。一个简单的实现方法如下所示:

class Semaphore {
                        private int count;
                        public Semaphore(int n) {
                        this.count = n;
                        }
                        public synchronized void acquire() {
                        while(count == 0) {
                        try {
                        wait();
                        } catch (InterruptedException e) {
                        //keep trying
                        }
                        }
                        count--;
                        }
                        public synchronized void release() {
                        count++;
                        notify(); //alert a thread that's blocking on this semaphore
                        }
                        }
                        





回页首


常见的上锁问题

不幸的是,使用上锁会带来其他问题。让我们来看一些常见问题以及相应的解决方法:

  • 死锁。 死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。假如线程 "A" 获得了刀,而线程 "B" 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。虽然要探测或推敲各种情况是非常困难的,但只要按照下面几条规则去设计系统,就能够避免死锁问题:
    • 让所有的线程按照同样的顺序获得一组锁。这种方法消除了 X 和 Y 的拥有者分别等待对方的资源的问题。

    • 将多个锁组成一组并放到同一个锁下。前面死锁的例子中,可以创建一个银器对象的锁。于是在获得刀或叉之前都必须获得这个银器的锁。

    • 将那些不会阻塞的可获得资源用变量标志出来。当某个线程获得银器对象的锁时,就可以通过检查变量来判断是否整个银器集合中的对象锁都可获得。如果是,它就可以获得相关的锁,否则,就要释放掉银器这个锁并稍后再尝试。

    • 最重要的是,在编写代码前认真仔细地设计整个系统。多线程是困难的,在开始编程之前详细设计系统能够帮助你避免难以发现死锁的问题。

  • Volatile 变量. volatile 关键字是 Java 语言为优化编译器设计的。以下面的代码为例:
    class VolatileTest {
                                public void foo() {
                                boolean flag = false;
                                if(flag) {
                                //this could happen
                                }
                                }
                                }
                                

    一个优化的编译器可能会判断出 if 部分的语句永远不会被执行,就根本不会编译这部分的代码。如果这个类被多线程访问, flag 被前面某个线程设置之后,在它被 if 语句测试之前,可以被其他线程重新设置。用 volatile 关键字来声明变量,就可以告诉编译器在编译的时候,不需要通过预测变量值来优化这部分的代码。

  • 无法访问的线程 有时候虽然获取对象锁没有问题,线程依然有可能进入阻塞状态。在 Java 编程中 IO 就是这类问题最好的例子。当线程因为对象内的 IO 调用而阻塞时,此对象应当仍能被其他线程访问。该对象通常有责任取消这个阻塞的 IO 操作。造成阻塞调用的线程常常会令同步任务失败。如果该对象的其他方法也是同步的,当线程被阻塞时,此对象也就相当于被冷冻住了。其他的线程由于不能获得对象的锁,就不能给此对象发消息(例如,取消 IO 操作)。必须确保不在同步代码中包含那些阻塞调用,或确认在一个用同步阻塞代码的对象中存在非同步方法。尽管这种方法需要花费一些注意力来保证结果代码安全运行,但它允许在拥有对象的线程发生阻塞后,该对象仍能够响应其他线程。




回页首


为不同的线程模型进行设计

判断是抢占式还是协作式的线程模型,取决于虚拟机的实现者,并根据各种实现而不同。因此,Java 开发员必须编写那些能够在两种模型上工作的程序。

正如前面所提到的,在抢占式模型中线程可以在代码的任何一个部分的中间被打断,除非那是一个原子操作代码块。原子操作代码块中的代码段一旦开始执行,就要在该线程被换出处理器之前执行完毕。在 Java 编程中,分配一个小于 32 位的变量空间是一种原子操作,而此外象 doublelong 这两个 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 编程都需要多线程来为用户提供完美的体验。如果依照本文所提到的若干基本规则,并在开始编程前仔细设计系统――包括它对共享资源的访问等,你就可以避免许多常见和难以发觉的线程陷阱。



参考资料



关于作者

 

Alex Roetter 已经有数年关于用 Java 以及其他编程语言编写多线程应用程序的经验,在斯坦福大学获得了计算机科学学士学位。你可以通过 aroetter@CS.Stanford.edu 与 Alex 联系。

如果我是国王:关于解决 Java编程语言线程问题的建议

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项



级别: 初级

Allen Holub自由撰稿人

2000 年 10 月 01 日

Allen Holub 指出,Java 编程语言的线程模型可能是此语言中最薄弱的部分。它完全不适合实际复杂程序的要求,而且也完全不是面向对象的。本文建议对 Java 语言进行重大修改和补充,以解决这些问题。

Java 语言的线程模型是此语言的一个最难另人满意的部分。尽管 Java 语言本身就支持线程编程是件好事,但是它对线程的语法和类包的支持太少,只能适用于极小型的应用环境。

关于 Java 线程编程的大多数书籍都长篇累牍地指出了 Java 线程模型的缺陷,并提供了解决这些问题的急救包(Band-Aid/邦迪创可贴)类库。我称这些类为急救包,是因为它们所能解决的问题本应是由 Java 语言本身语法所包含的。从长远来看,以语法而不是类库方法,将能产生更高效的代码。这是因为编译器和 Java 虚拟器 (JVM) 能一同优化程序代码,而这些优化对于类库中的代码是很难或无法实现的。

在我的《 Taming Java Threads》(请参阅 参考资料 )书中以及本文中,我进一步建议对 Java 编程语言本身进行一些修改,以使得它能够真正解决这些线程编程的问题。本文和我这本书的主要区别是,我在撰写本文时进行了更多的思考, 所以对书中的提议加以了提高。这些建议只是尝试性的 -- 只是我个人对这些问题的想法,而且实现这些想法需要进行大量的工作以及同行们的评价。但这是毕竟是一个开端,我有意为解决这些问题成立一个专门的工作组,如果您感兴趣,请发 e-mail 到 threading@holub.com。一旦我真正着手进行,我就会给您发通知。

这里提出的建议是非常大胆的。有些人建议对 Java 语言规范 (JLS)(请参阅 参考资料 )进行细微和少量的修改以解决当前模糊的 JVM 行为,但是我却想对其进行更为彻底的改进。

在实际草稿中,我的许多建议包括为此语言引入新的关键字。虽然通常要求不要突破一个语言的现有代码是正确的,但是如果该语言的并不是要保持不变以至于过时的话,它就必须能引入新的关键字。为了使引入的关键字与现有的标识符不产生冲突,经过细心考虑,我将使用一个 ($) 字符,而这个字符在现有的标识符中是非法的。(例如,使用 $task,而不是 task)。此时需要编译器的命令行开关提供支持,能使用这些关键字的变体,而不是忽略这个美元符号。

task(任务)的概念

Java 线程模型的根本问题是它完全不是面向对象的。面向对象 (OO) 设计人员根本不按线程角度考虑问题;他们考虑的是 同步信息 异步 信息(同步信息被立即处理 -- 直到信息处理完成才返回消息句柄;异步信息收到后将在后台处理一段时间 -- 而早在信息处理结束前就返回消息句柄)。Java 编程语言中的 Toolkit.getImage() 方法就是异步信息的一个好例子。 getImage() 的消息句柄将被立即返回,而不必等到整个图像被后台线程取回。

这是面向对象 (OO) 的处理方法。但是,如前所述,Java 的线程模型是非面向对象的。一个 Java 编程语言线程实际上只是一个 run() 过程,它调用了其它的过程。在这里就根本没有对象、异步或同步信息以及其它概念。

对于此问题,在我的书中深入讨论过的一个解决方法是,使用一个 Active_object。 active 对象是可以接收异步请求的对象,它在接收到请求后的一段时间内以后台方式得以处理。在 Java 编程语言中,一个请求可被封装在一个对象中。例如,你可以把一个通过 Runnable 接口实现的实例传送给此 active 对象,该接口的 run() 方法封装了需要完成的工作。该 runnable 对象被此 active 对象排入到队列中,当轮到它执行时,active 对象使用一个后台线程来执行它。

在一个 active 对象上运行的异步信息实际上是同步的,因为它们被一个单一的服务线程按顺序从队列中取出并执行。因此,使用一个 active 对象以一种更为过程化的模型可以消除大多数的同步问题。

在某种意义上,Java 编程语言的整个 Swing/AWT 子系统是一个 active 对象。向一个 Swing 队列传送一条讯息的唯一安全的途径是,调用一个类似 SwingUtilities.invokeLater() 的方法,这样就在 Swing 事件队列上发送了一个 runnable 对象,当轮到它执行时, Swing 事件处理线程将会处理它。

那么我的第一个建议是,向 Java 编程语言中加入一个 task (任务)的概念,从而将active 对象集成到语言中。( task的概念是从 Intel 的 RMX 操作系统和 Ada 编程语言借鉴过来的。大多数实时操作系统都支持类似的概念。)

一个任务有一个内置的 active 对象分发程序,并自动管理那些处理异步信息的全部机制。

定义一个任务和定义一个类基本相同,不同的只是需要在任务的方法前加一个 asynchronous 修饰符来指示 active 对象的分配程序在后台处理这些方法。请参考我的书中第九章的基于类方法,再看以下的 file_io 类,它使用了在《 Taming Java Threads 》中所讨论的 Active_object 类来实现异步写操作:

                        interface Exception_handler
                        {   void handle_exception( Throwable e ); }
                        class File_io_task {   Active_object dispatcher = new Active_object();
                        final OutputStream      file;
                        final Exception_handler handler;
                        File_io_task( String file_name, Exception_handler handler )
                        throws IOException
                        {   file = new FileOutputStream( file_name );
                        this.handler = handler;
                        }
                        public void write( final byte[] bytes ) {
                        // The following call asks the active-object dispatcher
                        // to enqueue the Runnable object on its request
                        // queue. A thread associated with the active object
                        // dequeues the runnable objects and executes them
                        // one at a time.
                        dispatcher.dispatch
                        (   new Runnable()
                        {   public void run() {
                        try
                        {   byte[] copy new byte[ bytes.length ];
                        System.arrayCopy(   bytes,  0,
                        copy,   0,
                        bytes.length );
                        file.write( copy );
                        }
                        catch( Throwable problem )
                        {   handler.handle_exception( problem );
                        }
                        }
                        }
                        );
                        }
                        }
                        

所有的写请求都用一个 dispatch() 过程调用被放在 active-object 的输入队列中排队。在后台处理这些异步信息时出现的任何异常 (exception) 都由 Exception_handler 对象处理,此 Exception_handler 对象被传送到 File_io_task 的构造函数中。您要写内容到文件时,代码如下:

   File_io_task io =   new File_io_task
                        ( "foo.txt"
                        new Exception_handler
                        {   public void handle( Throwable e ) {   e.printStackTrace();
                        }
                        }
                        );
                        //...
                        io.write( some_bytes );
                        

这种基于类的处理方法,其主要问题是太复杂了 -- 对于一个这样简单的操作,代码太杂了。向 Java 语言引入 $task$asynchronous 关键字后,就可以按下面这样重写以前的代码:

   $task File_io $error{ $.printStackTrace(); }
                        {
                        OutputStream file;
                        File_io( String file_name ) throws IOException
                        {   file = new FileOutputStream( file_name );
                        }
                        asynchronous public write( byte[] bytes )
                        {   file.write( bytes );
                        }
                        }
                        

注意,异步方法并没有指定返回值,因为其句柄将被立即返回,而不用等到请求的操作处理完成后。所以,此时没有合理的返回值。对于派生出的模型, $task 关键字和 class 一样同效: $task 可以实现接口、继承类和继承的其它任务。标有 asynchronous 关键字的方法由 $task 在后台处理。其它的方法将同步运行,就像在类中一样。

$task 关键字可以用一个可选的 $error 从句修饰 (如上所示),它表明对任何无法被异步方法本身捕捉的异常将有一个缺省的处理程序。我使用 $ 来代表被抛出的异常对象。如果没有指定 $error 从句,就将打印出一个合理的出错信息(很可能是堆栈跟踪信息)。

注意,为确保线程安全,异步方法的参数必须是不变 (immutable) 的。运行时系统应通过相关语义来保证这种不变性(简单的复制通常是不够的)。

所有的 task 对象必须支持一些伪信息 (pseudo-message),例如:

some_task.close() 在此调用后发送的任何异步信息都产生一个 TaskClosedException 。但是,在 active 对象队列上等候的消息仍能被提供。
some_task.join() 调用程序被阻断,直到此任务关闭、而且所有未完成的请求都被处理完毕。

除了常用的修饰符( public 等), task 关键字还应接受一个 $pooled(n) 修饰符,它导致 task 使用一个线程池,而不是使用单个线程来运行异步请求。 n 指定了所需线程池的大小;必要时,此线程池可以增加,但是当不再需要线程时,它应该缩到原来的大小。伪域 (pseudo-field) $pool_size 返回在 $pooled(n) 中指定的原始 n 参数值。

在《 Taming Java Threads 》的第八章中,我给出了一个服务器端的 socket 处理程序,作为线程池的例子。它是关于使用线程池的任务的一个好例子。其基本思路是产生一个独立对象,它的任务是监控一个服务器端的 socket。每当一个客户机连接到服务器时,服务器端的对象会从池中抓取一个预先创建的睡眠线程,并把此线程设置为服务于客户端连接。socket 服务器会产出一个额外的客户服务线程,但是当连接关闭时,这些额外的线程将被删除。实现 socket 服务器的推荐语法如下:

                        public $pooled(10) $task Client_handler {
                        PrintWriter log = new PrintWriter( System.out );
                        public asynchronous void handle( Socket connection_to_the_client ) {
                        log.println("writing");
                        // client-handling code goes here. Every call to
                        // handle()  is executed on its own thread, but 10
                        // threads are pre-created for this purpose. Additional
                        // threads are created on an as-needed basis, but are
                        // discarded when handle() returns.
                        }
                        }
                        $task Socket_server
                        {
                        ServerSocket server;
                        Client_handler client_handlers = new Client_handler();
                        public Socket_server( int port_number ) {   server = new ServerSocket(port_number);
                        }
                        public $asynchronous listen(Client_handler client) {
                        // This method is executed on its own thread.
                        while( true )
                        {   client_handlers.handle( server.accept() );
                        }
                        }
                        }
                        //...
                        Socket_server = new Socket_server( the_port_number );
                        server.listen()
                        

Socket_server 对象使用一个独立的后台线程处理异步的 listen() 请求,它封装 socket 的“接受”循环。当每个客户端连接时, listen() 请求一个 Client_handler 通过调用 handle() 来处理请求。每个 handle() 请求在它们自己的线程中执行(因为这是一个 $pooled 任务)。

注意,每个传送到 $pooled $task 的异步消息实际上都使用它们自己的线程来处理。典型情况下,由于一个 $pooled $task 用于实现一个自主操作;所以对于解决与访问状态变量有关的潜在的同步问题,最好的解决方法是在 $asynchronous 方法中使用 this 是指向的对象的一个独有副本。这就是说,当向一个 $pooled $task 发送一个异步请求时,将执行一个 clone() 操作,并且此方法的 this 指针会指向此克隆对象。线程之间的通信可通过对 static 区的同步访问实现。





回页首


改进 synchronized

虽然在多数情况下, $task 消除了同步操作的要求,但是不是所有的多线程系统都用任务来实现。所以,还需要改进现有的线程模块。 synchronized 关键字有下列缺点:

  • 无法指定一个超时值。
  • 无法中断一个正在等待请求锁的线程。
  • 无法安全地请求多个锁 。(多个锁只能以依次序获得。)

解决这些问题的办法是:扩展 synchronized 的语法,使它支持多个参数和能接受一个超时说明(在下面的括弧中指定)。下面是我希望的语法:

synchronized(x && y && z) 获得 xyz 对象的锁。
synchronized(x || y || z) 获得 xyz 对象的锁。
synchronized( (x && y ) || z) 对于前面代码的一些扩展。
synchronized(...)[1000] 设置 1 秒超时以获得一个锁。
synchronized[1000] f(){...} 在进入 f() 函数时获得 this 的锁,但可有 1 秒超时。

TimeoutExceptionRuntimeException 派生类,它在等待超时后即被抛出。

超时是需要的,但还不足以使代码强壮。您还需要具备从外部中止请求锁等待的能力。所以,当向一个等待锁的线程传送一个 interrupt() 方法后,此方法应抛出一个 SynchronizationException 对象,并中断等待的线程。这个异常应是 RuntimeException 的一个派生类,这样不必特别处理它。

synchronized 语法这些推荐的更改方法的主要问题是,它们需要在二进制代码级上修改。而目前这些代码使用进入监控(enter-monitor)和退出监控(exit-monitor)指令来实现 synchronized 。而这些指令没有参数,所以需要扩展二进制代码的定义以支持多个锁定请求。但是这种修改不会比在 Java 2 中修改 Java 虚拟机的更轻松,但它是向下兼容现存的 Java 代码。

另一个可解决的问题是最常见的死锁情况,在这种情况下,两个线程都在等待对方完成某个操作。设想下面的一个例子(假设的):

class Broken
                        {   Object lock1 = new Object();
                        Object lock2 = new Object();
                        void a()
                        {   synchronized( lock1 )
                        {   synchronized( lock2 )
                        {   // do something
                        }
                        }
                        }
                        void b()
                        {   synchronized( lock2 )
                        {   synchronized( lock1 )
                        {   // do something
                        }
                        }
                        }
                        

设想一个线程调用 a() ,但在获得  lock1 之后在获得 lock2 之前被剥夺运行权。 第二个线程进入运行,调用 b() ,获得了 lock2 ,但是由于第一个线程占用 lock1 ,所以它无法获得 lock1 ,所以它随后处于等待状态。此时第一个线程被唤醒,它试图获得 lock2 ,但是由于被第二个线程占据,所以无法获得。此时出现死锁。下面的 synchronize-on-multiple-objects 的语法可解决这个问题:

    //...
                        void a()
                        {   synchronized( lock1 && lock2 )
                        {
                        }
                        }
                        void b()
                        {   synchronized( lock2 && lock3 )
                        {
                        }
                        }
                        

编译器(或虚拟机)会重新排列请求锁的顺序,使 lock1 总是被首先获得,这就消除了死锁。

但是,这种方法对多线程不一定总成功,所以得提供一些方法来自动打破死锁。一个简单的办法就是在等待第二个锁时常释放已获得的锁。这就是说,应采取如下的等待方式,而不是永远等待:

    while( true )
                        {   try
                        {   synchronized( some_lock )[10]
                        {   // do the work here.
                        break;
                        }
                        }
                        catch( TimeoutException e )
                        {   continue;
                        }
                        }
                        

如果等待锁的每个程序使用不同的超时值,就可打破死锁而其中一个线程就可运行。我建议用以下的语法来取代前面的代码:

    synchronized( some_lock )[]
                        {   // do the work here.
                        }
                        

synchronized 语句将永远等待,但是它时常会放弃已获得的锁以打破潜在的死锁可能。在理想情况下,每个重复等待的超时值比前一个相差一随机值。





回页首


改进 wait() 和 notify()

wait() / notify() 系统也有一些问题:

  • 无法检测 wait() 是正常返回还是因超时返回。
  • 无法使用传统条件变量来实现处于一个“信号”(signaled)状态。
  • 太容易发生嵌套的监控(monitor)锁定。

超时检测问题可以通过重新定义 wait() 使它返回一个 boolean 变量 (而不是 void ) 来解决。一个 true 返回值指示一个正常返回,而 false 指示因超时返回。

基于状态的条件变量的概念是很重要的。如果此变量被设置成 false 状态,那么等待的线程将要被阻断,直到此变量进入 true 状态;任何等待 true 的条件变量的等待线程会被自动释放。 (在这种情况下, wait() 调用不会发生阻断。)。通过如下扩展 notify() 的语法,可以支持这个功能:

notify(); 释放所有等待的线程,而不改变其下面的条件变量的状态。
notify(true); 把条件变量的状态设置为 true 并释放任何等待的进程。其后对于 wait() 的调用不会发生阻断。
notify(false); 把条件变量的状态设置为 false (其后对于 wait() 的调用会发生阻断)。

嵌套监控锁定问题非常麻烦,我并没有简单的解决办法。嵌套监控锁定是一种死锁形式,当某个锁的占有线程在挂起其自身之前不释放锁时,会发生这种嵌套监控封锁。下面是此问题的一个例子(还是假设的),但是实际的例子是非常多的:

class Stack
                        {
                        LinkedList list = new LinkedList();
                        public synchronized void push(Object x)
                        {   synchronized(list)
                        {   list.addLast( x );
                        notify();
                        }
                        }
                        public synchronized Object pop()
                        {   synchronized(list)
                        {   if( list.size() <= 0 )
                        wait();
                        return list.removeLast();
                        }
                        }
                        }
                        

此例中,在 get()put() 操作中涉及两个锁:一个在 Stack 对象上,另一个在 LinkedList 对象上。下面我们考虑当一个线程试图调用一个空栈的 pop() 操作时的情况。此线程获得这两个锁,然后调用 wait() 释放 Stack 对象上 的锁,但是没有释放在 list 上的锁。如果此时第二个线程试图向堆栈中压入一个对象,它会在 synchronized(list) 语句上永远挂起,而且永远不会被允许压入一个对象。由于第一个线程等待的是一个非空栈,这样就会发生死锁。这就是说,第一个线程永远无法从 wait() 返回,因为由于它占据着锁,而导致第二个线程永远无法运行到 notify() 语句。

在这个例子中,有很多明显的办法来解决问题:例如,对任何的方法都使用同步。但是在真实世界中,解决方法通常不是这么简单。

一个可行的方法是,在 wait() 中按照反顺序释放当前线程获取的 所有 锁,然后当等待条件满足后,重新按原始获取顺序取得它们。但是,我能想象出利用这种方式的代码对于人们来说简直无法理解,所以我认为它不是一个真正可行的方法。如果您有好的方法,请给我发 e-mail。

我也希望能等到下述复杂条件被实现的一天。例如:

(a && (b || c)).wait();
                        

其中 abc 是任意对象。





回页首


修改 Thread 类

同时支持抢占式和协作式线程的能力在某些服务器应用程序中是基本要求,尤其是在想使系统达到最高性能的情况下。我认为 Java 编程语言在简化线程模型上走得太远了,并且 Java 编程语言应支持 Posix/Solaris 的“绿色(green)线程”和“轻便(lightweight)进程”概念(在“( Taming Java Threads ”第一章中讨论)。 这就是说,有些 Java 虚拟机的实现(例如在 NT 上的 Java 虚拟机)应在其内部仿真协作式进程,其它 Java 虚拟机应仿真抢占式线程。而且向 Java 虚拟机加入这些扩展是很容易的。

一个 Java 的 Thread 应始终是抢占式的。这就是说,一个 Java 编程语言的线程应像 Solaris 的轻便进程一样工作。 Runnable 接口可以用于定义一个 Solaris 式的“绿色线程”,此线程必需能把控制权转给运行在相同轻便进程中的其它绿色线程。

例如,目前的语法:

   class My_thread implements Runnable
                        {   public void run(){ /*...*/ }
                        }
                        new Thread( new My_thread );
                        

能有效地为 Runnable 对象产生一个绿色线程,并把它绑定到由 Thread 对象代表的轻便进程中。这种实现对于现有代码是透明的,因为它的有效性和现有的完全一样。

Runnable 对象想成为绿色线程,使用这种方法,只需向 Thread 的构造函数传递几个 Runnable 对象,就可以扩展 Java 编程语言的现有语法,以支持在一个单一轻便线程有多个绿色线程。(绿色线程之间可以相互协作,但是它们可被运行在其它轻便进程 ( Thread 对象) 上的绿色进程( Runnable 对象) 抢占。)。例如,下面的代码会为每个 runnable 对象创建一个绿色线程,这些绿色线程会共享由 Thread 对象代表的轻便进程。

new Thread( new My_runnable_object(), new My_other_runnable_object() );
                        

现有的覆盖(override) Thread 对象并实现 run() 的习惯继续有效,但是它应映射到一个被绑定到一轻便进程的绿色线程。(在 Thread() 类中的缺省 run() 方法会在内部有效地创建第二个 Runnable 对象。)





回页首


线程间的协作

应在语言中加入更多的功能以支持线程间的相互通信。目前, PipedInputStreamPipedOutputStream 类可用于这个目的。但是对于大多数应用程序,它们太弱了。我建议向 Thread 类加入下列函数:

  1. 增加一个 wait_for_start() 方法,它通常处于阻塞状态,直到一个线程的 run() 方法启动。(如果等待的线程在调用 run 之前被释放,这没有什么问题)。用这种方法,一个线程可以创建一个或多个辅助线程,并保证在创建线程继续执行操作之前,这些辅助线程会处于运行状态。
  2. (向 Object 类)增加 $send (Object o)Object=$receive() 方法,它们将使用一个内部阻断队列在线程之间传送对象。阻断队列应作为第一个 $send() 调用的副产品被自动创建。 $send() 调用会把对象加入队列。 $receive() 调用通常处于阻塞状态,直到有一个对象被加入队列,然后它返回此对象。这种方法中的变量应支持设定入队和出队的操作超时能力: $send (Object o, long timeout)$receive (long timeout)。




回页首


对于读写锁的内部支持

读写锁的概念应内置到 Java 编程语言中。读写器锁在“ Taming Java Threads ”(和其它地方)中有详细讨论,概括地说:一个读写锁支持多个线程同时访问一个对象,但是在同一时刻只有一个线程可以修改此对象,并且在访问进行时不能修改。读写锁的语法可以借用 synchronized 关键字:

    static Object global_resource;
                        //...
                        public void a()
                        {
                        $reading( global_resource )
                        {   // While in this block, other threads requesting read
                        // access to global_resource will get it, but threads
                        // requesting write access will block.
                        }
                        }
                        public void b()
                        {
                        $writing( global_resource )
                        {   // Blocks until all ongoing read or write operations on
                        // global_resource are complete. No read or write
                        // operation or global_resource can be initiated while
                        // within this block.
                        }
                        }
                        public $reading void c()
                        {   // just like $reading(this)...
                        }
                        public $writing void d()
                        {   // just like $writing(this)...
                        }
                        

对于一个对象,应该只有在 $writing 块中没有线程时,才支持多个线程进入 $reading 块。在进行读操作时,一个试图进入 $writing 块的线程会被阻断,直到读线程退出 $reading 块。 当有其它线程处于 $writing 块时,试图进入 $reading$writing 块的线程会被阻断,直到此写线程退出 $writing 块。

如果读和写线程都在等待,缺省情况下,读线程会首先进行。但是,可以使用 $writer_priority 属性修改类的定义来改变这种缺省方式。如:

$write_priority class IO
                        {
                        $writing write( byte[] data )
                        {   //...
                        }
                        $reading byte[] read( )
                        {   //...
                        }
                        }
                        





回页首


访问部分创建的对象应是非法的

当前情况下,JLS 允许访问部分创建的对象。例如,在一个构造函数中创建的线程可以访问正被创建的对象,既使此对象没有完全被创建。下面代码的结果无法确定:

    class Broken
                        {   private long x;
                        Broken()
                        {   new Thread()
                        {   public void run()
                        {   x = -1;
                        }
                        }.start();
                        x = 0;
                        }
                        }
                        

设置 x 为 -1 的线程可以和设置 x 为 0 的线程同时进行。所以,此时 x 的值无法预测。

对此问题的一个解决方法是,在构造函数没有返回之前,对于在此构造函数中创建的线程,既使它的优先级比调用 new 的线程高,也要禁止运行它的 run() 方法。

这就是说,在构造函数返回之前, start() 请求必须被推迟。

另外,Java 编程语言应可允许构造函数的同步。换句话说,下面的代码(在当前情况下是非法的)会象预期的那样工作:

    class Illegal
                        {   private long x;
                        synchronized Broken()
                        {   new Thread()
                        {   public void run()
                        {
                        synchronized( Illegal.this )
                        { x = -1;
                        } }
                        }.start();
                        x = 0;
                        }
                        }
                        

我认为第一种方法比第二种更简洁,但实现起来更为困难。





回页首


volatile 关键字应象预期的那样工作

JLS 要求保留对于 volatile 操作的请求。大多数 Java 虚拟机都简单地忽略了这部分内容,这是不应该的。在多处理器的情况下,许多主机都出现了这种问题,但是它本应由 JLS 加以解决的。如果您对这方面感兴趣,马里兰大学的 Bill Pugh 正在致力于这项工作(请参阅 参考资料)。





回页首


访问的问题

如果缺少良好的访问控制,会使线程编程非常困难。大多数情况下,如果能保证线程只从同步子系统中调用,不必考虑线程安全(threadsafe)问题。我建议对 Java 编程语言的访问权限概念做如下限制;

  1. 应精确使用 package 关键字来限制包访问权。我认为当缺省行为的存在是任何一种计算机语言的一个瑕疵,我对现在存在这种缺省权限感到很迷惑(而且这种缺省是“包(package)”级别的而不是“私有(private)”)。在其它方面,Java 编程语言都不提供等同的缺省关键字。虽然使用显式的 package 的限定词会破坏现有代码,但是它将使代码的可读性更强,并能消除整个类的潜在错误 (例如,如果访问权是由于错误被忽略,而不是被故意忽略)。
  2. 重新引入 private protected ,它的功能应和现在的 protected 一样,但是不应允许包级别的访问。
  3. 允许 private private 语法指定“实现的访问”对于所有外部对象是私有的,甚至是当前对象是的同一个类的。对于“.”左边的唯一引用(隐式或显式)应是 this
  4. 扩展 public 的语法,以授权它可制定特定类的访问。例如,下面的代码应允许 Fred 类的对象可调用 some_method() ,但是对其它类的对象,这个方法应是私有的。
        public(Fred) void some_method()
                                {
                                }
                                

  5. 这种建议不同于 C++ 的 "friend" 机制。 在 "friend" 机制中,它授权一个类访问另一个类的 所有私有部分。在这里,我建议对有限的方法集合进行严格控制的访问。用这种方法,一个类可以为另一个类定义一个接口,而这个接口对系统的其余类是不可见的。一个明显的变化是:

        public(Fred, Wilma) void some_method()
                                {
                                }
                                

  6. 除非域引用的是真正不变(immutable)的对象或 static final 基本类型,否则所有域的定义应是 private 。对于一个类中域的直接访问违反了 OO 设计的两个基本规则:抽象和封装。从线程的观点来看,允许直接访问域只使对它进行非同步访问更容易一些。

  7. 增加 $property 关键字。带有此关键字的对象可被一个“bean 盒”应用程序访问,这个程序使用在 Class 类中定义的反射操作(introspection) API,否则与 private private 同效。 $property 属性可用在域和方法,这样现有的 JavaBean getter/setter 方法可以很容易地被定义为属性。

不变性(immutability)

由于对不变对象的访问不需要同步,所以在多线程条件下,不变的概念(一个对象的值在创建后不可更改)是无价的。Java 编程言语中,对于不变性的实现不够严格,有两个原因:

  • 对于一个不变对象,在其被未完全创建之前,可以对它进行访问。这种访问对于某些域可以产生不正确的值。
  • 对于恒定 (类的所有域都是 final) 的定义太松散。对于由 final 引用指定的对象,虽然引用本身不能改变,但是对象本身可以改变状态。

第一个问题可以解决,不允许线程在构造函数中开始执行 (或者在构造函数返回之前不能执行开始请求)。

对于第二个问题,通过限定 final 修饰符指向恒定对象,可以解决此问题。这就是说,对于一个对象,只有所有的域是 final ,并且所有引用的对象的域也都是 final ,此对象才真正是恒定的。为了不打破现有代码,这个定义可以使用编译器加强,即只有一个类被显式标为不变时,此类才是不变类。方法如下:

                        $immutable public class Fred
                        {
                        // all fields in this class must be final, and if the
                        // field is a reference, all fields in the referenced
                        // class must be final as well (recursively).
                        static int x constant = 0;  // use of `final` is optional when $immutable
                        // is present.
                        }
                        

有了 $immutable 修饰符后,在域定义中的 final 修饰符是可选的。

最后,当使用内部类(inner class)后,在 Java 编译器中的一个错误使它无法可靠地创建不变对象。当一个类有重要的内部类时(我的代码常有),编译器经常不正确地显示下列错误信息:

"Blank final variable 'name' may not have been initialized.
                        It must be assigned a value in an initializer, or in every constructor."
                        

既使空的 final 在每个构造函数中都有初始化,还是会出现这个错误信息。自从在 1.1 版本中引入内部类后,编译器中一直有这个错误。在此版本中(三年以后),这个错误依然存在。现在,该是改正这个错误的时候了。

对于类级域的实例级访问

除了访问权限外,还有一个问题,即类级(静态)方法和实例(非静态)方法都能直接访问类级(静态)域。这种访问是非常危险的,因为实例方法的同步不会获取类级的锁,所以一个 synchronized static 方法和一个 synchronized 方法还是能同时访问类的域。改正此问题的一个明显的方法是,要求在实例方法中只有使用 static 访问方法才能访问非不变类的 static 域。当然,这种要求需要编译器和运行时间检查。在这种规定下,下面的代码是非法的:

    class Broken
                        {
                        static long x;
                        synchronized static void f()
                        {   x = 0;
                        }
                        synchronized void g()
                        {   x = -1;
                        }
                        };
                        

由于 f()g() 可以并行运行,所以它们能同时改变 x 的值(产生不定的结果)。请记住,这里有两个锁: static 方法要求属于 Class 对象的锁,而非静态方法要求属于此类实例的锁。当从实例方法中访问非不变 static 域时,编译器应要求满足下面两个结构中的任意一个:

    class Broken
                        {
                        static long x;
                        synchronized private static accessor( long value )
                        {   x = value;
                        } synchronized static void f()
                        {   x = 0;
                        }
                        synchronized void g()
                        {
                        accessor( -1 ); }
                        }
                        

或则,编译器应获得读/写锁的使用:

    class Broken
                        {
                        static long x;
                        synchronized static void f()
                        {   $writing(x){ x = 0 };
                        }
                        synchronized void g()
                        {   $writing(x){ x = -1 };
                        }
                        }
                        

另外一种方法是(这也是一种 理想的 方法)-- 编译器应 自动 使用一个读/写锁来同步访问非不变 static 域,这样,程序员就不必担心这个问题。





回页首


后台线程的突然结束

当所有的非后台线程终止后,后台线程都被突然结束。当后台线程创建了一些全局资源(例如一个数据库连接或一个临时文件),而后台线程结束时这些资源没有被关闭或删除就会导致问题。

对于这个问题,我建议制定规则,使 Java 虚拟机在下列情况下不关闭应用程序:

  1. 有任何非后台线程正在运行,或者:
  2. 有任何后台线程正在执行一个 synchronized 方法或 synchronized 代码块。

后台线程在它执行完 synchronized 块或 synchronized 方法后可被立即关闭。





回页首


重新引入 stop() 、 suspend() 和 resume() 关键字

由于实用原因这也许不可行,但是我希望不要废除 stop() (在 ThreadThreadGroup 中)。但是,我会改变 stop() 的语义,使得调用它时不会破坏已有代码。但是,关于 stop() 的问题,请记住,当线程终止后, stop() 将释放所有锁,这样可能潜在地使正在此对象上工作的线程进入一种不稳定(局部修改)的状态。由于停止的线程已释放它在此对象上的所有锁,所以这些对象无法再被访问。

对于这个问题,可以重新定义 stop() 的行为,使线程只有在不占有任何锁时才立即终止。如果它占据着锁,我建议在此线程释放最后一个锁后才终止它。可以使用一个和抛出异常相似的机制来实现此行为。被停止线程应设置一个标志,并且当退出所有同步块时立即测试此标志。如果设置了此标志,就抛出一个隐式的异常,但是此异常应不再能被捕捉并且当线程结束时不会产生任何输出。注意,微软的 NT 操作系统不能很好地处理一个外部指示的突然停止(abrupt)。(它不把 stop 消息通知动态连接库,所以可能导致系统级的资源漏洞。)这就是我建议使用类似异常的方法简单地导致 run() 返回的原因。

与这种和异常类似的处理方法带来的实际问题是,你必需在每个 synchronized 块后都插入代码来测试“stopped”标志。并且这种附加的代码会降低系统性能并增加代码长度。我想到的另外一个办法是使 stop() 实现一个“延迟的(lazy)”停止,在这种情况下,在下次调用 wait()yield() 时才终止。我还想向 Thread 中加入一个 isStopped()stopped() 方法(此时, Thread 将像 isInterrupted()interrupted() 一样工作,但是会检测 “stop-requested”的状态)。这种方法不向第一种那样通用,但是可行并且不会产生过载。

应把 suspend()resume() 方法放回到 Java 编程语言中,它们是很有用的,我不想被当成是幼儿园的小孩。由于它们可能产生潜在的危险(当被挂起时,一个线程可以占据一个锁)而去掉它们是没有道理的。请让我自己来决定是否使用它们。如果接收的线程正占据着锁,Sun 公司应该把它们作为调用 suspend() 的一个运行时间异常处理(run-time exception);或者更好的方法是,延迟实际的挂起过程,直到线程释放所有的锁。





回页首


被阻断的 I/O 应正确工作

应该能打断任何被阻断的操作,而不是只让它们 wait()sleep() 。我在“ Taming Java Threads ”的第二章中的 socket 部分讨论了此问题。但是现在,对于一个被阻断的 socket 上的 I/O 操作,打断它的唯一办法是关闭这个 socket,而没有办法打断一个被阻断的文件 I/O 操作。例如,一旦开始一个读请求并且进入阻断状态后,除非到它实际读出一些东西,否则线程一直出于阻断状态。既使关掉文件句柄也不能打断读操作。

还有,程序应支持 I/O 操作的超时。所有可能出现阻断操作的对象(例如 InputStream 对象)也都应支持这种方法:

    InputStream s = ...;
                        s.set_timeout( 1000 );
                        

这和 Socket 类的 setSoTimeout(time) 方法是等价的。同样地,应该支持把超时作为参数传递到阻断的调用。





回页首


ThreadGroup 类

ThreadGroup 应该实现 Thread 中能够改变线程状态的所有方法。我特别想让它实现 join() 方法,这样我就可等待组中的所有线程的终止。





回页首


总结

以上是我的建议。就像我在标题中所说的那样,如果我是国王...(哎)。我希望这些改变(或其它等同的方法)最终能被引入 Java 语言中。我确实认为 Java 语言是一种伟大的编程语言;但是我也认为 Java 的线程模型设计得还不够完善,这是一件很可惜的事情。但是,Java 编程语言正在演变,所以还有可提高的前景。

Allen 撰写了八本书籍,最近新出的一本讨论了 Java 线程的陷阱和缺陷《 Taming Java Threads 》。他长期从事设计和编制面向对象软件。从事了 8 年的 C++ 编程工作后,Allen 在 1996 年由 C++ 转向 Java。他现在视 C++ 为一个噩梦,其可怕的经历正被逐渐淡忘。他从 1982 年起就自己和为加利弗尼亚大学伯克利分校教授计算机编程(首先是 C,然后是 C++ 和 MFC,现在是面向对象设计和 Java)。 Allen 也提供 Java 和面向对象设计方面的公众课程和私授 (in-house) 课程。他还提供面向对象设计的咨询并承包 Java 编程项目。请通过此 Web 站点和 Allen 取得联系并获取信息: www.holub.com



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 本文是对 Taming Java Threads 的更新摘编。该书探讨了在 Java 语言中多线程编程的陷阱和问题,并提供了一个与线程相关的 Java 程序包来解决这些问题。

  • 马里兰大学的 Bill Pugh 正在致力修改 JLS 来提高其线程模型。Bill 的提议并不如本文所推荐的那么广,他主要致力于让现有的线程模型以更为合理方式运行。更多信息可从 www.cs.umd.edu/~pugh/java/memoryModel/ 获得。

  • Sun 网站可找到全部 Java 语言的规范。

  • 要从一个纯技术角度来审视线程,参阅 Doug Lea 编著的 Concurrent Programming in Java: Design Principles and Patterns 第二版。这是本很棒的书,但是它的风格是非常学术化的并不一定适合所有的读者。对《 Taming Java Threads》是个很好的补充读物。

  • 由 Scott Oaks 和 Henry Wong 编写的 Java ThreadsTaming Java Threads 要轻量些,但是如果您从未编写过线程程序这本书更为适合。Oaks 和 Wong 同样实现了 Holub 提供的帮助类,而且看看对同一问题的不同解决方案总是有益的。

  • 由 Bill Lewis 和 Daniel J. Berg 编写的 Threads Primer: A Guide to Multithreaded Programming 是对线程(不限于 Java)的很好入门介绍。

  • Java 线程的一些技术信息可在 Sun 网站上找到。

  • "Multiprocessor Safety and Java" 中 Paul Jakubik 讨论了多线程系统的 SMP 问题。


地震让大伙知道:居安思危,才是生存之道。
posted on 2007-08-03 14:23 小寻 阅读(963) 评论(0)  编辑  收藏 所属分类: j2se/j2ee/j2me

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


网站导航: