0%

安全发布对象

安全发布对象

发布

简单来说就是提供一个对象的引用给作用域之外的代码。比如return一个对象,或者作为参数传递到其他类的方法中。

逸出

如果一个类还没有构造结束就已经提供给了外部代码一个对象引用即发布了该对象,此时叫做对象逸出,对象的逸出会破坏线程的安全性。

1
2
3
4
5
6
7
class UnsafeStates{
private String[] states = new String[]{"AK", "AL"};

public String[] getStates(){
return states;
}
}

states变量作用域是private而我们在getStates方法中却把它发布了,这样就称为数组states逸出了它所在的作用域。

然而更加隐蔽和需要我们注意的是this逸出,这个问题要引起重点关注。观察以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThisEscape{
private int value;
public ThisEscape(EventSource source){
source.registerListener{
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
}
}
//一些初始化工作
value = 7;
}

public void doSomething(Event e){
System.out.println(value);
}

}

在构造方法中我们定义了一个匿名内部类,匿名内部类是一个事件监听类,当事件监听类注册完毕后,实际上我们已经将EventListener匿名内部类发布出去了,而此时我们实际上已经携带了this逸出,重点在于这个时候我们还有一些初始化工作没有做完,这也就是上面所说的,一个类还没有构造结束我们已经将发布了。

安全发布对象

安全发布对象,共有四种方法

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型域或者AtomicReference对象中
  • 对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

单例模式

下面我们用各种单例模式来演示其中的几种方法:

懒汉式

懒汉式(最简式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonExample {
//私有构造函数
private SingletonExample(){
}

//单例对象
private static SingletonExample instance = null;

//静态工厂方法
public static SingletonExample getInstance(){
if(instance==null){
return new SingletonExample();
}
return instance;
}
}

在多线程环境下,当两个线程同时访问这个方法,同时制定到instance==null的判断。都判断为null,接下来同时执行new操作,这样类的构造函数被执行了两次。一旦构造函数中涉及到某些资源的处理,那么就会发生错误。所以说最简懒汉式是线程不安全的

懒汉式(synchronized)

1
2
3
4
//在类的静态方法上使用synchronized修饰
public static synchronized SingletonExample getInstance(){
//code
}

使用synchronized修饰静态方法后,同一时间只有一个线程访问该方,保证了线程安全性,但会造成性能损耗。

懒汉式(双重同步锁模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SingletonExample {
// 私有构造函数
private SingletonExample() {
}
// 单例对象
//在对象声明时使用volatile关键字修饰,阻止CPU的指令重排。
private volatile static SingletonExample instance = null;
// 静态的工厂方法
public static SingletonExample getInstance() {
if (instance == null) { // 双重检测机制
synchronized (SingletonExample.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample();
}
}
}
return instance;
}
}

先判断一次后,再锁定整个类,再加上一次的双重同步锁,保证了最大程度上的避免耗损性能。

为什么要使用volatile关键字修饰对象引用?

执行new操作的时候,CPU会进行了三次指令:

  1. memory = allocate() 分配对象的内存空间
  2. ctorInstance() 初始化对象
  3. instance = memory 设置instance指向刚分配的内存

在程序运行过程中,CPU为提高运算速度会做出违背代码原有顺序的优化。我们称之为乱序执行优化或者说是指令重排。
那么上面知识点中的三步指令极有可能被优化为1、3、2的顺序。当我们有两个线程A与B,A线程遵从1、3、2的顺序,经过了两此instance的空值判断后,执行了new操作,并且cpu在某一瞬间刚结束指令3,并且还没有执行指令2。而在此时线程B恰巧在进行第一次的instance空值判断,由于线程A执行完3指令,为instance分配了内存,线程B判断instance不为空,直接执行return,返回了instance,这样就出现了错误。

img

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonExample {
// 私有构造函数
private SingletonExample() {

}
// 单例对象
private static SingletonExample instance = new SingletonExample();

// 静态的工厂方法
public static SingletonExample getInstance() {
return instance;
}
}

饿汉模式由于单例实例是在类装载的时候进行创建,因此只会被执行一次,所以它是线程安全的。如果构造函数中有着大量的操作要做,类的装载时间会很长。要是只做了类的构造,却没有使用构造好的对象,那么会造成资源浪费。所以饿汉模式适用于以下场景:私有构造函数在实现的时候没有太多的处理,占用系统资源少;这个类在实例化后很大概率会被使用。

枚举式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SingletonExample {

private SingletonExample() {
}

public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}

private enum Singleton {
INSTANCE;
private SingletonExample singleton;

Singleton() {
singleton = new SingletonExample();
}

public SingletonExample getInstance() {
return singleton;
}
}
}

由于枚举类的特殊性,因为Java保证枚举类的每个枚举都是单例,上诉代码构造函数Singleton方法只会被实例化一次,且是这个类被调用之前。对比懒汉与饿汉模式,它的优势很明显