欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单例模式的五种写法

程序员文章站 2022-07-13 23:38:04
...

一、饿汉式单例

饿汉式单例模式,实例对象在初始化的时候创建,不管有没有得到调用。好处是没有线程安全的问题,坏处是浪费内存空间。

//饿汉式单例
public class ehan {

    /**
     * 存在的问题:
     * 饿汉式强调的是一上来就初始化对象,初始化的对象里面又有很多像如下定义的很多数组同时也被初始化,
     * 但没有得到调用,就会存在浪费空间的情况
     * */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];


    //构造器私有,保证别的方法无法调用生成这个对象
    private ehan(){}

    //实例对象初始化时创建
    private static final ehan HUNGRY = new ehan();

    public static ehan getInstance(){
        return HUNGRY;
    }

    public static void main(String[] args) {
        ehan ehan1 = ehan.getInstance();
        ehan ehan2 = ehan.getInstance();
        System.out.println(ehan1==ehan2);//true
    }

}

二、懒汉式

我们知道饿汉式在初始化的时候就完成了对象的创建,但存在浪费空间的问题,因此,懒汉式单例模式应运而生。
懒汉式顾名思义就是只有当需要的时候才会去初始化数据。

//懒汉式单例模式
public class lanhan {
    //构造器私有
    private lanhan(){
    }

    private static lanhan lazyMan;

    public static lanhan getInstance(){
        if(lazyMan==null){
            lazyMan = new lanhan();
        }
        return lazyMan;
    }

    public static void main(String[] args) {

        lanhan instance1 = lanhan.getInstance();
        lanhan instance2 = lanhan.getInstance();
        System.out.println(instance1==instance2);
    }


}

三、DCL(Double Check Lock)懒汉式

由于上述的懒汉式在单线程里确实是安全的,但是在多线程中,其实是并不安全的,我们可以通过以下代码测试:
我们会发现,得出的结果并不是单例的。

//懒汉式单例模式
public class lanhan {
    //构造器私有
    private lanhan(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }

    private static lanhan lazyMan;

    public static lanhan getInstance(){
        if(lazyMan==null){
            lazyMan = new lanhan();
        }
        return lazyMan;
    }

    /**
     * 单线程下确实单例安全,但是多线程下是存在安全问题的
     *  我们得加锁
     * */
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                lanhan.getInstance();
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);

        lanhan instance1 = lanhan.getInstance();
        lanhan instance2 = lanhan.getInstance();
        System.out.println(instance1==instance2);

    }

}

因此我们得给他上锁,保证一个线程只能创建一个对象,也有且只能有一个对象。同时为了防止指令重排现象出现,我们得对变量加volatile锁。

//DCL懒汉式单例模式
public class lanhan {
    //构造器私有
    private lanhan(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }
	//防止指令重排
    private static volatile lanhan lazyMan;

    //双重检测锁模式--DCL懒汉式检测
    public static lanhan getInstance(){
        //先判断有没有对象实例被创建,没有的话就锁住这个类,确保只有一个Class对象
        if(lazyMan==null){
            synchronized (lanhan.class){
                if(lazyMan==null){
                    //但是还可能存在一个问题,因为new lanhan()不是一个原子操作
                    /*
                     * 1.分配内存
                     * 2.执行构造方法
                     * 3.指向地址
                     * 有可能存在的问题就是由于指令重排,它的执行顺序完全有可能是132,这个时候由于指向了内存地址,
                     * 会被以为创建了对象,导致无法创建对象,为了保证不发生指令重排现象,我们必须加上volatile关键字
                     * */
                    lazyMan = new lanhan();
                }
            }
        }
        return lazyMan;
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                lanhan.getInstance();
            }).start();
        }

        TimeUnit.SECONDS.sleep(3);

        lanhan instance1 = lanhan.getInstance();
        lanhan instance2 = lanhan.getInstance();
        System.out.println(instance1==instance2);

    }

}

四、静态内部类

可以延时加载,并且线程安全

//静态内部类实现
public class Holder {
    //构造器私有
    private Holder(){

    }

    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    //静态内部类
    public static class InnerClass{
    private static final Holder HOLDER = new Holder();
    }

    public static void main(String[] args) {
        Holder instance = Holder.getInstance();
        Holder instance2 = Holder.getInstance();
        System.out.println(instance==instance2);
    }

}

五、反射破坏单例模式

java中存在反射机制,能够改变内部的private关键字,因此使用private修饰的其实都不算是安全的,由此引出枚举单例模式。

//反射破坏单例模式
public class fanshe {
    //构造器私有
    private fanshe(){
        System.out.println(Thread.currentThread().getName()+"OK");
    }

    private static volatile fanshe lazyMan;

    public static fanshe getInstance(){
        if(lazyMan==null){
            synchronized (fanshe.class){
                if(lazyMan==null){
                    lazyMan = new fanshe();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        fanshe instance = fanshe.getInstance();
        //利用反射获取其对应的构造器方法
        Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
        //无视私有构造器,通过反射创建对象
        declaredConstructor.setAccessible(true);
        fanshe instance2 = declaredConstructor.newInstance();

        System.out.println(instance==instance2);

    }
}

如何解决反射破坏单例现象?第一种方法,我们可以在构造器里面对类加锁。

//反射破坏单例模式
public class fanshe {
    //构造器私有
    private fanshe(){
       synchronized (fanshe.class){
           if(lazyMan==null){
               throw new RuntimeException("不要试图使用反射破坏异常");
           }
       }
    }

    private static volatile fanshe lazyMan;

    public static fanshe getInstance(){
        if(lazyMan==null){
            synchronized (fanshe.class){
                if(lazyMan==null){
                    lazyMan = new fanshe();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        fanshe instance = fanshe.getInstance();
        //利用反射获取其对应的构造器方法
        Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
        //无视私有构造器,通过反射创建对象
        declaredConstructor.setAccessible(true);
        fanshe instance2 = declaredConstructor.newInstance();

        System.out.println(instance==instance2);

    }

}

但是上面那种方法治标不治本,因为第一个instance我们是通过getInstance方法创建的,没经过反射,如果我们两个对象都是通过反射创建,如下代码所示,还是会存在问题。

//反射破坏单例模式
public class fanshe {
   //构造器私有
    private fanshe(){
       synchronized (fanshe.class){
           if(lazyMan!=null){
               throw new RuntimeException("不要试图使用反射破坏异常");
           }
       }
    }

    private static volatile fanshe lazyMan;

    public static fanshe getInstance(){
        if(lazyMan==null){
            synchronized (fanshe.class){
                if(lazyMan==null){
                    lazyMan = new fanshe();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //fanshe instance = fanshe.getInstance();
        //利用反射获取其对应的构造器方法
        Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
        //无视私有构造器,通过反射创建对象
        declaredConstructor.setAccessible(true);
        fanshe instance2 = declaredConstructor.newInstance();
        fanshe instance3 = declaredConstructor.newInstance();
        System.out.println(instance3==instance2);

    }
}

由此引入第二种方法,通过定义一个信号量来解决:

//反射破坏单例模式
public class fanshe {

    //定义一个信号量
    private static boolean flag = false;

    //构造器私有
    private fanshe(){
       synchronized (fanshe.class){
           //通过反射的方式无法获取到这个信号量,因此不能通过反射创建
           if(flag==false){
               flag=true;
           }
          else{
               throw new RuntimeException("不要试图使用反射破坏异常");
           }
       }
    }

    private static volatile fanshe lazyMan;

    public static fanshe getInstance(){
        if(lazyMan==null){
            synchronized (fanshe.class){
                if(lazyMan==null){
                    lazyMan = new fanshe();
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        //fanshe instance = fanshe.getInstance();
        //利用反射获取其对应的构造器方法
        Constructor<fanshe> declaredConstructor = fanshe.class.getDeclaredConstructor(null);
        //无视私有构造器,通过反射创建对象
        declaredConstructor.setAccessible(true);
        fanshe instance2 = declaredConstructor.newInstance();
        fanshe instance3 = declaredConstructor.newInstance();
        System.out.println(instance3==instance2);

    }

}


六、枚举单例

利用枚举的方式创建单例模式,可以避免反射对其内部进行关键字的破坏,继而导致单例模式失效。它的缺点也是不能延时加载

public enum meiju{
    INSTANCE;
    public meijugetInstance(){
        return INSTANCE;
    }
}
class test05{
    public static void main(String[] args) {
        meijuinstance1 = SingletonTest05.INSTANCE;
        meijuinstance2 = SingletonTest05.INSTANCE;
        System.out.println(instance1==instance2);
    }
}