Java设计模式—单例模式的实现方式和使用场景

简介: 那么为什么要有单例模式呢?这是因为有的对象的创建和销毁开销比较大,比如数据库的连接对象。所以我们就可以使用单例模式来对这些对象进行复用,从而避免频繁创建对象而造成大量的资源开销。

单例模式可以说是Java中最简单的设计模式,也是技术面试中频率极高的面试题。因为它不仅涉及到设计模式,还包括了关于线程安全、内存模型、类加载等机制。所以下面就来分别从单例模式的实现方法和应用场景来介绍一下单例模式

一、单例模式介绍

1.1 单例模式是什么

单例模式也就是指在整个运行时域中,一个类只能有一个实例对象。

那么为什么要有单例模式呢?这是因为有的对象的创建和销毁开销比较大,比如数据库的连接对象。所以我们就可以使用单例模式来对这些对象进行复用,从而避免频繁创建对象而造成大量的资源开销。

1.2 单例模式的原则

为了到达单例这个全局唯一的访问点的效果,必须要让单例满足以下原则:

  1. 阻止类被通过常规方法实例化(私有构造方法)
  2. 保证实例对象的唯一性(以静态方法或者枚举返回实例)
  3. 保证在创建实例时的线程安全(确保多线程环境下实例只有一个)
  4. 对象不会被外界破坏(确保在有序列化、反序列化时不会重新构建对象)

二、单例模式的实现方式

关于单例模式的写法,网上归纳的已经有很多,但是感觉大多数只是列出了写法,不去解释为什么这样写的好处和原理。我偶然在B站看了寒食君归纳的单例模式总结思路还不错,故这里借鉴他的思路来分别说明这些单例模式的写法。

按照单例模式中是否线程安全、是否懒加载和能否被反射破坏可以分为以下的几类

2.1 懒加载

2.1.1 懒加载(线程不安全)

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**初始化对象为null**/
    private static Singleton instance = null;

    public static Singleton getInstance() {
   
        //判断是否被构造过,保证对象的唯一
        if (instance == null) {
   
            instance = new Singleton();
        }
        return instance;
    }
}
AI 代码解读

从上面我们可以看到,通过public class Singleton我们可以全局访问该类;通过私有化构造方法,能够避免该对象被外界类所创建;以及后面的getInstance方法能够保证创建对象实例的唯一。

但是我们可以看到,这个实例不是在程序启动后就创建的,而是在第一次被调用后才真正的构建,所以这样的延迟加载也叫做懒加载

然而我们发现getInstance这个方法在多线程环境下是线程不安全的—如果有多个线程同时执行该方法会产生多个实例。那么该怎么办呢?我们想到可以将该方法变成线程安全的,加上synchronized关键字。

2.1.2 懒加载(线程安全)

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**初始化对象为null**/
    private static Singleton instance;

    //判断是否被构造过,保证对象的唯一,而且synchronize也能保证线程安全
    public synchronized static Singleton getInstance() {
   

        if (instance == null) {
   
            instance = new Singleton();
        }
        return instance;
    }
}
AI 代码解读

但是我们知道,如果一个静态方法被synchronized所修饰,会把当前类的class 对象锁住,会增大同步开销,降低程序的执行效率。所以可以从缩小锁粒度角度去考虑,把synchronized放到方法里面去,也就是让其修饰同步代码块,如下所示:

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**初始化对象为null**/
    private static Singleton instance;

    public static Singleton getInstance() {
    
        if (instance == null) {
   
            //利用同步代码块,锁的是当前实例对象
            synchronized(Singleton.class) {
   
                instance = new Singleton();
            }

        }
        return instance;
    }
}
AI 代码解读

但是这个时候,我们发现if(instance == null)是没有锁的,所以当两个线程都执行到该语句并都判断为true时,还是会排队创建新的对象,那么有没有新的解决方式?

2.1.3 懒加载(线程安全,双重检测锁)

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**初始化对象**/
    private static Singleton instance;

    public static Singleton getInstance() {
   
        //第一次判断
        if (instance == null) {
   
            synchronized (Singleton.class) {
   
                //第二次判断
                if (instance == null) {
   
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
AI 代码解读

我们在上一节的代码上再加上一次判断,就是双重检测锁(Double Checked Lock, DCL)。但是上述代码也存在一些问题,比如在instance = new Singleton() 这行代码中,它并不是一个原子操作,实际上是有三步:

  • 给对象实例分配内存空间

  • new Singleton() 调用构造方法,初始化成员字段

  • instance对象指向分配的内存空间

所以会涉及到内存模型中的指令重排,那么这个时候可以用 volatile关键字来修饰 instance对象,防止指令重排,写出如下代码:

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**初始化对象,加上volatile防止指令重排**/
    private volatile static Singleton instance;

    public static Singleton getInstance() {
   
        //第一次判断
        if (instance == null) {
   
            synchronized (Singleton.class) {
   
                //第二次判断
                if (instance == null) {
   
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
AI 代码解读

此外,我们也可以尝试使用一些乐观锁的方式达到线程安全的效果,比如CAS。

2.1.4 懒加载(线程安全,CAS乐观锁)

public class Singleton {
   
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
    private static Singleton instance;

    private Singleton(){
   }
    public static final Singleton getInstance() {
   
        for(;;) {
   
            Singleton instance = INSTANCE.get();
            if(instance != null) {
   
                return instance;
            }
            instance = new Singleton();
            if(INSTANCE.compareAndSet(null, instance)) {
   
                return instance;
            }
        }
    }
}
AI 代码解读

CAS 是一种乐观锁,依赖于底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并发度,但是如果忙等待一直执行不成功,也会对CPU造成较大的执行开销。

2.2 饿汉(线程安全)

不同于懒加载的延迟实现实例,我们也可以在程序启动时就加载好单例对象:

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**直接获取实例对象**/
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
   
        return instance;
    }
}
AI 代码解读

这样的好处是线程安全,单例对象在类加载时就已经被初始化,当调用单例对象时只是把早已经创建好的对象赋值给变量。缺点就是如果一直没有调用该单例对象的话,就会造成资源浪费。除此之外还有其他的实现方式。

2.3 静态内部类

public class Singleton {
   
    /**保证构造方法私有,不被外界类所创建**/
    private Singleton() {
   }
    /**利用静态内部类获取单例对象**/
    private static class SingletonInstance {
   
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
   
        return SingletonInstance.instance;
    }
}
AI 代码解读

静态内部类的方法结合了饿汉方式,它们都采用了类加载机制来保证当初始化实例时只有一个线程执行,从而保证了多线程下的安全操作。原因就是JVM在类初始化阶段时会创建一个锁,该锁可以保证多个线程同步执行类初始化工作。

但是静态内部类不会在程序启动时创建单例对象,它是在外界调用 getInstance方法时才会装载内部类,从而完成单例对象的初始化工作,不会造成资源浪费。

然而这种方法也存在缺点,它可以通过反射来进行破坏。下面就该提到枚举方式了

2.4 枚举

枚举是《Effective Java》作者推荐的单例实现方式,枚举只会装载一次,无论是序列化、反序列化、反射还是克隆都不会新创建对象。因此它也不会被反射所破坏。

public enum Singleton {
   
    INSTANCE;
}
AI 代码解读

所以这种方式是线程安全的,而且无法被反射而破坏

三、单例模式的应用场景

3.1 Windows 任务管理器

在一个windows 系统中只有一个任务管理器,这就是一种单例模式的应用。

3.2 网站的计数器

因为计数器的作用,就必须保证计数器对象保证唯一

3.3 JDK中的单例

3.3.1java.lang.Runtime

Every Java application has a single instance of class Runtime that allows the application to interface with the environment in which the application is running. The current runtime can be obtained from the getRuntime method.

An application cannot create its own instance of this class.

每个java程序都含有唯一的Runtime实例,保证实例和运行环境相连接。当前运行时可以通过getRuntime方法获得

我们来看看具体的代码:

public class Runtime {
   
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
   
        return currentRuntime;
    }

    private Runtime() {
   }
AI 代码解读

我们发现这就是单例模式的饿汉加载方式。

3.3.2 java.awt.Desktop

类似的,在java.awt.Desktop中也存在单例模式的使用,比如:

public class Desktop {
   

    private DesktopPeer peer;

    private Desktop() {
   
        peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
    }
    //懒加载
    public static synchronized Desktop getDesktop(){
   
        if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
   
            throw new UnsupportedOperationException("Desktop API is not " +
                                                    "supported on the current platform");
        }

        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class);

        if (desktop == null) {
   
            desktop = new Desktop();
            context.put(Desktop.class, desktop);
        }

        return desktop;
    }
AI 代码解读

这种方法就是一种延迟加载的方式。

3.4 Spring Bean 作用域

比较常见的就是Spring Bean作用域里的单例了,这个比较常见,可以通过配置文件进行配置:

<bean class="..."></bean>

参考资料

https://www.zhihu.com/search?type=content&q=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F

https://www.bilibili.com/video/BV1pt4y1X7kt?spm_id_from=333.337.search-card.all.click

https://www.jianshu.com/p/137e65eb38ce

目录
打赏
0
0
1
0
17
分享
相关文章
设计模式2:单例模式
单例模式是一种创建型模式,确保一个类只有一个实例,并提供全局访问点。分为懒汉式和饿汉式: - **懒汉式**:延迟加载,首次调用时创建实例,线程安全通过双重检查锁(double check locking)实现,使用`volatile`防止指令重排序。 - **饿汉式**:类加载时即创建实例,线程安全但可能浪费内存。 示例代码展示了如何使用Java实现这两种模式。
14 4
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
62 9
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
42 2
Java 设计模式——观察者模式:从优衣库不使用新疆棉事件看系统的动态响应
【11月更文挑战第17天】观察者模式是一种行为设计模式,定义了一对多的依赖关系,使多个观察者对象能直接监听并响应某一主题对象的状态变化。本文介绍了观察者模式的基本概念、商业系统中的应用实例,如优衣库事件中各相关方的动态响应,以及模式的优势和实际系统设计中的应用建议,包括事件驱动架构和消息队列的使用。
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
58 4

热门文章

最新文章