对象的公布与逸出
“公布(Publish)“一个对象是指使对象可以在当前作用域之外的代码中使用。可以通过 公有静态变量。非私有方法。构造方法内隐含引用 三种方式。
假设对象构造完毕之前就公布该对象,就会破坏线程安全性。当某个不应该公布的对象被公布时。这样的情况就被称为逸出(Escape)。
以下我们首先来看看一个对象是怎样逸出的。
公布对象最简单的方法便是将对象的引用保存到一个共同拥有的静态变量中,以便不论什么类和线程都能看见对象,如以下代码。
public static Set<String> mySet; public void initialize() { mySet = new HashSet<String>(); }
当公布某个对象时,可能会间接地公布其它对象。假设将一个 String 对象加入到集合 mySet 中,那么相同会公布这个对象,由于不论什么代码都能够遍历这个集合。并获得对这个 String 对象的引用。
相同,假设从非私有方法中返回一个引用,那么相同会公布返回的对象。
如以下代码 UnsafeStates 公布了本应为私有的状态数组。
class UnsafeState { private String[] states = new String[] { "AK", "AL" }; public String[] getStates() { return states; } }
不管其它的线程会对义公布的引用运行何种操作,事实上都不重要。由于误用该引用的风险始终存在。当hadoop某个对象逸出后,你必须如果有某个类或者线程可能会误用该对象。
这正是须要使用封装的的最基本的原因:封装能使得对正确性分析变得可能,并使降低无意中破坏设计约束条件的行为。
最后一种公布对象或其内部状态的机制就是公布一个内部的类实例,如以下类 ThisEscape 所看到的。
当 ThisEscape 公布 EventListener 时。也隐含的公布了 ThisEscape 实例本身,由于在这个内部类的实例中包括了对 ThisEscape 实例的隐含对象。
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
安全的构造过程
ThisEscape 中给出了逸出的一个特殊演示样例,即 this 引用在构造函数中逸出。内部 EventListener 实例公布时,在外部封装的 ThisEscape 实例也逸出了。
当且仅当对象的构造函数返回时。对象才处于可预測的和一致的状态。因此,当从对象的构造函数中公布对象时,仅仅是公布了一个尚未构造完毕的对象。即使公布对象的语句位于构造函数的最后一行也是如此。假设 this 引用在构造过程中逸出,那么这样的对象就被觉得是不对构造。
在构造过程中使 this 引用逸出的一个常见错误是。在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,不管是显示创建(通过将它传给构造函数)还是隐式创建(因为 Thread 或 Runnable 是该对象的一个内部类), this 引用都会被新创建的线程共享。
在对象尚未被创建完毕之前,新的线程就能够看见它。
在构造函数中创建线程并没有错误。但最好不要马上启动它,而是通过一个 start 或 initialize 方法来启动。
在构造函数中调用一个可改写的实例方法时,相同会导致 this 引用在构造过程中逸出。
假设想在构造函数中注冊一个事件监听器或启动线程,那么能够使用一个私有的构造函数和一个公共的工厂方法(Factory Method)。从而避免不对的构造过程,如以下的 SafeListener 。
public class SafeListener{ private final EventListener listener; private SafeListener(){ listener = new EventListener(){ public void onEvent(Event e){ doSomething(e); } }; } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe; } }
线程封闭
当訪问共享的可变数据时,通常须要使用同步。一种避免使用同步的方式就是不共享数据。假设仅在单线程内訪问数据,就不须要同步。
这样的技术被称为线程封闭(Thread Confinement),它是实现线程安全型的最简单方式之中的一个。
当某个对象封闭在一个线程中时。这样的使用方法将自己主动实现线程安全性。即使被封闭的对象本身不是线程安全的。
线程封闭的一种常见的应用是 JDBC 的 Connection 对象。JDBC 规范并不要求 Connection 对象必须是线程安全的。在典型的server应用程序中,线程从连接池中获得一个 Connection 对象,而且用该对象来处理请求,使用完之后再将对象返还给连接池。因为大多数请求(比如 Servlet 请求或 EJB 调用等)都是由单个线程採用同步的方式来处理。而且在 Connection 对象返回之前。连接池都不会将它分配给其他线程。因此,这样的连接管理模式在处理请求时隐含的将 Connection 对象封闭在线程中。
Java 语言及其核心库提供了一些机制来帮助维持线程封闭性。比如局部变量和 ThreadLocal 类,即便如此,程序猿仍然须要确保封闭在线程中的对象不会从线程中逸出。
Ad-hoc 线程封闭
Ad-hoc线程封闭是指,维护线程封闭性的职责全然由程序实现来承担。
Ad-hoc线程封闭是很脆弱的,由于没有不论什么一种语言特性,比如可见性修饰符或局部变量,能将对象封闭到目标线程上。其实。对线程封闭对象(比如。GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。
当决定使用线程封闭技术时,一般是由于要将某个特定的子系统实现为一个单线程子系统。在某些情况下。单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。
在volatile变量上存在一种特殊的线程封闭。
仅仅要你能确保仅仅有单个线程对共享的volatile变量运行写入操作,那么就能够安全地在这些共享的volatile变量上运行“读取-改动-写入”的操作。在这样的情况下,相当于将改动操作封闭在单个线程中以防止发生竞态条件。而且volatile变量的可见性保证还确保了其它线程能看到最新的值。
因为Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(比如,栈封闭或ThreadLocal类)。
栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中。仅仅能通过局部变量才干訪问对象。正如封装能使代码更easy维持不变性条件那样。同步变量也能使对象更易于封闭在线程中。
对于基本类型的局部变量。比如以下 loadTheArk 方法的 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; }
在维持对象引用的栈封闭性时,程序猿须要多做一些工作以确保被引用的对象不会逸出。在loadTheArk中实例化一个TreeSet对象,并将指向该对象的一个引用保存到animals中。
此时,仅仅有一个引用指向集合animals。这个引用被封闭在局部变量中,因此也被封闭在运行线程中。
然而,假设公布了对集合animals(或者该对象中的不论什么内部数据)的引用,那么封闭性将被破坏,并导致对象animals的逸出。
假设在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。然而,要小心的是,仅仅有编写代码的开发者才知道哪些对象须要被封闭到运行线程中。以及被封闭的对象是否是线程安全的。假设没有明白地说明这些需求。那么兴许的维护人员非常easy错误地使对象逸出。
ThreadLocal 类
维持线程封闭性的一种更规范方法是使用ThreadLocal。这个类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal提供了get与set等訪问接口或方法,这些方法为每一个使用该变量的线程都存有一份独立的副本。因此get总是返回由当前运行线程在调用set时设置的最新值。
ThreadLocal对象通经常使用于防止对可变的单实例变量(Singleton)或全局变量进行共享。
比如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象。从而避免在调用每一个方法时都要传递一个Connection对象。
因为JDBC的连接对象不一定是线程安全的。因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将JDBC的连接保存到ThreadLocal对象中,每一个线程都会拥有属于自己的连接。
当某个频繁运行的操作须要一个暂时对象,比如一个缓冲区,而同一时候又希望避免在每次运行时都又一次分配该暂时对象。就能够使用这项技术。比如,在Java 5.0之前,Integer.toString()方法使用ThreadLocal对象来保存一个12字节大小的缓冲区,用于对结果进行格式化。而不是使用共享的静态缓冲区(这须要使用锁机制)或者在每次调用时都分配一个新的缓冲区。
当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。
从概念上看,你能够将ThreadLocal<T>视为包括了Map< Thread,T>对象,当中保存了特定于该线程的值。但ThreadLocal的实现并不是如此。这些特定于线程的值保存在Thread对象中。当线程终止后。这些值会作为垃圾回收。
假设你须要将一个单线程应用程序移植到多线程环境中。通过将共享的全局变量转换为ThreadLocal对象(假设全局变量的语义同意)。能够维持线程安全性。然而,假设将应用程序范围内的缓存转换为线程局部的缓存,就不会有太大作用。
在实现应用程序框架时大量使用了ThreadLocal。比如。在EJB调用期间,J2EE容器须要将一个事务上下文(Transaction Context)与某个运行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal对象中,能够非常easy地实现这个功能:当框架代码须要推断当前运行的是哪一个事务时,仅仅需从这个ThreadLocal对象中读取事务上下文。这样的机制非常方便,由于它避免了在调用每一个方法时都要传递运行上下文信息。然而这也将使用该机制的代码与框架耦合在一起。
开发者常常滥用ThreadLocal,比如将全部全局变量都作为ThreadLocal对象。或者作为一种“隐藏”方法參数的手段。ThreadLocal变量类似于全局变量,它能减少代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。