线程封闭与不变性 Java并发编程实战总结

线程封闭与不变性 Java并发编程实战总结_第1张图片

        当访问共享的可变数据时, 通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据, 就不需要同步。这种技术被称为线程封闭(ThreadConfinement), 它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时, 这种用法将自动实现线程安全性, 即使被封闭的对象本身不是线程安全的[CPJ 2.3.2]。

        线程封闭技术的另一种常见应用是JDBC(Java Database Connectivity)的Connection对象。JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中, 线程从连接池中获得一个Connection对象, 并且用该对象来处理请求, 使用完后再将对象返还给连接池。由于大多数请求(例如Servlet请求或EJB调用等) 都是由单个线程采用同步的方式来处理, 并且在Connection对象返回之前, 连接池不会再将它分配给其他线程, 因此, 这种连接管理模式在处理请求时隐含地将Connection对象封闭在线程中。

        在Java语言中并没有强制规定某个变量必须由锁来保护, 同样在Java语言中也无法强制将对象封闭在某个线程中。线程封闭是在程序设计中的一个考虑因素, 必须在程序中实现。Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此, 程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。

Ad-hoc线程封闭

        Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的, 因为没有任何一种语言特性, 例如可见性修饰符或局部变盘, 能将对象封闭到目标线程上。事实上, 对线程封闭对象(例如,GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。

        当决定使用线程封闭技术时, 通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。

        在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作, 那么就可以安全地在这些共享的volatile变量上执行“ 读取- 修改-写入” 的操作。在这种情况下, 相当于将修改操作封闭在单个线程中以防止发生竞态条件, 并且volatile变量的可见性保证还确保了其他线程能看到最新的值。

        由于 Ad-hoc线程封闭技术的脆弱性, 因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭或ThreadLocal类)。

栈封闭

        栈封闭是线程封闭的一种特例,在栈封闭中, 只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样, 同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中, 其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用, 不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护, 也更加健壮。

        对于基本类型的局部变量, 例如程序清单3-9中loadTheArk方法的numPairs, 无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用, 因此Java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。


线程封闭与不变性 Java并发编程实战总结_第2张图片

        
        在维持对象引用的栈封闭性时, 程序员需要多做一些工作以确保被引用的对象不会逸出。在loadTheArk中实例化一个TreeSet对象,并将指向该对象的一个引用保存到animals中。此时,只有一个引用指向集合animals, 这个引用被封闭在局部变量中, 因此也被封闭在执行线程中。然而, 如果发布了对集合animals(或者该对象中的任何内部数据)的引用, 那么封闭性将被破坏, 并导致对象animals的逸出。

        如果在线程内部(Within-Thread)上下文中使用非线程安全的对象, 那么该对象仍然是线程安全的。然而, 要小心的是, 只有编写代码的开发人员才知道哪些对象需要被封闭到执行线程中, 以及被封闭的对象是否是线程安全的。如果没有明确地说明这些需求, 那么后续的维护人员很容易错误地使对象逸出。

ThreadLocal类

        维持线程封闭性的一种更规范方法是使用ThreadLocal, 这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal 提供了get 与set 等访问接口或方法, 这些方法为每个使用该变量的线程都存有一份独立的副本,因此get 总是返回由当前执行线程在调用set 时设置的最新值。

        ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton) 或全局变量进行共享。

        例如, 在单线程应用程序中可能会维持一个全局的数据库连接, 井在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个Connection 对象。由于JDBC 的连接对象不一定是线程安全的, 因此,当多线程应用程序在没有协同的情况下使用全局变量时, 就不是线程安全的。通过将JDBC 的连接保存到ThreadLocal 对象中, 每个线程都会拥有属于自己的连接, 如程序清单3-10 中的ConnectionHolder 所示。


线程封闭与不变性 Java并发编程实战总结_第3张图片

        当某个频繁执行的操作需要一个临时对象, 例如一个缓冲区, 而同时又希望避免在每次执行时都重新分配该临时对象, 就可以使用这项技术。例如, 在Java 5.0 之前, Integer.toString()方法使用ThreadLocal 对象来保存一个12 字节大小的缓冲区, 用于对结果进行格式化, 而不是使用共享的静态缓冲区(这需要使用锁机制)或者在每次调用时都分配一个新的缓冲区.当某个线程初次ThreadLocal.get 方法时, 就会调用initialValue 来获取初始值。从概念上看,你可以ThreadLocal 视为包含了Map< Thread, T> 对象, 其中保存了特定于该线程的值, 但ThreadLocal 的实现并非如此。这些特定于线程的值保存在Thread 对象中, 当线程终止后, 这些值会作为垃圾回收。

        假设你需要将一个单线程应用程序移植到多线程环境中, 通过将共享的全局变址转换为ThreadLocal 对象(如果全局变量的语义允许), 可以维持线程安全性。然而, 如果将应用程序范围内的缓存转换为线程局部的缓存, 就不会有太大作用。

        在实现应用程序框架时大量使用了ThreadLocal。例如,在EJB调用期间, J2EE 容器需要将一个事务上下文(Transaction Context) 与某个执行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal 对象中, 可以很容易地实现这个功能: 当框架代码需要判断当前运行的是哪一个事务时,只需从这个ThreadLocal对象中读取事务上下文。这种机制很方便,因为它避免了在调用每个方法时都要传递执行上下文信息,然而这也将使用该机制的代码与框架耦合在一起。

        开发人员经常滥用ThreadLocal, 例如将所有全局变量都作为ThreadLocal对象,或者作为一种“ 隐藏” 方法参数的手段。ThreadLocal变量类似于全局变量, 它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。



不变性

        满足同步需求的另一种方法是使用不可变对象(Immutable Object) [EJ Item 13]。到目为止,我们介绍了许多与原子性和可见性相关的问题,例如得到失效数据,丢失更新操作或者观察到某个对象处于不一致的状态等等, 都与多线程试图同时访问同一个可变的状态相关。如果对象的状态不会改变,那么这些问题与复杂性也就自然消失了。

        如果某个对象在被创建后其状态就不能被修改, 那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一, 它们的不变性条件是由构造函数创建的,只要它们的状态不改变, 那么这些不变性条件就能得以维持。

        不可变对象一定是线程安全的。

        不可变对象很简单。它们只有一种状态, 并且该状态由构造函数来控制。在程序设计中,一个最困难的地方就是判断复杂对象的可能状态。然而,判断不可变对象的状态却很简单。

        同样,不可变对象也更加安全。如果将一个可变对象传递给不可信的代码, 或者将该对象发布到不可信代码可以访问它的地方,那么就很危险——不可信代码会改变它们的状态,更糟的是, 在代码中将保留一个对该对象的引用并稍后在其他线程中修改对象的状态。另一方面,不可变对象不会像这样被恶意代码或者有问题的代码破坏, 因此可以安全地共享和发布这些对象,而无须创建保护性的副本[EJItem 24]。

        虽然在Java语言规范和Java内存模型中都没有给出不可变性的正式定义,但不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。

        在不可变对象的内部仍可以使用可变对象来管理它们的状态, 如程序清单3-11中的 ThreeStooges所示。 尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到, 在Set对象构造完成后无法对其进行修改。 stooges是一个final类型的引用变量, 因此所有的对象状态都通过一个final域来访问。 最后一个要求是 “正确地构造对象 ”,这个要求很容易满足, 因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。            

线程封闭与不变性 Java并发编程实战总结_第4张图片


        由于程序的状态总在不断地变化, 你可能会认为需要使用不可变对象的地方不多, 但实际情况并非如此。在“不可变的对象” 与“不可变的对象引用” 之间存在着差异。保存在不可变对象中的程序状态仍然可以更新, 即通过将一个保存新状态的实例来“ 替换” 原有的不可变对象。

Final域

        关键字final可以视为C++中const机制的一种受限版本, 用于构造不可变性对象。final类型的域是不能修改的(但如果final域所引用的对象是可变的, 那么这些被引用的对象是可以修改的)。然而, 在Java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性, 从而可以不受限制地访问不可变对象, 并在共享这些对象时无须同步

        即使对象是可变的, 通过将对象的某些域声明为final类型, 仍然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“ 基本不可变” 对象仍然比包含多个可变状态的对象简单。通过将域声明为final类型, 也相当于告诉维护人员这些域是不会变化的。

你可能感兴趣的:(线程封闭与不变性 Java并发编程实战总结)