资讯专栏INFORMATION COLUMN

从未这么明白的设计模式(一):单例模式

NikoManiac / 1356人阅读

摘要:一般来说,这种单例实现有两种思路,私有构造器,枚举。而这种方式又分了饱汉式,饿汉式。通过关键字防止指令重排序。

什么是单例?为什么要用单例?

一个类被设计出来,就代表它表示具有某种行为(方法),属性(成员变量),而一般情况下,当我们想使用这个类时,会使用new关键字,这时候jvm会帮我们构造一个该类的实例。而我们知道,对于new这个关键字以及该实例,相对而言是比较耗费资源的。所以如果我们能够想办法在jvm启动时就new好,或者在某一次实例new好以后,以后不再需要这样的动作,就能够节省很多资源了。

哪些类可以使用单例?

一般而言,我们总是希望无状态的类能够设计成单例,那这个无状态代表什么呢? 简单而言,对于同一个实例,如果多个线程同时使用,并且不使用额外的线程同步手段,不会出现线程同步的问题,我们就可以认为是无状态的,再简单点:一个类没有成员变量,或者它的成员变量也是无状态的,我们就可以考虑设计成单例。

实现方法

好了,我们已经知道什么是单例,为什么要使用单例了,那我们接下来继续讨论下怎么实现单例。 一般来说,我们可以把单例分为行为上的单例管理上的单例行为上的单例代表不管如何操作(此处不谈cloneable,反射),至始至终jvm中都只有一个类的实例,而管理上的单例则可以理解为:不管谁去使用这个类,都要守一定的规矩,比方说,我们使用某个类,只能从指定的地方’去拿‘,这样拿到就是同一个类了。 而对于管理上的单例,相信大家最为熟悉的就是spring了,spring将所有的类放到一个容器中,以后使用该类都从该容器去取,这样就保证了单例。 所以这里我们剩下的就是接着来谈谈如何实现行为上的单例了。一般来说,这种单例实现有两种思路,私有构造器,枚举

枚举实现单例

枚举实现单例是最为推荐的一种方法,因为就算通过序列化,反射等也没办法破坏单例性,例子:

public enum SingletonEnum {
    INSTANCE;

    public static void main(String[] args) {
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}

结果自然是true,而如果我们尝试使用反射破坏单例性:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE;

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor();
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance();

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

结果如下:

Exception in thread "main" java.lang.NoSuchMethodException: cn.jsbintask.BadSingletonEnum.()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:18)

异常居然是没有init方法,这是为什么呢? 那我们反编译查看下这个枚举类的字节码:

// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum;
// declaration: cn/jsbintask/BadSingletonEnum extends java.lang.Enum
public final enum cn/jsbintask/BadSingletonEnum extends java/lang/Enum {

  // compiled from: BadSingletonEnum.java

  // access flags 0x4019
  public final static enum Lcn/jsbintask/BadSingletonEnum; INSTANCE

  // access flags 0x101A
  private final static synthetic [Lcn/jsbintask/BadSingletonEnum; $VALUES
}

结果发现这个枚举类继承了抽象类java.lang.Enum,我们接着看下Enum,发现构造器:

/**
    * Sole constructor.  Programmers cannot invoke this constructor.
    * It is for use by code emitted by the compiler in response to
    * enum type declarations.
    *
    * @param name - The name of this enum constant, which is the identifier
    *               used to declare it.
    * @param ordinal - The ordinal of this enumeration constant (its position
    *         in the enum declaration, where the initial constant is assigned
    *         an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

那我们接着改变代码,反射调用这个构造器:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE();

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor(String.class, int.class);
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance("test", 0);

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

结果如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:21)

这次虽然方法找到了,但是直接给我们了一句Cannot reflectively create enum objects,不能够反射创造枚举对象,接着我们继续看下**newInstance(...)**这个方法:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<");null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

关键代码就是:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");,所以就是jdk从根本上拒绝了使用反射去创建(知道为啥java推荐使用enum实现单例了吧),另外,我们再观察下Enum类的clone和序列化方法,如下:

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can"t deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
    throw new InvalidObjectException("can"t deserialize enum");
}

一眼看出,直接丢出异常,不允许这么做!(真亲儿子系列)。 所以,结论就是:枚举是最靠谱的实现单例的方式!

私有构造器

另外一个实现单例最普通的方法则是私有构造器,开放获取实例公共方法,虽然这种方法还是可以用clone,序列化,反射破坏单例性(除非特殊情况,我们不会这么做),但是却是最容易理解使用的。而这种方式又分了饱汉式饿汉式

饿汉式

看名字就知道,饥渴!(咳咳,开个玩笑),它指的是当一个类被jvm加载的时候就会被实例化,这样可以从根本上解决多个线程的同步问题,例子如下:

public class FullSingleton {
    private static FullSingleton ourInstance = new FullSingleton();

    public static FullSingleton getInstance() {
        return ourInstance;
    }

    private FullSingleton() {
    }

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

结果自然是true,虽然这种做法很方便的帮我们解决了多线程实例化的问题,但是缺点也很明显,因为这句代码**private static FullSingleton ourInstance = new FullSingleton();**的关系,所以该类一旦被jvm加载就会马上实例化,那如果我们不想用这个类怎么办呢? 是不是就浪费了呢?既然这样,我们来看下替代方案! 饱汉式。

饱汉式

既然是,就代表它不着急,那我们可以这么写:

public class HungryUnsafeSingleton {
    private static HungryUnsafeSingleton instance;
    
    public static HungryUnsafeSingleton getInstance() {
        if (instance == null) {
            instance = new HungryUnsafeSingleton();
        }
        
        return instance;
    }
    
    private HungryUnsafeSingleton() {}
}

用意很容易理解,就是用到**getInstance()**方法才去检查instance,如果为null,就new一个,这样就不怕浪费了,但是这个时候问题就来了:现在有这么一种情况,在有两个线程同时 运行到了 instane == null这个语句,并且都通过了,那他们就会都实例化一个对象,这样就又不是单例了。既然这样,哪有什么解决办法呢? 锁方法

    直接同步方法 这种方法比较干脆利落,那就是直接在getInstance()方法上加锁,这样就解决了线程问题:

public class HungrySafeSingleton {
    private static HungrySafeSingleton instance;

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

        return instance;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

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

很简单,很容易理解,加锁,只有一个线程能实例该对象。但是,此时问题又来了,我们知道对于静态方法而言,synchronized关键字会锁住整个 Class,这时候又会有性能问题了(尼玛墨迹),那有没有优化的办法呢? 双重检查锁

public class HungrySafeSingleton {
    private static volatile HungrySafeSingleton instance;

    public static HungrySafeSingleton getInstance() {
        /* 使用一个本地变量可以提高性能 */
        HungrySafeSingleton result = instance;

        if (result == null) {

            synchronized (HungrySafeSingleton.class) {

                result = instance;
                if (result == null) {
                    instance = result = new HungrySafeSingleton();
                }
            }
        }

        return result;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

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

    synchronized关键字只加在了关键的地方,并且通过本地变量提高了性能(effective java),这样线程安全并且不浪费资源的单例就完成了。

    通过volitalile关键字防止指令重排序。 对其他线程可见。

关注我,这里只有干货!

总结

本章,我们一步一步从什么是单例,到为什么要使用单例,再到怎么使用单例,并且从源码角度分析了为什么枚举是最适合的实现方式,然后接着讲解了饱汉式,饿汉式的写法以及好处,缺点。 例子源码:github.com/jsbintask22…

本文原创地址:jsbintask的博客,转载请注明出处。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7202.html

相关文章

  • 从未这么明白设计模式(二):观察者模式

    摘要:小丽总是会在朋友圈发布自己的各种生活状态。总结我们从观察者模式特点入手,通过一个案例,一步一步完善了观察着的写法,特点组后介绍了总已有的实现关注我,这里只有干货同系列文章从未这么明白的设计模式一单例模式 showImg(https://segmentfault.com/img/remote/1460000018874501); 本文原创地址,我的博客:https://jsbintask...

    dockerclub 评论0 收藏0
  • 大话PHP设计模式单例模式

    摘要:上面是简单的单例模式,自己写程序的话够用了,如果想继续延伸,请传送至大话设计模式之单例模式升级版 看了那么多单例的介绍,都是上来就说怎么做,也没见说为什么这么做的。那小的就来说说为什么会有单例这个模式以便更好的帮助初学者真正的理解这个设计模式,如果你是大神,也不妨看完指正一下O(∩_∩)O首先我不得不吐槽一下这个模式名字单例,初学者通过字面很难理解什么是单例,我觉得应该叫唯一模式更贴切...

    VEIGHTZ 评论0 收藏0
  • Java设计模式-单例模式

    摘要:那有什么办法保证只有一个领导人斯大林呢较常见的两种方式饿汉式和懒汉式二实战图这里提示一点,在学习设计模式的时候,图会让你更容易,而且深刻的去理解到该模式的核心。下一篇的设计模式是工厂方法模式。   就算不懂设计模式的兄弟姐妹们,想必也听说过单例模式,并且在项目中也会用上。但是,真正理解和熟悉单例模式的人有几个呢?接下来我们一起来学习设计模式中最简单的模式之一——单例模式 一、为什么叫单...

    Jensen 评论0 收藏0
  • Java设计模式-单例模式

    摘要:那有什么办法保证只有一个领导人斯大林呢较常见的两种方式饿汉式和懒汉式二实战图这里提示一点,在学习设计模式的时候,图会让你更容易,而且深刻的去理解到该模式的核心。下一篇的设计模式是工厂方法模式。   就算不懂设计模式的兄弟姐妹们,想必也听说过单例模式,并且在项目中也会用上。但是,真正理解和熟悉单例模式的人有几个呢?接下来我们一起来学习设计模式中最简单的模式之一——单例模式 一、为什么叫单...

    fox_soyoung 评论0 收藏0
  • Python单例模式(Singleton)N种实现

    摘要:本篇文章总结了目前主流的实现单例模式的方法供读者参考。使用实现单例模式同样,我们在类的创建时进行干预,从而达到实现单例的目的。 很多初学者喜欢用 全局变量 ,因为这比函数的参数传来传去更容易让人理解。确实在很多场景下用全局变量很方便。不过如果代码规模增大,并且有多个文件的时候,全局变量就会变得比较混乱。你可能不知道在哪个文件中定义了相同类型甚至重名的全局变量,也不知道这个变量在程序的某...

    Maxiye 评论0 收藏0

发表评论

0条评论

NikoManiac

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<