单例模式 建立型 设计模式(六)

  单例模式 Singleton
单例就是单一实例, only you  只有一个

意图

保证一个类仅有一个实例,而且提供一个访问他的全局访问点
单例模式的含义简单至极,复杂的是如何可以保障你真的只是建立了一个实例
 
怎样才能保证一个类只有一个实例,而且这个实例对象还易于被访问?
能够借助于全局变量,可是类就在那里,你不能防止实例化多个对象,可能一不当心谁就建立了一个对象
 
因此一般的作法是让类自身负责保存他的惟一实例,经过构造方法私有阻止外部实例对象,而且提供静态公共方法 
因此常说的单例模式有下面三个特色
  • 单例模式的类,只能有一个实例对象
  • 单例模式的类,自身建立本身惟一的实例对象
  • 单例模式的类,必须提供获取这一惟一实例的方式

结构

Singleton模式的结构简单,实现的步骤通常是:
自身建立而且保存维护这个惟一实例,而且这个惟一实例singleton  是私有的
将构造方法设置为私有,防止建立实例
设置公共的getInstance()方法获取实例,并且,这个方法必然是静态的
 
单例类自身负责建立维护惟一实例,按照实例对象建立的时机,分为两类 
  • 饿汉式:实例在类加载时建立
  • 懒汉式:实例在第一次使用时建立

饿汉式

package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class EagerSingleton {
private EagerSingleton() {
}
private static final EagerSingleton singleton = new EagerSingleton();
public static EagerSingleton getInstance() {
return singleton;
}
}
当类加载时,静态成员singleton 会被初始化,对象在此时被建立
饿汉式的缺点很明显:
若是初始化的太早,可能就会形成资源浪费。
在虚拟机相关的文章中,有介绍过,虚拟机的实现会保证:类加载会确保类和对象的初始化方法在多线程场景下可以正确的同步加锁
因此,饿汉式没必要担忧同步问题
若是对于该对象的使用也是“饿汉式”的,也就是应用程序老是会高频使用,应该优先考虑这种模式 

懒汉式

package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
private LazySingleton() {
}
private static LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
一个简单的懒汉式实现方式如上
静态singleton 初始为null 
每次经过getInstance()获取时,若是为null,那么建立一个实例,不然就直接返回已存在的实例singleton
同步问题
上述代码在单线程下是没有问题的,可是在多线程场景下,须要同步
假如两个线程都执行到if (singleton == null) ,都判断为空
那么接下来两个线程都会建立对象,就没法保证惟一实例
 
因此能够给方法加上synchronized关键字,变为同步方法
public synchronized static LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
若是内部逻辑不像上面这般简单,能够根据实际状况使用同步代码块的形式,好比
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
  }
}
return singleton;
}
同步的效率问题
多线程并发场景,并非必然出现的,只是在第一次建立实例对象时才会出现,几率很是小  
可是使用同步方法或者同步代码块,则会百分百的进行同步
同步就意味着也就是若是多个线程执行到同一地方,其他线程将会等待 
这样虽然能够防止建立多个实例,可是有明显的效率问题 
 
既然同步问题是小几率的,那么就能够尝试下降同步的几率
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
private LazySingleton() {
}
private static LazySingleton singleton = null;
public static LazySingleton getInstance() {
if (singleton == null) {
synchronized (LazySingleton.class) {
if (singleton == null) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
上面的方式被称为 双重检查
若是singleton不为空,那么直接返回惟一实例,不会进行同步
若是singleton为空,那么涉及到对象的建立,此时,才会须要同步
只会有一个线程进入同步代码块
他会校验是否的确为null,而后进行实例对象的建立
既解决了同步问题,又没有严重的效率问题
原子操做问题
计算机中不会由于线程调度被打断的操做,也就是不可分割的操做,被称做原子操做 
能够理解为计算机对指令的执行的最小单位
好比 i=1;这就是一个原子操做,要么1被赋值给变量i,要么没有
可是若是是int i = 1;这就不是一个原子操做
他至少须要先建立变量i 而后在进行赋值运算
 
咱们实例建立语句,就不是一个原子操做
singleton = new LazySingleton();
他可能须要下面三个步骤
  • 分配对象须要的内存空间
  • 将singleton指向分配的内存空间
  • 调用构造函数来初始化对象
计算机为了提升执行效率,会作的一些优化,在不影响最终结果的状况下,可能会对一些语句的执行顺序进行调整 
也就是上面三个步骤的顺序是不可以保证惟一的
若是先分配对象须要的内存,而后将singleton指向分配的内存空间,最后调用构造方法初始化的话 
 
假如当singleton指向分配的内存空间后,此时被另外线程抢占(因为不是原子操做因此可能被中间抢占)
线程二此时执行到第一个if (singleton == null)
此时不为空,那么不须要等待线程1结束,直接返回singleton了
显然,此时的singleton都尚未彻底初始化,就被拿出去使用了
根本问题就在于写操做未结束,就进行了读操做
能够给 singleton 的声明加上volatile关键字,来解决这些问题
能够保障在完成写操做以前,不会调用读操做
 
完整代码以下
package singleton;
/**
* Created by noteless on 2018/10/11.
* Description:
*/
public class LazySingleton {
    private LazySingleton() {
    }
    private static volatile LazySingleton singleton = null;
        public static LazySingleton getInstance() {
        if (singleton == null) {
            synchronized (LazySingleton.class) {
                if (singleton == null) {
                singleton = new LazySingleton();
                }
            }
        }
    return singleton;
    }
}

内部类的懒汉式

上面的这段代码,能够在实际项目中直接使用
可是,双重检查难免看起来有些啰嗦
还有其余的实现方式
内部类是延时加载的,也就是说只会在第一次使用时加载
内部类不使用就不加载的特性,很是适合作单例模式
package singleton;
 
/**
* Created by noteless on 2018/10/11.
* Description:
* @author
*/
public class Singleton {
    private Singleton() {
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
SingletonHolder做为静态内部类,内部持有一个Singleton实例,采用“饿汉式”建立加载
不过内部类在使用时才会被加载
私有的静态内部类,只有在getInstance被调用的时候,才会加载
此时才会建立实例,因此,从总体效果看是懒汉式
不使用不会加载,节省资源开销,也不须要复杂的代码逻辑 
依靠类的初始化保障线程安全问题,依靠内部类特性实现懒加载

枚举单例

《Effective Java》中提到过枚举针对于单例的应用

使用场景

是否只是须要一个实例,是由业务逻辑决定的
有一些对象本质业务逻辑上就只是须要一个 
好比线程池,windows的任务管理器,计算机的注册表管理器等等
计算机中只须要一个任务管理器,不须要也不必分开成多个,一个任务管理器管理全部任务简单方便高效
若是qq一个任务管理器idea一个任务管理器,你受得了么
因此说,是否须要单例模式,彻底根据你的业务场景决定
好比,若是当你须要一个全局的实例变量时,单例模式或许就是一种很好的解决方案

总结

因为单例模式在内存中只有一个实例,减小了内存开支和系统的性能开销
单例模式与单一职责模式有冲突
承担了实例的建立和逻辑功能提供两种职责
单例模式中没有抽象层,因此单例类的扩展比较困难
单例模式的选用跟业务逻辑息息相关,好比系统只须要一个实例对象时,就能够考虑使用单例模式 
单例模式的重点在于单例的惟一性的保障实现
能够直接复制上面的代码使用
 
单例模式向多个实例的扩展
单例模式的意图是“保证一个类仅有一个实例,而且提供一个访问他的全局访问点” 
单例模式的根本逻辑就是限制实例个数,而且个数限制为1
 
因此,能够仍旧限制实例个数,而且将限制个数设置为大于等于1
这种单例模式的扩展,又被称之为多例模式
  • 多例模式下能够建立多个实例
  • 多例模式本身建立、管理本身的实例,并向外界提供访问方式获取实例
多例模式其实就是单例模式的天然扩展,同单例模式同样,也确定须要构造方法私有,多例类本身维护等,惟一不一样就是实例个数扩展为多
 
自定义类加载器时的问题
在虚拟机相关的介绍中有详细介绍了类加载机制与命名空间以及类加载机制的安全性问题
不一样的类加载器维护了各自的命名空间,他们是相互隔离的
不一样的类加载器可能会加载同一个类
若是这种事情发生在单例模式上,系统中就可能存在不止一个实例对象
尽管在不一样的命名空间中是隔离的
可是在整个应用中就是不止一个,因此若是你自定义了类加载器
你就须要当心,你能够指定一样的类加载器以免这个问题
若是没有自定义类加载器则不须要关心这个问题
自定义的类都会使用内置的  应用程序   类加载器进行加载
相关文章
相关标签/搜索