设计模式【1】-- 单例模式到底几种写法?

[TOC]

单例模式,是一种比较简单的设计模式,也是属于创建型模式(提供一种创建对象的模式或者方式)。
要点:

  • 1.涉及一个单一的类,这个类来创建自己的对象(不能在其他地方重写创建方法,初始化类的时候创建或者提供私有的方法进行访问或者创建,必须确保只有单个的对象被创建)。
  • 2.单例模式不一定是线程不安全的。
  • 3.单例模式可以分为两种:懒汉模式(在第一次使用类的时候才创建,可以理解为类加载的时候特别懒,要用的时候才去获取,要是没有就创建,由于是单例,所以只有第一次使用的时候没有,创建后就可以一直用同一个对象),饿汉模式(在类加载的时候就已经创建,可以理解为饿汉已经饿得饥渴难耐,肯定先把资源紧紧拽在自己手中,所以在类加载的时候就会先创建实例)

关键字:

  • 单例:singleton
  • 实例:instance
  • 同步: synchronized

饿汉模式

1.私有属性

第一种singlepublic,可以直接通过Singleton类名来访问。

 public class Singleton {
    // 私有化构造方法,以防止外界使用该构造方法创建新的实例
    private Singleton(){
    }
    // 默认是public,访问可以直接通过Singleton.instance来访问
    static Singleton instance = new Singleton();
}

2.公有属性

第二种是用private修饰singleton,那么就需要提供static 方法来访问。

public class Singleton {
    private Singleton(){
    }
    // 使用private修饰,那么就需要提供get方法供外界访问
    private static Singleton instance = new Singleton();
    // static将方法归类所有,直接通过类名来访问
    public static Singleton getInstance(){
        return instance;.
    }
}

3. 懒加载

饿汉模式,这样的写法是没有问题的,不会有线程安全问题(类的static成员创建的时候默认是上锁的,不会同时被多个线程获取到),但是是有缺点的,因为instance的初始化是在类加载的时候就在进行的,所以类加载是由ClassLoader来实现的,那么初始化得比较早好处是后来直接可以用,坏处也就是浪费了资源,要是只是个别类使用这样的方法,依赖的数据量比较少,那么这样的方法也是一种比较好的单例方法。
在单例模式中一般是调用getInstance()方法来触发类装载,以上的两种饿汉模式显然没有实现lazyload(个人理解是用的时候才触发类加载)
所以下面有一种饿汉模式的改进版,利用内部类实现懒加载。
这种方式Singleton类被加载了,但是instance也不一定被初始化,要等到SingletonHolder被主动使用的时候,也就是显式调用getInstance()方法的时候,才会显式的装载SingletonHolder类,从而实例化instance。这种方法使用类装载器保证了只有一个线程能够初始化instance,那么也就保证了单例,并且实现了懒加载。

值得注意的是:静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

public class Singleton {
    private Singleton(){
    }
    //内部类
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    //对外提供的不允许重写的获取方法
    public static final Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

懒汉模式

最基础的代码(线程不安全)

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

这种写法,是在每次获取实例instance的时候进行判断,如果没有那么就会new一个出来,否则就直接返回之前已经存在的instance。但是这样的写法不是线程安全的,当有多个线程都执行getInstance()方法的时候,都判断是否等于null的时候,就会各自创建新的实例,这样就不能保证单例了。所以我们就会想到同步锁,使用synchronized关键字:
加同步锁的代码(线程安全,效率不高)

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

这样的话,getInstance()方法就会被锁上,当有两个线程同时访问这个方法的时候,总会有一个线程先获得了同步锁,那么这个线程就可以执行下去,而另一个线程就必须等待,等待第一个线程执行完getInstance()方法之后,才可以执行。这段代码是线程安全的,但是效率不高,因为假如有很多线程,那么就必须让所有的都等待正在访问的线程,这样就会大大降低了效率。那么我们有一种思路就是,将锁出现等待的概率再降低,也就是我们所说的双重校验锁(双检锁)。

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

1.第一个if判断,是为了降低锁的出现概率,前一段代码,只要执行到同一个方法都会触发锁,而这里只有singleton为空的时候才会触发,第一个进入的线程会创建对象,等其他线程再进入时对象已创建就不会继续创建,如果对整个方法同步,所有获取单例的线程都要排队,效率就会降低。
2.第二个if判断是和之前的代码起一样的作用。

上面的代码看起来已经像是没有问题了,事实上,还有有很小的概率出现问题,那么我们先来了解:原子操作指令重排

1.原子操作

  • 原子操作,可以理解为不可分割的操作,就是它已经小到不可以再切分为多个操作进行,那么在计算机中要么它完全执行了,要么它完全没有执行,它不会存在执行到中间状态,可以理解为没有中间状态。比如:赋值语句就是一个原子操作:
 n = 1; //这是一个原子操作 

假设n的值以前是0,那么这个操作的背后就是要么执行成功n等于1,要么没有执行成功n等于0,不会存在中间状态,就算是并发的过程中也是一样的。
下面看一句不是原子操作的代码:

int n =1;  //不是原子操作

原因:这个语句中可以拆分为两个操作,1.声明变量n,2.给变量赋值为1,从中我们可以看出有一种状态是n被声明后但是没有来得及赋值的状态,这样的情况,在并发中,如果多个线程同时使用n,那么就会可能导致不稳定的结果。

2.指令重排

所谓指令重排,就是计算机会对我们代码进行优化,优化的过程中会在不影响最后结果的前提下,调整原子操作的顺序。比如下面的代码:

int a ;   // 语句1 
a = 1 ;   // 语句2
int b = 2 ;     // 语句3
int c = a + b ; // 语句4

正常的情况,执行顺序应该是1234,但是实际有可能是3124,或者1324,这是因为语句3和4都没有原子性问题,那么就有可能被拆分成原子操作,然后重排.
原子操作以及指令重排的基本了解到这里结束,看回我们的代码:

主要是instance = new Singleton(),根据我们所说的,这个语句不是原子操作,那么就会被拆分,事实上JVM(java虚拟机)对这个语句做的操作:

  • 1.给instance分配了内存
  • 2.调用Singleton的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中
  • 3.将instance对象指向分配的内存空间,执行完这一步才算真的完成了,instance才不是null。

在一个线程里面是没有问题的,那么在多个线程中,JVM做了指令重排的优化就有可能导致问题,因为第二步和第三步的顺序是不能够保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回instance,然后使用,就会报空指针。
从更上一层来说,有一个线程是instance已经不为null但是仍没有完成初始化中间状态,这个时候有一个线程刚刚好执行到第一个if(instance==null),这里得到的instance已经不是null,然后他直接拿来用了,就会出现错误。
对于这个问题,我们使用的方案是加上volatile关键字。

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;
   }
}

volatile的作用:禁止指令重排,把instance声明为volatile之后,这样,在它的赋值完成之前,就不会调用读操作。也就是在一个线程没有彻底完成instance = new Singleton();之前,其他线程不能够去调用读操作。

  • 上面的方法实现单例都是基于没有复杂序列化和反射的时候,否则还是有可能有问题的,还有最后一种方法是使用枚举来实现单例,这个可以说的比较理想化的单例模式,自动支持序列化机制,绝对防止多次实例化。
public enum Singleton {
    INSTANCE;
    public void doSomething() {

    }
}

以上最推荐枚举方式,当然现在计算机的资源还是比较足够的,饿汉方式也是不错的,其中懒汉模式下,如果涉及多线程的问题,也需要注意写法。

最后提醒一下,volatile关键字,只禁止指令重排序,保证可见性(一个线程修改了变量,对任何其他线程来说都是立即可见的,因为会立即同步到主内存),但是不保证原子性。

【作者简介】
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

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

推荐阅读更多精彩内容