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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 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源码设计模式解析与实战》
相关文章
|
16天前
|
设计模式 安全 Java
Java编程中的单例模式深入解析
【10月更文挑战第31天】在编程世界中,设计模式就像是建筑中的蓝图,它们定义了解决常见问题的最佳实践。本文将通过浅显易懂的语言带你深入了解Java中广泛应用的单例模式,并展示如何实现它。
|
27天前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
44 0
[Java]23种设计模式
|
11天前
|
设计模式 JavaScript Java
Java设计模式:建造者模式详解
建造者模式是一种创建型设计模式,通过将复杂对象的构建过程与表示分离,使得相同的构建过程可以创建不同的表示。本文详细介绍了建造者模式的原理、背景、应用场景及实际Demo,帮助读者更好地理解和应用这一模式。
|
23天前
|
设计模式 SQL 安全
Java编程中的单例模式深入解析
【10月更文挑战第24天】在软件工程中,单例模式是设计模式的一种,它确保一个类只有一个实例,并提供一个全局访问点。本文将探讨如何在Java中使用单例模式,并分析其优缺点以及适用场景。
12 0
|
1月前
|
设计模式 算法 PHP
PHP中的设计模式:策略模式的深入解析与实践
【10月更文挑战第12天】 在软件开发的世界中,设计模式是解决常见问题的最佳实践。它们不是具体的代码,而是一种编码和设计经验的总结。在PHP开发中,合理运用设计模式可以极大地提高代码的可维护性、扩展性和复用性。本文将深入探讨策略模式(Strategy Pattern)的原理、实现方式及其在PHP中的应用。通过具体示例,我们将展示如何利用策略模式来解耦算法与对象,从而让代码更加灵活和易于管理。
19 0
|
1月前
|
设计模式 存储 安全
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和重用性的关键技术之一。本文将深入探讨单例模式(Singleton Pattern)的原理、实现方式及其在PHP中的应用,同时通过实例展示如何在具体的项目场景中有效利用单例模式来管理和组织对象,确保全局唯一性的实现和最佳实践。
|
20天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
22天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
15天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
36 1

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    43
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    50
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    58
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    63
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    58
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    42
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    110
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78
  • 推荐镜像

    更多