可见性
内存可见性, 是指当一个线程修改了对象状态后, 其他线程能够看到发生的状态变化.
如下图. 加锁的含义不仅仅局限于互斥行为, 还包括内存可见性, 为了确保所有的线程都能看到共享变量的最新值, 所有执行读操作或者写操作的线程都必须在同一个锁上同步.
volatile变量
当把变量声明为volatile类型后, 编译器与运行时都会注意到这个变量是共享的, 因此不会将该变量上的操作与其他内存操作一起重排序, volatile变量不会被缓存在寄存器或者其他对处理器不可见的地方, 因此在读取volatile类型变量时总会返回最新写入的值
volatile变量的正确使用方式包括:
- 确保自身状态的可见性
- 确保它们所引用对象的状态的可见性
- 标识一些重要的程序生命周期事件的发生, 例如初始化或关闭
加锁机制既可以确保可见性又可以确保原子性, 而volatile变量只能确保可见性.
发布与移除
发布(poblish)一个对象的意思是指, 使对象能够在当前作用域之外的代码中使用.
this溢出
当且仅当对象的构造函数返回时, 对象才处于可预测的和一致的状态, 因此, 当从对象的构造函数中发布对象时, 只是发布了一个尚未构造完成的对象
如下图, 当ThisEscape发布EventListener时, 也隐含的发布了ThisEscape实例本身, 因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用.
this引用溢出的常见错误:
- 在构造函数中启动一个线程, 在构造函数中创建一个线程时, 无论是显式创建(将this传递给构造函数)还是隐式创建(Thread/Runnable是其内部类), this引用都会被新创建的线程共享, 在对象尚未完全构造之前, 新的线程就可以看见它
- 在构造函数创建线程并没有错误, 但最好不要立即启动它, 而是通过一个start或initialize方法来启动.
- 可以使用一个私有的构造器函数和一个公共的方法, 如下图
- 在构造函数中调用一个可改写的实例方法时(既不是私有, 也不是final), 同样也会导致this引用溢出.
线程封闭
一种避免同步的方式就是不共享数据, 仅在单线程内访问数据, 称之为线程封闭(Thread Confinement).
示例:
- JDBC的connection对象, 线程会从连接池中获取一个connection对象, 使用完后再返还给连接池, connection对象只会分配给单个线程
- java语言中的局部变量以及核心库中的ThreadLocal类
不变性
不可变对象(Immutable Object)一定是线程安全的.
如果某个对象在被创建后其状态就不能被修改, 那么这个对象就称为不可变对象, 线程安全性是不可变对象的固有属性之一, 他们的不变性条件是由构造函数创建的.
当满足以下条件时, 对象才是不可变的:
- 对象创建以后其状态就不能修改
- 对象的所有域都是final类型
- 对象是正确创建的(在对象的创建期间, this引用没有溢出)
使用volatile类型来发布不可变对象
如下, OneValueCache是不可变的, VolatileCachedFactorizer通过使用volatile类型的引用来确保可见性, 在没有显式使用锁的情况下仍然是线程安全的
安全发布
要安全的发布一个对象, 对象的引用以及对象的状态必须同时对其他线程可见, 一个正确构造的对象可以通过以下方式来安全的发布:
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存在volatile类型的域或者AtomicReferance对象中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
在并发程序中使用和共享对象时的策略:
- 线程封闭, 只有一个线程拥有线程封闭的对象, 且只能由这个线程修改
- 只读共享, 可以并发访问只读对象
- 线程安全共享, 线程安全的对象可以共享
- 保护对象, 只能通过持有特定的锁来访问