Java设计模式之单例模式

单例模式,是特别常见的一种设计模式,因此我们有必要对它的概念和几种常见的写法非常了解,而且这也是面试中常问的知识点。

所谓单例模式,就是所有的请求都用一个对象来处理,如我们常用的Spring默认就是单例的,而多例模式是每一次请求都创建一个新的对象来处理,如structs2中的action。

使用单例模式,可以确保一个类只有一个实例,并且易于外部访问,还可以节省系统资源。如果在系统中,希望某个类的对象只存在一个,就可以使用单例模式。

那怎么确保一个类只有一个实例呢?

我们知道,通常我们会通过new关键字来创建一个新的对象。这个时候类的构造函数是public公有的,你可以随意创建多个类的实例。所以,首先我们需要把构造函数改为private私有的,这样就不能随意new对象了,也就控制了多个实例的随意创建。

然后,定义一个私有的静态属性,来代表类的实例,它只能类内部访问,不允许外部直接访问。

最后,通过一个静态的公有方法,把这个私有静态属性返回出去,这就为系统创建了一个全局唯一的访问点。

以上,就是单例模式的三个要素。总结为:

  1. 私有构造方法
  2. 指向自己实例的私有静态变量
  3. 对外的静态公共访问方法

单例模式分为饿汉式和懒汉式。它们的主要区别就是,实例化对象的时机不同。饿汉式,是在类加载时就会实例化一个对象。懒汉式,则是在真正使用的时候才会实例化对象。

饿汉式单例代码实现:

public class Singleton {

    // 饿汉式单例,直接创建一个私有的静态实例
    private static Singleton singleton = new Singleton();

    //私有构造方法
    private Singleton(){

    }

    //提供一个对外的静态公有方法
    public static Singleton getInstance(){
        return singleton;

    }
}

懒汉式单例代码实现

public class Singleton {

    // 懒汉式单例,类加载时先不创建实例
    private static Singleton singleton = null;

    //私有构造方法
    private Singleton(){

    }

    //真正使用时才创建类的实例
    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

稍有经验的程序员就发现了,以上懒汉式单例的实现方式,在单线程下是没有问题的。但是,如果在多线程中使用,就会发现它们返回的实例有可能不是同一个。我们可以通过代码来验证一下。创建十个线程,分别启动,线程内去获得类的实例,把实例的 hashcode 打印出来,只要相同则认为是同一个实例;若不同,则说明创建了多个实例。

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new MyThread().start();
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.hashCode());
    }
}
/**
运行多次,就会发现,hashcode会出现不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/

所以,以上懒汉式的实现方式是线程不安全的。那饿汉式呢?你可以手动测试一下,会发现不管运行多少次,返回的hashcode都是相同的。因此,认为饿汉式单例是线程安全的。

那为什么饿汉式就是线程安全的呢?这是因为,饿汉式单例在类加载时,就创建了类的实例,也就是说在线程去访问单例对象之前就已经创建好实例了。而一个类在整个生命周期中只会被加载一次。因此,也就可以保证实例只有一个。所以说,饿汉式单例天生就是线程安全的。(可以了解一下类加载机制)

既然懒汉式单例不是线程安全的,那么我们就需要去改造一下,让它在多线程环境下也能正常工作。以下介绍几种常见的写法:

  1. 使用synchronized方法

实现非常简单,只需要在方法上加一个synchronized关键字即可

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    //使用synchronized修饰方法,即可保证线程安全
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种方式,虽然可以保证线程安全,但是同步方法的作用域太大,锁的粒度比较粗,因此,执行效率就比较低。

  1. synchronized 同步块

既然,同步整个方法的作用域大,那我缩小范围,在方法里边,只同步创建实例的那一小部分代码块不就可以了吗(因为方法较简单,所以锁代码块和锁方法没什么明显区别)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //synchronized只修饰方法内部的部分代码块
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

这种方法,本质上和第一种没什么区别,因此,效率提升不大,可以忽略不计。

  1. 双重检测(double check)

可以看到,以上的第二种方法只要调用getInstance方法,就会走到同步代码块里。因此,会对效率产生影响。其实,我们完全可以先判断实例是否已经存在。若已经存在,则说明已经创建好实例了,也就不需要走同步代码块了;若不存在即为空,才进入同步代码块,这样可以提高执行效率。因此,就有以下双重检测了:

public class Singleton {

    //注意,此变量需要用volatile修饰以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //进入方法内,先判断实例是否为空,以确定是否需要进入同步代码块
        if(singleton == null){
            synchronized (Singleton.class){
                //进入同步代码块时也需要判断实例是否为空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

需要注意的一点是,此方式中,静态实例变量需要用volatile修饰。因为,new Singleton() 是一个非原子性操作,其流程为:

a.给 singleton 实例分配内存空间
b.调用Singleton类的构造函数创建实例
c.将 singleton 实例指向分配的内存空间,这时认为singleton实例不为空

正常顺序为 a->b->c,但是,jvm为了优化编译程序,有时候会进行指令重排序。就会出现执行顺序为 a->c->b。这在多线程中就会表现为,线程1执行了new对象操作,然后发生了指令重排序,会导致singleton实例已经指向了分配的内存空间(c),但是实际上,实例还没创建完成呢(b)。

这个时候,线程2就会认为实例不为空,判断 if(singleton == null)为false,于是不走同步代码块,直接返回singleton实例(此时拿到的是未实例化的对象),因此,就会导致线程2的对象不可用而使用时报错。

4)使用静态内部类

思考一下,由于类加载是按需加载,并且只加载一次,所以能保证线程安全,这也是为什么说饿汉式单例是天生线程安全的。同样的道理,我们是不是也可以通过定义一个静态内部类来保证类属性只被加载一次呢。

public class Singleton {

    private Singleton(){

    }

    //静态内部类
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        //调用内部类的属性,获取单例对象
        return Holder.singleton;
    }
}

而且,JVM在加载外部类的时候,不会加载静态内部类,只有在内部类的方法或属性(此处即指singleton实例)被调用时才会加载,因此不会造成空间的浪费。

5)使用枚举类

因为枚举类是线程安全的,并且只会加载一次,所以利用这个特性,可以通过枚举类来实现单例。

public class Singleton {

    private Singleton(){

    }

    //定义一个枚举类
    private enum SingletonEnum {
        //创建一个枚举实例
        INSTANCE;

        private Singleton singleton;

        //在枚举类的构造方法内实例化单例类
        SingletonEnum(){
            singleton = new Singleton();
        }

        private Singleton getInstance(){
            return singleton;
        }
    }

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

推荐阅读更多精彩内容