单例模式(Singleton Pattern)
介绍
意图:保证系统中一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
为何使用:
1、单例模式节省公共资源
比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。
对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。
2、单例模式方便控制
就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1、要求生产唯一序列号。 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
==注意事项==:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
实现单例模式的思路
- 构造私有:
如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化。
- 以静态方法返回实例。
因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。
- 确保对象实例只有一个。
只对类进行一次实例化,以后都直接获取第一次实例化的对象。
- 单例模式案例
public class Singleton {
//确保对象实例只有一个。
private static final Singleton singleton = new Singleton();
//构造方法私有
private Singleton() {
}
//以静态方法返回实例
public static Singleton getInstance() {
return singleton;
}
}这里类的实例在类初始化的时候已经生成,不再进行第二次实例化了,而外界只能通过SingleCase.getInstance()方法来获取SingleCase对象, 所以这样就保证整个系统只能获取一个类的对象实例。
几种单例模式的区别
饿汉模式
饿汉模式的意思是,我先把对象(面包)创建好,等我要用(吃)的直接直接来拿就行了。
public class Singleton {
//先把对象创建好
private static final Singleton singleton = new Singleton();
private Singleton() {
}
//其他人来拿的时候直接返回已创建好的对象
public static Singleton getInstance() {
return singleton;
}
}懒汉模式
因为饿汉模式可能会造成资源浪费的问题,所以就有了懒汉模式,
懒汉模式的意思是,我先不创建类的对象实例,等你需要的时候我再创建。
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
//获取对象的时候再进行实例化
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}懒汉模式在并发情况下可能引起的问题
懒汉模式解决了饿汉模式可能引起的资源浪费问题,因为这种模式只有在用户要使用的时候才会实例化对象。但是这种模式在并发情况下会出现创建多个对象的情况。
因为可能出现外界多人同时访问SingleCase.getInstance()方法,这里可能会出现因为并发问题导致类被实例化多次,所以懒汉模式需要加上锁synchronized (Singleton.class) 来控制类只允许被实例化一次。
懒汉模式加锁引起的性能问题
我们通过锁的方式保证了单例模式的安全性,因为获取对象的方法加锁,多人同时访问只能排队等上一个人执行完才能继续执行,但加锁的方式会严重影响性能。
解决方案一:双重检查加锁(DCL)
public static Singleton getInstance() {
if (singleton == null) {//先验证对象是否创建
synchronized (Singleton.class) {//只有当对象未创建的时候才上锁
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}双检测锁定的方式 是只有当对象未创建的时候才对请求加锁,对象创建以后都不会上锁,这样有效的提升了程序的效率,也可以保证只会创建一个对象的实例。
DCL是完美的解决了单例模式中性能和资源浪费的问题,但是DCL在并发情下也会存在一个问题,因为Jvm指令是乱序的;
情况如下:
线程1调用getInstance 获取对象实例,因为对象还是空未进行初始化,此时线程1会执行new Singleton()进行对象实例化,而当线程1的进行new Singleton()的时候JVM会生成三个指令。
指令1:分配对象内存。
指令2:调用构造器,初始化对象属性。
指令3:构建对象引用指向内存。
解决方案二:用内部类实现懒汉模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHoler.singleton;
}
//定义静态内部类
private static class SingletonHoler {
//当内部类第一次访问时,创建对象实例
private static Singleton singleton = new Singleton();
}
}静态内部类原理:
当外部内被访问时,并不会加载内部类,所以只要不访问 SingletonHoler 这个内部类, private static Singleton singleton = new Singleton() 不会实例化,这就相当于实现懒加载的效果,只有当SingletonHoler.singleton 被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了恶汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。
一种更好的单例实现方法
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:
//Initialization on Demand Holder
public static class SingletonIoDH {
private SingletonIoDH() {}
private static class HolderClass {
private static final SingletonIoDH instance = new SingletonIoDH();
}
public static SingletonIoDH getInstance() {
System.out.println("SingletonIoDH getInstance");
return HolderClass.instance;
}
}