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

目录
相关文章
|
20天前
|
设计模式 Java 开发者
设计模式揭秘:Java世界的七大奇迹
【4月更文挑战第7天】探索Java设计模式:单例、工厂方法、抽象工厂、建造者、原型、适配器和观察者,助你构建健壮、灵活的软件系统。了解这些模式如何提升代码复用、可维护性,以及在特定场景下的应用,如资源管理、接口兼容和事件监听。掌握设计模式,但也需根据实际情况权衡,打造高效、优雅的软件解决方案。
|
21天前
|
设计模式 存储 Java
23种设计模式,享元模式的概念优缺点以及JAVA代码举例
【4月更文挑战第6天】享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享技术有效地支持大量细粒度对象的重用。这个模式在处理大量对象时非常有用,特别是当这些对象中的许多实例实际上可以共享相同的状态时,从而可以减少内存占用,提高程序效率
35 4
|
21天前
|
设计模式 Java 中间件
23种设计模式,适配器模式的概念优缺点以及JAVA代码举例
【4月更文挑战第6天】适配器模式(Adapter Pattern)是一种结构型设计模式,它的主要目标是让原本由于接口不匹配而不能一起工作的类可以一起工作。适配器模式主要有两种形式:类适配器和对象适配器。类适配器模式通过继承来实现适配,而对象适配器模式则通过组合来实现
31 4
|
20天前
|
设计模式 监控 Java
设计模式 - 观察者模式(Observer):Java中的战术与策略
【4月更文挑战第7天】观察者模式是构建可维护、可扩展系统的关键,它在Java中通过`Observable`和`Observer`实现对象间一对多的依赖关系,常用于事件处理、数据绑定和同步。该模式支持事件驱动架构、数据同步和实时系统,但需注意避免循环依赖、控制通知粒度,并关注性能和内存泄漏问题。通过明确角色、使用抽象和管理观察者注册,可最大化其效果。
|
3天前
|
设计模式 算法 Java
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
[设计模式Java实现附plantuml源码~行为型]定义算法的框架——模板方法模式
|
3天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式
|
3天前
|
设计模式 存储 JavaScript
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
[设计模式Java实现附plantuml源码~创建型] 多态工厂的实现——工厂方法模式
|
3天前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
[设计模式Java实现附plantuml源码~创建型] 集中式工厂的实现~简单工厂模式
|
3天前
|
设计模式 存储 前端开发
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
Java从入门到精通:2.2.1学习Java Web开发,了解Servlet和JSP技术,掌握MVC设计模式
|
9天前
|
设计模式 算法 Java
Java中的设计模式及其应用
【4月更文挑战第18天】本文介绍了Java设计模式的重要性及分类,包括创建型、结构型和行为型模式。创建型模式如单例、工厂方法用于对象创建;结构型模式如适配器、组合关注对象组合;行为型模式如策略、观察者关注对象交互。文中还举例说明了单例模式在配置管理器中的应用,工厂方法在图形编辑器中的使用,以及策略模式在电商折扣计算中的实践。设计模式能提升代码可读性、可维护性和可扩展性,是Java开发者的必备知识。