改进synchronized
虽然在多数情况下,$task 消除了同步操作的要求,但是不是所有的多线程系统都用任务来实现。所以,还需要改进现有的线程模块。 synchronized 关键字有下列缺点: 无法指定一个超时值。 无法中断一个正在等待请求锁的线程。 无法安全地请求多个锁 。(多个锁只能以依次序获得。)
解决这些问题的办法是:扩展synchronized 的语法,使它支持多个参数和能接受一个超时说明(在下面的括弧中指定)。下面是我希望的语法:
synchronized(x && y && z)获得 x、y 和 z 对象的锁。
synchronized(x || y || z)获得 x、y 或 z 对象的锁。
synchronized( (x && y ) || z)对于前面代码的一些扩展。
synchronized(...)[1000] 设置 1 秒超时以获得一个锁。
synchronized[1000] f(){...}在进入 f() 函数时获得 this 的锁,但可有 1 秒超时。
TimeoutException是 RuntimeException 派生类,它在等待超时后即被抛出。
超时是需要的,但还不足以使代码强壮。您还需要具备从外部中止请求锁等待的能力。所以,当向一个等待锁的线程传送一个interrupt() 方法后,此方法应抛出一个 SynchronizationException 对象,并中断等待的线程。这个异常应是 RuntimeException 的一个派生类,这样不必特别处理它。
对synchronized 语法这些推荐的更改方法的主要问题是,它们需要在二进制代码级上修改。而目前这些代码使用进入监控(enter-monitor)和退出监控(exit-monitor)指令来实现 synchronized 。而这些指令没有参数,所以需要扩展二进制代码的定义以支持多个锁定请求。但是这种修改不会比在 Java 2 中修改 Java 虚拟机的更轻松,但它是向下兼容现存的 Java 代码。
另一个可解决的问题是最常见的死锁情况,在这种情况下,两个线程都在等待对方完成某个操作。
设想一个线程调用a() ,但在获得 lock1 之后在获得 lock2 之前被剥夺运行权。 第二个线程进入运行,调用 b() ,获得了 lock2 ,但是由于第一个线程占用 lock1 ,所以它无法获得 lock1 ,所以它随后处于等待状态。此时第一个线程被唤醒,它试图获得 lock2 ,但是由于被第二个线程占据,所以无法获得。此时出现死锁。
编译器(或虚拟机)会重新排列请求锁的顺序,使lock1 总是被首先获得,这就消除了死锁。
但是,这种方法对多线程不一定总成功,所以得提供一些方法来自动打破死锁。一个简单的办法就是在等待第二个锁时常释放已获得的锁。
如果等待锁的每个程序使用不同的超时值,就可打破死锁而其中一个线程就可运行。我建议用以下的语法来取代前面的代码:
synchronized语句将永远等待,但是它时常会放弃已获得的锁以打破潜在的死锁可能。在理想情况下,每个重复等待的超时值比前一个相差一随机值。
改进wait() 和 notify()
wait()/ notify() 系统也有一些问题: 无法检测 wait() 是正常返回还是因超时返回。 无法使用传统条件变量来实现处于一个“信号”(signaled)状态。 太容易发生嵌套的监控(monitor)锁定。
超时检测问题可以通过重新定义wait() 使它返回一个 boolean 变量 (而不是 void ) 来解决。一个 true 返回值指示一个正常返回,而 false 指示因超时返回。
基于状态的条件变量的概念是很重要的。如果此变量被设置成false 状态,那么等待的线程将要被阻断,直到此变量进入 true 状态;任何等待 true 的条件变量的等待线程会被自动释放。 (在这种情况下, wait() 调用不会发生阻断。)。
嵌套监控锁定问题非常麻烦,我并没有简单的解决办法。嵌套监控锁定是一种死锁形式,当某个锁的占有线程在挂起其自身之前不释放锁时,会发生这种嵌套监控封锁。
此例中,在get() 和 put() 操作中涉及两个锁:一个在 Stack 对象上,另一个在 LinkedList 对象上。下面我们考虑当一个线程试图调用一个空栈的 pop() 操作时的情况。此线程获得这两个锁,然后调用 wait() 释放 Stack 对象上 的锁,但是没有释放在 list 上的锁。如果此时第二个线程试图向堆栈中压入一个对象,它会在 synchronized(list) 语句上永远挂起,而且永远不会被允许压入一个对象。由于第一个线程等待的是一个非空栈,这样就会发生死锁。这就是说,第一个线程永远无法从 wait() 返回,因为由于它占据着锁,而导致第二个线程永远无法运行到 notify() 语句。
在这个例子中,有很多明显的办法来解决问题:例如,对任何的方法都使用同步。但是在真实世界中,解决方法通常不是这么简单。
一个可行的方法是,在wait() 中按照反顺序释放当前线程获取的 所有 锁,然后当等待条件满足后,重新按原始获取顺序取得它们。但是,我能想象出利用这种方式的代码对于人们来说简直无法理解,所以我认为它不是一个真正可行的方法。如果您有好的方法,请给我发 e-mail。
我也希望能等到下述复杂条件被实现的一天。例如:
其中a 、 b 和 c 是任意对象。
修改Thread 类
同时支持抢占式和协作式线程的能力在某些服务器应用程序中是基本要求,尤其是在想使系统达到最高性能的情况下。我认为 Java 编程语言在简化线程模型上走得太远了,并且 Java 编程语言应支持 Posix/Solaris 的“绿色(green)线程”和“轻便(lightweight)进程”概念(在“(Taming Java Threads ”第一章中讨论)。 这就是说,有些 Java 虚拟机的实现(例如在 NT 上的 Java 虚拟机)应在其内部仿真协作式进程,其它 Java 虚拟机应仿真抢占式线程。而且向 Java 虚拟机加入这些扩展是很容易的。
一个 Java 的Thread 应始终是抢占式的。这就是说,一个 Java 编程语言的线程应像 Solaris 的轻便进程一样工作。 Runnable 接口可以用于定义一个 Solaris 式的“绿色线程”,此线程必需能把控制权转给运行在相同轻便进程中的其它绿色线程。
能有效地为Runnable 对象产生一个绿色线程,并把它绑定到由 Thread 对象代表的轻便进程中。这种实现对于现有代码是透明的,因为它的有效性和现有的完全一样。
把Runnable 对象想成为绿色线程,使用这种方法,只需向 Thread 的构造函数传递几个 Runnable 对象,就可以扩展 Java 编程语言的现有语法,以支持在一个单一轻便线程有多个绿色线程。(绿色线程之间可以相互协作,但是它们可被运行在其它轻便进程 ( Thread 对象) 上的绿色进程( Runnable 对象) 抢占。)。例如,下面的代码会为每个 runnable 对象创建一个绿色线程,这些绿色线程会共享由 Thread 对象代表的轻便进程。
现有的覆盖(override)Thread 对象并实现 run() 的习惯继续有效,但是它应映射到一个被绑定到一轻便进程的绿色线程。(在 Thread() 类中的缺省 run() 方法会在内部有效地创建第二个 Runnable 对象。)
线程间的协作
应在语言中加入更多的功能以支持线程间的相互通信。目前,PipedInputStream 和 PipedOutputStream 类可用于这个目的。但是对于大多数应用程序,它们太弱了。我建议向 Thread 类加入下列函数: 增加一个 wait_for_start() 方法,它通常处于阻塞状态,直到一个线程的 run() 方法启动。(如果等待的线程在调用 run 之前被释放,这没有什么问题)。用这种方法,一个线程可以创建一个或多个辅助线程,并保证在创建线程继续执行操作之前,这些辅助线程会处于运行状态。 (向 Object 类)增加 $send (Object o) 和 Object=$receive() 方法,它们将使用一个内部阻断队列在线程之间传送对象。阻断队列应作为第一个 $send() 调用的副产品被自动创建。 $send() 调用会把对象加入队列。 $receive() 调用通常处于阻塞状态,直到有一个对象被加入队列,然后它返回此对象。这种方法中的变量应支持设定入队和出队的操作超时能力: $send (Object o, long timeout) 和 $receive (long timeout)。
Java线程模型缺陷(2)
内容版权声明:除非注明,否则皆为本站原创文章。