文章目录
对象的共享
- 要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理。
- 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关健字synchronized只能用于实现原子性或者确定“临界区(Critical Section)”。同步还有另一个重要的方面:内存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态化。如果没有同步,那么这种情况就无法实现。你可以通过显式的同步或者类库中内置的同步来保证对象被安全地发布。
可见性
- 通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
public class Novisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run () {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main (String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
- 在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
失效数据
非原子的64位操作
- 当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-airsafety)
加锁与可见性
- 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。
- 加锁的含义不仅仅局限于互斥行为,还包括内存的可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
Volatile变量
发布与逸出
- “发布(Publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。
public static Set<Secret> knownSecrets;
public void initialize () {
knowSecrets = new HashSet<Secret> ();
}
当发布某个对象时,可能会间接地发布其他对象。
安全的对象构造过程
- 不要在构造过程中使this引用逸出
- 在构造过程中使this引用逸出的一个常见错误是,在构造函致中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动(请参见第7章了解更多关于服务生命周期的内容)。在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。
- 如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函
数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如程序清单3・8中
SafeListener 所示.
// 隐式地使this引用逸出
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListenner(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
}
)
}
}
- 不要在构造过程中使this引用逸出。
// 使用工厂方法来防止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;
}
}
- 具体来说,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。
线程封闭
- 当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(Thread Confinement),他是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。
- 在Java语言中并没有强制规定某个变量必须由锁来保护,同样在Java语言中也无法强制将对象封闭在某个线程中。线程封闭是在程序设计中的一个考虑因素,必须在程序中实现。Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。
Ad-hoc线程封闭
- Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭式非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。
- 由于Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术。
栈封闭
- 栈封闭式线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样,同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护,也更加健壮。
// 基本类型的局部变量与引用变量的线程封闭性
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;
}
}
ThreadLocal类
- 维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection> () {
public Connection initialValue () {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection () {
return connectionHolder.get();
}
不变性
- 如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。
- 不可变对象一定是线程安全的。
- 当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改。
- 对象的所有域都是final类型。
- 对象时正确创建的(在对象的创建期间,this引用没有逸出)。
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges () {
stooges.add("More");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge (String name) {
return stooges.contains(name);
}
}
Final域
示例:使用Volatile类型来发布不可变对象
// 对数值及其因数分解结果进行缓存的不可变容器类
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache (BigInteger i, BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors (BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i)) {
return null;
} else {
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
}
// 使用指向不可变容器对象的volatile类型引用以缓存最新的结果
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service (ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache == new OneValueCache(i, factors);
}
encodeIntResponse(resp, factors);
}
}
安全发布
// 在没有足够同步的情况下发布对象
// 不安全的发布
public Holder hlder;
public void initialize () {
holder = new Holder(42);
}
不正确的发布:正确的对象被破坏
- 你不能指望一个尚未被完全创建的对象拥有完整性。
// 由于未被正确发布,因此这个类可能出现故障
public class Holder {
private int n;
public Holder (int n) {
this.n = n;
}
public void assertSanity () {
if (n != n) {
throw new AssertionError("This statement is false");
}
}
}
- 由于没有使用同步来确保Holder对象对其他线程可见,因此将Holder称为“未被正确发布”。
不可变对象与初始化安全性
- 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
安全发布的常用模式
-
可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。
-
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到 volatile 类型的域或者 AtomicReferance 对象中。
- 将对象的引用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个由锁保护的域中。
-
在线程安全容器内部的同步意味着,在将对象放入到某个容器,例如 Vector 或 synchronizedList时,将满足上述最后一条需求。
- 通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。
- 通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet 中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
- 通过将某个元素放入BlockingQueue 或者 ConcurrentLinkedQueue 中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。
-
静态初始化器由JVM在类的初始化阶段执行。
事实不可变对象
- 如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”。
- 在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
可变对象
- 对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布。
- 事实不可变对象必须通过安全方式来发布。
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
安全地共享对象
- 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
- 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
- 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
- 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的共有接口来进行访问而不需要进一步同步。
- 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。