第三章:对象的共享
如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。
一、可见性
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些问题想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
1、失效数据
在缺少同步的情况下,JAVA内存模型允许编译对操作顺序进行重排序,并将数值缓存在寄存器中。 此外,它还允许CPU对操作顺序进行重排序,并将数值缓存在处理器特定的缓存中。
2、非原子的64位操作
关键字volatile来声明它们或者用锁保护起来。
3、加锁与可见性
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。
4、volatile变量
volatile是一种稍弱的同步机制,用来确保将变量的更新操作通知到其它线程,在读取volatile类型的变量时总会返回最新写入的值。
访问volatile变量不会执行加锁操作,因此不会使线程阻塞,volatile是一种比synchronized关键字更轻量的同步机制。
使用volatile的前提条件
① 对变量的写入不依赖当前值,或者你能确保只有单个线程更新变量的值。
② 在访问变量时不需要加锁。
二、发布与逸出
1、使对象能够在当前作用域之外的代码中使用,叫发布,
当某个不应该发布的对象被发布时,叫逸出。
2、实例的对象构造过程
不要在构造过程中使this引用逸出。
如果想在构造函数中注册一个事件监听器或者启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。
三、线程封闭
实现线程安全性最简单的方式之一:仅在线程内访问的数据。
ThreadLocal类
ThreadLocal类能使线程中的某个值与保存值的对象关联起来。TheadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新值。
当某个线程初次调用TheadLocal.get方法时,就会调用initialValue来获取初始值。这些特定于线程的值保存在Thead对象中,当线程终止后,这些值会作为垃圾回收。
假设将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为TheadLocal对象,可以维持线程安全性。然而,如果将应用程序范围内的缓存转换为线程局部的缓存,就不会有太大的作用。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
四、不变性
不可变对象一定是线程安全的。
不可变对象只有一种状态,并且该状态由构造函数来控制。
关键字final用于构造不可变对象。
1、除非需要更高的可见性,否则应将所有的域都声明为私有域,除非需要某个域是可变的,否则应将其声明为final域。
2、使用volatile类型来发布不可变对象
使用指向不可变容器对象的volatile类型引用以缓存最新的结果。
五、安全发布
1、不正确的发布:正确的对象被破坏
尚未完全创建的对象没有完整性。由于没有使用同步来确保Holder对象对其他线程可见,因此将Holder称为”未被正确发布”。在未被正确发布的对象中存在两个问题。首先,除了发布对象的线程外,其他线程可以看到的Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。然而,更糟糕的情况是,线程看到Holder的引用的值是最新的,但Holder状态的值却是失效的。
2、不可变对象与初始化安全性
为了维持这种初始化安全性保证,必须满足不可变性的所有要求:状态不可改变,所有域都是final类型以及正确的构造函数。
3、安全发布的常用模式
① 在静态初始化函数中初始化一个对象引用。
② 将对象的引用保存到volatile类型的域或者AtomicReference对象中
③ 将对象的引用保存到某个正确构造对象的final类型域中
④ 将对象的引用保存到一个由锁保护的域中
要安全地共享可变对象,必须安全地发布,并且必须是线程安全的或者由某个锁保护起来。
4、安全的共享对象
在并发程序中使用和共享对象时,可以使用一些实用的策略:
① 线程封闭
线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
② 只读共享
在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
③ 线程安全共享
线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口进行访问
④ 保护对象
通过持有特定的锁来访问