百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

设计模式——单例模式(设计模式之单例模式)

wxin55 2025-04-11 08:47 16 浏览 0 评论

本文整理了共 7 种单例模式的实现方式,小伙伴儿们可以先收藏起来,慢慢看!

单例模式

单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式分为饿汉模式和懒汉模式。

定义

确保一个类只有一个实例,并提供一个全局访问点

饿汉模式

所谓饿汉模式,是指在类加载的时候立即初始化,并创建单例对象。它能保证绝对线程安全,在线程还没有出现之前就实例化了。

代码实现

package com.study.design.Factory.Singleton;

public class HungrySingleton {
    /*静态实例在类加载时直接初始化创建*/
    private static final HungrySingleton instance = new HungrySingleton();
    /*构造方法私有化, 禁止外部进行创建该类实例*/
    private HungrySingleton(){};
    /*提供一个静态方法,返回唯一的实例对象*/
    public static HungrySingleton getInstance(){
        return instance;
    }
}

饿汉模式适用于单例对象较少的情况。这样写可以保证绝对的线程安全,执行效率比较高。缺点是所有对象类在加载时就实例化,这样一来,如果系统中有大量的单例对象存在,在系统初始化时会造成系统内存浪费,导致系统内存不可控。也就是说,系统初始化时,无论用或者不用的单例对象都会初始化,并占用内存空间。可以通过懒汉模式解决。

懒汉模式

为了解决饿汉模式可能带来的内存浪费,于是出现了懒汉模式的写法。懒汉模式也就是延迟加载,只有单例对象在被使用时才会初始化。

懒汉模式需要解决线程安全的问题。在多线程环境下,在单例对象还未初始化,如果同时多个线程访问单例对象,会造成对象重复创建的情况,这就违背了单例的初衷,也可能会造成程序执行异常。

synchronize代码实现

package com.study.design.Factory.Singleton;

public class LazySynchronizeSingleton {
    /*声明一个单例实例对象引用*/
    private static LazySynchronizeSingleton instance= null;
    /*私有化构造方法,禁止外部创建对象实例*/
    private LazySynchronizeSingleton(){}

    /**
     * 通过 synchronized 同步锁保证线程安全
     * 在使用单例对象的时候创建
     * @return
     */
    public synchronized static LazySynchronizeSingleton getInstance() {
        if (instance == null) {
            instance = new LazySynchronizeSingleton();
        }
        return instance;
    }
}

synchronized 固然可以保证线程的安全,但是在线程数量剧增的情况下,会导致大批线程阻塞,从而导致程序性能下降。而且,单例对象只有在第一次使用的时候才需要加锁 ,一旦实例化之后,以后每次使用并不需要同步加锁。通过下面双重锁机制来优化

双重检查锁

package com.study.design.Factory.Singleton;

public class LazyDoubleCheckSingleton {
    /**
     * volatile 关键字修饰单例保证线程之间的可见性,在多个线程同时调用时正确处理 instance 变量
     */
    private volatile static LazyDoubleCheckSingleton instance;
    /*私有化构造方法, 禁止外部创建实例对象*/
    private LazyDoubleCheckSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static LazyDoubleCheckSingleton getInstance(){
        /**
         * 第一次 null 判断,多个线程可以同时执行此处,如果单例对象还未创建,只有一个会继续执行,而其他线程会阻塞
         * 如果单例对象已经创建,则会直接返回实例对象
         */
        if (instance == null) {
            // 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
            synchronized (LazyDoubleCheckSingleton.class){
                /**
                 * 第二次 null 判断,这里是避免对象重复创建的关键
                 * 早些被阻塞的线程,获取到锁之后, 会再次进行一次 null 判断
                 * 此时,实例对象应该是已经被创建了 instance == null 为 false,不会重新创建对象
                 */
                if (instance == null)
                    instance  = new LazyDoubleCheckSingleton();
            }
        }
        // 返回单例对象
        return instance;
    }
}

双重检查锁机制,解决了线程安全问题和性能问题。

volatile 关键字保证了线程之间的可见性,双重 null 判断解决了对象重复实例化的问题。但只要是使用 synchronized 关键字总是要上锁,对程序还是有一定影响的。我们可以从类初始化的角度考虑,采用静态内部类的方式来实现单例模式。

静态内部类

package com.study.design.Factory.Singleton;

public class LazyStaticInnerClassSingleton {

    /*私有化构造方法,禁止外部创建实例对象*/
    private LazyStaticInnerClassSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    private LazyStaticInnerClassSingleton geInstance(){
        return LazyHolder.INSTANCE;
    }
    /*默认是不加载内部类的*/
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

利用 java 虚拟机对类实例化的机制,内部类只有在被使用的时候才会初始化。这种模式兼顾了饿汉模式写法的内存浪费和 synchronized 性能问题,实现也比较简单。

禁止反射机制破坏单例模式

以上三种写法,构造方法只是使用了 private 关键字修复,并无其他任何处理。如果是这样,我们仍然可以通过使用反射机制来调用其构造方法,从而创建另外一个实例对象 ,所以以上三种还不能保证绝对的单例。

我们需要在构造方法中加上一个判断,如果实例已经创建过,直接抛出异常:

if (LazyHolder.INSTANCE != null){
    throw new RuntimeException("不允许创建多个实例");
}

关于序列化

对于一个单例模式的实例,在其被实例化后,重新被反序列化为一个对象的时候,应该与被序列化时的对象是同一个实例,这样才是更为严格的单例模式。

为了将上述单例模式实现方法变成可序列化的,单纯的实现 Serializeble 接口,是不够的。必须声明所有的实例变量都是transient 类型,并且提供一个readResolve() 方法,否则每次反序列化时都会创建一个新的实例。

package com.study.design.Factory.Singleton;

import java.io.Serializable;

public class SerializableSingleton implements Serializable {

    private transient volatile static SerializableSingleton instance;
    /*私有化构造方法, 禁止外部创建实例对象*/
    private SerializableSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static SerializableSingleton getInstance(){
        if (instance == null) {
            // 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
            synchronized (LazyDoubleCheckSingleton.class){
                if (instance == null)
                    instance  = new SerializableSingleton();
            }
        }
        // 返回单例对象
        return instance;
    }

    private SerializableSingleton readResolve(){
        return instance;
    }

}

对于一个正在被反序列化的对象,如果它的类定义了一个readResolve() 方法,并且具备正确的声明,那么在反序列化之后 ,新建对象上的readResolve() 方法就会被调用,然后该方法返回的对象应用将会被返回,取代新建的对象。

但其实新对象还是被创建了的,只是创建之后没有任何引用会被垃圾回收器回收。

枚举

package com.study.design.Factory.Singleton;

public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

这种试下方式更加简洁,无偿提供了序列化机制。可以绝对放置多次实例化,也可以防止反射机制创建新的对象。枚举类型写法是 Effective Java 一书中推荐的写法。

容器式单例写法

虽然枚举式单例写法更加优雅,但是也会存在一些问题,因为它在类加载时将所有对象初始化都放在类内存中,这其实和饿汉式写法并无差异,不适合大量单例对象的场景。通过使用容器式单例写法可以解决大规模生产单例的问题。

package com.study.design.Factory.Singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 容器式单例模式
 * 决大规模生产单例的问题
 */
public class ContainerSingleton {
    /*定义一个容器*/
    private static Map ioc = new ConcurrentHashMap<>();
    /*构造方法私有化, 禁止外部进行创建该类实例*/
    private ContainerSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            }else{
                return ioc.get(className);
            }
        }
    }
}


都看到这儿了,点个关注,转发一下呗

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: