第二章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据,本章将介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。
同步还有另一个重要的方面:内存可见性(Memory Visiblity)。我们不仅希望防止某个线程正在使用状态对象而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。如果没有同步,这种情况就无法实现。

可见性

示例:在没有同步的情况下共享变量

public classs NoVisibility {
	private static boolean ready;
	private static int number;

	private static class ReaderThread extends Thread {
		public void run() {
		while (!ready)
			Thread.tield();
		System.out.println(number);
		}
	}
	public static void main(String args) {
		new ReaderThread().start();
		number = 42;
		ready = true;
	}
}

NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种奇怪的现象是,可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序(Reordering)”。

  • 在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想堆内存操作的执行顺序进行判断,几乎无法得出正确的结论。

失效数据

非线程安全的可变整数类:

@NotThreadSafe
public class MutableInteger {
	private int value;
	public int get() {
		return value;
	}
	public void set(int value) { this.value = value; }
}

线程安全的可变整数类:

@ThreadSafe
public class MutableInteger {
	@GuardedBt("this")private int value;
	public synchronized int get() {
		return value;
	}
	public synchronized void set(int value) { this.value = value; }
}

非原子的64位操作

最低安全性(out-of-thin-air-safety)适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(long和double)。JVM允许将64位的读或写操作分解为两个32位的操作。如果对该变量的读或写分别在不同的线程中执行,可能会读到某个值的高32位和另一个值的低32位。除非用volatile关键字声明,或者用锁保护起来。

加锁与可见性

加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

Volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明位volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存操作一起重排序。volatile变量不会被换存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  • 仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式是:
    • 确保它们自身状态的可见性
    • 确保它们所引用的对象的状态的可见性
    • 标识一些重要的程序生命周期事件的发生(例如,初始化或关闭)

volatile变量的典型用法:检查某个状态标记以判断是否退出循环。
数绵羊:

volatile boolean asleep;
...
	while(!asleep)
		countSomeSheep();

虽然volatile变量很方便,但是存在一些局限性:通常用作某个操作完成、发生中断或者状态的标志,例如上述asleep标志。volatile的语义不足以表示递增操作的原子性,除非你能确保只有一个线程对变量执行写操作。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量值可以确保可见性
当且仅当满足以下所有条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
  • 该变量不会与其他状态变量一起纳入不变性条件中;
  • 在访问时不需要加锁

发布与逸出

发布(publish)一个对象的意思是指,使对象能在当前作用域范围之外的代码中使用,例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
当某个不该被发布的对象被发布时,称为逸出(Escape)。
发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中,以便在任何类和线程都能看见该对象。

public static Set<Secret> knownSecrets;

public void initialize() {
	knownSecrets = new HashSet<Secret>();
}

当发某个对象时,可能会简介地发布其他对象。如果将一个Secret对象添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码都可以遍历这个集合,并获得一个新Secret对象引用。
使内部的可变状态逸出(不要这么做):

class UnsafeStates {
	private String[] states = new String() {
		"AK","AL"...
	};
	public String[] getStates() { return states;}
}

按照上述方式来发布states,就会出现问题,因为任何调用者都能修改这个数组的内容。在这个示例中,数组states已经逸出了它所在的作用域,因为这个本应是私有的变量已经被发布了。
当发布一个变量时,在该对象的非私有域中引用的所有对象同样被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用达到其他的对象,那么这些对象也都会被发布。当某个对象逸出后,你必须假设有某个类或者线程可能会误用该对象。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性进行分析变的可能,并使得无意中破坏设计约束条件变难。

最后一种发布对象或其内部状态的机制就是发布一个内部的类实例。
示例:隐式地使this引用逸出

public class ThisEscape {
	public ThisEscape(EventSource source) {
		source.registerListener(
			new EventListener(){
				public void onEvent(Event e){
					do something(e);
				}
			});
	}
}

安全的对象构造过程

上述示例中,当EventListener实例发布时,在外部封装的ThisEscape实例也逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造。
this逸出:在对象还没有实例化完成时,其他线程就要用到对象中的属性。
在构造过程中使用this引用逸出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将其传给构造函数)还是隐式创建(由于Thread或是Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象未被完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或者initialize方法来启动。在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。
如果想在构造函数中注册一个时间监听器或启动线程,可以使用一个私有的构造函数和一个公共的工厂方法。

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),是实现线程安全性的最简单方法之一。在Java语言中并没有强制规定某个变量必须由锁来保护,同样也无法将对象封闭在某个线程中。

Ad-hoc线程封闭

Ad-hoc线程封闭是指,维护线程封闭性的指责完全由程序来实现承担。Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。
当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为单线程子系统。
在volatile变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入“操作,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性保证还确保了其他线程能看到最新的值。
在可能的情况下,应当使用更强的线程封闭技术(例如栈封闭或ThreadLocal)。

栈封闭

在栈封闭中,只有通过局部变量才能访问对象。
基本类型的局部变量与引用变量的线程封闭性:

public int loadTheArk(Collection<Animal> candidates) {
	SortedSet<Animal> animals;
	int numPairs = 0;//基本类型int
	Animal candidate = null;
	//animals被封闭在方法中,不要使它们逸出(逸出loadTheArk)
	animals= newTreeSet<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;
}			

如果发布类对集合animal或者对该对象中任何内部数据的引用,封闭性就将被破坏。
如果在线程内部上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。

ThreadLocal类

维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法微每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。例如,在单线程应用程序中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象。通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会有自己的连接。

private static ThreadLocal<Connection> connectionHolder
	= new ThreadLocal<Connection>() {
		public Connection initialValue() {
			return DriverManager.getConnection(DB_URL);
		}
	};

public static Connection getConnection() {
	return connectionHolder.get();
}

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
概念上可以ThreadLocal视为Map<ThreadLocal, T>对象,但是当线程终止后,特定的ThreadLocal内的值会被垃圾回收。

不变性

满足同步需求的另一种方法是使用不可变对象(Immutable Object)

  • 不可变对象一定是线程安全的
    不可变对象只有一种状态,并且该状态由构造函数来控制。
    当满足以下条件时,对象才是不可变的:
  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型
  • 对象是正确创建的(在对象的创建期间,this引用没有逸出)
    光有第二条不行,因为即使对象中所有的域都是final类型,在final类型的域中可以保存对可变对象的引用。

Fianl域

final域能确保初始化过程的安全性,从而可以不受控制地访问不可变对象,并在共享这些对象时无需同步。

  • 除非需要更高的可见性,否则应该将所有的域都声明为私有域
  • 除非需要某个域是可变的,否则应该将其声明为final域

安全发布

不正确的发布:正确的对象被破坏

不可变对象与初始化安全性

  • 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
  • 然而,如果final类型的域所指向的是可变对象,那么在访问这些域指向的对象状态时仍然需要同步。

安全发布的常用模式

一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中
    通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);

静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。
在线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器)
  • Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布发布到任何从这些容器中访问该元素的线程。
  • BlockingQueue、ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

事实不可变对象

Effectively Immutable Object

  • 在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

安全地共享对象

当发布一个对象时,必须明确地说明对象的访问方式。
常用策略:线程封闭、只读共享、线程安全共享、保护对象

Q.E.D.


南七技校练习时长四年的练习生