第三章 对象的共享
synchronize 的另一个作用 : 内存可见性.
3.1 可见性
为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制.
指令重排序 : 编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段.
3.1.1 失效数据
非线程安全下读取变量时可能读取到一个失效的值.
3.1.2 非原子的64位操作
最低安全性: 非线程安全下读取变量时可能得到一个失效值,但至少这个值是由之前某个线程设置的值, 而不是一个随机值.
非原子协定: 对于非volatile 类型的 long 和 double 变量,JVM 允许将64位的读写操作分解为两个32位的操作.
多线程下对该类型的变量进行读写操作时可能读取到某个值的高32位和另一个值的低32位.
一般商用虚拟机几乎都把该操作实现为具有原子性的操作.
3.1.3 加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性.
在同步代码块进入之前进行 lock 操作, 退出时进行 unlock 操作.
lock : 对一个变量执行 lock 操作, 将会清空工作内存中此变量的值,重新执行 load 或 assign 初始化变量的值.
unlock : 对一个变量执行 unlock 之前, 必须将此变量同步回主内存.
3.1.4 Volatile 变量
一种比synchronize 更轻量级的同步机制.
确保将变量的更新操作通知到其他线程. 当变量的值改变后,立即对其他线程可见.
volatile 变量在赋值后会多执行一个"lock addl $0x0, (%esp)" 操作,
这个操作相当于内存屏障, 指令重排序时不能把后面的指令重排序到内存屏障之前的位置,
这个操作是将ESP寄存器的值加0, 这个空操作使本CPU的Cache写入内存, 并使得其他cache的变量无效化. 相当于 store, write 两个原子操作. 可让volitale 变量的修改对其他线程立即可见.
一般用作某个状态的标志.
加锁机制可以确保可见性和原子性. volatile 只能确保可见性.
3.2 发布与逸出
发布(Publish) 一个对象 :使对象能够在当前作用域外的代码中使用.
逸出 : 某个不应该发布的对象被发布.
不要在对象构造过程中使 this 引用逸出.
常见错误: 在构造方法中启动线程.
3.3 线程封闭
定义 : 仅在单线程内访问数据.
Java提供机制帮助维持线程封闭; 局部变量 和 ThreadLocal类.
3.3.1 Ad-hoc线程封闭
维护线程封闭的职责完全有程序实现来承担.
3.3.2 栈封闭
定义 : 线程封闭的一种特例,在栈封闭中, 只能通过局部变量才能访问对象.
局部变量的固有属性之一就是封闭在执行线程中,他们位于执行线程的栈中,其他线程无法访问这个栈.
维持栈封闭时,需要确保引用的对象不会被发布.
3.3.3 ThreadLocal 类
这个类使线程中的某个值 与 ThreadLocal 实例关联起来,为该线程存储一份该变量的副本.
ThreadLocal 变量类似与全局变量(线程内各个方法及层次都可以使用该变量), 降低代码的可重用性, 在类之间引入隐含的耦合性.
3.4 不变性
不可变对象一定是线程安全的.
不可变对象的条件:
- 对象创建后其状态不能修改;
- 对象的所有域都是 final 类型;
- 对象是正确的创建的(没有 this 引用逸出)
3.4.1 final 域
final 修饰的对象不可以改变它的引用, 但是如果引用的对象是可变的,则可以修改引用的对象的属性.
3.4.2 使用 volatile + 不可变对象实现线程安全操作
当访问和更新多个相关变量出现竞态条件时, 可以使用 volatile 修饰的不可变对象来解决线程安全问题.
3.5 安全发布
一个尚未完全创建的对象不拥有其完整性.
创建对象的基本步骤:
- 分配内存空间
- 默认初始化
- <init>
先检查后执行的getInstance() 就可能访问到一个不完整的对象.
使用静态内部类实现单例时,虚拟机的内部同步机制会保证其线程安全性.