美文网首页设计模式笔记
设计模式笔记(五): 单例模式

设计模式笔记(五): 单例模式

作者: yeonon | 来源:发表于2018-07-24 13:10 被阅读10次

    单例模式可以说是最简单的设计模式了,但是也是最容易写错的一个模式。下面来看看几种写法。

    懒汉模式(线程不安全)

    public class Singleton1 {
    
        private static Singleton1 instence;
    
        private Singleton1() {
    
        }
    
        public static Singleton1 getInstance() {
            if (instence == null) {
                instence = new Singleton1();
            }
            return instence;
        }
    }
    

    首先在类里声明了私有的静态字段,但是先不初始化,在第一次获取实例的时候,如果对象没有被初始化过,就先初始化,最后返回对象实例,否则直接返回对象实例。

    单线程下这种写法没有什么问题,还节省了一些启动时的资源。但在多线程环境下,可能会实例化多个对象,不符合单例的要求。

    懒汉模式(线程安全)

    public class Singleton2 {
    
        private static Singleton2 instence = new Singleton2();
    
        private Singleton2() {
    
        }
    
        public static Singleton2 getInstance() {
            return instence;
        }
    }
    

    在声明静态字段的时候进行初始化,因为字段是静态字段,所以在该类被加载的时候就会执行初始化操作了,而类加载的过程肯定是线程安全的(这由JVM保证的)。这种方式在类加载的到时候会比较慢。

    懒汉模式(线程安全)

    public class Singleton3 {
    
        private static Singleton3 instence;
    
        private Singleton3() {
    
        }
    
        public synchronized static Singleton3 getInstance() {
            if (instence == null) {
                instence = new Singleton3();
            }
            return instence;
        }
    }
    

    和线程不安全的懒汉模式只是在方法上多了synchronized 关键字,即给该方法上了内置锁。这种方法确实是线程安全的,每次访问都是有同步开销,造成不必要的资源浪费,而且其实大部分情况下是不需要同步操作的,不推荐这种方式。

    双重检查模式(DCL)

    public class Singleton4 {
    
        private volatile static Singleton4 instence;
    
        private Singleton4() {
    
        }
    
        public static Singleton4 getInstance() {
            if (instence == null) {
                synchronized (Singleton4.class) {
                    if (instence == null) {
                        instence = new Singleton4();
                    }
                }
            }
            return instence;
        }
    }
    

    之所以要双重检查,是因为有两次判断instance == null。这样做比上面的那种同步算是减少了锁的粒度,减少了一些同步开销。下图是两个线程执行getInstance的大致流程图(有些地方可能不太准确)


    流程图

    除此之外,注意到instance被声明成volatile。在本例中,volatile的主要作用是禁止指令重排。为什么要禁止指令重排呢?因为其实JVM初始化对象的时候一般分为三步。

    1. 为对象开辟内存空间
    2. 执行初始化过程(一般是构造函数)
    3. 将引用指向内存

    上述2,3的顺序完全可以调换,因为他们之间不存在依赖关系,也许JVM会为了优化二调换顺序。如果调换了顺序会怎样呢?假设线程A已经获取了锁,并且执行到了将引用指向内存(注意,执行初始化过程还没开始,此时instance已经不为null),这时候线程C调用getInstance方法,然后判断instance == null ? 结果会是false,最后返回instance,这样调用方就会拿到一个无效的instance!!volatile的作用就是保证先执行初始化过程,再将引用指向内存,这样就可以避免上述的异常情况。

    静态内部类单例模式

    public class Singleton5 {
        
        private Singleton5() {
    
        }
    
        public static Singleton5 getInstance() {
            return Singleton5Holder.instance;
        }
    
        private static class Singleton5Holder{
            private static final Singleton5 instance = new Singleton5();
        }
    }
    
    

    这里静态内部类是不会被提前加载的,只有再第一次调用getInstance的时候才会加载该类。所以这种方式既保证了线程安全,又不会导致类的提前加载,防止了资源浪费。是比较推荐的一种做法。

    枚举单例

    public enum  Singleton6 {
        INSTANCE;
    }
    

    对,就一行代码,因为枚举类型默认是单例的,而且是线程安全的。但是说句实话,枚举的可读性挺差的,所以很少在项目中看到这样的单例写法。

    使用容器实现单例模式

    public class Singleton7 {
    
        private static Map<String, Object> objMap = new HashMap<>();
        
        private Singleton7() {
            
        }
        
        public void addInstance(String key, Object instance) {
            objMap.putIfAbsent(key, instance);
        }
        public Object getInstance(String key) {
            return objMap.get(key);
        }
    }
    

    这里使用HashMap作为容器实现单例,putIfAbsent()方法保证了如果容器里某个键的值为null的时候才插入,这就只会有一个实例对象。

    Spring的IOC容器就是类似的方式实现的。

    小结

    到此,一共写了7中单例模式(已经不少了吧)。各个方式都有各自的优缺点,具体使用哪种方式,应该取决于项目,多多斟酌。

    单例模式的定义:确保一个类只有一个实例,并提供全局访问点

    本系列文章参考书籍是《Head First 设计模式》,文中代码示例出自书中。

    相关文章

      网友评论

        本文标题:设计模式笔记(五): 单例模式

        本文链接:https://www.haomeiwen.com/subject/rvwjmftx.html