第三章:共享对象
可见性
除了synchronize
,同步还具有另一个重要、微妙的方面:内存可见性。我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态,而且希望确保当一个线程修改了对象的状态后,其他的线程能够真正看到改变。
在一个单线程化的环境里,如果向一个变量先写入值,然后在没有写干涉的情况下读取这个变量,是可以得到相同的返回值。但是当读和写发生在不同的线程中时,情况却根本不是这样。
/*
读线程启动后,主线程虽然把ready赋值为true,
但是由于线程之间存在缓存问题,读线程的reday仍然是false,未能及时获取最新的reday值。
导致程序的运行结果与预期不符。
*/
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
锁和可见性
当线程A执行一个同步块时,线程B也随后进入了被同一个锁监视的同步块中,这时可以保证,在锁释放之前对A可见的变量的值,B获得锁之后同样是可见的。
锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,涉及同一共享变量的读取和写入线程必须使用公共的锁进行同步。
volatile变量
Java语言也提供了其他的选择,即一种同步的弱形式:volatile
变量。
它确保对一个变量的更新以可预见的方式告知其他的线程。
当一个域声明为volatile
类型后,编译器与运行时会监视这个变量:它是共享的,而且对它的操作不会与其他的内存操作起被重排序。
所以,读一个volatile
类型的变量时,总会返回由某一线程所写入的最新值。
访问volatile
变量的操作不会加锁,也就不会引起执行线程的阻塞,这使得volatile
变量相对于sychronized
而言,只是轻量级的同步机制。
加锁可以保证可见性与原子性;
volatile
变量只能保证可见性。
只有满足了下面所有的标准后,你才能使用volatile
变量:
- 写入变量时并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
- 变量不需要 与其他的状态变量共同参与不变约束;
- 而且访问变量时,没有其他的原因需要加锁。
备注:原子变量(
java.util.concurrent.atomic
)
线程封闭
访问共享的、可变的数据要求使用同步。一个可以避免同步的方式就是不共数据。如果数据仅在单线程中被访问,就不需要任何同步。
线程封闭(Thread confinement)技术是实现线程安全的最简单的方式之一。
当对象封闭在一个线程中时,这种做法会自动成为线程安全的,即使被封闭的对象本身并不是。
一种常见的使用线程限制的应用程序是应用池化的JDBC ( Java Database Connectivity) Connection对象。
JDBC规范并没有要求Connection对象是线程安全的。
每个线程都会同步地处理大多数请求(比如 Servlet 请求或者EJB (Enterprise Java Bean)调用),而且在Connection对象在被归还前,池不会将它再分配给其他线程,因此,这种连接管理模式隐式地将Connection对象限制在处于请求处理期间的线程中。
栈限制
栈限制(也称线程内部或者线程本地用法,但是不要与核心库类的ThreadLocal
混淆)是线程限制的一种特例, 在栈限制中,只能通过本地变量才可以触及对象。
正如封装使不变约束更容易被保持,本地变量使对象更容易被限制在线程本地中。
本地变量本身就被限制在执行线程中,它们存在于执行线程栈。其他线程无法访问这个栈。
ThreadLocal
一种维护线程限制的更加规范的方式是使用Threadlocal
,它允许你将每个线程与持有数值的对象关联在一起。ThreadLocal
提供了get
与set
访问器,为每个使用它的线程维护一份单独的拷贝。所以get
总是返回由当前执行线程通过set
设置的最新值。
不可变性
创建后状态不能被修改的对象叫做不可变对象。
不可变对象永远是线程安全的。
无论是Java语言规范还是Java存储模型都没有关于不可变性的正式定义,但是不可变性并不简单地等于将对象中的所有域都声明为final
类型,所有域都是final
类型的对象仍然可以是可变的,因为final
域可以获得一个到可变对象的引用。
只有满足如下状态,一个对象才是不可变的:
- 它的状态不能在创建后再被修改;
- 所有域都是
final
类型;并且,- 它被正确创建(创建期间没有发生this引用的逸出)。
String
是一个Java中不可变对象的典型例子。
第四章:组合对象
设计线程安全的类
设计线程安全类的过程应该包括下面3个基本要素:
- 确定对象状态是由哪些变量构成的;
- 确定限制状态变量的不变约束;
- 制定一个管理并发访问对象状态的策略。
名词解释:
- 先验条件(precondition):针对方法(method),它规定了在调用该方法之前必须为真的条件。例如,你无法从空队列中移除一一个条目:在你删除元素前,队列必须处于“非空”状态。
- 后验条件(postcondition):也是针对方法,它规定了方法顺利执行完毕之后必须为真的条件。例如,如果+1自增计数器当前状态是17,下一个唯一合法的状态是18。
实例限制
即使一个对象不是线程安全的,仍然有许多技术可以让它安全地用于多线程程序。
比如,你可以确保它只被单一的线程访问(线程限制),也可以确保所有的访问都正确地被锁保护。
将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁。
/**
* 使用限制确保线程安全
*/
@ThreadSafe
public class PersonSet {
@GuardedBy("this")
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
平台类库中有很多线程限制的实例,包括一些类,它的存在就是为了把非线程安全的类转化为线程安全的。
ArrayList
和HashMap
这样的基本容器类是非线程安全的,但是类库提供了包装器工厂方法(Collections.synchronizedList
及其同族的方法),使这些非线程安全的类可以安全地用于多线程环境中。
向已有的线程安全类添加功能
“缺少即加入(put-if-absent)”的概念相当直观一一在向容器中添加一个元素前先查看它是否已经存在,如果存在,就不添加。
(“检查再运行(check-then-act) ”的警钟现在该响起了)。
对类的线程安全需求会隐式地加入另一个条件一一像 “缺少即加入”这样的操作必须是原子的。
任何有合理的要求都会告诉你,如果你有一个不包含对象X的List,并且执行两次“缺少即加入”的addX,那么结果容器只应该包含一个X的拷贝。
但是,倘若“缺少即加入”操作不是原子的,在一些偶发的时序中,两个线程都看到X不在容器中,并且都执行了addX,导致容器中包含两份X的拷贝。
-
添加一个新原子操作的最安全的方式是,修改原始的类,以支持期望的操作。
但是你可能无法访问源代码或者没有修改的自由,所以这通常是不可能的。
即使你可以修改原始的类,也需要理解其实现的同步策略,才能在维持原有设计的前提下完善它的功能。
直接向类中加入新方法,意味着所有实现类同步策略的代码仍然包含在一个源代码文件中,因此便于理解与维护。 -
另一种方法是扩展这个类,假如这个类在设计上是可以扩展的。
清单4.13的BetterVector
扩展了Vector
,并添加了一个putIfAbsent
方法。虽然扩展Vector
的做法相当直观,但是并非所有的类都给子类暴露了足够多的状态,以支持这种方案。
@ThreadSafe
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent) {
add(x);
}
return absent;
}
}