17.保障线程安全的设计技术:线程特有对象
1.定义
对于一个非线程安全对象,每个线程都创建一个该对象的实例,各个线程仅访问各自创建的实例,且一个线程不能访问另外一个线程创建的实例。这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就被称为线程特有对象,相对应的线程就被称为该线程特有对象的持有线程。
2.使用
ThreadLocal类相当于线程访问其线程特有对象的代理 (Proxy),即各个线程通过这个对象可以创建并访问各自的线程特有对象,其类型参数 T 指定了相应线程特有对象的类型。一个线程可以使用不同的ThreadLocal 实例来创建并访问其不同的线程特有对象。多个线程使用同一个ThreadLocal实例所访问到的对象是类型 T 的不同实例,即这些线程各自的线程特有对象实例。因此, ThreadLocal 类也可以理解为当前线程访问其线程特有对象的代理对象,这种代理与被代理的关系如下图。
ThreadLocal 实例也被称为线程局部变量,ThreadLocal 类的方法如下表。
设 t1Var 为任意一个线程局部变量。初始状态下,t1Var 并没有与之关联的线程特有对象。当一个线程初次执行 t1Var.get() 的时候,t1Var.get()会调用t1Var.initialValue()。t1Var.initialValue()的返回值就会成为 t1Var 所关联的当前线程(即 t1Var.get()的执行线程)的线程特有对象。这个线程后续再次执行t1Var.get()所返回的线程特有对象始终都是同一个对象(即 t1Var.initialValue()的返回值)除非这个线程中途执行了 t1Var.set(T) 。由于ThreadLocal的 initialValue 方法的返回值为 null, 因此要设置线程局部变量关联的初始线程特有对象。我们需要创建ThreadLocal 的子类(通常是匿名子类)并在子类中覆盖initialValue方法,然后在该方法中返回初始线程特有对象。
线程局部变量通常是会被声明为某个类的静态变量。这是因为:如果把线程局部变量声明为某个类的实例变量,那么每创建该类的一个实例都会导致新的ThreadLocal 实例被创建。这就可能导致当前线程中同一个类型的线程特有对象会被多次创建。而这即便不会导致错误,也会导致重复创建对象带来的浪费 。
3.线程特有对象可能导致的问题及其规避
- 退化与数据错乱。由于线程和任务之间可以是一对多的关系,即一个线程可以先后执行多个任务,因此线程特有对象就相当于一个线程所执行的多个任务之间的共享对象。如果线程特有对象是个有状态对象且其状态会随着相应线程所执行的任务而改变,那么这个线程所执行的下一个任务可能“看到”来自前一个任务的数据,而这个数据可能与该任务并不匹配,从而导致数据错乱。 因此,在一个线程可以执行多个任务的情况下(比如在生产者—消费者模式中)使用线程特有对象,我们需要确保每个任务的处理逻辑被执行前相应的线程特有对象的状态不受前一个被执行的任务影响。这通常可以通过在任务处理逻辑被执行前为线程局部变量重新关联一个线程特有对象(通过调用ThreadLocal.set(T)实现)或者重置线程特有对象的状态来实现。
- ThreadLocal 可能导致内存泄漏、伪内存泄漏 。在 Web 应用中使用ThreadLocal极易导致内存泄漏、伪内存泄漏的问题。
4.线程特有对象的典型应用场景
- 需要使用非线程安全对象,但又不希望因此而引入锁。
- 使用线程安全对象,但希望避免其使用的锁的开销和相关问题。
- 隐式参数传递。线程特有对象在一个具体的线程中,它是线程全局可见的。一个类的方法中设置的线程特有对象对于该方法调用的任何其他方法(包括其他类的方法)都是可见的。这就可以形成隐式传递参数的效果,即一个类的方法调用另一个类的方法时,前者向后者传递数据可以借助 ThreadLocal 而不必通过方法参数传递 。隐式参数传递的实现通常是使用一个只包括静态方法的类或者单例类(包装类)来封装对线程特有对象的访问。其他相应访问线程特有对象的代码只需要调用包装类的静态方法或者实例方法即可以访问线程特有对象。
- 特定于线程的单例模式。广为使用的单例模式所实现的效果是在一个 Java 虚拟机中的一个类加载器下某个类有且仅有一个实例。如果我们希望对于某个类每个线程有且仅有该类的一个实例,那么就可以使用线程特有对象 。