设计模式——单例模式

255 阅读3分钟

为什么用单例模式

减少重复创建多余的对象,浪费资源。有些对象是可以多次重复使用的。

应用场景

在常用的应用场景有线程池或是数据库连接池等。

实现类型

饿汉式

实现

代码实现

/**
 * 饿汉式实现单例
 */
public class SingletonHungry {
    private static SingletonHungry instance = new SingletonHungry();

    public static SingletonHungry getInstance(){
        return instance;
    }
}

测试:

在一般情况:

public class Test {
    public static void main(String[] args) {
        SingletonHungry s1 = SingletonHungry.getInstance();
        SingletonHungry s2 = SingletonHungry.getInstance();
        System.out.println(s1);
        System.out.println(s2);
    }
}
/*
com.company.DesignPatterns.Singleton.SingletonHungry@4554617c
com.company.DesignPatterns.Singleton.SingletonHungry@4554617c
*/

高并发情况:

**public class Test{
    static int queryTimes = 0;
    public static int getTimes(){
        queryTimes = queryTimes +1;
        return queryTimes;
    }

    public static void parallelTesk(int threadNum, Runnable task){

        // 1. 定义闭锁来拦截线程
        final CountDownLatch startGate = new CountDownLatch(1);
        final CountDownLatch endGate  = new CountDownLatch(threadNum);

        // 2. 创建指定数量的线程
        for (int i = 0; i <threadNum; i++) {
            Thread t = new Thread(() -> {
                try {
                    startGate.await();
                    SingletonHungry s1 = SingletonHungry.getInstance();
                    System.out.println(s1);
                    try {
                        task.run();
                    } finally {
                        endGate.countDown();
                    }
                } catch (InterruptedException e) {

                }
            });

            t.start();
        }

        // 3. 线程统一放行,并记录时间!
        long start =  System.nanoTime();

        startGate.countDown();
        try {
            endGate.await();
        } catch (InterruptedException e) {
            System.out.println(e.toString());
        }

        long end = System.nanoTime();
        System.out.println("cost times :" +(end - start));
    }

    public static void main(String[] args) {
//        SingletonHungry s1 = SingletonHungry.getInstance();
//        SingletonHungry s2 = SingletonHungry.getInstance();
//        System.out.println(s1);
//        System.out.println(s2);
        parallelTesk(8000, new Runnable() {
            @Override
            public void run() {
                System.out.println(getTimes());
            }
        });

    }
}

/*
com.company.DesignPatterns.Singleton.SingletonHungry@5a6a993
com.company.DesignPatterns.Singleton.SingletonHungry@5a6a993
com.company.DesignPatterns.Singleton.SingletonHungry@5a6a993
*/**

优劣

优点:

线程安全。从代码实现已经注定是线程安全了,当类被加载,就已经创建了。

缺点:

可能会浪费资源,在第一次加载的时候就实例化了。

懒汉式

public class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){
        if(instance==null){
            instance = new SingletonLazy();
        }
        return instance;
    }
}

测试:

一般情况:

/*
com.company.DesignPatterns.Singleton.SingletonLazy@4554617c
com.company.DesignPatterns.Singleton.SingletonLazy@4554617c
*/

高并发情况:

/*
com.company.DesignPatterns.Singleton.SingletonLazy@109ec86
com.company.DesignPatterns.Singleton.SingletonLazy@3d3dbc1b
com.company.DesignPatterns.Singleton.SingletonLazy@3d3dbc1b
*/

优劣

优点:

只有在调用的情况下,才会实例化,节省资源

缺点:

每次调用都会多一次判断

双重锁检测

在懒汉式的基础上进行改进,双重判空的情况下,还加了锁。

public class SingletonDCL {
    private static SingletonDCL instance = null;

    public static SingletonDCL getInstance(){
        if(instance==null){                                    // 1
            synchronized (SingletonLazy.class){                // 2 
                if(instance==null){                            // 3 
                    instance = new SingletonDCL();             // 4
                }
            }
        }
        return instance;
    }
}

理论上是已经足够的线程安全,但是还可能会有jvm指令重排的情况,有线程不安全问题。

原先的指令是:
a. 分配对象内存空间
b. 初始化对象
c. 指向内存地址

线程A经过了123步的,同时线程B准备到达了1步
线程A执行4步,指令是b,而线程B是执行1步时,已经不是null
做到了线程安全
/*************************************************************************/
经过优化的指令可能是:
a. 分配对象内存空间
b. 指向内存地址
c. 初始化对象

线程A经过了123步的,同时线程B准备到达了1步
线程A执行4步,指令是b,而线程B是执行1步时,已经不是null
线程B return instance时,线程A还没有执行c指令
线程B拿到了一个null的对象

改进:

public class SingletonDCL {
    private volatile static SingletonDCL instance = null;

    public static SingletonDCL getInstance(){
        if(instance==null){
            synchronized (SingletonLazy.class){
                if(instance==null){
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

通过加volatile,使对象在内存有可见性,也同时防止指令重排的情况。

即使是这样可能也会出现不安全的情况,就是利用反射原理。

Constructor con = SingletonDCL.class.getDeclaredConstructor();
SingletonDCL s1 = (SingletonDCL)con.newInstance();
SingletonDCL s2 = (SingletonDCL)con.newInstance();

静态内部类

public class SingletonHolder {
    private static class LazyHolder{
        private static final SingletonHolder INSTANCE = new SingletonHolder();
    }
    private SingletonHolder(){}

    public static SingletonHolder getInstance(){
        return LazyHolder.INSTANCE;
    }
}

利用classloader的加载机制来保证线程的安全,但是反射机制依然可以打破约束。

测试:

通过反射可以获得两个不同的对象

Constructor con = SingletonHolder.class.getDeclaredConstructor();
con.setAccessible(true);
SingletonHolder s1 = (SingletonHolder)con.newInstance();
SingletonHolder s2 = (SingletonHolder)con.newInstance();
System.out.println(s1);
System.out.println(s2);

枚举

public enum SingletonEnum {
    INSTANCE;
    public SingletonEnum getInstance(){
        return INSTANCE;
    }
}

属于饿汉式的一种,所以线程安全。

防止通过反射机制来实例化两个不同的对象。

容器

public class SingletonMap {
    private static Map<String, Object> objectMap = new HashMap<>();

    public static void regSingleton(String key, Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key, instance);
        }
    }

    public static Object getInstance(String key){
        return objectMap.get(key);
    }
}

从代码的特性来看,就是线程不安全,还不防止反射,获取之前需要注册,理论上是属于懒加载模式。

总结

给以上六种类型做个表格

实现模式线程安全懒加载防止反射
饿汉式安全不是不防止
懒汉式不安全不防止
双重锁检测安全不防止
静态内部类安全不防止
枚举安全不是防止
容器不安全不防止