单例模式

在日常的开发过程中,我们需要使用到设计模式,而单例模式可谓是最常见的一种。正确的使用单例模式,不仅可以降低内存的使用率,也可以提升整个程序的运行效率。下面我来谈谈自己对单例模式的理解。


【1】懒汉式

特点:

(1)是一种牺牲时间换取空间的策略

(2)懒加载,只在需要的时候才实例化对象


public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

然而这样的单例模式存在缺点:

(1)多线程情况下,容易被多个线程实例化出多个对象,违背”单例“的原则


【2】线程安全的懒汉式

考虑到在多线程的情况下,懒汉式存在问题,最普遍的想法,就是给getInstance()方法加上同步锁

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

}

不过,由于对整个方法加锁,系统性能带来严重的降低,每次获取单例都需要先获取同步锁,而我们只需要在创建单例时加锁就行了。


【3】线程安全且性能较好的懒汉式(DCL)

public class SingletonDCL {
    private static SingletonDCL instance;

    private SingletonDCL() {
    }

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

}

这样的写法也称双重检验锁法(DCL),只在实例为null时才进行加锁。

双重判断使得当两个线程同时运行到加锁前的一步时,再顺序获得锁之后不会创建出两个实例来。

 

第二种方法和第三种方法的效率比较

public class Test {

    public static void main(String args[]) throws InterruptedException {
        long start_1 = System.currentTimeMillis();
        List<Thread> list1 = new ArrayList<>();

        //使用20000个线程去获取单例
        for (int i = 0; i < 20000; i++) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Singleton.getInstance();
                }
            });
            list1.add(thread1);
            thread1.start();
        }

        //等待线程执行完毕
        for (Thread thread : list1) {
            thread.join();
        }
        long end_1 = System.currentTimeMillis();
        System.out.println("非DCL方法的耗时:" + (end_1 - start_1));


        long start_2 = System.currentTimeMillis();
        List<Thread> list2 = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    SingletonDCL.getInstance();
                }
            });
            list2.add(thread2);
            thread2.start();
        }
        for (Thread thread : list2) {
            thread.join();
        }
        long end_2 = System.currentTimeMillis();
        System.out.println("DCL方法的耗时:" + (end_2 - start_2));
    }
}

输出为

可以看得出使用双重检验锁之后,系统的性能带来了提高。


【4】饿汉式

特点:

(1)是一种牺牲空间换取空间的策略

(2)类加载时,就直接实例化对象

(3)使用类加载机制,避免了多线程的同步问题

public class Singleton {
    private Singleton() {
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

}

这样的写法确实很简单,但存在以下的缺点

(1)无论当前类的实例什么时候用,都在类加载时一起实例化所有使用此模式创建的实例,大量实例存在于内存中,内存的使用率提高


【5】静态内部类

public class Singleton {
    private Singleton() {
    }

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

}

这样做得好处是

(1)静态内部类不会在类加载时就创建此类的实例,只有在显示调用此静态内部类时,此静态内部类才会被加载,内部的对象实例才会被赋值,起到懒加载的作用。

(2)和饿汉式一样,使用类加载机制,避免多线程下的同步问题。


【6】枚举(“单例的最佳实践”)

public enum EnumSingleton {

    INSTANCE;
    Resource instance;

    EnumSingleton() {
        instance = new Resource();
    }

    public Resource getInstance() {
        return instance;
    }

}

Effective Java作者Josh Bloch 提倡的方式,Resource类代表需要应用单例模式的资源。在main方法中通过EnumSingleton.INSTANCE.getInstance()获得Resource的单例,
那么这个单例是如何被保证的呢? 
在枚举类中,构造方法是私有的,在我们访问枚举实例时会执行构造方法,同时每个枚举实例被public static final修饰且为EnumSingleton类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。 
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的Resource也被保证实例化一次。 


【7】登记式(不常用)

将整个项目中用到的单例作集中化管理,即使用容器进行管理。

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

    private SingletonManager() {
    }

    public static void addSingleton(String key, Object singleton) {
        if (!map.containsKey(key)) {
            map.put(key, singleton);
        }
    }

    public static Object getSingleton(String key) {
        return map.get(key);
    }

}

这样写的好处是什么呢?

(1)提供统一的接口增加或获取实例,提高对单例的管理效率


碍于博主才疏学浅,对单例模式的讨论也就这么多了,我会在以后的学习还有接下来的工作中,不断地修改博客,尽量将错误减少到最少,将优质的博文呈现给大家。