安全发布对象

1.什么叫安全发布对象

发布对象:使一个对象能够被当前范围之外的代码使用。

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

 

不安全发布对象实例:

/**
 * 一个类中有一个私有的容器,并为其提供getter方法,容器中值是可以改变的
 * 存在多线程并发操作安全隐患
 */
@Slf4j
@NotThreadSafe
public class UnSafePublish {
    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 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)线程看到的被发布线程的引用是最新的,然而被发布对象的状态却是过期的

 

2.安全发布对象实例(多种单例模式演示)

如何安全发布对象?

1.在静态初始化函数中初始化一个对象的引用。

2.将对象的引用保存到volatile类型域或者AtomicReference对象中。

3.将对象的引用保存到某个正确构造对象的final域类型域中。

4.将对象的引用保存到一个由锁保护的域中。

 

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

 

1、懒汉式(不安全)

/**
 * 懒汉模式
 * 单例的实例在第一次使用时创建
 */
@NotThreadSafe
public class SingletonExample1 {
    //单例对象
    private static SingletonExample1 instance = null;

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

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

}

 分析:

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

 

2、懒汉式(synchronized)

/**
 * 懒汉模式
 * 单例的实例在第一次使用时创建
 * 存在线程安全隐患 在静态方法上加synchronized关键字能够保证线程安全
 * 但影响了性能 在并发获取该单例实例调用该静态方法时
 * 所以需要将synchronized关键字下沉到方法体中的判空里面去
 */
@ThreadSafe
public class SingletonExample3 {
    //单例对象
    private static SingletonExample3 instance = null;

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

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

}

  分析:
1、使用synchronized修饰静态方法后,保证了方法的线程安全性,同一时间只有一个线程访问该方法。
2、有缺陷:会造成性能损耗。

 

3、懒汉式双重同步锁单例模式 + volatile禁止指令重排序

/**
 * 懒汉模式 ==> 双重同步锁单例模式 + volatile禁止指令重排序
 * 单例的实例在第一次使用时创建
 * 存在线程安全隐患 在静态方法上加synchronized关键字能够保证线程安全
 * 但影响了性能 在并发获取该单例实例调用该静态方法时
 * 所以需要将synchronized关键字下沉到方法体中的判空里面去
 */
@ThreadSafe
public class SingletonExample4 {
    //单例对象
    private volatile static SingletonExample4 instance = null;//volatile禁止指令重排序

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

    //1.分配对象的内存空间
    //2.初始化对象
    //3.设置instance指向刚分配的内存

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

    //1.分配对象的内存空间
    //3.设置instance指向刚分配的内存
    //2.初始化对象

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

}

 

4、饿汉式(最简式)

/**
 * 饿汉模式
 * 单例的实例在类装载时进行创建
 */
@ThreadSafe
public class SingletonExample2 {
    //单例对象
    private static SingletonExample2 instance = new SingletonExample2();

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

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

}

 分析:
1、饿汉模式由于单例实例是在类装载的时候进行创建,因此只会被执行一次,所以它是线程安全的。
2、该方法存在缺陷:如果构造函数中有着大量的事情操作要做,那么类的装载时间会很长,影响性能。如果只是做的类的构造,却没有引用,那么会造成资源浪费。
3、饿汉模式适用场景为:(1)私有构造函数在实现的时候没有太多的处理(2)这个类在实例化后肯定会被使用。

5、饿汉式(静态块初始化)

/**
 * 饿汉模式
 * 单例的实例在类装载时进行创建
 */
@ThreadSafe
public class SingletonExample5 {
    //单例对象
    private static SingletonExample5 instance = null;

    static {
        instance = new SingletonExample5();
    }

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

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

}

分析:
1、除了使用静态域直接初始化单例对象,还可以用静态块初始化单例对象。
2、值得注意的一点是,静态域与静态块的顺序一定不要反,在写静态域和静态方法的时候,一定要注意顺序,不同的静态代码块是按照顺序执行的,它跟我们正常定义的静态方法和普通方法是不一样的。

 

6、枚举式

@ThreadSafe
@ReCommend
public class SingletonExample6 {

    private SingletonExample6(){}

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

    private enum Singleton{
        INSTANCE;

        private SingletonExample6 singleton;

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

        public SingletonExample6 getSingleton() {
            return singleton;
        }
    }
}

分析:

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

 

posted @ 2020-01-08 16:02  张天赐的博客  阅读(229)  评论(0编辑  收藏  举报