Java单例模式的各种实现方式对比

1. 懒汉式

public class Singleton {
    private static Singleton INSTANCE = null;

    private Singleton() {
    }

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

分析:

 懒汉式给getInstance方法加上了同步锁,解决了多线程的情况下可能创建多个实例的问题。但是每次调用getInstance方法都有一个获取/释放同步锁的过程。加锁是很耗时的,这是一种低效率的实现方式。

结论:

 理论上是正确的单例实现方式,但效率不高,不推荐使用。

2. 双重校验锁定

双重校验锁定又分为两种:

2.1 synchronized方式

public class Singleton {
    private static volatile Singleton INSTANCE = null;

    private Singleton() {
    }

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

2.2 Lock方式:

import java.util.concurrent.locks.ReentrantLock;

public class Singleton {
    private static volatile Singleton INSTANCE = null;

    private static ReentrantLock lock = new ReentrantLock();

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            try {
                lock.lock();
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            } finally {
                lock.unlock();
            }
        }
        return INSTANCE;
    }
}

分析:

 双重校验锁定其实原理与懒汉式一样,是懒汉式的优化版本。我们知道,getInstance方法可能被调用很多次,但实例仅需要创建一次,后面直接返回第一次创建的实例即可。直接对getInstance方法加同步锁并不划算,要满足仅创建一个实例的需求,只需要对创建实例的过程进行加锁即可。只有当INSTANCE为null时,需要获取同步锁,创建一个实例。实例创建之后,获取实例无需再获取锁。

 值得注意的是INSTANCE需要用volatile关键字修饰,这里volatile的作用是保证可见性和禁止指令重排。
 INSTANCE = new Singleton();是一个非原子操作,编译后会生成多条字节码指令,可能会出现指令重排,假设编译后生成a,b,c三条指令:
  a. memory = allocate();//开辟内存空间
  b. ctorInstance(memory);//实例初始化
  c. INSTANCE = memory;//引用赋值,使INSTANCE指向开辟的内存地址。
 我们期望的执行顺序应该是:a, b, c,但是由于JVM运行时可能发生指令的重排序,可能会出现的执行顺序:a, c, b
假定有两个线程1、2,

指令重排的情况(a, c, b顺序执行):
 如果线程1获取到锁进入创建实例,执行了指令a和c,此时线程2刚好进入第一个if判断语句,由于此时INSTANCE已经不为 null,线程2可以访问该实例引用指向的地址,但实际上由于指令b还未执行,实例还未初始化,这块内存空间仅仅开辟了,但是还没有任何有意义的内容,线程2的访问就有可能出现异常。

禁止指令重排的情况(a,b,c顺序执行):
 禁止指令重排后,只能按照顺序a,b,c执行。如果线程1获取到锁进入创建实例,执行了指令a和b,线程2进入第一个if判断语句,由于此时INSTANCE为 null,线程2就必须等待线程1的指令c执行完后释放锁,然后线程2进入第二if判断语句,此时INSTANCE已经不为 null,并且实例也初始化好了。

 为什么指令a不会被重排到指令b,c后面?这是因为:
  Java 语言规定了线程执行程序时需要遵守 intra-thread semantics规则。
 这个规则保证指令重排不会改变单线程内的程序执行结果。因为指令b,c都是依赖指令a的执行后才有内存地址的,所以按照单线程的自然顺序,指令a永远在b,c前面。
 综上,所以需要加volatile关键字禁止指令重排序实现线程安全的单例。
 如果一定要说双重校验锁定有什么缺点的话那就是:写法稍显复杂,容易写错。

结论:

 双重校验锁定是一种高效并且实现了lazy loading的单例实现方式,推荐使用。

3. 饿汉式

public class Singleton {
    private static Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

分析:
 饿汉式利用了JVM的类加载机制的特性:

  类加载的初始化阶段是线程安全的,同一个类加载器下,一个类型只会初始化一次。

 类中的所有静态变量的赋值动作和静态语句块的执行都是在初始化阶段进行的。因此,在饿汉式实现中,创建Singleton实例并给静态变量INSTANCE赋值是线程安全的。通过这个机制,在Singleton类被加载的时候就早早的创建了实例。即在调用getInstance的方法之前,就已经创建好了实例。饿汉式简单、安全、可靠,缺点是在还不需要实例的时候就已经创建了实例,没有实现lazy loading,降低了内存的使用效率。

结论:

 饿汉式是一种简单、安全、可靠,但没有实现lazy loading的单例实现方式,实例占用内存不大时,可以使用。

4. 静态内部类

public class Singleton {
    private Singleton() {
    }

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

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

分析:
 静态内部类式巧妙地利用了JVM的类加载机制的两个特性:

a. 类加载的初始化阶段是线程安全的。
b. 类只有在首次被用到的时候才会被加载(首次使用new或者反射创建实例、调用该类静态方法、静态属性、初始化其子类等等才会加载)。

 在Singleton类被加载的时候其内部类SingletonHolder并不会加载,在调用getInstance()方法时用到了SingletonHolder才进行SingletonHolder类的加载,这时去创建Singleton的实例就实现了lazy loading。

结论:

 静态内部类式是一种安全、可靠、简单,并且实现了lazy loading的单例实现方式,推荐使用。

题外话

我们还需要其它写法吗?双重校验锁和静态内部类似乎已经很完美了。但是,在考虑反射和序列化的情况下,前面的四种实现方式都存在问题。

 首先,我们先看反射:

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.getInstance();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton newSingleton = constructor.newInstance();
        System.out.println(singleton == newSingleton);
    }

 我们看上面这段代码。考虑到反射以后,私有构造函数似乎并不能确保只有一个实例。外部调用者仍能不调用getInstance方法而通过反射创建一个实例。

 然后,我们再看序列化:

import org.apache.commons.lang3.SerializationUtils;

public class Test {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }
}

 同样的,考虑到序列化以后,外部调用者也能通过序列化创建一个实例。还有,clone方法也是一样的问题。

 所以,我们还需要一种能防反射攻击和序列化攻击的单例模式实现,这就是枚举。

5. 枚举

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }
}

调用:

public class Test {
    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }
}

分析:

 这种写法主要是利用了枚举的特性:

  a. 枚举类的实例必须在枚举类中显式的指定(首行开始的以第一个分号结束)
  b. 除了在枚举类中显式指定的实例外, 没有任何方式(new,clone,反射,序列化)可以手动创建枚举实例。

 枚举类的构造方法是私有的,保证外部不能通过new创建枚举的实例。clone、反射、序列化都会抛出异常(java枚举(enum)全面解读),这样又保证了外部不能通过clone、反射、序列化创建实例。和饿汉式和静态内部类相似,枚举类的实例实际上是通过静态代码块创建,由类的加载机制保证线程安全。需要注意的是,虽然枚举实现单例足够安全,但由于其也是在枚举类加载的初始化阶段就创建了实例,实际上是一种安全性加强的饿汉式单例模式,也存在内存使用效率不够高的问题。

结论:

 枚举类是一种非常安全可靠、实现简单的单例实现方式,推荐使用。

6. CAS

public class Singleton {

    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();

    private Singleton() {
    }

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

分析:

 用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。

结论:

 由于笔者对于CAS了解不深,CAS部分基本全部拷贝原文(不使用synchronized和lock,如何实现一个线程安全的单例?),但是这里还是勉强说一下个人见解,不一定准确,谨慎采纳:CAS是用于高并发的单例实现方式,高并发意味着需要更高效的CPU和更宽裕的内存。如果是移动端应用采用CAS单例实现方式,一旦出现死循环或者OOM,影响将是灾难性的。除非使用场景真的需要高并发,否则建议慎重考虑使用CAS方式实现单例。

7.总结

 讨论完了6种不同的实现方式,我们总结下其中比较好的单例实现方式的使用场景:

  1. 实例占用内存大需要延迟加载,可以使用双重校验锁定和静态内部类,推荐使用静态内部类。
  2. 实例占用内存小或者需要防反射、防序列化、防clone,使用枚举。
  3. 高并发,使用CAS。

8. 参考

Java单例模式:为什么我强烈推荐你用枚举来实现单例模式

【单例深思】饿汉式与类加载

java枚举(enum)全面解读

双重检查锁单例模式为什么要用volatile关键字?

不使用synchronized和lock,如何实现一个线程安全的单例?

感谢几位原作者辛勤付出。

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