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;
    }
}

从上面我们可以看到,通过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;
    }
}

但是我们知道,如果一个静态方法被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;
    }
}

但是这个时候,我们发现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;
    }
}

我们在上一节的代码上再加上一次判断,就是双重检测锁(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;
    }
}

此外,我们也可以尝试使用一些乐观锁的方式达到线程安全的效果,比如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;
            }
        }
    }
}

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

2.2 饿汉(线程安全)

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

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

    public static Singleton getInstance() {
   
        return instance;
    }
}

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

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;
    }
}

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

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

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

2.4 枚举

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

public enum Singleton {
   
    INSTANCE;
}

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

三、单例模式的应用场景

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() {
   }

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

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;
    }

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

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

目录
相关文章
|
1月前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
213 2
|
1月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
260 0
|
3月前
|
设计模式 缓存 Java
Java设计模式(二):观察者模式与装饰器模式
本文深入讲解观察者模式与装饰器模式的核心概念及实现方式,涵盖从基础理论到实战应用的全面内容。观察者模式实现对象间松耦合通信,适用于事件通知机制;装饰器模式通过组合方式动态扩展对象功能,避免子类爆炸。文章通过Java示例展示两者在GUI、IO流、Web中间件等场景的应用,并提供常见陷阱与面试高频问题解析,助你写出灵活、可维护的代码。
|
1月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
259 35
|
1月前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
220 8
|
1月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
178 1
|
6月前
|
设计模式 缓存 安全
【设计模式】【创建型模式】单例模式(Singleton)
一、入门 什么是单例模式? 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。它常用于需要全局唯一对象的场景,如配置管理、连接池等。 为什么要单例模式? 节省资源 场景:某些对象创
245 15
|
6月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
163 0
|
3月前
|
设计模式 安全 Java
Java设计模式(一):单例模式与工厂模式
本文详解单例模式与工厂模式的核心实现及应用,涵盖饿汉式、懒汉式、双重检查锁、工厂方法、抽象工厂等设计模式,并结合数据库连接池与支付系统实战案例,助你掌握设计模式精髓,提升代码专业性与可维护性。
|
3月前
|
设计模式 XML 安全
Java枚举(Enum)与设计模式应用
Java枚举不仅是类型安全的常量,还具备面向对象能力,可添加属性与方法,实现接口。通过枚举能优雅实现单例、策略、状态等设计模式,具备线程安全、序列化安全等特性,是编写高效、安全代码的利器。

热门文章

最新文章

下一篇
oss云网关配置