单例模式

  单例是最简单的设计模式,没有之一,常见于缓存管理对象、数据库连接对象、帮助类对象等只需要唯一实例的地方。但是简单不代表用起来就真的容易,很多老手可能都不清楚真正该怎么用它,最常见的错误就是没有定义一个私有的无参构造器,见下面代码:

    private static final OsCache cache = new OsCache();
    
    /**
     * 单例
     */
    public static OsCache getInstance()
    {
        return cache;
    }

  在使用该OsCache对象时,我们通过getInstance方法能获取唯一的单例对象,这点是很明确的,但我们同样能new出一个OsCache对象,这时就不能叫单例了。因此正确的单例模式应该补一个构造器:

    private static final OsCache cache = new OsCache();
    
    /**
     * 私有无参构造器
     */
    private OsCache()
    {
    }
    
    /**
     * 单例
     */
    public static OsCache getInstance()
    {
        return cache;
    }

  这样如果别处用到该OsCache对象时,就无法通过new来实例化它了,直接编译报错。像上面这种模式有个形象的叫法:饿汉模式,因为我们一开始就new出来这个实例,表示我们很饿,需要马上吃了这个实例。为什么叫单例?因为所有用到OsCache对象的地方,其实都是在用这个实例。就好比目前我们人类一样,一来到这个世界,就只有我们自己,每个人都是唯一的,没有克隆人存在。既然有饿汉,就有不饿的,需要时才实例化出一个单例:

    private static OsCache cache = null;
    
    /**
     * 私有无参构造器
     */
    private OsCache()
    {
    }
    
    /**
     * 单例
     */
    public static OsCache getInstance()
    {
        if(cache == null)
        {
            cache = new OsCache();
        }
        return cache;
    }

  这里在获取单例之前,会先判断下单例是否已经生成,没有再去实例化,此种模式曰懒汉模式。懒汉虽懒,却有两个变种。一个是考虑到并发调用getInstance方法时,A线程看到cache实例是null,刚进去new实例的这一刻,B线程刚来到cache实例为null这里,那么接下来A把单例new出来后,B也会new出来另一个实例。这时单例就不叫单例了,因为有两个实例。怎么办?加锁:

    /**
     * 私有无参构造器
     */
    private OsCache()
    {
    }
    
    /**
     * 单例
     */
    public static OsCache getInstance()
    {
        synchronized (OsCache.class)
        {
            if (cache == null)
            {
                cache = new OsCache();
            }
        }
        return cache;
    }

  加锁之后可堵住并发时new出一堆实例来这个漏洞,但如果并发获取该单例的情况存在,第一个获得锁的A线程将实例化对象,后面所有排队的线程都是冤大头,因为A已经帮他们new好了单例。所以getInstance方法的性能会随之下降。更好一点的写法是再套一层判断:

   private volatile static OsCache cache = null;
    
    /**
     * 私有无参构造器
     */
    private OsCache()
    {
    }
    
    /**
     * 单例
     */
    public static OsCache getInstance()
    {
        if (cache == null)
        {
            synchronized (OsCache.class)
            {
                if (cache == null)
                {
                    cache = new OsCache();
                }
            }
        }
        return cache;
    }

  这样A线程领头取到单例后,后面还没排队的请求一看单例有就立马拿走,无需排队了。这种写法叫“双重检查锁”(double-checked loking),在锁住的门外问一声:单例在不在?在就领走了,不在才去门口排队,因为拿到钥匙开门时可能其他人已经把单例创造出来了,所以排到了进门还得再问一声:单例在不在,在就领走,不在new它出来。这里注意单例需要设置为volatile,让所有排队的人都能看到单例的状态,否则可能别人已经创造了一个单例,但我在第一个非空判断时还是看不到。这里涉及到volatile对多线程状态下的可见性问题。小小单例,细说起来还是有不少可以聊的。

  最后科普下,如果是一开始就是实例化出对象来,说明对该单例资源很饥渴,被称为“饿汉”,加了非空判断延迟加载的,则称为“饱汉”或者“懒汉”。

原文地址:https://www.cnblogs.com/wuxun1997/p/8672514.html