4-1 安全发布对象-发布与逸出

image-20190802073314325

发布对象

使一个对象能够被当前范围之外的代码所使用。
在我们的日常开发中,我们经常要发布一些对象,比如通过类的非私有方法返回对象的引用,或者通过公有静态变量发布对象。

对象逸出

一种错误的发布。当一个对象还没有构造完成时,就使它被其他线程所见。

代码演示

不安全发布对象

public class UnsafePublish {

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String[] states = {"a", "b", "c"};

//类的非私有方法,返回私有对象的引用
public String[] getStates() {
return states;
}

public static void main(String[] args) {
UnsafePublish unsafePublish = new UnsafePublish();
log.info("{}", Arrays.toString(unsafePublish.getStates()));

unsafePublish.getStates()[0] = "d";
log.info("{}", Arrays.toString(unsafePublish.getStates()));
}

分析:

  • 这个代码通过public访问级别发布了类的域,在类的任何外部的线程都可以访问这些域
  • 我们无法保证其他线程会不会修改这个域,从而使私有域内的值错误(上述代码中就对私有域进行了修改)

对象逸出

public class Escape {

private Integer thisCanBeEscape = 0;

public Escape () {
    new InnerClass();
    thisCanBeEscape = null;
}

//内部类构造方法调用外部类的私有域
private class InnerClass {

    public InnerClass() {
        log.info("{}", Escape.this.thisCanBeEscape);
    }
}

public static void main(String[] args) {
    new Escape();
}
}

分析:

这个内部类的实例里面包含了对封装实例的私有域对象的引用,在对象没有被正确构造完成之前就会被发布,有可能有不安全的因素在里面,会导致this引用在构造期间溢出的错误。
上述代码在函数构造过程中启动了一个线程。无论是隐式的启动还是显式的启动,都会造成这个this引用的溢出。新线程总会在所属对象构造完毕之前就已经看到它了。
因此要在构造函数中创建线程,那么不要启动它,而是应该采用一个专有的start或者初始化的方法统一启动线程
这里其实我们可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册等等,这样才可以避免错误
——————————————————————————————————————————————————-
如果不正确的发布对象会导致两种错误:
(1)发布线程意外的任何线程都可以看到被发布对象的过期的值

( 2)线程看到的被发布线程的引用是最新的,然而被发布对象的状态却是过期的

image-20190802073720423

image-20190802073743762


4-2 安全发布对象-四种方法

![image-20190802073846857](../../../../../Users/apple/Library/Application Support/typora-user-images/image-20190802073846857.png)

四种方法概述

  • 在静态初始化函数中初始化一个函数的引用
  • 将对象的引用保存到volatile类型域AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中(后续再进行补充! )
  • 将对象的引用保存到一个由锁保存的域中

通过在Spring框架中构造线程安全且只被初始化一次的不同单例(Singleton)进行演示。

懒汉模式:(单例实例在第一次使用时进行创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@NotThreadSafe
public class SingletonExample1 {

// 私有构造函数
private SingletonExample1() {
}

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

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

分析一下:

  1. 单线程时使用没问题。
  2. 多线程时,当多个线程同时判断到instance == null,那么该多个线程都会创建一个实例,即非线程安全,因此该方法无法保证实例只被初始化一次。
饿汉模式:(单例实例在类装载时进行创建)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ThreadSafe
public class SingletonExample2 {

// 私有构造函数
private SingletonExample2() {

}

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

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

分析一下:

  1. 该方式为线程安全。
  2. 不足:如果单例类的构造方法中有较多的处理逻辑,导致类加载慢,可能会引起性能问题。
  3. 由于是饿汉模式,如果只声明了该类但实际不调用该类,即造成系统资源的浪费。
改造的懒汉模式-1:synchronized标识工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ThreadSafe
@NotRecommend
public class SingletonExample3 {

// 私有构造函数
private SingletonExample3() {
}

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

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

分析一下:

  1. 是线程安全的。
  2. 通过给工厂方法添加synchronized关键字实现。
  3. 不推荐使用:通过阻塞线程->牺牲性能,达到线程安全目的。
改造的懒汉模式-2:(双重同步锁单例模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@NotThreadSafe
public class SingletonExample4 {

// 私有构造函数
private SingletonExample4() {
}

// JVM和cpu优化,发生了指令重排

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

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

// 静态的工厂方法
public static SingletonExample4 getInstance() {
if (instance == null) { // 双重检测机制 // B
synchronized (SingletonExample4.class) { // 同步锁
if (instance == null) {
instance = new SingletonExample4(); // A - 3
}
}
}
return instance;
}
}

分析一下:

  1. 非线程安全
  2. 将synchronized标识下沉到方法的实现中
  3. 外层instance == null和方法实现中的synchronized标识共同保证只有一个线程进行初始化。
  4. 内层的instance == null为防止上一时刻中可能存在的线程进行了初始化。
非线程安全分析:

当执行实例初始化instance = new SingletonExample4()时,CPU内部过程为:

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

但是!!
多线程环境中,由于JVM和CPU优化,会发生指令重排,CPU内部顺序为:(其中的1、2、3是指令间的原始顺序)

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

因此,此时的双重同步锁机制中产生了变化:

若两个线程A和B,其中线程A执行到初始化instance = new SingletonExample4(),此时恰好正处于指令重排的instance = memory ,即设置instance指向刚分配的内存,同时线程B恰好处于外层的instance == null判断,发现此时内存中该instance指向的内存地址不为nul,则会直接return instance,但此时的instance只是分配了内存还未进行初始化,即产生错误! 为非线程安全!!

但是!!!!!
既然是内存中实例未完全初始化,怎么解决呢??
想起来之前说过的volatile关键字的用法了没???
它通过加入内存屏障,限制JVM或CPU进行指令重排!!

将该实例用volatile标识声明:

1
private volatile static SingletonExample5 instance = null;

此时,该. 双重同步锁单例模式. 就是线程安全的了!!


实例枚举模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SingletonExample7 {

// 私有构造函数
private SingletonExample7() {
}

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

//枚举类
private enum Singleton {
INSTANCE;

private SingletonExample7 singleton;

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

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

分析一下:

  1. 枚举模式是最安全的! 较推荐的写法!
  2. 通过枚举类中指定一个单例Singleton的实例INSTANCE枚举实现。
  3. 其中Singleton(){xxxx}域,是通过JVM保证这个方法在被调用前初始化,并绝对只调用一次。
  4. 相比于懒汉模式,它的安全性更易保证;相比于饿汉模式,它是在实际调用时才进行初始化,并直接取到其值,不会有系统资源的占用浪费。

总结

image-20190802093554075