3.1 可见性
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。
3.1.1 失效数据
3.1.2 非原子的64位操作
最低安全性:即使是失效值,但至少是之前的值,不是随机值。
例外:非volatile类型的64位数值变量(long、double)。
Java内存模型要求:变量的读取或写入都是原子操作,但是对非volatile的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。
当读取一个非volatile的long变量时,如果对该变量的读写操作在不同的线程中执行,就可能督导某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据,在多线程中使用共享且可变的long和double等是不安全的,除非用volatile声明、或用锁保护起来。
3.1.3 加锁与可见性
访问某个共享且可变的变量时要求素有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。
3.1.4 volatile变量
Java提供了一种稍弱的同步机制——volatile,用来确保将变量的更新操作通知到其他线程。
对于volatile变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其它内存操作一起重排序。volatile变量不会被缓存在寄存器或其它处理器不可见的地方,因此读取volatile变量时,总会返回最新写入的值。
访问volatile时不会加锁,因此比synchronize轻量。写入相当于退出同步代码块、读取相当于进入。通常不建议使用
例如:典型的数绵羊方法
volatile boolean asleep;
while (!asleep)
countSomeSheep();
volatile变量只能确保可见性,不能确保原子性。(递增操作++的原子性也不能保证)
当且仅当满足一下所有条件,才使用volatile:
- 对变量的写入操作不依赖变量的当前值,或者能保证只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁
3.2 发布与逸出
发布:publish,使对象能够在当前作用域之外的代码中使用。
逸出:escape,当某个不应该发布的对象被发布时,成为逸出。例如,如果在对象构造完成之前就发布,就会破坏线程安全性。
- 不要在构造过程中使this引用逸出
例如:构造函数中创建了一个匿名内部类(内部类实例中包含了对类类实例的隐含引用);构造函数中启动一个线程(在外部启动没关系),this引用会被新创建的线程共享,在未完成构造之前,新线程就可以看到它。
3.3 线程封闭
线程封闭:如果仅在单线程内访问数据,就不需要同步。例如:JDBC的connection对象,在返回之前,连接池不会再将它分配给其他线程。
3.3.1 Ad-hoc线程封闭
Ad-hoc指:维护线程封闭性的职责完全由程序实现来承担。
脆弱:尽量不用,因为没有任何一种语言特性,能将对象封闭到目标线程上。事实上,对现成封闭对象的引用通常保存在公有变量中。
3.3.2 栈封闭
局部变量、基本变量、未发布的引用
3.3.3 ThreadLocal
ThreadLocal使线程中的某个值与保存值的对象关联起来。提供了get和set方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在set时设置的最新值。
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象。
例如:框架中的事务上下文课存在ThreadLocal中。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此不能滥用。
3.4 不变性
不可变对象一定是线程安全的
final对象只是域不可变,但引用的对象是可变的。
满足以下条件时,final对象才是不可变的:
- 对象创建以后其状态就不能修改
- 对象的所有域都是final类型
- 对象是正确创建的(创建期间,this引用没有逸出)
3.4.1 final域
final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无须同步。
除非需要,所有域都应该声明为私有域、final域。