3.1 可见性
- 重排序
3.1.1 失效数据
只要在某个线程中无法检测到重排序情况,无法确保线程中的操作按照程序中指定的顺序来执行
- 只要数据在多个线程中共享,那么就要使用正确的同步
public class NoVisiblity {
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;
}
}
上方程序在多线程情况下可能出现N中情况:
- 正常输出42
代码按预想的顺序执行
- 一直循环,无法输出数字
读线程永远读不到ready的值
- 输出数字0
读线程看到了ready的值,没看到之后写入number的值
上方程序演示了,当读线程查看ready变量时,可能会得到一个已经失效的值。更糟糕的是,失效值可能不会同时出现:一个线程可能获得某个变量的更新值,而获得另一个变量的失效值
3.1.2 非原子的64位操作
- 非volatile类型的64位数值变量(double和long)
JVM允许将64位的读操作或写操作分解为两个32位的操作。顾名思义如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值得低32位,到底数值不准确。因此在多线程程序中使用共享且可变的long和double等类型的变量时,应该使用volatile声明它们或者用锁保护起来。
3.1.3 加锁与可见性
- 加锁的含义不仅仅局限于互斥行为,还包括可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或写操作的线程都必须在同一个锁上同步
3.1.4 Volatile变量
- 加锁机制即可保证可见性还可以保证原子性,volatile只能保证可见性
- 把变量声明为volatile后,不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会缓存在寄存器或者其他对处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
- volatile正确使用方式
- 确保它们自身状态的可见性
- 确保它们所引用对象的状态的可见性
- 标识一些重要程序的生命周期事件的发生(例如:初始化或关闭)
- 当且仅当满足下列条件,才可以使用volatile变量:
- 对变量的写入操作不依赖于变量的当前值,或者确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
3.2 发布与逸出
- 发布
使对象能在当前作用域之外的代码中使用
- 逸出
当某个不应被发布的对象被发布了
不要在构造过程中使this引用逸出,否则认为是不正确构造
错误做法
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
}
);
}
}
当从对象构造函数中发布对象时,只是发布了一个尚未构造完成的对象。如果是构造过程中创建一个线程,并且使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){
SafeListenter safe=new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
3.3 线程封闭
- 仅在单线程内部访问数据,实现线程安全性最简单的方式之一
- 例:JDBC的Connection对象
3.3.1 Ad-hoc线程封闭
- 维护线程封闭性的职责完全由程序实现来承担(非常脆弱)
- 没有一种语言特性,能将对象封闭到目标线程上。
- 尽量少使用Ad-hoc线程封闭,应该使用栈封闭或者ThreadLocal类等更强的线程封闭技术硬
3.3.2 栈封闭
- 只有通过局部变量才能访问对象。
- 局部变量的固有属性之一就是封闭在执行线程中。
- 它们位于执行线程的栈中,由于栈是线程私有,所以其他线程无法访问这个栈
- 示例:
public int loadTheArk(Collection candidates){
SortedSet animals;
int numPairs=0;
Animal candidate=null;
animals=new TreeSet(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;
}
3.3.3 ThreadLocal类
- ThreadLocal类能使线程中的某个值与保存值得对象关联起来。
- ThreadLocal提供了get和set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
-当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值
- 可以将ThreadLocal视为包含了Map对象,其中保存了特定于该线程的值,当线程终止后,这些值会作为垃圾回收
- ThreadLocal变量类似于全局变量,能降低代码的可重用性,并在类之间引入隐含的耦合性,使用时要格外小心
3.4 不变性
- 不可变对象(一定是线程安全的)
某个对象被创建后其状态就不能被修改
- 不可变性并不等同于将对象中所有的域声明为final
即使对象中所有的域都是final类型的,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用
- 对象不可变的条件
- 对象创建后其状态不能被修改
- 对象的所有域都是final类型
- 对象是正确创建的(在对象构造期间,this引用没有逸出)
- 在可变对象基础上构建不可变类
@Immutable
public final class ThreeStooges{
private final Set stooges=new HashSet();
public ThreeStooges(){
stooges.add("Moe"); stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name){
return stooges.contains(name);
}
}
- 不可变对象与不可变引用
保存在不可变对象中的程序状态仍可被更新,即通过将一个保存新状态的实例来“替换”原有的不可变对象
3.4.1 Final域
- 除非需要更高的可见性,否则应将所有的域声明为私有域
- 除非需要某个域是可变的,否则应将其声明为final域
3.4.2 使用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);
}
encodeIntoResponse(resp,factors);
}
}
@Immutable
public 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);
}
}
}
VolatileCachedFactorizer利用OneValueCache存储缓存的数字及其因数。当一个线程设置volatile类型的cache域并引用到一个新的OneValueCache后,新数据会立即对其他线程可见。
与cache相关的操作不会互相干扰,因为OneValueCache是不可变的,而且每次只有一条相应的代码路径访问它。
- 以下两个前提保证了即使VolatileCachedFactorizer没有显示地用到锁,但仍然是线程安全的。
- 不可变的容器对象(OneValueCache对象)持有与不变约束相关的多个状态变量(lastNumber,lastFactors)
- 利用volatile引用确保及时的可见性
3.5 安全发布
3.5.1 不正确的发布:正确的对象被破坏
- 示例(由于未被正确发布,这个类可能出现故障)
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域是一个失效值,因此将看到一个空引用或者之前的旧值
- 更糟的情况是,线程看到Holer引用的值是最新的,但Holder状态的值却是失效的
3.5.2 不可变对象与初始化安全性
- 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步
- 任何线程在不需要额外同步的情况下安全地访问final类型的域,然而,如果final类型的域指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步
3.5.3 安全发布的常用模式
- 一个正确构造的对象可以通过以下方式来安全地发布
- 在静态初始化函数中初始化一个对象的引用
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中
- 将对象的引用保存到某个正确构造的对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
3.5.4 事实不可变对象
- 事实不可变对象
对象从技术上来看是可变的,但其状态在发布后不会再改变
- 在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象
3.5.5 可变对象
- 如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性
- 对象的发布需求取决于它的可变性
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
3.5.6 安全地共享对象
- 当发布一个对象时,必须明确说明对象的访问方式
- 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
- 线程封闭
- 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
- 只读共享
- 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改他。共享的只读对象包括不可变对象和事实不可变对象
- 线程安全共享
- 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
- 保护对象
- 被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象