jlearning.cn

《java并发编程实战》读书笔记(二)——对象的共享

可见性

只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序),那么就无法确保线程中的操作将按照程序中指定的顺序来执行。

主线程首先写入a,然后在没有同步的情况下写入b。那么读线程看到的顺序可能与写入的顺序完全相反。

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

只要有数据在多个线程之间共享,就使用正确的同步。

失效数据

非原子的64位操作

最低安全性保证:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,至少这个值是之前某个线程设置的值,而不是一个随机值。

对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位操作。如果读操作和写操作在不同的线程中执行,那么很可能会读到某个值的高32位,和另一个值的低32位。

除非用volatile来声明它们。

加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

访问某个共享且可变的变量时,要求所有线程都在同一个锁上同步,以确保某个线程程写入该变量的值对于其他变量变量来说,都是可见的。

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

Volatile变量

用于确保将变量的更新操作通知到其他线程。

当把变量生命为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

不会被缓存再寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

仅当volatile变量能简化代码的实现以及对同步策略的验证时,才使用它们。

加锁可以确保原子性和可见性,volatile只能确保可见性。

满足一下所有条件时,才使用volatile变量:

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

发布与逸出

发布(Publish):是对象能够在当前作用域之外的代码中使用。

e.g. 将一个指向该对象的引用保存到其他代码可以访问的地方、在某一个非私有的方法中返回该引用、将引用传递到其他类的方法中。

逸出(Escape):当某个不应该发布的对象被发布。

外部方法(Alien):行为并不完全由类来规定的方法。包括其他类中定义的方法以及类中可以被改写的方法。非private final

当把一个对象传递给某个外部方法,就相当于发布了这个对象。

使用封装的原因:封装能使对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得更难。

安全的对象构造过程

不要再构造过程中使this引用逸出。

一个错误:在构造函数中启动一个线程。

当对象在其构造函数中创建一个线程时,无论是显示创建(通过将它传给构造函数),还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的额线程共享。在对象尚未完全构造之前,新的线程就可以看见他。

如果想在构造函数中注册一个时间监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)

Ad-hoc线程封闭

维护线程封闭性的指责完全由程序来承担。

非常脆弱,尽量少用它。

栈封闭

是线程封闭的一个特例,在栈封闭中,只能通过局部变量才能访问对象。

局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。

局部变量:离开了他的域就访问不到

在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被应用的对象不会逸出。

ThreadLocal类

能使线程中的某个值与保存值的对象关联起来。

ThreadLocal提供类get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本。因此,get总是返回由当前执行线程在调用set时设置的最新值。

通常用于防止对单实例变量(Singleton)或全局变量进行共享。

当某个频繁执行的操作需要一个临时对象,又希望避免在每次执行时都重新分配该临时对象。使用ThreadLocal。

这些特定于线程的值保存值唉Thread对象中,当线程终止后,这些值会作为垃圾回收。

download movie

不变性

不可变对象一定是线程安全的。

对象不可变的条件:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型
  • 对象是正确创建的(在创建期间,this引用没有逸出)

Final域

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

使用Volatile类型来发布不可变对象

对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。

当线程获得了对该对象的引用后,就不必担心另一个线程会修改对象的状态。

通过包含多个状态变量的容器对象来维持不变形条件,并使用一个volatile类型的引用来确保可见性,是的Volatile Cached Factorizer在没有显式的使用锁的情况下仍然是线程安全的。

安全发布

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