圣诞节,让我们聊聊单例模式

圣诞节到了,是时候对单例有一个新的认识了,不然一个就会变成两个、四个...很多个...嗯,我说的是圣诞老人...

很久之前看到一篇讲单例的文章,看完才知道看似简单的单例模式,其实有很大的考究,最近又看到了几篇类似的文章,发现单例其实很复杂。费了很大力气,理顺了思路,顿时又觉得单例模式可以不用那么复杂了。

首先,我们得问自己一个问题:为什么要使用单例?

为什么要使用单例

单例,顾名思义,就是让一个类只存在一个实例对象,那么什么时候我们会需要单例呢?最常见的有以下两种情形:

  • 无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
  • 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

单例起到的好处主要有两点:

  • 节省内存
  • 方便管理

值得注意的是,单例往往都可以通过static来实现,把一个实例方法变成静态方法,或者把一个实例变量变成静态变量,都可以起到单例的效果。在我看来,这只是面向对象和面向过程的区别。

一个完美的懒汉模式

了解完为什么要使用单例,接下来让我们来实现一个完美的单例模式。
实现单例模式,你只需要注意以下几点:

  1. 构造函数私有化,防止别的开发人员调用而创建出多个实例
  2. 在类的内部创建实例,创建时要注意多线程并发访问可能导致的new出多个实例的问题
  3. 提供获取唯一实例的方法

基于以上三点,我们实现了下面这个“懒汉”单例模式(本文的所有代码,可到Github上下载):

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

    private PerfectLazyManSingleton() {
    }

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

这个单例在实际使用中已经是完美的了:

  • 使用私有构造函数防止new出多个实例
  • 使用Double-Check + synchronized同步锁,解决多线程并发访问可能导致的在内部调用多次new的问题
  • 使用volatile关键字,解决由于指令重排而可能出现的在内部调用多次new的问题

至于很多文章里说的利用类加载器、利用反射等创建多个实例的问题,我们只需要知道有这个可能性就好,因为这些都不是正常创建对象的方式,我们使用单例模式是为了防止其他开发人员不小心new出多个实例,而如果开发人员都动用了反射和ClassLoader这些重型武器了,那我想这绝对不是“不小心”了。

与其浪费心思、牺牲代码可读性、牺牲性能,去获取“绝对意义”上的单例,还不如在类上面加上行注释——“This is a single-instance class. Do not try to create another instance”,来提示那些看到私有构造函数还不知道这是个单例的新手们,不要尝试创建新的实例了!

如果真想实现“绝对意义”上的单例,那就使用枚举吧。

单例工厂

消除重复是程序员的天性,如果我们每次需要单例对象时,都按照上面的模式把类设计成单例,那显然是不可接受的。这时候我们就可以设计一个单例工厂,这个单例工厂就像民政局一样,我给他一个身份证号码,他给我返回唯一一个对应的人。

public class SingletonRegistry {
    public static SingletonRegistry REGISTRY = new SingletonRegistry();
    private static HashMap map = new HashMap();
    private static Logger logger = LoggerFactory.getLogger(SingletonRegistry.class);

    private SingletonRegistry() {
    }

    public static synchronized Object getInstance(String classname) {
        Object singleton = map.get(classname);
        if (singleton != null) {
            return singleton;
        }
        try {
            singleton = Class.forName(classname).newInstance();
            logger.info("created singleton: " + singleton);
        } catch (ClassNotFoundException cnf) {
            logger.warn("Couldn't find class " + classname);
        } catch (InstantiationException ie) {
            logger.warn("Couldn't instantiate an object of type " +
                    classname);
        } catch (IllegalAccessException ia) {
            logger.warn("Couldn't access class " + classname);
        }
        map.put(classname, singleton);
        return singleton;
    }
}

关于这个SingletonRegistry,有以下几点需要注意的:

  • 这个SingletonRegistry本身也是单例,使用的是“饿汉”版的单例模式
  • 由于getInstance方法要返回的实例不再是类的成员变量,因此不再能够使用volatile来获得线程之间的可见性,因此要将整个getInstance方法加上同步锁

这个单例工厂的用法非常简单:

public class Singleton {
   private Singleton() {
   }
   public static Singleton getInstance() {
      return (Singleton)SingletonRegistry.REGISTRY.getInstance(classname);
   }
}

饿汉版单例模式

饿汉版的单例模式非常简单,上面的SingletonRegistry其实就是“饿汉”版的单例模式,一个完美的饿汉单例模式代码如下:

public class SingletonHungryMan {
    public final static SingletonHungryMan INSTANCE = new SingletonHungryMan();
    private SingletonHungryMan() {
        // Exists only to defeat instantiation.
    }
    public void sayHello() {
        System.out.println("hello");
    }

}

为什么这里就不用担心多线程并发导致的new了多个示例呢?
关键在于这是static静态变量,而静态变量归属于类,会在类加载的过程中被初始化,而Java类加载的过程默认是线程安全的,除非自定义的类加载器覆写了loadClass函数。
下面就是ClassLoader的loadClass方法,这个方法很好的展示了什么是双亲委派模型:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

当然了,饿汉版的单例模式如果受到非常规的攻击,还是会生出二胎出来的,比如利用反射把私有的构造器设为Accessible,抑或是使用自定义的类加载器进行加载,产生新的实例。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性 —— 《深入理解Java虚拟机》 第7章 虚拟机类加载机制

我分别使用了反射和类加载器,对上面的SingletonHungryMan进行了攻击,代码如下:

public class SingletonHungryManTest {
    private SingletonHungryMan sone = null;
    private Object stwo = null;
    private Object sthree = null;
    private static Logger logger = LoggerFactory.getLogger(SingletonHungryManTest.class);

    @Before
    public void setUp() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        sone = SingletonHungryMan.INSTANCE;
        stwo = createAnotherInstanceUsingRelection();
        sthree = createAnotherInstanceUsingAnotherClassLoader();
    }

    private Object createAnotherInstanceUsingRelection() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Class<SingletonHungryMan> singletonHungryManClass = SingletonHungryMan.class;
        Constructor<?> declaredConstructor = singletonHungryManClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        return declaredConstructor.newInstance();
    }

    private Object createAnotherInstanceUsingAnotherClassLoader() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchFieldException {
        // use custom class loader to load class
        ClassLoader myLoader = getMyLoader();
        Class<?> myClass = myLoader.loadClass("com.sexycode.codepractice.singleton.SingletonHungryMan");
        // use reflection to get field
        Field field = myClass.getField("INSTANCE");
        // return the field's value
        return field.get(null);
    }

    private ClassLoader getMyLoader() throws ClassNotFoundException {
        return new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
    }

    @Test
    public void testUnique() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        logger.info("checking singletons for equality");
        sone.sayHello();
        invokeMethod(stwo, "sayHello");
        invokeMethod(sthree, "sayHello");
        Assert.assertNotEquals(true, sone == stwo);
        Assert.assertNotEquals(true, sone == sthree);
    }

    private void invokeMethod(Object obj, String method) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Method sayHello = obj.getClass().getMethod(method);
        sayHello.invoke(obj);
    }
}

可序列化对象的单例

可序列化对象,在进行序列化之后,可以进行多次的反序列化,这时候如果要维持单例,就要实现readResolve方法:

public class SingletonSerializable implements java.io.Serializable {
    public static SingletonSerializable INSTANCE = new SingletonSerializable();

    private SingletonSerializable() {
        // Exists only to thwart instantiation.
    }

    private Object readResolve() {
        return INSTANCE;
    }

}

小结

实现单例模式,其实没有那么复杂,我们要考虑的只是如何防止其他开发人员在常规操作下创建多个实例,至于那些非常规的手段,并不值得牺牲代码可读性和性能去进行防御。

最后再抛出一个问题,Spring的@Scope("singleton")是怎么实现单例的呢?

最最重要的是,圣诞节来了,你知道怎么实现单例、防止多例了么?

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,227评论 4 34
  • 1 场景问题# 1.1 读取配置文件的内容## 考虑这样一个应用,读取配置文件的内容。 很多应用项目,都有与应用相...
    七寸知架构阅读 6,653评论 12 68
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,601评论 18 139
  • 1 单例模式的动机 对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务...
    justCode_阅读 1,431评论 2 9
  • 就是不为什么的为什么 每年过年时的一大盛事就是同学朋友之间的聚会。有的三五好友在一起吃饭聊天,有的一群人在饭店餐馆...
    陶之夭夭1阅读 430评论 5 5