对象的共享
可见性
有点类似于数据库事务中的脏读,不可重复读问题,不同的事务就相当于不同的线程,如果没有设置事务的隔离级别,即线程之间没有使用同步机制,这时候 A事务 对数据库的 修改操作由于事务没有提交,所以B事务是看不到A事务修改后的数据的,即发生了脏读。类似的在多线程没有使用同步的情况下,A线程对变量修改,B线程读取变量,在没有同步的情况下,AB的执行顺序是不能保证的。示例代码如下,输出结果可能为0或者没有输出。
package net.jcip.examples;
/**
* NoVisibility
* <p/>
* Sharing variables without synchronization
*
* @author Brian Goetz and Tim Peierls
*/
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序,进行一些意想不到的调整,在缺乏足够同步的多线程中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
失效的数据
NoVisibility中当读线程查看ready变量时,可能会得到一个已经失效的值。
非原子的64位操作
对于 非volatile类型的64位数值变量,JVM允许将64位的读操作或写操作分解为两个32位操作,当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位的组合结果。这样就不仅仅是内存可见性问题了。
加锁与可见性
加锁的含义不仅仅局限于线程间的互斥行为,还包括内存可见性。
volatile变量
当把变量声明为volatile变量之后,编译与运行时,JVM都会注意到这个变量时共享的,时内存可见的,在该变量上的操作,不会与其他的内存操作一起重排序,volatile不会被缓存在寄存器或者其他处理器不可见的地方,因此读取volatile变量时,总会返回最新写入的值。
volatile 只保证了内存的可见性,不保证线程之间的的互斥性
发布与逸出
发布一个对象的意思是,是对象能够在他的作用域之外的代码中被使用,逸出只的是不该发布的对象被发布出去。
- 发布一个对象
public static Set<Secret> knowSecrets;
public void initialize(){
knownSecrets = new HashSet<Secret>();
}
- 逸出对象
package net.jcip.examples;
/**
* UnsafeStates
* <p/>
* Allowing internal mutable state to escape
*
* @author Brian Goetz and Tim Peierls
*/
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] getStates() {
return states;
}
}
在这里大家不要误解本书作者的意思,在我们的日常开发中pojo类或者我们的domain经常以这种形式出现,但是在这里作者想表达的意思是对于复杂的业务对象来说的,并不是针对pojo对象。业务对象的可变状态变量的所有操作应该是由这个业务对象自己控制的,而不是把他的私有状态变量发布出去,这样会造成很多不确定、不可控的操作,当然在多线程环境中也不是线程安全的。 可控的复杂的业务对象模型(伪代码)如下。
class ComplexControlStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] opration1() {
...一系列复杂的操作
返回 states 的拷贝对象
}
public String[] opration2() {
...一系列复杂的操作
返回 states 的拷贝对象
}
public String[] opration3() {
...一系列复杂的操作
返回 states 的拷贝对象
}
}
- this的隐式逸出
ThisEscape发布EventListener的时候,ThisEscape实例本身也会溢出,因为这个内部类的实例中包含了对ThisEscape实例的隐含引用。个人理解如下(纯属个人理解): 匿名内部类可以访问外部类的final域,如果final域为一个指向复杂对象的引用,这时候就相当于将这个复杂对象发布了出去,从而造成逸出
package net.jcip.examples;
/**
* ThisEscape
* <p/>
* Implicitly allowing the this reference to escape
*
* @author Brian Goetz and Tim Peierls
*/
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
- 书中的解释
在构造过程中使this引用一处的一个常见错误是,在构造函数中启用一个线程,无论是显示构建(使用构造函数)还是隐式的构建(使用Thread或Runnable),this引用都会被新创建的线程共享,在对象未完全构造完成之前,新的线程可以看见他。
线程封闭
当访问可变的共享变量时,通常需要同步,一种避免使用同步的方法就是不共享,即将变量封闭在单个线程中。这种技术叫做线程封闭。
常见的应用是JDBC的Connection对象。数据库连接池在将一个connection发布给一个线程之后,在connection还池之前不会再将它分配给其他线程。 详解 见ThreadLocal对象。
Ad-hoc线程封闭
指,维护线程封闭性的职责完全由程序实现来承担.。
例子:
volatile变量上存在一种特殊的线程封闭,如果能保证只有一个线程可以对volatile变量进行写操作,而其他任意多的变量都只能进行读操作,这时候就相当于将volatile的写操作封装在了线程内,由于volatile变量的内存可见性,可以保证线程安全。
栈封闭
栈封闭与JVM内存模型相关,当一个线程执行某个方法的时候,JVM会给这个线程单独开辟一个线程栈空间,里面存储了这个方法所有局部变量,而且这些变量是被该线程独享的。栈封闭是线程封闭的的一种特例。
ThreadLocal 类
不要滥用ThreadLocal,列如将所有全局变量都作为ThreadLocal对象,或者作为一种隐藏方法参数的手段。ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,使用时需格外小心。
- 举一个在实际开发中遇到的例子。
之前做过一个struts2
转spring mvc
的任务,代码中大量使用了,Struts2的ActionContext
对象来获取Request或Session对象,并且耦合在service层甚至是dao层。总之重写的工作量很大,所以通过一个顶层的Filter来将Request对象保存在ThreadLocal中,并模拟了一个ActionContext
对象来代替struts2的ActionContext
。
不变性
不可变对象一定是线程安全的
安全的发布
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的变量中或AtomicReferance中。
- 将对象的引用保存到某个正确构造对象的final域中
- 将对象的引用保存在一个由锁保护的域中 。