单例模式(Singleton Pattern)

目录

设计模式(Design Pattern),简称DP

一、单例设计模式描述

单例模式(singleton Pattern),涉及到一个单一的类,该类只有一个实例,并提供提个访问该实例的全局访问点(被public static修饰的方法)。

二、单例模型的特点

在上面的描述中我们能看出来,要想满足单例模式的要求,需要满足如下特点:

1.构造方法私有化(即构造方法被private修饰),用来保证类只有一个实例对象.

2 该单例对象必须由单例类自行创建

3.内部提供一个公共静态的方法给外界进行访问(方法被public static 修饰)

构造函数的特点:

  1. 构造函数的主要作用是完成对象的初始化工作,(如果写的类里面没有构造函数,那么编译器会默认加上一个无参数且方法体为空的构造函数)。

  2. 它能够把定义对象时的参数传给对象的属性。意即当创建一个对象时,这个对象就被初始化.如果这时构造函数不为空,则会在创建对象时就执行构造函数里面的代码。

  3. 构造函数的名称必须与类名相同,包括大小写;

  4. 构造函数没有返回值,也不能用void修饰。

    如果不小心给构造函数前面添加了返回值类型,那么这将使这个构造函数变成一个普通的方法,在运行时将产生找不到构造方法的错误。

  5. 一个类可以定义多个构造方法,如果在定义类时没有定义构造方法,则编译系统会自动插入一个无参数的默认构造器,这个构造器不执行任何代码。

  6. 构造方法可以重载,以参数的个数,类型,顺序,不同来区分。

  7. 被private修饰的构造方法,只有在内部可以调用该构造方法。

三、单例模型的优势与缺点

优势

可以避免频繁创建和销毁全局使用的类实例的,有助于控制实例数目,节省系统资源。

缺点

  • 没有接口,不能继承。
  • 与Java设计的单一原则(一个类应该只关心内部逻辑,而不关心实例化方式)有冲突。

四、应用实例和使用场景

1. 应用实例

  1. 一个班级只有一个班主任.
  2. 在Windows系统中,‌任务管理器(‌Task Manager)‌,用户不能打开两个任务管理器窗口,‌因为系统确保只有一个实例在运行。‌

2. 使用场景

五、单例模式的实现方案

1.饿汉式

java 复制代码
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

instance是一个被private static 修饰的Singleton类实例,所以此属性只能被Singleton类中的public static方法获得,由于instance是静态的属性,所以instance会在被类加载时完成初始化。

​ 在饿汉式的模型中,在调用**getInstance()时会发生类加载,但是也有其他方式会导致类加载(比如调用类中的其他静态方法),我们在只有在调用getInstance()**方法时才会是真正想要使用对象实例的,,这样却是无法完成懒加载(Lazy loading)的目的。

2.懒汉式

懒汉式有两种,一种是线程安全的,一种是线程不安全的,我们先来看下面线程不的这种。

(1)线程不安全的

java 复制代码
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

​ 这种例子符合懒加载的理念,在这个类里面,只有getInstance()只有一个可以获得Singleron的接口,只有调用getInstance()方法才可以给对象初始化,并且依靠getInstance()中的if (instance == null)才会给instance赋值,如果instance已经被赋过值的话就会直接返回类中已经创建好的instance

​ 这样的例子看起来很美好,很符合单例模型的全部要求了,可是这是线程不安全的,只适用于单线程访问。当并发访问的时候,第一个调用getInstance()方法的线程A,在判断完instance是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了instance是否为null的判断,这时instance依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就不符合单例的设计理念了。

(2)线程安全的

​ 要想解决这个问题有一个很简单的办法,那就是加锁,由于在上面线程不安全的模型中getInstance(),每个线程都可以获取到instance导致了线程不安全,所以只需要给getInstance()加上锁,保证同一时间只有一个线程可以去完成instance的初始化,就出现了下面的模型。

java 复制代码
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

​ 可是这样就出现了一个问题,**getInstance()**属于静态方法,由于线程去访问加锁的静态方法时相当于锁住了整个类,其他线程若想要访问类中的其他方法或属性也会受阻,导致效率降低。

3.双检锁(DCL)

双检锁或者说是双重校验锁(Double-checked locking)

(1)对懒汉式模型的思考与改进

​ 通过上面懒汉式模型,我们发现了其实需要加锁的阶段,只有在实例对象初始化的时候,也就是**new Singleton()的时候才需要加锁,所以没必要在整个静态方法上加锁,那我们能不能想办法只在new Singleton()**部分的代码块上加锁呢?

​ 好我们现在只对**new Singleton()**代码块加锁,改进成下面的代码:

java 复制代码
public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  					//第一次检查
        synchronized (Singleton.class) {  		//加锁
            if (singleton == null) {  			//第二次检测
                singleton = new Singleton();  	//初始化
            }  
        }  
    }  
    return singleton;  
    }  
}

​ 加上锁可以保证只有一个线程可以创建对象,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,在对象创建好之后,执行**getInstance()**方法将不需要获取锁,直接返回已创建好的对象。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美,其实不然。

​ 为什么说其实并不两全其美呢?

这是由于在双检索代码的实力初始化的这一过程中(singleton = new Singleton(); )创建了一个对象。这一行代码可以分解为如下的3行伪代码。

java 复制代码
1--	memory = allocate(); // 1:分配对象的内存空间
2--	ctorInstance(memory); // 2:初始化对象
3--	instance = memory; // 3:设置instance指向刚分配的内存地址

三行代码的2与3之间可能会发生指令重排序顺序变为:1,3,2

  1. 给对象分配内存空间
  2. 让instance引用指向刚分配的内存地址
  3. 初始化对象

这样的指令重排在单线程访问时并不会影响对象初始化的过程,可是在多线程中可能就会遇到下面的问题了

线程A在执行singleton = new Singleton(); 时,另一个并发执行的线程B就有可能在第一次检查时(if (singleton == null) )判断instance不为null。线程B接下来将访问instance所引用的对象(return singleton; ),但此时这个对象可能还没有被A线程初始化,此时,线程B将会访问到一个还未初始化的对象。

(2)使用volatile的双检锁(DCL)

我们要想解决上面的问题,只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。

volatile关键字的作用

保证可见性: 当一个变量被volatile修饰时,在一个线程中对该变量的修改会立即被其他线程所见。这是因为volatile修饰的变量会被立即写回主内存,并且从主内存中读取最新的值,保证了各个线程之间对变量的修改是可见的。

禁止指令重排序: volatile关键字还会禁止指令重排序优化,保证了程序执行的顺序与代码的顺序一致。这样可以防止某些情况下的线程安全问题,例如双重检查锁定问题。

这里使用volatile利用了禁止指令重排序的功能。

4.使用静态内部类实现单例模型

使用静态内部类的方式完成的单例模型,使用了懒加载进行初始化,而且可以保证线程安全的。

​ 由于SingletonHolder属于静态内部类,在Singleton类加载的时候不会加载SingletonHolder,只有调用 getInstance 方法时,才会显式装载 SingletonHolder 类,这样便能完成懒加载的目的,这种方式能达到和双检索相同的目的,那么这两种实现方式各自在什么时候使用呢。

​ **双检锁方式:**可在实例对象懒加载时使用

​ **使用静态内部类的单例模型:**在静态域懒加载时使用

java 复制代码
public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}

5.使用枚举实现单例模型

这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

​ 除此以外的所有的单例实现模型都是可以通过反射来打破单例的,因为通过反射可以获得私有的构造方法,使用私有构造方法可以创造新的实例,由此创建新的实例对象。只有枚举类型无法使用反射来调用私有构造方法创造新实例。

java 复制代码
public enum Singleton {  
    INSTANCE;  
    public void getmessage() {  
    }  
}
相关推荐
一只爱打拳的程序猿4 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧5 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck7 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
为将者,自当识天晓地。26 分钟前
c++多线程
java·开发语言
daqinzl34 分钟前
java获取机器ip、mac
java·mac·ip
激流丶1 小时前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue1 小时前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
让学习成为一种生活方式1 小时前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
晨曦_子画1 小时前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
南宫生2 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法