Java设计模式 | 单例模式解析与实战

定义

确保某一个类只有一个实例
而且自行实例化并向整个系统提供这个实例

使用场景

确保某个类有且只有一个对象的场景,
避免产生多个对象消耗过多的资源,
或者
某种类型的对象只应该有且只有一个。
例如,
创建一个对象需要消耗的资源过多,
如要访问IO和数据库等资源,这时就要考虑使用单例模式。

单例模式UML类图

  • 角色:

(1)Client——高层客户端;
(2)Singleton——单例类。

  • 实现单例模式的关键点:
    (1)构造函数不对外开放,一般为Private
    (2)通过一个静态方法或者枚举返回单例类对象
    (3)确保单例类的对象有且只有一个,尤其是在多线程环境下;
    (4)确保单例类对象反序列化时不会重新构建对象

  • 通过将单例类的构造函数私有化
    使得客户端代码不能通过 new 的形式手动构造单例类的对象

  • 单例类会暴露一个公有静态方法
    客户端需要调用这个静态方法获取到单例类唯一对象

  • 在获取这个单例对象的过程中需要确保线程安全
    即在多线程环境下构造单例类的对象也是有且只有一个
    这也是实现的难点


重点,注意单例模式中 volatile的重要性


单例的几种实现方式

1. 饿汉模式

声明一个静态类对象,在声明时就己经初始化
用户调用类对象get方法时,可以直接拿去用;
【一声明就初始化,所谓“饿”】

如下,
CEO类使用了饿汉单例模式;

/**
 * 普通员工
 */
class Staff {
    public void work() {
        // 干活
    }
}
// 副总裁
class VP extends Staff {
    @Override
    public void work() {
        // 管理下面的经理
    }
}
// CEO, 饿汉单例模式
class CEO extends Staff {
    private static final CEO mCeo = new CEO();

    // 构造函数私有
    private CEO() {
    }

    // 公有的静态函数,对外暴露获取单例对象的接口
    public static CEO getCeo() {
        return mCeo;
    }
    @Override
    public void work() {
        // 管理VP
    }

}
// 公司类
class Company {
    private List<Staff> allPersons = new ArrayList<Staff>();

    public void addStaff(Staff per) {
        allPersons.add(per);
    }

    public void showAllStaffs() {
        for (Staff per : allPersons) {
            System.out.println("Obj : " + per.toString());
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Company cp = new Company();
        // CEO对象只能通过getCeo函数获取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);
        // 通过new创建VP对象
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        // 通过new创建Staff对象
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();

        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(staff1);
        cp.addStaff(staff2);
        cp.addStaff(staff3);

        cp.showAllStaffs();
    }
}

2. 懒汉模式

  • 懒汉模式是声明一个静态对象,
    并且在用户第一次调用getInstance进行初始化
    【“拖延”,等到调用才初始化,所谓“懒”!】
    public class Singleton {
        private volatile static Singleton instance;
        private Singleton () {}
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton ();
            }
            return instance;
        }
    }
  • getInstance()中添加了 synchronized 关键字,
    也就是 getInstance()是一个同步方法,
    即上面所说的在多线程情况下保证单例对象唯一性的手段。

  • 只不过这里可能有一个问题,
    即使instance己经被初始化(第一次调用时就会被初始化instance),
    每次调用getInstance方法都会进行同步
    这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

  • 优点:单例只有在使用时才会被实例化,在一定程度上节约了资源

  • 缺点:第一次加载时需要及时进行实例化,反应稍慢,
    最大的问题是每次调用 getInstance都进行同步,造成不必要的同步开销

这种模式一般不建议使用!!!!!!!!!!

3. DoubleCheckLock(DCL)实现单例【双重校验锁】

  • 优点:
    资源利用率高,
    第一次执行getInstance()时单例对象才会被实例化,效率高。
    既能够在需要时才初始化单例,
    又能够保证线程安全
    且单例对象初始化后每次调用getInstance()不进行同步锁
    减少不必要的同步开销
  • 缺点:第一次加载时反应稍慢,
    也由于 Java 内存模型的原因偶尔会失败。
    在高并发环境下也有一定的缺陷,虽然发生概率很小。
public class Singleton {
        private volatile static Singleton sInstance = null;
        private Singleton() {
        }
        public void doSomething() {
            System.out.println("do sth.");
        }
        public static Singleton getInstance() {
            if (mInstance == null) {
                synchronized (Singleton.class) {
                    if (mInstance == null) {
                        sInstance = new Singleton();
                    }
                }
            }
            return sInstance;
        }
  • static 保证单例;
    volatile 禁止重排序;
    getInstance() 用来获取实例;
    synchronized 保证原子性、可见性、线程安全;

  • 亮点:getInstance()方法中对instance进行了两次判空:
    第一层判断主要是为了避免不必要的同步【有实例则直接返回,没必要同步】,
    第二层的判断则是为了在null的情况创建实例
    【可能第一层与第二层判断中途有其他线程初始化完成了单例,
    单例不为null,就不用创建了】:

    假设线程A和线程B先后访问了getInstance();
    线程A执行到sInstance = new Singleton()语句,
    这里看起来是一句代码,但实际上它并不是一个原子操作
    这句代码最终会被编译成多条汇编指令,它大致做了3件事情:
    (1)给Singleton的实例分配内存
    (2)调用Singleton()构造函数初始化成员字段;
    (3)将sInstance对象指向分配的内存空间(此时sInstance就不是null了)。

    但是,由于Java编译器允许处理器乱序执行
    以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache、
    寄存器到主内存回写顺序的规定,
    上面的第二和第三的顺序是无法保证的。【指令重排序】

    即,执行顺序可能是1-2-3也可能是1-3-2。
    如果是后者,并且在3执行完毕2未执行之前,被切换到线程B上,
    这时候sInstance因为己经在线程A内执行过了第三点,
    sInstance己经是非空了,
    所以,
    线程B通过getInstance()直接取走sInstance
    再使用时就会出错,这就是DCL失效问题
    而且这种难以跟踪难以重现的错误很可能会隐藏很久。
    在JDK1.5之后,SUN官方己经注意到这种问题,
    调整了JVM,具体化了volatile关键字,
    因此,
    如果JDK是1.5或之后的版本,
    只需要将sInstance的定义改成private volatile static Singleton sInstance = null就可以保证sInstance对象每次都是从主内存中读取
    就可以使用DCL的写法来完成单例模式
    当然,volatile 或多或少也会影响到性能
    但考虑到程序的正确性,牺牲这点性能还是值得的。^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  • DCL 模式是使用最多的单例实现方式!!!!
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    它能够在需要时才实例化单例对象,
    并且能够在绝大多数场景下保证单例对象的唯一性
    除非代码在并发场景比较复杂或者低于JDK 6版本下使用,
    否则,这种方式一般能够满足需求。


4. 静态内部类单例模式

  • DCL虽然在一定程度上解决了资源消耗、多余的同步、线程安全等问题,
    但是,它还是在某些情况下出现失效的问题。
    就是刚说的双重检查锁定(DCL)失效
  • 在《Java 并发编程实践》一书的最后谈到了这个问题,
    并指出这种“优化”是丑陋的,不赞成使用
    而建议使用如下的代码替代
public class Singleton {
        private Singleton() { }
        public static Singleton getInstance () {
            return SingletonHolder.sInstance;
        }
        /**
         * 静态内部类
         */
        private static class SingletonHolder {
            private static final Singleton sInstance = new Singleton();
        }
    }
  • 当第一次加载Singleton类时并不会初始化sInstance
    只有在第一次调用SingletongetInstance()sInstance才会被初始化!!!
    因此,
    第一次调用getInstance()会导致虚拟机加载SingletonHolder类
    这种方式不仅能够确保线程安全
    也能够保证单例对象的唯一性,同时也延迟了单例的实例化
    所以这是推荐使用的单例模式实现方式。


5. 枚举单例

除了以上几种方式,还有更简单的实现方式——枚举!:

public enum SingletonEnum {
        INSTANCE;
        public void doSomething() {
            System.out.println("do sth.");
        }
    }
  • 优点突出:写法简单;

    枚举在Java中与普通的类是一样的,
    不仅能够有字段,还能够有自己的方法。

    最重要的是默认枚举实例创建线程安全的,
    并且在任何情况下它都是一个单例

    在上述的几种单例模式实现中,
    在一个情况下它们会出现重新创建对象的情况,那就是反序列化

    通过序列化可以将一个单例实例对象写到磁盘
    然后再读回来,从而有效地获得一个实例

    即使构造函数私有的,
    反序列化时依然可以通过特殊的途径去创建类的一个新的实例
    相当于调用该类的构造函数
    反序列化操作提供了一个很特别的钩子函数
    类中具有一个私有的、被实例化的方法readResolve()
    这个方法可以让开发人员控制对象的反序列化
    例如,
    上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,
    那么必须加入如下方法:
private Object readResolve() throws ObjectStreamException {
    return sInstance;
}

即在readResolve()中将sInstance对象返回,
而不是默认的重新生成一个新的对象。
而对于枚举,并不存在这个问题,
因为即使反序列化它也不会重新生成新的实例。








参考:

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

推荐阅读更多精彩内容