目录
- 一、单例设计模式描述
- 二、单例模型的特点
- 三、单例模型的优势与缺点
- 四、应用实例和使用场景
-
- [1. 应用实例](#1. 应用实例)
- [2. 使用场景](#2. 使用场景)
- 五、单例模式的实现方案
设计模式(Design Pattern),简称DP
一、单例设计模式描述
单例模式(singleton Pattern),涉及到一个单一的类,该类只有一个实例,并提供提个访问该实例的全局访问点(被public static修饰的方法)。
二、单例模型的特点
在上面的描述中我们能看出来,要想满足单例模式的要求,需要满足如下特点:
1.构造方法私有化(即构造方法被private修饰),用来保证类只有一个实例对象.
2 该单例对象必须由单例类自行创建
3.内部提供一个公共静态的方法给外界进行访问(方法被public static 修饰)
构造函数的特点:
-
构造函数的主要作用是完成对象的初始化工作,(如果写的类里面没有构造函数,那么编译器会默认加上一个无参数且方法体为空的构造函数)。
-
它能够把定义对象时的参数传给对象的属性。意即当创建一个对象时,这个对象就被初始化.如果这时构造函数不为空,则会在创建对象时就执行构造函数里面的代码。
-
构造函数的名称必须与类名相同,包括大小写;
-
构造函数没有返回值,也不能用void修饰。
如果不小心给构造函数前面添加了返回值类型,那么这将使这个构造函数变成一个普通的方法,在运行时将产生找不到构造方法的错误。
-
一个类可以定义多个构造方法,如果在定义类时没有定义构造方法,则编译系统会自动插入一个无参数的默认构造器,这个构造器不执行任何代码。
-
构造方法可以重载,以参数的个数,类型,顺序,不同来区分。
-
被private修饰的构造方法,只有在内部可以调用该构造方法。
三、单例模型的优势与缺点
优势
可以避免频繁创建和销毁全局使用的类实例的,有助于控制实例数目,节省系统资源。
缺点
- 没有接口,不能继承。
- 与Java设计的单一原则(一个类应该只关心内部逻辑,而不关心实例化方式)有冲突。
四、应用实例和使用场景
1. 应用实例
- 一个班级只有一个班主任.
- 在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行伪代码。
java1-- memory = allocate(); // 1:分配对象的内存空间 2-- ctorInstance(memory); // 2:初始化对象 3-- instance = memory; // 3:设置instance指向刚分配的内存地址
三行代码的2与3之间可能会发生指令重排序顺序变为:1,3,2
- 给对象分配内存空间
- 让instance引用指向刚分配的内存地址
- 初始化对象
这样的指令重排在单线程访问时并不会影响对象初始化的过程,可是在多线程中可能就会遇到下面的问题了
线程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() {
}
}