Java线程模型缺陷
Java编程语言的线程模型:面向对象的改进建议
Java的线程模型可能是其语言中最具挑战性的部分。它未能满足复杂程序的需求,且并不具备面向对象的特点。本文旨在提出对Java语言的重大修改和补充建议,以解决这些问题。
Java的线程模型一直令人不甚满意。虽然Java支持线程编程是好的,但它对线程的语法和类库的支持远远不够,仅适用于小型应用环境。大多数关于Java线程编程的书籍都在指出其线程模型的缺陷,并提供了应急类库来解决问题。这些类库并不能从根本上解决问题,这些问题应该是Java语言本身语法所应该包含的。
我认为,为了产生更高效的代码,我们应该在语法层面而不是仅仅依靠类库来解决这些问题。编译器和Java虚拟机(JVM)可以共同优化程序代码,而这些优化在类库中可能难以实现或无法实现。
在我的著作《Taming Java Threads》以及本文中,我进一步建议对Java编程语言本身进行修改,以真正解决线程编程的问题。相对于我的书,本文的建议经过了更深入的思考和提炼。尽管这些建议只是尝试性的——只是我个人对这些问题的看法,并且实现这些想法需要大量的工作和同行的评价——但我希望成立一个专门的工作组来解决这些问题。如果您对此感兴趣,请通过e-mail与我联系。
我的建议相当大胆。一些人建议对Java语言规范(JLS)进行细微和少量的修改以解决当前模糊的JVM行为,而我则希望能进行更彻底的改进。
在实际草案中,我提议为此语言引入新的关键字。虽然通常不建议突破一个语言的现有代码稳定性,但如果语言要保持活力而不是过时,就必须能够引入新的关键字。为了与现有的标识符不冲突,我提议使用一个美元符号($)作为新的关键字前缀。例如,使用$task而不是task。这需要编译器的支持,能识别并使用这些带美元符号的关键字。
Java线程模型的根本问题是它缺乏面向对象的特点。面向对象的设计师并不从线程角度考虑问题,而是关注同步和异步信息。Java的线程模型却不是面向对象的。一个Java线程实际上只是一个运行其他过程的过程,缺乏对象、异步或同步信息的概念。
为了解决这一问题,我提议在Java中加入task(任务)的概念,将active对象集成到语言中。Active对象可以接收异步请求,并在后台处理这些请求。在Java中,一个请求可以被封装在一个对象中,例如通过实现Runnable接口来封装需要完成的工作。该runnable对象被active对象排队,当轮到执行时,由一个后台线程执行。使用active对象和task概念可以消除大部分的同步问题。
在某种意义上,Java的Swing/AWT子系统就是一个active对象。向Swing队列发送消息的唯一安全方式是调用类似SwingUtilitiesvokeLater()的方法,将一个runnable对象发送到Swing事件队列,当轮到执行时,由Swing事件处理线程处理。我的建议是借鉴其他编程语言的经验,向Java中加入task的概念,将active对象集成到语言中。这不仅将简化线程管理,还将使Java更面向对象的处理并发问题。
实时操作系统中的任务管理:从复杂到简化
在实时操作系统中,每个任务都配备了一个内置的active对象分发程序,该程序自动管理处理异步信息的所有机制。定义任务的方式与定义类相似,只需在任务的方法前添加asynchronous修饰符,指示active对象在后台处理这些方法。
所有写请求都通过dispatch()过程进入active-object的输入队列。处理这些异步信息时发生的任何异常,都由Exception_handler对象处理。这个处理机制在File_io_task的构造函数中被激活。这种基于类的处理方式虽然功能强大,却显得复杂。
为了简化操作,我们可以借鉴狼蚁网站SEO优化的理念,引入$task和$asynchronous关键字。值得注意的是,异步方法并不指定返回值,因为它们会在请求操作完成之前立即返回句柄。对于派生的模型,$task关键字与class有相同的效力。标有asynchronous关键字的方法将由$task在后台处理,而其他方法将同步运行,如同在类中一样。
为了确保线程安全,异步方法的参数必须是不变的(immutable)。为了确保这种不变性,运行时系统会通过相关语义来保证(简单的复制通常是不够的)。所有的task对象都必须支持一些伪信息(pseudo-message)。除了常见的修饰符(如public),task关键字还可以接受一个$pooled(n)修饰符。当使用此修饰符时,任务将使用一个线程池来运行异步请求,而不是单个线程。n指定了所需线程池的大小,必要时可以扩展和缩小。
在《Taming Java Threads》的第八章中,我提供了一个服务器端的socket处理程序作为线程池的例子。当客户端连接到服务器时,服务器会从池中抓取一个预先创建的睡眠线程来服务这个连接。Socket_server对象使用一个独立的后台线程来处理异步的listen()请求。当每个客户端连接时,listen()会请求一个Client_handler来通过handle()处理请求。每个handle()请求都在它自己的线程中执行(因为这是一个$pooled任务)。值得注意的是,传送到$pooled $task的异步消息实际上都使用它们自己的线程来处理。对于解决与访问状态变量有关的潜在同步问题,最好的方法是在$asynchronous方法中使用对象的独有副本。这意味着当向一个$pooled $task发送异步请求时,会执行一个clone()操作,并且该方法的this指针会指向这个克隆对象。线程之间的通信可以通过对静态区域的同步访问来实现。尽管$task在很大程度上消除了同步操作的需求,但不是所有的多线程系统都使用任务来实现。还需要改进现有的线程模块中的synchronized关键字。该关键字存在无法指定超时值、无法中断等待锁的线程以及无法安全地请求多个锁等问题。为了解决这些问题,我们可以扩展synchronized的语法,使其支持多个参数和接受超时说明的功能。这样我们就能更好地控制多线程环境中的并发操作,提高系统的性能和稳定性。狼蚁网站SEO优化的同步机制与锁策略改进
在狼蚁网站的SEO优化过程中,我们深入了现有的同步机制与锁策略,并对其进行了必要的改进思考。在并发编程中,synchronized关键字起着至关重要的作用,但为了更好地应对复杂的多线程场景,我们需要对其语法和功能进行一定的扩展和改进。
现有的synchronized语法在处理多个对象的锁时存在一定的局限性。为了解决这个问题,我们提出了以下改进思路:
1. 使用更灵活的表达式来获取锁。例如,使用"synchronized(x && y && z)"可以一次性获得x、y和z对象的锁,确保三个对象同时可用时才执行相关代码。类似的,"synchronized(x || y || z)"则允许在任何一者可用时执行代码。这种扩展能够更好地适应复杂的场景。
2. 引入超时机制。在某些情况下,我们希望线程在获取锁时能够设置超时时间,避免无限期的等待。例如,"synchronized(...)[1000]"表示设置1秒的超时时间以获得一个锁。这种改进有助于增强代码的健壮性。
3. 处理中断请求的能力。当一个线程在等待锁的过程中被中断时,应该能够抛出异常并中断等待的线程。这种异常应作为RuntimeException的一个派生类,以便于处理。
这些改进的synchronized语法需要在二进制代码级别进行修改,目前主要依赖于进入监控(enter-monitor)和退出监控(exit-monitor)指令来实现。为了支持更多的锁定请求,我们需要扩展这些指令的功能或重新定义二进制代码的定义。这种修改虽然具有挑战性,但有助于更好地适应复杂的多线程环境并向下兼容现有的Java代码。
另一个重要的问题是死锁问题。当两个或多个线程相互等待对方释放资源时,就会发生死锁。为了解决这个问题,我们可以采取一些策略:
1. 通过编译器或虚拟机的优化来重新排列请求锁的顺序,减少死锁的发生概率。例如,始终按照固定的顺序获取锁可以消除某些情况下的死锁。
2. 在等待锁的过程中,适时地释放已获得的锁,以打破潜在的死锁。例如,可以使用超时机制来放弃已获得的锁并重新尝试获取所需的锁。这样可以在一定程度上打破死锁并恢复系统的正常运行。
我们还了wait()和notify()方法的改进方案。针对现有的一些问题,如无法检测wait()是正常返回还是因超时返回、无法有效使用条件变量等,我们提出了以下解决方案:
1. 通过重新定义wait()方法使其返回一个boolean变量来解决超时检测问题。true表示正常返回,false表示因超时返回。这样可以更好地控制等待锁的线程的行为。
2. 基于状态的条件变量是一个重要的概念。我们可以设置一个条件变量来表示某个状态是否处于"信号"(signaled)状态。当等待的线程在满足某个条件时被释放并继续执行。这样可以更有效地管理线程的同步和通信。
对于嵌套监控锁定问题,虽然目前还没有简单的解决方案,但我们可以通过深入分析具体的场景并采取相应的措施来避免或解决这种问题。例如,在涉及多个锁的代码中,确保在释放任何锁之前不释放所有必要的锁,以避免发生死锁。
一种可能的策略是,线程在调用wait()时,以反序释放已获取的锁,并在条件满足后再按原顺序重新获取。虽然这样的代码逻辑对于大多数人来说可能难以理解,但我个人认为这并不是一个切实可行的解决方案。如果您有更高明的办法,不妨通过电子邮件与我联系。
我也期待以下复杂条件得以实现。设想一下,对象a、b和c任意存在。我们希望能对Thread类进行一些修改,让它支持抢占式和协作式线程的功能。这在某些服务器应用程序中尤为重要,特别是在追求系统极致性能的情况下。我认为Java语言在简化线程模型方面走得太远,需要借鉴Posix/Solaris的“绿色线程”和“轻便进程”概念(在“驾驭Java线程”第一章中有详述)。这意味着某些Java虚拟机的实现,如在NT上的Java虚拟机,应该模拟协作式进程,而其他Java虚拟机则应模拟抢占式线程。这样的扩展很容易加入到Java虚拟机中。
一个Java的Thread应当始终采用抢占式工作模式。这意味着Java语言的线程应像Solaris的轻便进程一样工作。我们可以利用Runnable接口定义一个类似Solaris风格的“绿色线程”,这种线程需要能够将控制权交给在同一轻便进程中的其他绿色线程。通过这种方式,我们可以有效地为Runnable对象创建一个绿色线程,并将其绑定到一个由Thread对象代表的轻便进程中。这种实现对于现有代码是透明的,其运行效率与现有代码相同。
将Runnable对象视为绿色线程,只需向Thread构造函数传递几个Runnable对象,就可以扩展Java语言的现有语法,以支持在一个单一的轻便进程中有多个绿色线程。这些绿色线程可以相互协作,同时它们也可能被其他轻便进程(由Thread对象代表)中的绿色线程抢占。例如,在狼蚁网站的SEO优化代码中,每个runnable对象都会创建一个绿色线程,这些绿色线程共享由Thread对象代表的轻便进程。
现有的通过覆盖Thread对象并实现run()方法的习惯将继续有效。这种方法应映射到一个绑定到轻便进程的绿色线程。在Thread类中,默认的run()方法会在内部有效地创建第二个Runnable对象。
我们应该在语言中增加更多功能来支持线程间的相互通信。目前,PipedInputStream和PipedOutputStream类可以用于此目的,但对于大多数应用程序来说,它们的功能相对较弱。我建议为Thread类增加以下功能:添加一个wait_for_start()方法,该方法通常处于阻塞状态,直到一个线程的run()方法启动。通过这种方式,一个线程可以创建一两个辅助线程,并确保这些辅助线程在继续执行操作之前已经处于运行状态。我们还可以向Object类增加$send(Object o)和Object=$receive()方法,它们将使用一个内部阻断队列在线程之间传送对象。这个方法中的变量应支持设定入队和出队的操作超时能力,如$send(Object o, long timeout)和$receive(long timeout)。
关于读写锁的内部支持也是非常重要的。读写锁的概念应该被内置到Java语言中。一个读写锁允许多个线程同时访问一个对象进行读取操作,但在同一时刻只允许一个线程进行写操作。对于某个对象来说,只有在没有线程进行写操作时,才允许多个线程进行读操作。这样的设计可以确保同时进行读操作的线程不会被写操作的线程打断。如果读写线程都在等待,默认情况下应该优先允许读线程进入。这样的设计可以大大提高多线程应用程序的效率和稳定性。Java编程语言中的线程安全性与访问控制
在Java编程语言中,线程安全性和访问控制是确保并发编程正确性的关键因素。当前Java语言在某些方面对于这两个问题的处理存在缺陷,需要进行改进。
关于线程安全性,一个主要问题是构造函数中的线程访问。当前情况下,构造函数中创建的线程可以访问尚未完全创建的对象,这可能导致不可预测的结果。对于这个问题,一种解决方法是在构造函数返回之前,禁止运行其中的任何线程。这意味着start()请求会被推迟,以确保对象已经完全创建且处于可访问状态。Java语言应允许构造函数的同步,以确保线程安全地执行。
volatile关键字的使用也是值得关注的问题。Java语言规范要求保留对volatile操作的请求,但在实际的多处理器环境中,许多Java虚拟机忽略了这部分内容。这导致了在多线程环境下可能出现的问题,尤其是在某些主机上。为了解决这一问题,我们需要确保volatile关键字能够按预期工作,对于这一问题的深入研究可以由对这方面感兴趣的开发者如Bill Pugh进行。
关于访问控制的问题,良好的访问控制对于线程编程至关重要。为了确保线程安全,我们应当限制线程只能从同步子系统中调用。对于Java编程语言的访问权限概念,我建议限制包访问权,并使用package关键字来精确控制。对于缺省行为的存在,我感到困惑,因为这可能导致潜在错误。为了解决这个问题,我们可以重新引入private protected关键字,其功能类似于现有的protected关键字,但不允许包级别的访问。我们可以增加对私有访问的控制,使得只有同一类的对象能够访问某个方法的实现细节。我们还可以扩展public语法的功能,使其能够指定特定类的访问权限。
不变性在多线程环境下是无价的。Java语言在不变性的实现上存在不足。对于不变对象,在其完全创建之前进行访问可能会导致不正确的值。为了解决这一问题,我们可以采取上述措施禁止在构造函数中开始执行线程或在构造函数返回之前执行开始请求。对于final修饰符指向的对象,只有当所有域都是final且所有引用的对象的域也都是final时,该对象才是真正恒定的。这样我们可以更严格地保证不变性。
让被阻断的I/O华丽转身,绽放正确工作的光彩
在Java的世界里,我们时常遭遇被阻断的I/O操作。这些操作应当能被优雅地打断,而不是仅仅让它们进入等待或休眠状态。在“驯服Java线程”的第二章中,关于socket的部分,我详细了这个问题。
对于被阻断的socket上的I/O操作,现有的打断方式显得捉襟见肘。我们常常不得不选择关闭整个socket来打断一个被阻断的操作。当面对文件I/O操作时,情况更为棘手。一旦发起读请求并进入阻断状态,线程将一直等待,除非成功读取到数据,即使尝试关闭文件句柄也无法打断读操作。
我们的程序需要更强有力的支持,为I/O操作设置超时机制。这一功能应当被融入到所有可能出现阻断操作的对象中,比如InputStream对象。这与Socket类的setSoTimeout(time)方法异曲同工,为阻断的调用提供超时参数支持将大大增强我们的编程体验。
再谈谈ThreadGroup类。这个类如果能实现Thread中改变线程状态的所有方法,将为我们管理线程带来极大便利。我特别期待它能实现join()方法,让我能够轻松等待组中的所有线程终止。
以上是我个人的期望与建议。如同我在标题中所言,如果我能成为国王(哎,只是一种想象),我希望能推动这些改变(或其他同等重要的改进)最终被引入到Java语言中。我深知Java是一种卓越的编程语言,其线程模型虽然已相当成熟,但仍存在可完善的空间。好在Java编程语言仍在不断演变,让我们期待其未来更加辉煌的前景吧!