设计模式之单例模式(一)

简介

         安全的单例模式在开发过程中,保障一个类仅有一个实例,并提供一个访问它的全局访问点。可以让类自己负责保存它唯一的实例,这个类可以保证没有其他实例可以被创建,并且提供一个访问该实例的方法。单例可以说是所有设计模式中最简单的模式了。

实现方式

         单例模式的实现方式有两种(懒汉模式和饿汉模式),具体在项目中的使用情况需要根据实际来判断。

饿汉模式

         饿汉模式相对于懒汉模式来说对于安全性更加容易保证,因为饿汉模式的对象实例化是随项目启动就创建的。不会有线程安全性问题。下面是饿汉模式代码示例:

package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public class SingletonPattern {

    //静态常亮,项目启动就创建好对象
    private static final SingletonPattern sp = new SingletonPattern();

    //私有构造函数不允许外部对象创建
    private SingletonPattern() {}

    //提供公有方法返回唯一实例
    public static SingletonPattern newInstances() {
        return sp;
    }

    //对象的其他方法等

}

饿汉模式优缺点:由于饿汉模式在项目启动时候就创建了对象,因此不会涉及多线程安全性问题,但是在启动过程中需要额外的资源。如果初始化的过程中需要大量的资源的话,可能会占用内存,如果只是简单的初始化可以使用者中模式,因为线程安全性容易控制。

懒汉模式

懒汉模式对象的初始化并不是在项目启动,而是在线程第一次访问的时候。因此,如果是高并发情况下会出现线程安全问题。下面将一步一步介绍:

初始代码:

package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public class SingletonPattern {

    //定义全局变量
    private static SingletonPattern sp = null;

    //私有构造函数不允许外部对象创建
    private SingletonPattern() {}

    //提供公有方法返回唯一实例
    public static SingletonPattern newInstances() {
        if (sp == null) {//1
            sp = new SingletonPattern();//2
        }//3
        return sp;
    }

    //对象的其他方法等

}

这里的代码有线程安全性问题在于:如果A和B两个线程访问,线程A执行代码1处的代码,这时候判断sp==null准备去执行2处的代码创建的时候,被线程B抢夺执行,这时候线程B执行1处的代码,判断sp==null然后执行了2处的代码创建了sp对象并返回。然后线程A继续执行2处的代码也创建了一个sp对象并返回,所有这里线程安全有问题。

 

同步机制代码:

同步机制代码:
package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public class SingletonPattern {

    //定义全局变量
    private static SingletonPattern sp = null;

    //私有构造函数不允许外部对象创建
    private SingletonPattern() {}

    //提供公有方法返回唯一实例
    public static SingletonPattern newInstances() {
        synchronized (SingletonPattern.class) {
            if (sp == null) {//1
                sp = new SingletonPattern();//2
            }//3
        }
        return sp;
    }

    //对象的其他方法等

}

这种代码存在性能方面的问题,为什么呢?因为每次线程过来都需要等待前面的线程执行完毕释放锁。当线程A和线程B同时执行的时候,线程A先获取到锁然后执行1处的代码判断sp==null是否成立,如果为true则创建对象并返回,如果为false则直接跳过2处的代码直接返回对象并是否锁,线程B获取到锁并继续执行。

 

双重检测机制代码:

package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public class SingletonPattern {

    //定义全局变量
    private static SingletonPattern sp = null;

    //私有构造函数不允许外部对象创建
    private SingletonPattern() {}

    //提供公有方法返回唯一实例
    public SingletonPattern newInstances() {
        if (sp == null) {//1
            synchronized (SingletonPattern.class) {//2
                if (sp == null) {//3
                    sp = new SingletonPattern();//4
                }//5
            }//6
        }//7
        return sp;
    }

    //对象的其他方法等

}

双重检测机制代码就解决了同步机制代码带来的性能问题。当线程A通过了1,2,3处的代码执行4处创建的时候,线程B抢夺到CPU执行了1处的代码,在2处的时候等待,当线程A创建执行完毕,线程B获取到锁执行3处的代码直接返回对象。可能有人觉得这样也要等待锁,不是和同步机制一样的问题了吗?我们试试下面的情景,假如sp对象已经创建完毕了,线程A和线程B都来执行,当执行到1处的代码就判断到了sp对象已经创建就可以直接返回了,而不用继续等待锁。

 

终极1版代码:

即使是双重检测机制版的代码也会引发问题。原因在于sf = new SingletonPattern();这个代码中,在JVM中创建对象的过程会涉及3个步骤:1)分配对象的内存空间。2)初始化对象。3)设置sf指向刚分配的内存地址。由于这3个步骤经过JVMCPU的优化,可能出现指令重拍的问题,即指向步骤可能不是1,2,3而是1,3,2。在“双重检测机制”的代码中如果出现指令重排:当线程A执行完毕了1,2,3处的代码在执行4处的代码准备去创建的时候执行到了1)分配对象的内存空间。3)设置sf指向刚分配的内存地址。还没来的及执行初始化,线程B抢夺CPU资源执行1处的代码,sf==null返回false,从而返回一个没有初始化的sf对象。

避免JVMCPU出现指令重排的情况,可以使用volatile关键字修饰sf对象。


package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public class SingletonPattern {

    //定义全局变量
    private volatile SingletonPattern sp = null;

    //私有构造函数不允许外部对象创建
    private SingletonPattern() {}

    //提供公有方法返回唯一实例
    public SingletonPattern newInstances() {
        if (sp == null) {//1
            synchronized (SingletonPattern.class) {//2
                if (sp == null) {//3
                    sp = new SingletonPattern();//4
                }//5
            }//6
        }//7
        return sp;
    }

    //对象的其他方法等

}

反射引发的单例模式安全性问题

package com.rabbit.pattern.singteton;

import java.lang.reflect.Constructor;

/**
 * Created by vip on 2018/1/30.
 */
public class Test {
    
    @org.junit.Test
    public void test1() throws Exception {
        //获取反射对象
        Class<SingletonPattern> clazz = SingletonPattern.class;
        //获取默认构造函数,由于私有需要设置可以访问
        Constructor<SingletonPattern> constructor = clazz.getDeclaredConstructor();
        //设置可以访问
        constructor.setAccessible(true);
        SingletonPattern s1 = constructor.newInstance();
        SingletonPattern s2 = constructor.newInstance();
        System.out.println(s1.equals(s2));
    }
}

以上代码输出结果是false,说明即使是单例模式也可以通过反射创建多个对象,这样就无法确保唯一性。

枚举实现单例模式:

通过枚举获取SingletonPattern对象,JVM会阻止反射获取枚举的私有构造函数。同时可以保证线程安全性。单例对象在枚举类被加载的时候就初始化。

package com.rabbit.pattern.singteton;

/**
 * Created by vip on 2018/1/30.
 */
public enum SingletonEnum {

    SingletonPattern;

}







展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie
应支付0元
点击重新获取
扫码支付

支付成功即可阅读