从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使得我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销。

7.1 Java运行时存储空间

Java运行时(Java runtime)空间可以分为栈区、堆区与方法区(非堆空间)。
栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间。在线程栈中每调用一个方法就是给方法分配一个栈桢,栈帧用于存储方法的局部变量,返回值等私有数据,即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值(引用)也是存储在栈空间中,引用的对象存储在堆中。由于线程栈是相互独立的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。
堆空间(Heap Space)用于存储对象,是在JVM启动时分配的一段可以动态扩容的内存空间,创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享。多个线程同时操作实例变量可能线程安全问题。
非堆空间(Non-Heap Space)用于存储常量,类的元数据等,非堆空间也是JVM启动时分配的一段可以动态扩容的存储空间,类的元数据包括静态变量、方法元数据(方法名、参数、返回值等),非堆空间也是多个线程共享的,因此 访问非堆空间中的静态变量也可能存在线程安全问题。

7.2 无状态对象

对象就是数据及对数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量。如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态,那么该类的实例就称为无状态对象(Stateless Object),反之,如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象。实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象。
线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多线程之间共享数据,使用无状态对象就是这种方法。

7.3 不可变对象

不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性。当不可变对象实体状态发生变化时,系统会创建一个新的不可变对象,就如String字符串对象。一个不可变对象需要满足以下条件:

  1. 类本身使用final修饰,防止通过创建子类来改变它的定义;
  2. 所有字段都是final修饰的,final字段在创建对象时必须显示初始化,不能被修改;
  3. 如果字体引用了其他状态可变的对象(集合、数组),则这些字段必须是private私有的。

不可变对象主要的应用场景:

  1. 被建模对象的状态变化不频繁;
  2. 同时对一组相关数据进行写数据,可以应用不可变对象,既可以保障原子性,也可以避免锁的使用;
  3. 使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与健的hashCode()有关,如果键的内部状态发生了变化会导致键的哈希码,可能会影响键值对的存储位置。如果HashMap的键是一个不可变对象,则hashCode()方法的返回值恒定,存储位置是固定的。

    7.4 线程特有对象

    非线程安全的对象可以选择不共享,每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,一个线程不能访问另外 一个线程创建的实例,这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象。线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销,线程特有对象也具有固有的线程安全性。
    ThreadLocal类相当于线程访问其特有对象的代理,即各个线程通过ThreadLocal对象可以创建并访问各自的线程特有对象,泛型T指定了线程特有对象的类型。一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象。

    7.5 装饰器模式

    基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象,而是访问它的外包装对象,外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全方式调用相应的非线程安全对象的方法。
    在java.util.Collections工具类中提供了一组synchronizedXXX(xxx)可以把不是线程安全的xxx集合转换为线程安全的集合,它就是采用了这种装饰器模式,这个方法返回值就是指定集合的外包装对象,这类集合又称为同步集合。
    使用装饰器模式的好处就是实现关注点分离,在这种设计中,实现同一组功能对象的两个版本:非线程安全的对象与线程安全的,对于非线程安全的对象在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性。