第三章:共享对象(Sharing Objects)
3.1 可见性
为了确保跨线程写入的内存可见性,你必须使用同步机制。
重排序:在单个线程中,只要重排序不会对结果产生影响,那么就不能保证其中的操作一定按照程序写定的顺序执行——即使重排序对于其他线程来说会产生明显的影响。
在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误。
简单的方法来避免这些复杂的问题:只要数据需要被跨线程共享,就进行恰当的同步.
3.1.1过期数据
除非每次访问变量都是同步的,当读线程检查ready变量时,它可以看到一个过期的值。除非每一次访问变量都是同步的,否则很可能得到变量的过期值。(一个线程可能得到一个变量最新的值,但是也可能得到另一个变量县写入的过期值)
重要性:虽然一个过期的数据应用到Web程序的计数器可能还不至于太危险,但是过期值能够引起严重的安全错误,或者带来生死攸关的失败。可能使对象引用中的数据更加复杂,可能引起严重且混乱的错误。
以下情况对过期数据尤为敏感:如果一个线程调用了set,而另一个线程此时正在调用get,它可能就看不到更新的数据了。
3.1.2非原子的64位操作
当一个线程在没有同步的情况下读取变量,它可能会得到一个过期值。但是至少它可以看到某个线程在那里设定了一个真是数值,而不是一个凭空而来的值。这样的安全保证被称为最低限的安全性(out-of-thin-air safety)
最低限的安全性应用于所有的变量,除了一个例外:没有声明为volatile的64伟数值变量(double和long)
3.1.3 锁和可见性
同步对可见性的保证
当访问一个共享可变变量时,为什么要求所有线程由同一个锁进行同步,我们现在给出一个理由——为了保证一个线程对于数值进行的写入,其他线程都可看见。另一方面,如果一个线程在没有恰当地使用锁的情况下读取了变量,那么这个变量很可能是一个过期的数据。
锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。
3.1.4 Volatile变量
同步弱形式:volatile变量。它确保对一个变量的更新可以预见的方式告知其他线程。当一个域声明为volatile类型后,编译器与运行时会监视这个变量:它是共享的,而且对它的操作不会与其他内存操作仪器被重排序。
Volatice变量不会缓存在寄存器或者缓存在对其他处理器隐藏的地方,所以读一个volatile类型的变量时,总会返回由某一线程所写入的最新值。
只有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量的方式包括:用户确保他们所引用的对象状态的可见性,或者用户标识中好重要性的生命周期事件(比如初始化或者关闭)的发生。
限制:volatile变量固然方便,但也存在限制。他们通常被当做标识完成、中断、状态的标记使用。
加锁可以保证可见性与原子性;volatile变量之恩给你保证可见性;
需要满足下面的标准后,才能使用volatile变量
1. 习入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值;
2. 变量不需要与其他的状态变量共同参与不变约束;
3. 而且,访问变量时,没有其他的原因需要加锁。
3.2 发布和逸出
发布(publishing)一个对象的意思是使它能够被当前范围之外的代码使用。
一个对象在尚未准备好时就将它发布,这种情况称为逸出(escape)
发布了一个对象,同样也发布了该对象所有非私有域所引用的对象。更一般的,在一个已经发布的对象中,那些非私有域的引用链,和方法调用链中的可获得对象也都会被发布。
3.2.1 安全构建的实践
对象只有通过构造函数返回后,才处于可预言的、稳定的状态,所以从构造函数内部发布的对象,只是一个未完成构造的对象。甚至即使是在构造函数的最后一行发布的引用也是如此。如果this引用的构造过程中逸出,这样的对象被认为是“没有正确构件的(not properly constructed)”
不要让this引用在构件期间逸出;(导致this引用在构件期间逸出的常见错误,是在构造函数中启动一个线程)
this引用在构造函数完成前不会从线程逸出。只要构造函数结束前没有其他线程使用this引用,this引用就可以通过构造函数存储到某处。
3.3 线程封闭
一个可以避免同步的方式是不共享数据。如果数据仅在单线程中被访问,就不需要任何同步。线程封闭(Thread confinement)技术是实现线程安全的最简单的方式之一。
Swing发展了线程封闭技术。为了正确使用Swing,运行在不同实践线程(eventthread)的其他线程中的代码不应该访问这些对象。
JDBC(Java Database Connectivity)是另一种常见的使用线程限制的应用程序,
3.3.1 Ad-hoc线程限制
3.3.2 栈限制
栈限制是线程限制的一种特列,只有通过本地变量才可以触及对象。
对于基本类型的本地变量,你无法尝试去利用栈限制。由于午饭获得基本类型的引用,所以语言语义确保了基本本地变量总是线程封闭的。
在线程内部上下文使用非线程安全的对象仍然可以保证线程安全性。
3.3.3 ThreadLocal(线程本地)
线程本地(ThreadLocal)变量通常用于防止在基本可变的单体(Singleton)或全局变量的设计中,出现(不正确的)共享。
这项技术还用于描述下面的情况:一个频繁执行的操作既需要像buffer这样的临时对象,同时还需要避免每次都重新分配(realloate)该临时对象。
假设你正在将一个单线程的应用迁移到多线程环境中,你可以将共享的全局变量都转换为ThreadLocal类型,这样可以确保线程安全。
实现一个应用程序框架会广泛地使用ThreadLocal。
ThreadLocal很用以被滥用:比如将他们锁封闭的属性作为使用全局变量的许可证,或者是创建一种方法的参数“隐藏”起来的方法。
3.4 不可变性
为了满足同步的需要,另一种方法是使用不可变对象。
创建后状态不能被修改的对象叫做不可变对象。不可变对象天生是线程安全的。他们的常量(域)实在构造函数中创创建的,这些变量永远不会变。
不可变对象永远是线程安全的
不可变对象是简单的。它们只有一种状态,构造函数谨慎地控制着这个状态。
不可对象也是更安全的。
只用满足如下状态,一个对象才是不可变的:
1).它的状态不能再创建后再被修改;
2).所有域都是final类型,并且,
3).它被正确创建(创建器件没有发生this引用的逸出)。
“对象是不可变的”与“到对象的引用是不可变的”之间并不相等。
3.4.1 Final域
final关键字是来自C++的const机制,不过受到了很多限制。Final域是不能修改的,final域使得确保初始化安全性(initializationsafety)成为可能,初始化安全性让不可变性对象不需要同步就能自由地访问和共享。
3.5 安全发布
由于可见性的问题,容器还是会在其他线程中被设置为一个不一致的状态,即使它的不变约束已经在构造函数中得以正确简历!这种不正确的发布导致其他线程可以观察到“局部创建对象(partially constructed object)”
3.5.2 不可变对象与初始化安全性
对象的引用对其他线程可见,并不意味着对象的状态一定对消费线程可见。为了保证对象状态有一个一致性视图,我们需要同步。
即使发布对象引用时没有使用同步,不可变对象仍然可以被安全地访问。
不可变对象可以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。
3.5.3 安全发布的模式
如果一个对象不是不可变的,它就必须安全地发布,通常发布线程与消费线程都必须同步。
为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
1. 通过静态初始化器初始化对象的引用;
2. 将它的引用存储到volatile域或AtomicReference
3. 将它的引用存储到正确创建的对象的final域中
4. 将他的引用存储到由锁正确保护的域中
3.5.4 高效不可变对象(Effectively immutable objects)
一个对象在技术上是不可变的,但是它的状态不会在发布后被修改,这样的对象称作有效不可变对象。
任何线程都可以在没有额外的同步下安全地使用一个安全发布的高效不可变对象。
3.5.5 可变对象
发布对象的必要条件依赖于对象的可变性:
1. 不可变对象可以通过任意机制发布;
2. 高效不可变对象必须要安全发布;
3. 可变对象必须要安全发布,同时必须要线程安全或者被锁保护;
3.5.6 安全地共享对象
在并发程序中,使用和共享对象的一些最有效的策略如下:
线程限制:一个线程限制的对象,通过限制在线程中,被线程独占,且只能被占有它的线程修改。
共享只读(shared read-only):一个共享的只读对象,在没有额外同步的情况下,可以被对个线程并发地访问,但是任何线程都不能修改它,共享只读对象包括可变对象与高校不可变对象。
共享线程安全(shared thread-safe): 一个线程安全的对象在内部进行同步,所以其他线程无须额外同步,就可以通过公共接口随意地访问它;
被守护的(Guarded):一个被守护的对象只能通过特定的锁来访问,被守护的对象包括那些被线程安全对象封锁的对象,和已知被特定的锁保护起来的已发布对象。