【设计模式】单例模式

系列文章目录



在这里插入图片描述

类也需要计划生育

我们写一个窗体程序时,当中有一个是‘工具箱’窗体,问题就是,我们希望工具箱要么不出现,出现也只出现一个,而实际上却是我每次单击菜单,实例化‘工具箱’,就会出现一个,多次点击就会出现多个?这里我们应该怎么办呢?

我们的第一版代码是这样的,首先我们建立一个Java的swing窗体应用程序,默认窗体为JFame,左上角有一个打开工具箱按钮,我们希望单击此按钮后,可以另建一个窗体,也就是‘工具箱’窗体,里面可以有一些相关工具按钮。

public class Test{
        public static void main(String[] args) {
            new SingletonWindow();
        }
    }
    class SingletonWindow{
        public SingletonWindow(){
            JFrame frame = new JFrame("单例模式");
            frame.setSize(1024,768);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            JPanel panel = new JPanel();
            frame.add(panel);
            panel.setLayout(null);
            JButton button = new JButton("打开工具箱");  //容器中有一个JButton按钮,叫打开工具箱
            button.setBounds(10,10,120,25);
            button.addActionListener(new ActionListener() {   //按钮单击会触发打开一个叫工具箱的JFrame窗体
                @Override
                public void actionPerformed(ActionEvent e) {
                    JFrame toolkit = new JFrame("工具箱");
                    toolkit.setSize(150,300);
                    toolkit.setLocation(100,100);
                    toolkit.setResizable(false);
                    toolkit.setAlwaysOnTop(true);
                    toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                    toolkit.setVisible(true);
                }
            });
            panel.add(button);
            frame.setVisible(true);
        }
    }

上述代码,我们每单击一次‘打开工具箱’按钮,就会产生一个新的‘工具箱’窗体,但实际上,我们只希望它出现第一次,不会出现第二次。除非单击关闭之后,才会再次出现。

判断对象是否为null

这里我们需要做的是判断JFrame有没有实例化就好了。在上述代码中,我们是在单击了按钮时,才去JFrame tookit = new JFrame(“工具箱”);当然是新实例化了。但其实问题也就出现在这里,为什么要单击按钮时才声明JFrame对象呢?,我们完全可以把声明的工作放到类的全局变量中完成。这样就可以去判断这个变量是否被实例化过了。

public class Test{
        public static void main(String[] args) {
            new SingletonWindow();
        }
    }
    class SingletonWindow{
        public SingletonWindow(){
            JFrame frame = new JFrame("单例模式");
            frame.setSize(1024,768);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            JPanel panel = new JPanel();
            frame.add(panel);
            panel.setLayout(null);
            JButton button = new JButton("打开工具箱");  //容器中有一个JButton按钮,叫打开工具箱
            button.setBounds(10,10,120,25);
            button.addActionListener(new ActionListener() {//按钮单击会触发打开一个叫工具箱的JFrame窗体
                JFrame toolkit;
                @Override
                public void actionPerformed(ActionEvent e) {
                    if(toolkit == null || !toolkit.isVisible()){    //判断是否实例化,如果没有或隐藏,那么就重新实例化
                        JFrame toolkit = new JFrame("工具箱");
                        toolkit.setSize(150,300);
                        toolkit.setLocation(100,100);
                        toolkit.setResizable(false);
                        toolkit.setAlwaysOnTop(true);
                        toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                        toolkit.setVisible(true);
                    }
                  
                }
            });
            panel.add(button);
            frame.setVisible(true);
        }
    }

基本需求上述代码可以实现,现在我们需求,不但要在菜单里启动‘工具箱’,还需要‘工具栏’上有一个按钮来启动‘工具箱’,如何做?也就是要有两个按钮都要能打开这个工具箱。我们可以提炼一个方法来让它们调用。

//工具箱事件类
  class ToolkitListener implements ActionListener {
        JFrame toolkit;

        @Override
        public void actionPerformed(ActionEvent e) {
            if (toolkit == null || !toolkit.isVisible()) {    //判断是否实例化,如果没有或隐藏,那么就重新实例化
                JFrame toolkit = new JFrame("工具箱");
                toolkit.setSize(150, 300);
                toolkit.setLocation(100, 100);
                toolkit.setResizable(false);
                toolkit.setAlwaysOnTop(true);
                toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                toolkit.setVisible(true);
            }
        }
    }
  //窗体类
  class SingletonWindow {
        public SingletonWindow() {
            JFrame frame = new JFrame("单例模式");
            frame.setSize(1024, 768);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            JPanel panel = new JPanel();
            frame.add(panel);
            panel.setLayout(null);
            JButton button = new JButton("打开工具箱");  //容器中有一个JButton按钮,叫打开工具箱
            button.setBounds(10, 10, 120, 25);
            button.addActionListener(new ToolkitListener());
            panel.add(button); 
            
            JButton button2 = new JButton("打开工具箱2");  //容器中有一个JButton按钮,叫打开工具箱
            button.setBounds(130, 10, 120, 25);
            button.addActionListener(new ToolkitListener());
            panel.add(button2);
            frame.setVisible(true);
        }
    }

这样可以分别单击打开工具箱和打开工具箱2,此时两个按钮分别打开了一个工具箱窗体,这依然不符合我们只打开一个‘工具箱’的需求。

目前的代码中我们将工具箱‘JFrame’是否实例化都由外部的代码决定,这好像很不合逻辑。

 class ToolkitListener implements ActionListener {
        JFrame toolkit;   
        @Override
        public void actionPerformed(ActionEvent e) {
            if (toolkit == null || !toolkit.isVisible()) {    //判断是否实例化,如果没有或隐藏,那么就重新实例化
                JFrame toolkit = new JFrame("工具箱");
                toolkit.setSize(150, 300);
                toolkit.setLocation(100, 100);
                toolkit.setResizable(false);
                toolkit.setAlwaysOnTop(true);
                toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                toolkit.setVisible(true);
            }
        }
    }

在主窗体里,应该只是通知启动‘工具箱’,至于工具箱窗体是否实例化过,应该有工具箱自己的类来判断。实例化的过程就是new的过程,问题是如何让外部代码不使用new呢?这里我们只能对构造方法做改动,不可能阻止他人不使用new的,所以我们可以直接就把这个类的构造方法改成私有,所有类的构造方法,不编码则系统默认生成空的构造方法,若有显示的定义的构造方法,默认的构造方法就会失效。 所以我们需要将‘工具箱’类的构造方法写成是private的,那么外部程序就不能用new 来实例化他了。

私有的方法外界不能访问,但是这样来看,这个类一个实例化都不能了,这与我们的让这个类实例化一次又矛盾了。这里这样理解是有问题的,经过对构造方法的private修饰,我们只能说对于外部代码,不能用new来实例化它,但是我们完全可以再写一个public 方法叫做getInstance(),这个方法的目的就是返回一个类实例,而此方法中,去做是否有实例化的判断。如果没有实例化,由调用private的构造方法new出这个实例,之后它就可以调用,因为在同一个类中,所以private方法是可以被调用的。

//工具箱类
  class Toolkit extends JFrame {
        private static Toolkit toolkit;
        
        private Toolkit(String title){
            super(title);
        }
        
        public static Toolkit getInstance(){
            //判断是否实例化,如果没有或隐藏,可以实例化
            if (toolkit == null || !toolkit.isVisible()) {    
                JFrame toolkit = new JFrame("工具箱");
                toolkit.setSize(150, 300);
                toolkit.setLocation(100, 100);
                toolkit.setResizable(false);
                toolkit.setAlwaysOnTop(true);
                toolkit.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                toolkit.setVisible(true);
            }
            return toolkit;   //如果已经存在,就直接返回存在的实例,并不在new新的实例
        }
    }

有了上面的代码,我们在写工具箱事件类时,就可以改造如下

//工具箱事件类
class ToolkitListener implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            //Toolkit toolkit = new Toolkit("工具箱");  传统的new来获得实例会报错
            Toolkit.getInstance();  //实例化唯一的一个对象 -- 单例对象
        }
    }

上面的代码可以达到一个效果,只有在实例不存在时,才会去new新的实例,而但存在时,可以直接返回存在的同一个实例。这就是设计模式 – 单例模式

单例模式

> 单例模式: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的方法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且他可以提供一个访问该实例的方法 。

Singleton类,定义一个GetInstance操作,允许客户访问它的唯一实例。GetIntance是一个静态方法,主要负责创建自己的唯一实例

 //单例模式类
    class Singleton{
        private static Singleton instance;
        private Singleton{
            
        }
        public static Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }

客户端代码:

 public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        
        if(s1 == s2){
            System.out.println("两个对象是相同的实例");
        }
    }

单例模式不光可以保证只存在唯一的实例,另外单例模式因为Singleton类封装它的唯一实例,这样它可以严格的控制顾客怎样访问他以及何时访问它。简单的说就是对唯一实例的受控访问。

多线程时的单例

在多线程的程序中,多个线程同时访问Singleton类,调用getInstance(),调用getInstance()方法,会有可能造成创建多个实例。
我们可以给进程一把锁来处理,这里需要解释一下synchronized语句的含义。synchronized是Java中的关键字,是一种同步锁。意思就是当一个线程还没有退出之前,先锁住这段代码不被其他其他线程代码调用执行,以保证同一时间只有一个线程在执行此段代码。

 class Singleton{
        private static Singleton instance;
        private Singleton(){

        }
        //通过增加synchronized关键字到getInstance方法中,可以让每个线程在进入此方法之前,都要等到别的线程离开此方法
        public static synchronized Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }

这段代码使得对象实例由最先进入的那个线程来创建,以后的线程在进入时不会再去创建对象实例了,由于有了synchronized,就保证了多线程环境下的同时访问也不会造成多个实例的生成。这里为什么不直接锁实例,而是使用synchronized锁是因为加锁的时候,我们不清楚instance是否已经被创建,也就无法加锁。但是这样的话,每次调用getInstance()都需要锁,性能降低,所以这个代码还是有可以优化的地方的。

双重锁定

class Singleton{
        //volatile关键词是当synchronized变量初始化成Single时,多个线程能够正确地处理synchronized变量
        private volatile static Singleton instance;
        private Singleton(){

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

这版的代码,我们不用让线程每次都加锁,而只是在实例未被创建的时候再加锁处理。同时也能保证多线程的安全,这种方法叫做Double-Check Locking(双重否定)。

那么这里为什么在外面已经判断了instance的实例是否存在,为什么在synchronized里面还需要做一次instance实例是否存在的判断呢?对于instance存在的情况就直接返回,这没问题,当instance为null并且同时有两个线程调用getInstance()方法时,他们将都可以通过第一重instance == null的判断,然后由于锁机制,这两个线程则只有一个进入,另一个在排队等待,必须要其中的一个进入并出来后,另一个才能进去,而此时没有了instance == null的判断,就会创建多个实例,没有达到单例的目的

静态初始化

上述代码基本对实际应用中的大部分情况都能应付自如,不过为了确保实例唯一,还是会带来很大的性能代价,对于那些性能要求特别高的程序来说,传统单例代码实现还不是最好的办法。

   class Singleton{
      private static Singleton instance = new Singleton();
      //构造方法private化
        private Singleton(){}
        //得到Singleton的实例
        public static Singleton getInstance(){
            return instance;
        }
    }

这样的实现与前面的示例相似,解决了单例模式的全局访问和实例化控制,公共静态属性为访问实例提供了一个全局访问点。不同之处在于它依赖公共语言运行库来初始化变量。由于构造方法是私有的,因此不能在类本身以外实例化Singleton类;因此,变量引用的是可以在系统中存在的唯一实例。由于这种静态初始化的方式是在自己被加载时就将自己实例化,所以被形象的称之为饿汉式单例类,原先的单例模式的处理方式是要在第一次被引用时,才会将自己实例化,所以就被称为懒汉式单例类。

总结

以上就是本文全部内容,本文主要向大家介绍了设计模式中的单例模式,通过对窗口的单例创建为例,引出单例模式的模板代码,之后介绍了多线程下的单例模式。介绍了双重否定和静态初始化两种实现单例模式的方法。 感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

### 单例模式概述 单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。在C++中,单例模式常用于需要全局唯一实例的场景,如日志记录器、数据库连接池等。 ### 实现方法 #### 饿汉式单例 饿汉式单例在程序启动时就创建实例,因此是线程安全的。以下是饿汉式单例的实现代码: ```cpp #include <iostream> class Singleton { private: // 私有构造函数,防止外部实例化 Singleton() {} // 禁止拷贝构造函数 Singleton(const Singleton&) = delete; // 禁止赋值运算符 Singleton& operator=(const Singleton&) = delete; // 唯一实例 static Singleton instance; public: // 静态方法,用于获取唯一实例 static Singleton& getInstance() { return instance; } void doSomething() { std::cout << "Singleton is doing something." << std::endl; } }; // 初始化唯一实例 Singleton Singleton::instance; int main() { Singleton& singleton = Singleton::getInstance(); singleton.doSomething(); return 0; } ``` 在上述代码中,`Singleton` 类的构造函数被声明为私有,这意味着外部无法直接实例化该类。`getInstance` 方法用于获取唯一的实例,而 `instance` 是在类外进行初始化的。 #### 懒汉式单例 懒汉式单例在第一次使用时才创建实例。在多线程环境下,需要考虑线程安全问题。以下是一个线程安全的懒汉式单例实现: ```cpp #include <iostream> #include <mutex> class Singleton { private: // 私有构造函数,防止外部实例化 Singleton() {} // 禁止拷贝构造函数 Singleton(const Singleton&) = delete; // 禁止赋值运算符 Singleton& operator=(const Singleton&) = delete; // 唯一实例指针 static Singleton* instance; // 互斥锁,用于线程安全 static std::mutex mutex_; public: // 静态方法,用于获取唯一实例 static Singleton& getInstance() { std::lock_guard<std::mutex> lock(mutex_); if (instance == nullptr) { instance = new Singleton(); } return *instance; } void doSomething() { std::cout << "Singleton is doing something." << std::endl; } }; // 初始化唯一实例指针 Singleton* Singleton::instance = nullptr; // 初始化互斥锁 std::mutex Singleton::mutex_; int main() { Singleton& singleton = Singleton::getInstance(); singleton.doSomething(); return 0; } ``` 在上述代码中,使用了 `std::mutex` 来保证线程安全。在 `getInstance` 方法中,使用 `std::lock_guard` 来自动管理锁的获取和释放。 ### 使用场景 - **日志记录器**:在整个应用程序中,通常只需要一个日志记录器来记录所有的日志信息。 - **数据库连接池**:为了避免频繁地创建和销毁数据库连接,可以使用单例模式来管理数据库连接池。 - **配置管理**:应用程序的配置信息通常是全局唯一的,可以使用单例模式来管理配置信息。 ### 最佳实践 - **确保线程安全**:在多线程环境下,需要使用锁或其他同步机制来保证单例的线程安全。 - **防止拷贝和赋值**:将拷贝构造函数和赋值运算符声明为私有或删除,防止外部进行拷贝和赋值操作。 - **内存管理**:对于使用 `new` 创建的单例实例,需要确保在程序结束时正确释放内存,或者使用智能指针来管理内存。
评论 35
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值