Java并发编程之安全发布对象的四种方法

安全发布对象的四种方法

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

(2).讲对象的引用保存到volatile类型域或者AtomicReference对象中

(3).讲对象的引用保存到某个正确构造对象的final类型域

(4).讲对象的引用保存到一个由锁保护的域中

下面的写法分别体现上面的4种模式

(1)懒汉式单例模式

/**
 * 懒汉式单例
 * 在需要用到对象的时候才去创建对象,在多线程下是线程不安全的写法
 */
public class PublishExample1 {

    private static PublishExample1 publishExample1 = null;

    private PublishExample1(){

    }

    public static PublishExample1 getInstance(){
        //线程可能会同时走到这行代码同时判断为空,进而new出了不止一个对象,造成单例模式的失败
        if (publishExample1 == null){
            publishExample1 = new PublishExample1();
        }
        return publishExample1;
    }


}

最简单的懒汉式单例,多线程下是线程不安全的。

(2)饿汉式单例模式

/**
 * 饿汉式单例
 * 在类装载的时候创建对象,在多线程下是线程安全的
 */
public class PublishExample2 {

    private static PublishExample2 publishExample2 = new PublishExample2();

    public static PublishExample2 getInstance(){
        return publishExample2;
    }
}

或者还可以使用静态代码块来创建对象

public class PublishExample6 {

    private static PublishExample6 publishExample6 = null;

    static {
        publishExample6 = new PublishExample6();
    }

    public static PublishExample6 getInstance(){
        return publishExample6;
    }
}

在多线程下是线程安全的,但是缺点也很明显,就是当需要在类的构造函数中初始化过多资源的时候会造成速度上性能的减慢,而且必须要在程序中被引用到该对象才能不造成资源的浪费,因为该类实例对象的初始化是在类装载的时候就创建完成的。 

(3)懒汉式单例(使用静态方法synchronized关键字)

/**
 * 懒汉式单例
 * 在创建对象的静态工厂方法中加入了锁,在多线程下是线程安全的
 * 不推荐在多线程下使用这种单例模式创建对象
 */
public class PublishExample3 {

    private static PublishExample3 publishExample3 = null;

    private PublishExample3(){

    }

    public synchronized static PublishExample3 getInstance(){
        //线程可能会同时走到这行代码同时判断为空,进而new出了不止一个对象,造成单例模式的失败
        if (publishExample3 == null){
            publishExample3 = new PublishExample3();
        }
        return publishExample3;
    }


}

 这是线程安全的,加全局锁之后起到了每次只能有一个线程同时执行。缺点:性能低,多个线程需要等待上一个线程完成才能执行。不推荐使用。

(4)懒汉式单例(双重同步锁单例模式)

/**
 * 懒汉式单例(双重同步锁单例模式)
 * 在需要用到对象的时候才去创建对象,在多线程下是线程不安全的写法
 */
public class PublishExample4 {

    private static PublishExample4 publishExample4 = null;

    private PublishExample4(){

    }

    public static PublishExample4 getInstance(){
        //不用synchronized修饰整个方法,先在外边做一层非空判断,此操作时使多个线程没有同时
        //走到这一行代码时,又没有走完整个方法都能进行判空,不用等一个线程完全执行完才
        //去执行,减少性能上的开销
        if (publishExample4 == null){
            synchronized (PublishExample4.class){  //加锁
                if (publishExample4 == null){  //双重检测
                    publishExample4 = new PublishExample4();
                }
            }
        }
        return publishExample4;
    }


}

首先,这里还是使用了synchronized关键字来进行加锁,不过不同的是锁不是加在了整个静态方法上了,这样可以减少synchronized对性能产生的开销,因为当一个线程执行到创建对象这一行代码的时候,这时对象已经创建完成了,假如这个时候又有一个线程访问到这个方法,它可以不用等待上一个线程执行完就直接判断当前对象是否为空,这样大大地减少了synchronized关键字带来的性能上的开销,但是遗憾的是,在多线程下,这仍旧是线程不安全的。

理由:由于JVM和CPU的优化,会导致指令重排序的出现。重排序意思是JVM会让你的代码执行顺序发生改变,但这些改变并不会影响你代码顺序下的结果。

当我们创建对象时,主要可以分成3步:

1.分配对象的内存空间

2.初始化对象

3.设置instance指向刚分配的内存

但是当我们的JVM优化发生指令重排序之后可以变成

1.分配对象的内存空间

3.设置instance指向刚分配的内存

2.初始化对象

假如现在又两个线程A和B,A执行到了publishExample4 = new PulishExample4()这句代码,这句代码就是包含了第2步和第3步的操作,而B此时执行到了第一层判空的代码,如果发生了重排序,A先执行了第3步,此时第2步的初始化对象还没执行,就是说publishExample4这个引用指向的对象是空,而此时B刚好执行到判空条件引用就是等于空,直接返回该引用,最后得到的对象就是null。

那么我们怎样才能用双重同步锁单例来达到线程安全的使用尼?我们已经知道造成双重同步锁单例的线程不安全的原因是指令的重排序问题,那么怎样才能使禁止重排序尼?答案就是使用volatile关键字修饰引用变量。

private volatile static PublishExample5 publishExample5 = null;

使用volatitle关键字之后就能使得对于这个变量是不允许重排序的,此时就是线程安全的了。

(5)枚举单例模式

/**
 * 枚举单例模式
 * 在多线程模式下是线程安全的,并且推荐使用
 */
public class PublishExample7 {

    private PublishExample7() {

    }

    public static PublishExample7 getInstance() {
        return PublishEnum.INSTANCE.getInstance();
    }

    private enum PublishEnum {
        INSTANCE;

        private PublishExample7 publishExample7 = null;

        //JVM保证整个方法只能被执行一次
        PublishEnum() {
            publishExample7 = new PublishExample7();
        }

        public PublishExample7 getInstance() {
            return publishExample7;
        }
    }
}
 

猜你喜欢

转载自blog.csdn.net/weixin_37689658/article/details/88067619
今日推荐