将同步策略文档化

在维护线程安全性时,文档是最强大的(同时也是最未被充分利用的)工具之一。用户可以通过查阅文档来判断某个类是否是线程安全的,而维护人员也可以通过查阅文档来理解其

性能损失很小,因为在底层List 上的同步不存在竞争,所以速度很快,请参见第11章。

中的实现策略,避免在维护过程中破坏安全性。然而,通常人们从文档中获取的信息却是少之又少。

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

synchronized、volatile 或者任何一个线程安全类都对应于某种同步策略,用于在并发访问时确保数据的完整性。这种策略是程序设计的要素之一,因此应该将其文档化。当然,设计阶段是编写设计决策文档的最佳时间。这之后的几周或几个月后,一些设计细节会逐渐变得模糊,因此一定要在忘记之前将它们记录下来。

在设计同步策略时需要考虑多个方面,例如,将哪些变量声明为volatile 类型,哪些变量用锁来保护,哪些锁保护哪些变量,哪些变量必须是不可变的或者被封闭在线程中的,哪些操作必须是原子操作等。其中某些方面是严格的实现细节,应该将它们文档化以便于日后的维护。还有一些方面会影响类中加锁行为的外在表现,也应该将其作为规范的一部分写入文档。

最起码,应该保证将类中的线程安全性文档化。它是否是线程安全的?在执行回调时是否持有一个锁?是否有某些特定的锁会影响其行为?不要让客户冒着风险去猜测。如果你不想支持客户端加锁也是可以的,但一定要明确地指出来。如果你希望客户代码能够在类中添加新的原子操作,如4.4节所示,那么就需要在文档中说明需要获得哪些锁才能实现安全的原子操作。如果使用锁来保护状态,那么也要将其写入文档以便日后维护,这很简单,只需使用标注@GuardedBy即可。如果要使用更复杂的方法来维护线程安全性,那么一定要将它们写入文档,因为维护者通常很难发现它们。

甚至在平台的类库中,线程安全性方面的文档也是很难令人满意。当你阅读某个类的Javadoc时,是否曾怀疑过它是否是线程安全的?大多数类都没有给出任何提示。许多正式的Java 技术规范,例如Servlet 和JDBC,也没有在它们的文档中给出线程安全性的保证和需求。

尽管我们不应该对规范之外的行为进行猜测,但有时候出于工作需要,将不得不面对各种糟糕的假设。我们是否应该因为某个对象看上去是线程安全的而就假设它是安全的?是否可以假设通过获取对象的锁来确保对象访问的线程安全性?(只有当我们能控制所有访问该对象的代码时,才能使用这种带风险的技术,否则,这只能带来线程安全性的假象。)无论做出哪种选择都难以令人满意。

更糟糕的是,我们的直觉通常是错误的:我们认为“可能是线程安全“的类通常并不是线程安全的。例如, java. text. SimpleDateFormat并不是线程安全的,但JDK 1.4之前的Javadoc 并没有提到这点。许多开发人员都对这个类不是线程安全的而感到惊讶。有多少程序已经错误地生成了这种非线程安全的对象,并在多线程中使用它?这些程序没有意识到这将在高负载的情况下导致错误的结果。

如果某个类没有明确地声明是线程安全的,那么就不要假设它是线程安全的,从而有

如果你从未考虑过这些问题,那么你确实比较乐观。

效地避免类似于SimpleDateFormat的问题。而另一方面,如果不对容器提供对象(例如HttpSession)的线程安全性做某种有问题的假设,也就不可能开发出一个基于Servlet的应用程序。不要使你的客户或同事也做这样的猜测。

解释含糊的文档

许多Java技术规范都没有(或者至少不愿意)说明接口的线程安全性,例如ServletContext、HttpSession或DataSource⊖。这些接口是由容器或数据库供应商来实现的,而你通常无法通过查看其实现代码来了解细节功能。此外,你也不希望依赖于某个特定JDBC驱动的实现细节——你希望遵从标准,这样代码可以基于任何一个JDBC驱动工作。但在JDBC 的规范中从未出现“线程”和“并发”这些术语,同样在Servlet规范中也很少提到。那么你该做些什么呢?

你只能去猜测。一个提高猜测准确性的方法是,从实现者(例如容器或数据库的供应商)的角度去解释规范,而不是从使用者的角度去解释。Servlet通常是在容器管理的(Container-Managed)线程中调用的,因此可以安全地假设:如果有多个这种线程在运行,那么容器是知道这种情况的。Servlet容器能生成一些为多个Servlet提供服务的对象,例如HttpSession 或ServletContext。因此, Servlet容器应该预见到这些对象将被并发访问,因为它创建了多个线程,并且从这些线程中调用像Servlet. service 这样的方法,而这个方法很可能会访问ServletContext。

由于这些对象在单线程的上下文中很少是有用的,因此我们不得不假设它们已被实现为线程安全的,即使在规范中没有明确地说明。此外,如果它们需要客户端加锁,那么客户端代码应该在哪个锁上进行同步?在文档中没有说明这一点,而要猜测的话也不知从何猜起。在规范和正式手册中给出的如何访问ServletContext或HttpSession 的示例中进一步强调了这种“合理的假设”,并且没有使用任何客户端同步。

另一方面,通过把setAttribute放到ServletContext 中或者将HttpSession 的对象由Web应用程序拥有,而不是Servlet容器拥有。在Serviet 规范中没有给出任何机制来协调对这些共享属性的并发访问。因此,由容器代替Web应用程序来保存这些属性应该是线程安全的,或者是不可变的。如果容器的工作只是代替Web应用程序来保存这些属性,那么当从servlet 应用程序代码访问它们时,应该确保它们始终由同一个锁保护。但由于容器可能需要序列化HttpSession中的对象以实现复制或钝化等操作,并且容器不可能知道你的加锁协议,因此你要自己确保这些对象是线程安全的。

可以对JDBC DataSource 接口做出类似的推断,该接口表示一个可重用的数据库连接池。DataSource为应用程序提供服务,它在单线程应用程序中没有太大意义。我们很难想象不在多线程情况下使用getConnection。并且,与Servlet一样,在使用DataSource的许多示例代码中,JDBC规范并没有说明需要使用任何客户端加锁。因此,尽管JDBC规范没有说明DataSource是否是线程安全的,或者要求生产商提供线程安全的实现,但同样由于“如果不

令我们失望的是,在多次对规范的修订中一直都忽略了这些问题。

这么做将是不可思议的”,所以我们只能假设DataSource. getConnection不需要额外的客户端加锁。

另一方面,在DataSource分配JDBC Connection对象上没有这样的争议,因为在它们返回连接池之前,不会有其他操作将它们共享。因此,如果某个获取JDBC Connection 对象的操作跨越了多个线程,那么它必须通过同步来保护对Connection对象的访问。(大多数应用程序在实现使用JDBC Connection对象的操作时,通常都会把Connection对象封闭在某个特定的线程中。)

猜你喜欢

转载自blog.csdn.net/2301_78064339/article/details/131021135