Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >面向对象设计模式--单例模式详解+实际应用(Java)

面向对象设计模式--单例模式详解+实际应用(Java)

原创
作者头像
飞天葫芦侠
修改于 2023-03-22 21:15:34
修改于 2023-03-22 21:15:34
2.3K01
代码可运行
举报
运行总次数:1
代码可运行

单例模式

保证了一个类只有一个实例,并且提供了一个全局访问点。单例模式的主要作用是节省公共资源,方便控制,避免多个实例造成的问题。

实现单例模式的三点:

  • 私有构造函数
  • 私有静态变量维护对象实例
  • 公有静态方法提供获取实例对象

七种单例模式实现

1.静态类:第一次运行初始化,全局使用

2.懒汉模式(线程不安全):懒汉模式是指在第一次获取实例时才创建对象,实现了延迟加载,构造函数返回当前对象实例,但多个访问者同时获取对象实例,就会有多个同样的实例并存,不满足单例

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LazySingleton {
    // 私有化构造器
    private LazySingleton() {}

    // 定义一个静态变量存储唯一的LazySingleton对象
    private static LazySingleton instance = null;

    // 提供一个公共的静态方法获取LazySingleton对象
    public static LazySingleton getInstance() {
        // 如果instance为空,则创建一个新的对象
        if (instance == null) {
            instance = new LazySingleton();
        }
        // 返回instance对象
        return instance;
    }
}

懒汉模式(线程安全):加锁,保证线程安全,但是锁占用,导致资源浪费

3.饿汉模式(线程安全):与第一种方式基本一致,在程序启动时直接运行加载,但是可能造成资源浪费。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class HungrySingleton {
    // 私有化构造器
    private HungrySingleton() {}

    // 定义一个静态变量存储唯一的HungrySingleton对象,并且直接初始化
    private static HungrySingleton instance = new HungrySingleton();

    // 提供一个公共的静态方法获取HungrySingleton对象
    public static HungrySingleton getInstance() {
        // 直接返回instance对象
        return instance;
    }
}

4.使用类的静态内部类:既保证线程安全,又保证懒汉模式,没有加锁影响性能(推荐)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingletonInnerStatic {
    private static class SingletonHoler {
    // 静态初始化器,由JVM来保证线程安全
        private static SingletonInnerStatic instance = 
            new SingletonInnerStatic();
    }
    // 私有构造函数
    private SingletonInnerStatic() {
    }
    // 获取对象实例的方法
    public static SingletonInnerStatic getInstance() {
        return SingletonHoler.instance;
    }
}

5.双重锁校验:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingletonDoubleCheck {
    // 类引用
    private static volatile SingletonDoubleCheck singletonDoubleCheck;
    // 构造函数私有化
    private SingletonDoubleCheck() {
    }
    // 双重校验 + 锁实现单例
    public static SingletonDoubleCheck getInstance() {
        // 第一次校验是否为null
        if (singletonDoubleCheck == null) {
            // 不为空则加锁
            synchronized (SingletonDoubleCheck.class) {
                // 第二次校验是否为null
                if (singletonDoubleCheck == null) {
                    singletonDoubleCheck = new SingletonDoubleCheck();
                }
            }
        }
        return singletonDoubleCheck;
    }
}

第一次校验是否为null:

主要是为了实现返回单例,避免多余的加锁操作,以及锁的等待和竞争,如果条件不成立就说明已经生成实例,直接返回即可,提高程序执行的效率。

第二次校验是否为null:

第二次校验是关键,这里防止了多线程创建多个实例(一般为两个),这里的特殊情况是这样的:在未创建实例的情况下,A线程和B线程都通过了第一次校验(singletonDoubleCheck为空),这时如果通过竞争B线程拿到了锁就会执行一次new操作,生成一个实例,然后B执行完了A就会拿到资源的锁,如果没有第二次判断的话,这时A线程也会执行一次new操作,这里就出现了第二个类实例,违背了单例原则。所以说两次校验都是必不可少的

提一下上述代码中类引用中的volatile关键字是不能少的: 常见的,该关键字能够实现变量在内存中的可见性(告诉JVM在使用该关键字修饰的变量时在内存中取值,而不是用特定内存区域的副本,因为真实的值可能已经被修改过了),它的另外一种作用是防止JVM对指令进行重排。 其实,在new一个对象的时候会有如下步骤(指令): 1. 分配内存空间 2. 初始化引用 3. 将引用指向内存空间 正常的逻辑肯定以为是这样执行的 1 -> 2 -> 3,但是偏偏JVM拥有指令重排的能力,所以说执行顺序是随机的,可能是 1 -> 3 -> 2,这样的话在多线程环境下可能会拿到空引用:线程A先执行了1,3步骤,紧接着线程B执行getInstance,发现不为null(这里的==是判断实际的值,即引用指向的内存空间),就会返回引用,然而此时引用未初始化。所以说volatile在这里保证指令的执行顺序,在多线程情况下不可少。

6.AtomicReference是一个支持原子操作的对象引用变量,它可以利用CAS(比较并交换)技术来保证线程安全和高效性。一个基本的AtomicReference单例的代码示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.concurrent.atomic.AtomicReference;

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

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (singleton != null) {
                return singleton;
            }
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

要使用这个单例,只需调用Singleton.getInstance()即可。

7.枚举类实现单例模式是一种简洁、安全、有效的方法,它可以防止反射和序列化攻击,保证线程安全和唯一性。一个基本的枚举单例的代码示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum Singleton {
    INSTANCE;
    public void doSomething() {
        // ...
    }
}

要使用这个单例,只需调用Singleton.INSTANCE.doSomething()即可。

测试:

有以下几种方法可以测试单例的有效性,即是否能保证在多线程环境下,只有一个对象实例被创建和返回。:

  • 使用反射机制,尝试创建多个单例对象,检查它们的内存地址是否相同。
  • 使用序列化和反序列化机制,尝试创建多个单例对象,检查它们的内存地址是否相同。
  • 使用多线程并发调用getInstance()方法,检查返回的对象是否都是同一个实例。
  • 使用断言或者日志打印等方式,验证getInstance()方法返回的对象是否符合预期。

安全:

三种攻击方式:
  • 反射攻击:利用jdk反射API,修改单例类构造函数的访问权限,然后调用构造函数;
  • 序列化攻击:将单例对象实例以字节流的方式写入到文件中,然后再读取文件字节流,反序列化生成对象实例;
  • 调用对象的克隆方法。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingletonAtack {
    public static void main(String[] args) throws Exception {
        //正常单例对象
        Singleton single1 = Singleton.getInstance();
        //1. 反射攻击
        Class clazz = Singleton.class;
        Constructor cons = clazz.getDeclaredConstructor(null);
        cons.setAccessible(true);
        Singleton single2 = (Singleton) cons.newInstance(null);
        //2. 序列化攻击
        FileOutputStream fos = new FileOutputStream("a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(single1);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream("a.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton single3 = (Singleton) ois.readObject();
        //3. clone攻击
        Singleton single4 = (Singleton) single1.clone();
    }
}
防止攻击代码示例:
  • 防止反射攻击:在单例类的构造函数中添加判断逻辑,如果已经存在实例对象,就抛出异常。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
        // 防止反射攻击
        if (instance != null) {
            throw new RuntimeException("单例模式不允许多个实例");
        }
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 防止序列化攻击:在单例类中实现readResolve方法,返回已有的实例对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    // 防止序列化攻击
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}
  • 防止克隆攻击:在单例类中重写clone方法,返回已有的实例对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton implements Cloneable{
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

   // 防止克隆攻击
   @Override
   protected Object clone() throws CloneNotSupportedException{
       return instance;
   } 
}

场景:

  • 数据库连接池:为了避免频繁地创建和销毁数据库连接,可以使用单例模式来管理数据库连接池,保证只有一个连接池对象存在。
  • 配置文件读取器:为了提高配置文件的读取效率,可以使用单例模式来缓存配置文件的内容,保证只有一个配置文件读取器对象存在。
  • 日志记录器:为了统一管理日志的输出和格式,可以使用单例模式来创建日志记录器对象,保证只有一个日志记录器对象存在。

单例模式应用示例代码:

  • 日志对象,可以使用java.util.logging.Logger类来创建和获取单例的日志对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LogTest {
    // 使用LogTest类的名称作为日志对象的标识符
    private static final Logger logger = Logger.getLogger(LogTest.class.getName());

    public static void main(String[] args) {
        // 使用日志对象记录一条信息级别的消息
        logger.info("This is a log message");
    }
}
  • 驱动对象,可以使用java.sql.DriverManager类来注册和获取单例的驱动对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DriverTest {
    public static void main(String[] args) throws SQLException {
        // 注册MySQL驱动
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        // 获取MySQL驱动实例
        Driver driver = DriverManager.getDriver("jdbc:mysql://localhost:3306/test");
        // 使用驱动实例连接数据库
        Connection conn = driver.connect("jdbc:mysql://localhost:3306/test", null);
    }
}
  • 缓存对象,可以使用一个静态变量和一个私有构造器来实现单例的缓存对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Cache {
    // 创建并初始化一个缓存实例作为静态变量
    private static final Cache INSTANCE = new Cache();
    // 创建一个Map用于存储键值对数据
    private Map<String, Object> data;

    // 私有化构造器,防止外部创建新的缓存实例
    private Cache() {
        data = new HashMap<>();
    }

    // 提供一个公共方法用于获取缓存实例
    public static Cache getInstance() {
        return INSTANCE;
    }

    // 提供一个公共方法用于向缓存中添加数据
    public void put(String key, Object value) {
        data.put(key, value);
    }

     // 提供一个公共方法用于从缓存中获取数据
     public Object get(String key) {
         return data.get(key);
     }
}
  • 线程池对象,可以使用java.util.concurrent.Executors类来创建和获取单例的线程池对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ThreadPoolTest {
   // 创建并初始化一个固定大小为10的线程池作为静态变量 
   private static final ExecutorService executor = Executors.newFixedThreadPool(10);

   public static void main(String[] args) {
      for (int i = 0; i < 20; i++) {
         // 向线程池提交20个任务,并由线程池分配给空闲线程执行 
         executor.execute(new Runnable() {
            @Override 
            public void run() { 
               System.out.println(Thread.currentThread().getName() + " is running"); 
            } 
         }); 
      } 
      // 关闭线程池,不再接受新任务,并等待已提交任务完成后退出 
      executor.shutdown(); 
   } 
}
  • Runtime对象,可以使用java.lang.Runtime类的getRuntime方法来获取单例的Runtime对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class RuntimeTest { 
   public static void main(String[] args) throws IOException{ 
      // 获取单例的Runtime实例  
      Runtime runtime = Runtime.getRuntime();  
      // 使用Runtime实例执行一个命令  
      Process process = runtime.exec("notepad.exe");  
   }  
}
  • Desktop对象,可以使用java.awt.Desktop类的getDesktop方法来获取单例的Desktop对象。例如:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DesktopTest {   
   public static void main(String[] args) throws IOException{   
      // 获取单例的Desktop实例   
      Desktop desktop = Desktop.getDesktop();   
      // 使用Desktop实例打开一个文件   
      File file = new File("test.txt");   
      desktop.open(file);   
   }   
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
设计模式 | 单例模式及典型应用
单例是最常见的设计模式之一,实现的方式非常多,同时需要注意的问题也非常多。要内容:
小旋锋
2019/01/21
1K0
设计模式 | 创建型 | 单例模式
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
被水淹没
2023/02/25
4460
设计模式 | 创建型 | 单例模式
设计模式-单例模式
单例模式是保证系统实例唯一性的重要手段。单例模式首先通过将类的实例化方法私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例,用户只需要也只能通过调用该方法获取类的实例。
别团等shy哥发育
2023/10/17
2910
单例模式的迭代式优化过程
在软件设计架构中,单例模式是最为常用的一种设计模式,所谓单例模式是指在创建某一个类的对象实例时该系统中有且仅有该类的一个实例,从而可以合理解决实例化对象的性能开销、资源分配等问题。从实现角度看,单例模式主要分为两种,一般称为饿汉式单例模式和懒汉式单例模式,以下逐一介绍
用户7506105
2021/08/09
3110
详解设计模式@单例的进化之路
单例模式(Singleton Pattern)是设计模式中一个重要的模式之一,是确保一个类在任何情况下都绝对只有一个实例。单例模式一般会屏蔽构造器,单例对象提供一个全局访问点,属于创建型模式。
堆栈哲学
2023/03/08
2410
详解设计模式@单例的进化之路
探索单例模式的奥秘
单例模式确保了一个类只有一个实例,而且自行实例化并且向整个系统提供这个实例,这个类被称为单例类。它提供全局访问的方法。
码匠er
2024/02/28
1640
探索单例模式的奥秘
Java设计模式学习记录-单例模式
前言 已经介绍和学习了两个创建型模式了,今天来学习一下另一个非常常见的创建型模式,单例模式。 单例模式也被称为单件模式(或单体模式),主要作用是控制某个类型的实例数量是一个,而且只有一个。 单例模式 单例模式的实现方式  实现单例模式的方式有很多种,大体上可以划分为如下两种。 外部方式 在使用某些全局对象时,做一些“try-Use”的工作。就是如果要使用的这个全局对象不存在,就自己创建一个,把它放到全局的位置上;如果本来就有,则直接拿来使用。 内部实现方式 类型自己控制正常实例的数量,无论客户程序是否尝试过
纪莫
2018/07/04
3890
23种设计模式之单例模式
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
Java技术债务
2022/09/26
2500
23种设计模式之单例模式
一文搞懂设计模式—单例模式
在软件开发中,有些对象我们只需要一个实例,通过单例模式可以确保一个类只有一个实例,并提供了全局访问点以便其他对象可以使用该实例。本文将介绍单例模式的使用场景、实现方式和总结。
BookSea
2024/02/17
3120
一文搞懂设计模式—单例模式
一个单例模式,被问7个问题,难!
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。单例模式属于创建型模式,它提供了一种创建对象的最佳方式。
田维常
2022/04/19
7990
一个单例模式,被问7个问题,难!
设计模式—— 七 :单例模式
Sngleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实 例,并且是自行实例化的(在Singleton中自己使用new Singleton())。
三分恶
2020/07/16
2860
一个单例模式,没必要这么卷吧
老猫的设计模式专栏已经偷偷发车了。不甘愿做crud boy?看了好几遍的设计模式还记不住?那就不要刻意记了,跟上老猫的步伐,在一个个有趣的职场故事中领悟设计模式的精髓。还等什么?赶紧上车吧
程序员老猫
2024/02/22
1580
一个单例模式,没必要这么卷吧
Java中的单例模式大全一文掌握所有写法!
在Java的世界里,单例模式(Singleton Pattern)是一种常见的设计模式,它确保一个类只有一个实例,并提供一个全局访问点。今天,我们将深入探讨Java中所有单例模式的写法,包括经典的懒汉式、饿汉式,以及现代的枚举式和静态内部类式。此外,我们还将手写一个线程安全的单例模式,并分析其运行原理、应用场景以及源码。如果你对设计模式感兴趣,或者正在寻找提高代码质量的秘诀,那么这篇文章绝对不容错过!
疯狂的KK
2024/06/03
2110
Java中的单例模式大全一文掌握所有写法!
Java设计模式之单例模式
在软件工程中,单例模式是一种常用的设计模式,其核心目标是确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。Java作为一门广泛使用的编程语言,实现单例模式是面试和实际开发中的常见需求。本文将深入探讨Java中的单例模式,包括其优缺点分析、实现方式等。
修己xj
2024/03/04
1450
Java设计模式之单例模式
Java 设计模式 | 单例模式
单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中一个类只会产生一个实例。
utopia
2023/03/20
4350
Java 设计模式 | 单例模式
聊一聊最难的设计模式 - 单例模式
很多人上来肯定一脸懵逼,因为在你的印象中,单例模式实现起来还是很简单的。不要着急,慢慢往下看,你就知道为什么我说它最难了。 1. 基本概念 单例模式是一种常用的创建型设计模式。单例模式保证类仅有一个实例,并提供一个全局访问点。 2. 适用场景 想确保任何情况下都绝对只有一个实例。 典型的场景有:windows 的任务管理器、windows 的回收站、线程池的设计等。 3. 单例模式的优缺点 优点 内存中只有一个实例,减少了内存开销。 可以避免对资源的多重占用。 设置全局访问点,严格控制访问
山海散人
2021/03/03
2890
聊一聊最难的设计模式 - 单例模式
Java面向对象设计之单例模式
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。
Abalone
2022/07/14
6330
Java面向对象设计之单例模式
你知道几种单例模式?
http://www.jianshu.com/u/d5b531888b2b
陈宇明
2020/12/15
3830
单例模式【单例设计模式】
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
高大北
2022/06/14
1.8K0
单例模式【单例设计模式】
设计模式学习笔记(四)单例模式的实现方式和使用场景
单例模式可以说是Java中最简单的设计模式,也是技术面试中频率极高的面试题。因为它不仅涉及到设计模式,还包括了关于线程安全、内存模型、类加载等机制。所以下面就来分别从单例模式的实现方法和应用场景来介绍一下单例模式
归思君
2023/10/16
4540
相关推荐
设计模式 | 单例模式及典型应用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验