设计模式浅谈 —— 单例模式

作者 tanghuailong

如果喜欢那就去做吧

单例模式

初探单例
  • 定义
    单例模式 也叫单子模式,是一种比较常见的软件设计模式,在应用这个模式的时,单例模式的类确保只有一个实例的存在。因为在许多时候我们系统中只需要一个全局的实例对象,用来协调系统的整体行为。
  • 现状
    单例模式是最容易理解的设计模式,但存在很严重的滥用现状,有很多时候其实没有必要关系到单例模式。所以这篇文章的目的不是要教你如何写单例模式,而是要进行分析其中的优缺点,择优而用
实现的分类

单例模式的实现,其实总共就分为两种,一种为懒汉式,一种饿汉式。

提起单例模式的分类我又想起一个故事。今年初入职场面试的时候,面试的问我,你知道单例模式的懒汉模式和饿汉模式么。我当时的内心是,WTF? ,啥是懒汉?啥是饿汉。。。
后来才去查了一下,奥,这东西就叫懒汉呀,这特么不是懒加载么!

  • 饿汉式(eager initialization)
public class Singleton {
    //实例化singleton
    private static final Singleton instance = new Singleton();
   //定义为私有,确保外部不能使用此构造方法
    private Singleton() {}

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

上面的代码就是饿汉式的实现,在加载类的时候,就进行了实例化,如果 private singleton(){} 里面包含比较笨重的操作时候,使用这种方式,会在一开始启动的时候,比较浪费时间,带来一种不好的体验。另外代码里需要注意的点,我给加上了注释。

  • 懒汉式(lazy initialization)
  • 双重验证加锁(Double Checked Locking)
public class Singleton {
  //一开始并未进行 实例化,实例化放在调用 getInstance()时候才实例化
    private static volatile Singleton instance = null;

    private Singleton() {   }

    public static Singleton getInstance() {
        // A 点
        if (instance == null) {
       //B 点
            synchronized (Singleton.class){
  //C 点
                if (instance == null) {
                    instance = new Singleton();
                }
                // 错误写法(错误)
                // instance = new Singleton();
            }
        }
        return instance;
    }
}

说一下 双重验证加锁 ,这个名字是我自己根据英文起的,记不起叫什么名字了,这里把需要注意的点说一下。
为什么要在getInstance()方法中判断两次是非为null?
答: 因为在多线程的情况下,进入A 点的可能是多个线程,即进入getInstance()方法的可能是多个线程,当进入B点的时候,可能是线程1先到了B点,之后休眠,线程二也进入了B点。所以在B点的时候也有可能是多个线程。紧接着 到达了 scnchronized块中,因为同步块里只允许一个线程,所以当线程1运行完了之后,线程2才进入,如果用下面的错误写法,会导致线程2进入的时候也会实例化一个对象,这就和我们要求的目的不一样了,所以还要进行一个null判断。
关键词volatile起到的作用?
答:
1.首先应该明确关键词volitile在java中的作用

可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值

用volatile来包装变量来实现对多个线程的可见性,即线程1修改了变量,线程2中的变量值随之也发送变化。如果不使用volatile,当线程1执行完毕之后,线程2进入C点,因为没有使用volatile所以,这时候线程2中instance还有可能为null

2.禁止重排优化,关于这点可以参考这篇博客,写的非常好。
注意 这种单例模式的实现方式,并不是一个优秀的方式,太复杂了

  • 内部类(holder class)
class Singleton{
    private static class SingletonHolder{
        static final Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

关于内部类实现单例模式,有以下几点需要注意的
内部类 方式是如何实现懒加载?
答: 当它真正被用到的时候之前,即调用getInstance(), static class 不会被VM 加载,其实不光 static class ,任何的 Class 都不会被加载。参考 See the JLS -12.4.1 When Initialization Occurs
内部类方式是如何做到线程安全的?
答: 第一个线程调用 getInstance()的时候,JVM 会保持这个内部类,即singtonholder ,当第二个线程也在同时调用getInstance(),JVM 会等到第一个线程完成加载之后才会让给第二个线程访问。所以第二个线程得到的是已经加载完成的singletonholder。并且 JLS 规则保证每个类只会被第一次用到的时候加载一次。
参考 singleton-pattern-bill-pughs-solution

  • 枚举(enum)
public enum Sington {
// 只会有一个INSTANCE 实例
    INSTANCE;
    public void foo() {
        //to do someing
    }
}
//调用方式,在下面这样访问的情况下才会加载。
Sington.INSTANCE.foo();

其实enum的实现方式,enum里面的字段属性为 public static final,另外enum方式其实算伪懒加载吧,应该放在饿汉模式里面的。至于原因,我下面将会做出解释。

关于饿汉和懒汉的思考

在说到这点的时候,首先你确认你知道类是在什么时候加载的么?

当类第一次被用到的时候,才会被JVM加载,并且初始化所有static 变量。注意这里是说的是加载,并不是实例化。
Object object = new Object(),类似于这样叫实例化

正如下面这个例子。

public class LazyEnumTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Sleeping for 5 seconds...");
        Thread.sleep(5000);
        System.out.println("Accessing enum...");
        LazySingleton lazy = LazySingleton.INSTANCE;
        System.out.println("Done.");
    }
}
//测试一
enum LazySingleton {
    INSTANCE;
    static { System.out.println("Static Initializer"); }
}
// 测试二
public class Singleton {
  //private static final String testStr = "test";
 //private static final int testNum = 0;
 //private static final Object testObj = new Object();
    private static final Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
    
   public static void foo(){
    //to do some thing
  }
}

下面是运行结果

$ java LazyEnumTest
Sleeping for 5 seconds...
Accessing enum...
Static InitializerDone.

所以只有在访问 LazySingleton.INSTANCE才会被加载,所以Enum也会被当作懒加载。
到了这个时候你也许会说,那测试二,即饿汉模式岂不是也是懒加载了。对的,你说的没错,的确是懒加载。即使你进行下面的操作,它也是懒加载的。

public class LazyEnumTest {
//此时,并未Singleton类并未加载
    private static final Singleton singleton= null;
    public static void main(String[] args) throws InterruptedException {
//在这个时候Singleton类才加载
      singleton = Singleton.getInstance();
    }
}

所以饿汉模式和懒汉模式其实差不多,对吧,都是懒加载了,那为什么还需要懒汉模式那。
区别在上面我举的例子上。因为作为一个单例模式,你可能依赖一下其他的东西,才能在 private Singleton() {} 完成初始化,比如你需要上面的testObj

当你在一个地方使用如下的写法

//这个时候,类就要进行加载。这时候,我只是要打印一下这个testObj,
//但是这个类就已经加载。这并不是我期望的效果,这个时候就该使用懒汉模式的
System.out.println(Singleton.testObj);
// 或者调用foo()也会一样的效果
Sington.foo();
// 例外的情况,下面的调用,并不会引起类的加载
System.out.println(Singleton.testStr);
System.out.println(Singleton.testNum)

上面例外的原因如下

The use or assignment of a static field declared by a class or interface, except for static fields that are final and initialized by a compile-time constant expression (in bytecodes, the execution of a getstatic or putstatic instruction)
ps ---- it only applies to "static fields that are final and initialized by a compile-time constant expression":

大意就是,当 类或者接口里面的变量属性,为 staic final 时候,并且会被 编译期 常量初始化,该属性才会被加载。但这种操作不会引起类的加载

private static final String testStr = "test";  //compile time constant
 private static final int testNum = 0; //compile time constant
private static final Object testObj = new Object(); //initialised at runtime

参考 Singleton via enum way is lazy initialized?

总结

所以以后使用 单例模式,最好的应该 内部类的方式,但如果你进行初始化的时候,不需要依赖一些其他的东西,那用Enum方式是最好的选择,否则就是内部类的方式。此外Enum还保证了序列化的时候也只产生一个实例。

至于 单例模式和序列化就是另一个故事额。
好累,不想讲了。想了解的请点击这里。。。。单例模式和序列化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容