单例模式(Singleton)

简介: 单例模式是最简单的设计模式之一,它的核心就是类只存在一个实例,这在很多应用场景中有些类只需要创建一个全局对象,比如一个数据库连接,一个配置文件的资源,等等,可以节省不必要的内存开销。单例模式设计时要注意两点:类的构造方法私有化定义公共静态方法(getInstance())来获取实例


一、概述



单例模式是最简单的设计模式之一,它的核心就是类只存在一个实例,这在很多应用场景中有些类只需要创建一个全局对象,比如一个数据库连接,一个配置文件的资源,等等,可以节省不必要的内存开销。

单例模式设计时要注意两点:

  • 类的构造方法私有化
  • 定义公共静态方法(getInstance())来获取实例

这两点主要为了只能在该类内部创建对象实例,外部只能通过静态方法来获取,一般将单例模式分为懒汉式单例饿汉式单例,下面就讲解这两种方法创建的单例有什么不一样。


二、懒汉式单例



通过上面两个注意点,编写一个简单的单例类,并编写Main类来测试:

public class Singleton {
    private static Singleton instance;
    // 构造方法私有化
    private Singleton() {
    }
    // 通过公共静态方法来获取单对象
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

运行上面代码,控制台打印的对象是同一个。我们通过单例类中的 getInstance() 方法来获取实例而不能使用构造方法主动实例化该类,在第一次调用该方法的时候,才会创建单例对象,这种方式叫做懒加载。我们修改 main 方法如下:

public static void main(String[] args) {
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread(() -> System.out.println(Singleton.getInstance()));
    }
    for (Thread t : threads) {
        t.start();
    }
}

使用10个线程来获取单例对象的实例,运行上面代码发现控制台中打印的对象有时候并不是同一个,这是因为上面的单例代码实现是线程不安全的。我们想到的第一解决方案就是给 getInstance() 方法加上同步锁:

public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    // 同一时间内只能被一个线程调用
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

线程安全的问题解决了,但这样的写法并不是最完美的,因为此时每一次调用 getInstance() 方法都会使用一次同步锁,这种性能开销很没有必要,如果你觉得 synchronized 的性能损耗对你的代码没有影响,这倒是一种简单的解决方案。

这时候我们来分析代码线程不安全的因素,在多个线程中调用 getInstance() 方法,只有在 instancenull 的时候才有线程不安全的风险,因为可能在多个线程中同时判断实例为 null 而创建了新的实例,导致打印出来的对象不一致。

当实例不为 null 的时候,所有的线程调用 getInstance() 方法都只会返回同一个对象实例,此时的同步锁就没有什么用处了。所以我们可以只在 instance == null 的情况使用同步锁,其他情况就没有必要使用了。根据这种设计修改单例类如下:

public class Singleton {
    private volatile static Singleton instance;
    private Singleton() {
    }
    // 使用双检锁,只有当实例为 null 的时候使用锁
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
           }
       }
       return instance;
   }
}

上面的代码中用到了两次非空判断,第一次判断的作用是判断什么时候才使用同步锁,它并不是同步的。第二次判断位于 synchronized 代码块内部,它才是线程安全的,所以第二次非空判断才是关键,这样设计只有当线程判断 instancenull 的时候才使用同步锁,不为 null 的时候是不会获取到同步锁操作的,这种方法大大节省了 synchronized 的性能开销。在实例字段中添加 volatile 关键字保证了对象实例的可见性。这两次非空判断使用同步锁的方法我们把它叫做双检锁


三、饿汉式单例



前面我们讲的懒汉式单例有一个很大的特点,就是懒加载,正是因为这样才引起了线程的不安全,如果我们在定义静态变量的时候就创建对象实例,JVM在类加载的时候执行静态初始化就创建实例,这样就可以保证线程安全了。修改单例实现如下:

public class Singleton {
    // 定义变量时初始化实例
    private final static Singleton INSTANCE = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种方法简单有效的实现了线程安全的单例模式,但唯一的缺点是牺牲了内存空间,对于比较轻量的类或者整个程序执行都要使用到的类还是可以选择这种解决方案的。


目录
相关文章
|
7月前
|
设计模式 存储
static应用之 单例设计模式(饿汉单例&懒汉单例)
本章我们来学习单例模式中的饿汉单例和懒汉单例,那么什么是单例模式呢?应用该模式的这个类永远只有一个实列,即一个类只能创建一个对象例如电脑上的任务管理器对象只需要一个就能解决问题,可以节省内存空间先定义一个类,把构造器私有如下图,先来看一下没有把构造器私有化的SingleInstance类,此时Test类中可以随意创建多个SingleInstance的实例化。 在SingleInstance类中用private修饰无参构造器,此时左边new方法报错了。我们在右边创建一个静态变量来存储对象,变量名为instan
32 0
|
23天前
|
安全 Java
线程安全的单例模式(Singleton)
线程安全的单例模式(Singleton)
|
1月前
|
SQL 安全 Java
懒汉式单例的3个坑
懒汉式单例的3个坑
|
10月前
|
安全 Java
懒汉式单例
懒汉式单例
|
11月前
|
安全 C#
单例模式(Singleton)
单例模式(Singleton)
57 0
|
11月前
@Singleton和@ApplicationScoped的区别
在JakartaEE的CDI标准中@Singleton和@ApplicationScoped的区别
264 0
|
设计模式 安全 Java
Java设计模式-单例模式(Singleton)
Java设计模式-单例模式(Singleton)
|
设计模式 存储 安全
|
设计模式 Java
浅谈单例模式(Singleton)
单例模式的介绍 优点 缺点 Synchronized Synchronized示例 Synchronized与非Synchronized Singleton 第一个示例 第二个示例 第三个示例 第四个示例 第五个示例
120 0
|
存储 设计模式 安全
创建型-Singleton
what 单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
110 0