JAVA 设计模式之《单例模式》

单例模式介绍

单例模式: 一种对象创建模式,为了确保系统中一个类只创建一个实例。

这样的好处是什么呢?

  • 对于频繁使用的对象,可以进行复用;减少频繁创建对象和销毁所花费的时间和内存,减少了GC的压力。

一个成熟的单例模式要满足以下几点

  • 单例类在系统中只能有一个实例
  • 单例类必须自己创建自己的实例
  • 单例类要提供这一实例给其他对象

注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。

单例模式的实现

1. 懒汉模式(懒加载-线程不安全)


public class SingletonDemo {

    private static SingletonDemo instance;

    //私有构造方法
    private SingletonDemo(){}

    public static SingletonDemo getInstance(){
        if(instance == null){
            instance = new SingletonDemo();
        }
        return instance;
    }
}

上面代码使用private修饰了静态变量instance,让变量只可以在单例类内部调用,无参构造方法也使用private修饰,做到必须自己创建自己的实例;通过静态方法getInstance(),只创建一个实例类并且return出去;之所以称这种写法为懒汉模式,就是因为可以进行延迟加载,第一次调用才初始化,避免内存浪费。

但是这种写法是线程不安全的,在并发的情况下调用getInstance()方法,就有可能造成创建多个实例的情况。

针对这种情况,下面这种写法进行了优化。

2. 懒汉模式(懒加载-线程安全)


public class SingletonDemo {

    private static SingletonDemo instance;

    //私有构造方法
    private SingletonDemo(){}

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

添加了synchronized关键字,使整个方法内的代码全部同步进行。

虽然添加synchronized解决了线程安全的问题,实现了真正的单例模式,但是方法执行一次后,再次调用就不再需要同步执行,效率很低,所以我们改进一下。


public class SingletonDemo {
    private static SingletonDemo instance;

    //私有构造方法
    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if (instance == null) {
        //同步代码块
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

上面代码使用同步代码块synchronized (obj) 对一部分代码进行了加锁。

这就是双重检查锁(DCL),至于为什么要判断两次instance,大家设想一下两个线程的执行流程就明白了,这里就不多说了。

虽然上面使用同步代码块解决了并发时的效率问题,但是这里存在一个隐患,jvm为了提高执行效率,会进行指令重排序,指令重排序后,会打乱指令的先后执行顺序,我举个例子。

instance = new SingletonDemo();

上面创建对象的操作并不是一个原子操作,原子操作是指一个或者多个不可再分割的操作。这些操作的执行顺序不能被打乱,这些步骤也不可以被切割而只执行其中的一部分(不可中断性)。

而上面的指令执行顺序如下:

  1. A-分配内存空间
  2. B-初始化实例对象
  3. C-设置实例指向刚分配的内存地址(此时instance不等于null)

由于B和C都依赖于A,所以只会存在2种排序情况:

  • A -> B -> C
  • A -> C -> B

按照上面第一种分解顺序(A -> B -> C)不会有任何问题,是符合我们预期的,但是如果jvm按照第二种情况(A -> C -> B)执行,在多线程的情况下,就可能导致instance已经和初始对象内存建立关联,但是instance还没有进行初始化完成的情况,所以在判断if(instance == null)的时候,instance实际是不等于null的,这个时候如果使用instance实例,就会报错。

所以为了避免这种情况发生,我们需要用到 volatile 关键字。

使用了volatile的变量,会禁止jvm对其进行指令重排序,并且保证可见性

可见性:java编程语言允许线程访问共享变量,为了确保共享变量的准确性和一致,其他线程能够立即看得到修改的值。

我们把代码修改一下,加上volatile,这样就解决了所有隐患,但是这种方式的效率不是很高。


public class SingletonDemo {
    private static volatile SingletonDemo instance;

    //私有构造方法
    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if (instance == null) {
        //同步代码块
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

3. 饿汉模式(线程安全)

饿汉模式就是在类被加载的时候,就会创建实例,是线程安全的;缺点就是会在还不需要该实例的时候就已经把实例创建出来了,说白了就是不支持延迟加载。


public class SingletonDemo {

    private static final SingletonDemo instance = new SingletonDemo();

    //私有构造方法
    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        return instance;
    }
}

4. 静态内部类(懒加载-线程安全)


public class SingletonDemo {

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

    //私有构造方法
    private SingletonDemo() {}

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

静态内部类不会在单例类加载的时候就创建实例,而是在调用getInstance()方法的时候,也就是用到静态内部类的时候才会创建,这个时候jvm会加载SingletonHolder类,并且完成实例化的操作,在加载过程中,jvm会保证线程安全。

5. 枚举(线程安全)

枚举,支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化,而且线程安全,是实现单例模式的最佳方法。


public enum SingletonDemo {

    INSTANCE;

}

而在这几种实现方式中,不管是上面的双重检查锁还是静态内部类都可以用反射和反序列化进行暴力破解。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

而枚举不会被反射和反序列化破解。

Java中有规定:

枚举的序列化和反序列化是有特殊定制的,并且枚举是无法通过反射实现的。

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

推荐阅读更多精彩内容