线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术就是线程同步,它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
线程封闭技术的一种常见应用是 JDBC 的 Connection 对象。JDBC 规范并不要求 Connection 对象必须是线程安全的。线程从线程池中获取一个 Connection 对象,并且用该对象处理请求,使用完后再将对象返回给连接池由于大多数请求都是由单个线程采用同步的方式来处理,并且在 Connection 对象返回之前,连接池不会再将他分配给其他线程,因此,这种连接管理模式在处理请求时隐含地将 Connection 对象封闭在线程中。
在 Java 语言中并没有强制规定某个变量必须由锁来保护,同样在 Java 语言中也无法强制将对象封闭在某个线程中。线程封闭是在程序设计中的一个考虑因素。
1. Ad-hoc 线程封闭
Ad-hoc 是指:维护线程封闭的职责完全由程序实现来承担。Ad-hoc 线程封闭是非常脆弱的(没有任何一种语言特性,例如:修饰符或者局部变量)。
在 volatile 变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的 volatile 变量执行写入操作,那么就可以安全地在这些共享的 volatile 变量上执行“读取-修改-写入”的操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且 volatile 变量的可见性保证还确保了其他线程能看到最新的值。
由于 Ad-hoc 线程封闭技术的脆弱性,因此在程序中尽量少用他。
2. 栈封闭
栈封闭是线程封闭的一种特例(只能通过局部变量才能访问对象)。正如封装能使得代码更容易维持不变性条件那样,同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。
对于基本类型的局部变量,例如下面代码中的 numPairs,无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用,因此 Java 语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。
// 基本类型的局部变量与应用变量的线程封闭性(演示代码,代码逻辑不重要!)
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
// animals 被封闭在方法中,不要使他们逸出
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a)) {
candidate = a;
} else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被引用的对象不会逸出。在上面代码中实例化一个 TreeSet 对象,并将指向该对象的一个引用保存到 animals 中。此时只有一个引用指向集合 animals,这个引用被封闭在局部变量中,因此也被封闭在执行线程中。
如果在线程内部上下文中使用非线程安全对象,那么该对象任然是线程安全的。
3. ThreadLocal 类
维持线程封闭性的一种更规范方法是使用 ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal 提供了 get 与 set 等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。
ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个 Connection 对象。由于 JDBC 的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将 JDBC 的连接保存到 ThreadLocal 对象中,每个线程都会拥有属于自己的连接,如下面代码 ConnectionHolder 所示。
// 使用 ThreadLoacl 来维持线程封闭性
private static ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
public Connection initalValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。在 Java 5.0 之前,Integer.toString() 方法使用 ThreadLocal 对象来保存一个12字节大小的缓冲区,用于对结果进行格式化,而不是使用共享的静态缓冲区(这需要使用锁机制)或者在每次调用时都分配一个新的缓冲区。
当某个线程初次调用 ThreadLocal.get 方法时,就会调用 initialValue 来获取初始值。从概念上看,你可以将ThreadLoca 视为包含了 Map<Thread,T> 对象,其中保存了特定于该线程的值,但 ThreadLocal 的实现并非如此。这些特定于线程的值保存在 Thread 对象中,当线程终止后,这些值会作为垃圾回收。
开发人员经常滥用 ThreadLocal,例如将所有全局变量都作为 ThreadLocal 对象,或者作为一种“隐藏”方法参数的手段。ThreadLocal 变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。