Java设计模式_单例模式
创始人
2024-06-01 03:29:14
0

Java设计模式_单例模式

亦称: 单件模式、Singleton

意图

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-asf8w5DU-1678452651957)(./assets/singleton.png)]

问题

单例模式同时解决了两个问题, 所以违反了单一职责原则

  1. 保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。

    它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。

    注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T2l1OPib-1678452651960)(./assets/singleton-comic-1-zh.png)]

客户端甚至可能没有意识到它们一直都在使用同一个对象。

  1. 为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。

    和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。

    还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。

如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例

解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  • 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

真实世界类比

政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。

单例模式结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3PHaEzf-1678452651960)(./assets/structure-zh.png)]

单例 (Singleton) 类声明了一个名为 get­Instance获取实例的静态方法来返回其所属类的一个相同实例。

单例的构造函数必须对客户端 (Client) 代码隐藏。 调用 获取实例方法必须是获取单例对象的唯一方式。

伪代码

在本例中, 数据库连接类即是一个单例。 该类不提供公有构造函数, 因此获取该对象的唯一方式是调用 获取实例方法。 该方法将缓存首次生成的对象, 并为所有后续调用返回该对象。

// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处
// 都能访问相同的数据库连接实例。
class Database is// 保存单例实例的成员变量必须被声明为静态类型。private static field instance: Database// 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构// 造方法。private constructor Database() is// 部分初始化代码(例如到数据库服务器的实际连接)。// ……// 用于控制对单例实例的访问权限的静态方法。public static method getInstance() isif (Database.instance == null) thenacquireThreadLock() and then// 确保在该线程等待解锁时,其他线程没有初始化该实例。if (Database.instance == null) thenDatabase.instance = new Database()return Database.instance// 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。public method query(sql) is// 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以// 在这里添加限流或缓冲逻辑。// ……class Application ismethod main() isDatabase foo = Database.getInstance()foo.query("SELECT ……")// ……Database bar = Database.getInstance()bar.query("SELECT ……")// 变量 `bar` 和 `foo` 中将包含同一个对象。

单例模式适合应用场景

如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。

单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。

如果你需要更加严格地控制全局变量, 可以使用单例模式。

单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。

请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例方法, 即 getInstance 中的代码即可实现。

实现方式

  1. 在类中添加一个私有静态成员变量用于保存单例实例。
  2. 声明一个公有静态构建方法用于获取单例实例。
  3. 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
  4. 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
  5. 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。

单例模式优缺点

  • 你可以保证一个类只有一个实例。

  • 你获得了一个指向该实例的全局访问节点。

  • 仅在首次请求单例对象时对其进行初始化。

  • 违反了单一职责原则。 该模式同时解决了两个问题。

  • 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。

  • 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。

  • 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。

与其他模式的关系

  • 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
  • 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
    1. 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    2. 单例对象可以是可变的。 享元对象是不可变的。
  • 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。

AI代码示例

单例模式是一种创建型设计模式,用于保证一个类仅有一个实例,并提供全局访问点。

在实现单例模式时,需要确保一个类仅有一个实例,并提供一个全局的访问点,以便其他对象可以通过该访问点获取该实例。单例模式通常适用于需要对资源进行集中管理的情况,如数据库连接、线程池等。

在 Java 中,实现单例模式有多种方法,包括懒汉式、饿汉式、双重校验锁等。具体实现方式可以根据实际情况选择。

懒汉式单例模

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

在上面的实现中,Singleton类的构造方法被声明为私有,这意味着只有在Singleton类内部才能实例化它。

Singleton类中有一个私有的静态成员变量instance,用于存储Singleton类的唯一实例。

getInstance()方法是公共的静态方法,用于获取Singleton类的唯一实例。在第一次调用该方法时,instance被初始化为一个新的Singleton对象,以后每次调用该方法时,都会返回同一个instance对象。

这种懒汉式单例模式的实现方式具有懒加载的特点,即只有在第一次使用时才会创建对象。它的缺点是,在多线程环境下可能会出现线程安全问题,因此需要在getInstance()方法中添加同步锁来解决这个问题。

饿汉式单例模式

除了懒汉式单例模式,还有另一种实现单例模式的方式,称为饿汉式单例模式。在饿汉式单例模式中,类的实例在类加载时就已经被创建,因此不存在线程安全问题。

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

在这个实现中,instance成员变量被声明为私有的静态成员变量,并且在类加载时就被初始化为一个新的Singleton对象。getInstance()方法简单地返回instance对象即可。

饿汉式单例模式的缺点是,它无法实现懒加载,即使在应用程序中没有使用该对象,它也会被创建,可能会浪费一定的内存空间。

双重校验锁单例模式

双重校验锁单例模式(Double-Checked Locking Singleton Pattern)是一种高效的单例模式实现方式。在这种方式中,首先检查是否已经存在一个实例,如果不存在才进行同步,这样一来,只有第一次会同步,之后的获取实例操作就不会进行同步了,从而减少了程序的开销。

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

在这个示例代码中,volatile 关键字用于确保 instance 变量在多个线程之间的可见性,synchronized 关键字用于在多线程环境下保证线程安全。

枚举单例模式

枚举单例模式是一种实现单例模式的方式,相较于其他方式,它具有简单、安全、线程安全等优点。在Java 5之后,枚举单例模式成为了官方推荐的实现单例模式的方式之一。

public enum Singleton {INSTANCE;public void doSomething() {// 执行相关操作}
}

在这种实现方式中,Singleton是一个枚举类型,它只有一个枚举常量INSTANCE。因为枚举类型是线程安全的,并且在Java中保证只会有一个枚举常量,所以通过枚举类型实现的单例模式也是线程安全的。

在使用时,只需要调用Singleton.INSTANCE即可获取单例对象。例如:

Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();

枚举单例模式的优点包括:

  1. 线程安全:枚举类型保证了线程安全,任何时候都只会有一个实例存在。
  2. 简单:枚举类型的实现方式相对于其他单例模式的实现方式,代码更加简单易懂。
  3. 反序列化安全:由于枚举类型的实例是在枚举类型被加载时创建的,所以它们不会被反序列化多次,从而保证了反序列化的安全性。

需要注意的是,枚举单例模式虽然具有很多优点,但是它并不适用于所有的场景。例如,如果需要延迟加载单例对象,就不能使用枚举单例模式。在这种情况下,需要使用其他的单例模式实现方式,比如双重校验锁单例模式。

玄子代码示例

饿汉式

package CH_12.Singleton.Hungry;public class GirlFriend {public static int a = 1;// 在内部创建私有对象,使用 static 修饰,在类加载时就创建对象// 不管是否使用都提前创建了,饿汉private static GirlFriend girlFriend = new GirlFriend("小红");private String name;private GirlFriend(String name) {System.out.println("构造器被使用");// 检验构造器是否被创建this.name = name;}// 将构造器私有化,不允许外部创建对象public static GirlFriend getInstance() {return girlFriend;}// 再使用公共的 getInstance() 方法返回 GirlFriend 对象@Overridepublic String toString() {return "GirlFriend{" +"name='" + name + '\'' +'}';}
}
package CH_12.Singleton.Hungry;public class HungryMan {public static void main(String[] args) {System.out.println(GirlFriend.a);// 调用属性,对象就加载,造成资源浪费GirlFriend instance = GirlFriend.getInstance();System.out.println(instance);// 对象一GirlFriend instance2 = GirlFriend.getInstance();System.out.println(instance2);// 对象2System.out.println(instance == instance2);// 内存地址一致}
}

懒汉式

package CH_12.Singleton.Lazy;public class GirlFriend {public static int a = 1;// 在内部创建私有对象,使用 static 修饰,在类加载时就创建对象// 不管是否使用都提前创建了,饿汉private static GirlFriend girlFriend;private String name;private GirlFriend(String name) {System.out.println("创建成功");// 检验构造器是否被创建this.name = name;}// 将构造器私有化,不允许外部创建对象public static GirlFriend getInstance() {if (girlFriend == null) {girlFriend = new GirlFriend("小红");}// 如果对象为空才创建,二次调用就返回上次创建的对象// 但是存在线程安全的问题,可同时创建多个对象return girlFriend;}// 再使用公共的 getInstance() 方法返回 GirlFriend 对象@Overridepublic String toString() {return "GirlFriend{" +"name='" + name + '\'' +'}';}
}
package CH_12.Singleton.Lazy;public class Lazyman {public static void main(String[] args) {System.out.println(GirlFriend.a);
//         调用属性,对象就加载,造成资源浪费GirlFriend instance = GirlFriend.getInstance();System.out.println(instance);// 对象一GirlFriend instance2 = GirlFriend.getInstance();System.out.println(instance2);// 对象2System.out.println(instance == instance2);// 内存地址一致//}
}

双重校验锁

package CH_12.Singleton.DoubleLock;public class GirlFriend {public static int a = 1;// 在内部创建私有对象,使用 static 修饰,在类加载时就创建对象// 不管是否使用都提前创建了,饿汉private static GirlFriend girlFriend;private String name;private GirlFriend(String name) {System.out.println(Thread.currentThread().getName() + "-----OK");// 检验构造器是否被创建this.name = name;}// 将构造器私有化,不允许外部创建对象public static GirlFriend getInstance() {if (girlFriend == null) {// 双重校验锁 DCL 懒汉式synchronized (GirlFriend.class) {if (girlFriend == null) {girlFriend = new GirlFriend("小红");}}}// 如果对象为空才创建,二次调用就返回上次创建的对象// 但是存在线程安全的问题,可同时创建多个对象return girlFriend;}// 再使用公共的 getInstance() 方法返回 GirlFriend 对象@Overridepublic String toString() {return "GirlFriend{" +"name='" + name + '\'' +'}';}
}
package CH_12.Singleton.DoubleLock;public class Lazyman {public static void main(String[] args) {
//        System.out.println(GirlFriend.a);
         调用属性,对象就加载,造成资源浪费
//        GirlFriend instance = GirlFriend.getInstance();
//        System.out.println(instance);
//        // 对象一
//        GirlFriend instance2 = GirlFriend.getInstance();
//        System.out.println(instance2);
//        // 对象2
//        System.out.println(instance == instance2);
//        // 内存地址一致
//
//        // Runtimefor (int i = 0; i < 10; i++) {new Thread(() -> {GirlFriend.getInstance();}).start();}}
}

Java设计模式_单例模式-玄子3.7

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...