1.单例模式简介
单例模式也被称为单件模式(或单体模式),主要作用是控制某个类的实例的数量是一个,而且只有一个,他关心的是类实例的创建问题,并不关心具体的业务功能。
单例模式的范围:目前Java里面实现的单例是一个ClassLoader及其子类ClassLoader的范围。如果一个虚拟机里面有多个ClassLoader,而且这些ClassLoader都装在某个类的话,就算这个类是单例,也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少产生一个这个类的实例,也就是说整个机器上有很多个实例,也就不是单例了。所以我们这里讨论的单例模式不适应于集群环境。
2.创建单例的步骤
1.构造方法私有化
要想控制实例的只有一个,首先要控制创建实例的地方,构造方法私有化后,类的实例不能由外部创建而只能有自身创建。
2.提供获取实例的方法
构造方法私有化后,外部类不能创建类实例,此时单例类必须提供向相应的方法来返回类的实例,并且此方法必须是static的,这样才能通过类调用该方法获取实例。
3.定义存储实例的属性
客户端每次调用创建方法都会创建实例,就成了多个实例;因此要用一个属性来标记自己创建好的实例,第一次创建后保存下来,以后不再创建。该属性会在第一次调用创建实例的静态方法中被创建,所以该属性也就成了一个类变量,有static修饰;
4.实现控制实例的创建
具体事项发放的实现:根据创建时机不同,分为懒汉式和饿汉式。懒汉式是在第一次调用时创建实例,饿汉式是JVM启动时便已经实例化。
3.单例模式的种类
主要分三种:懒汉式单例、饿汉式单例、登记单是单例。懒汉式单例
第一次调用时创建单例,不被调用则不会产生实例到内存。
/**
* Created by duanxiangchao on 2016/7/24.
* 懒汉式单例
*/
public class LazySingleton {
/**
* 私有静态对象,加载时不做初始化
*/
private static LazySingleton instance = null;
/**
* 构造方法私有化,避免外部创建实例
*/
private LazySingleton(){}
/**
* 静态工厂犯法,如果没有实例则初始化,返回唯一实例
* @return LazySingleton
*/
private static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
饿汉式单例
JVM启动时创建单例类的实例,如果有过得的单例类,启动时创建太多造成启动变慢,启动时就会占据很多的内存。
/**
* Created by duanxiangchao on 2016/7/24.
* 饿汉式单例
*/
public class EagerSingleton {
/**
* 私有的唯一实例成员,在类加载的时候创建好了单例对象
*/
private static final EagerSingleton instance = new EagerSingleton();
/**
* 私有构造方法,避免外部创建实例
*/
private EagerSingleton(){}
/**
* 静态工厂方法,返回此类的唯一实例
* @return EagerSingleter
*/
public static EagerSingleton getInstance(){
return instance;
}
}
登记式单例
登记式单例实际上维护的是一组单例类的实例,将这些实例存放在一个Map(登记簿)中,对于已经登记过的实例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。
/**
* Created by duanxiangchao on 2016/7/24.
*/
public class RegSingleton {
/**
* 登记薄,用来存放所有登记的实例
*/
private static Map<String, RegSingleton> registry = new HashMap<String, RegSingleton>();
/**
* 类加载时添加一个实例到登记薄
*/
static {
RegSingleton singleton = new RegSingleton();
registry.put(singleton.getClass().getName(), singleton);
}
/**
* 受保护的默认构造方法
*/
protected RegSingleton(){}
/**
* 静态工厂方法,返回指定登记对象的唯一实例
* 对于已登记的直接取出返回,对于还未登记的,先登记,后取出返回
* @param name
* @return
*/
public static RegSingleton getInstance(String name){
if(name == null){
name = "RegSingleton";
}
if(registry.get(name) == null){
try {
registry.put(name, (RegSingleton)Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return registry.get(name);
}
}
4.双重检查加锁
懒汉式的实现是线程安全的,所以会降低整个访问速度,而且每次都要判断一次。有没有更好地实现方式呢?可以使用“双重检查枷锁”机制。
所谓“双重检查枷锁”机制,并不是每次进入getInstance方法都需要同步,而是先不同步。当进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再检查实例是否存在。如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,创建过程只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。
在使用“双重检查加锁”机制实现时,需要使用关键字volatile,含义是被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多线程能够正确的处理该变量。JDK1.4及以前的版本,很多JVM对于volatile关键字的实现都存在问题,会导致“双重检查加锁”的失败,因此只能在JDK1.5及以上的版本使用。
所谓“双重检查枷锁”机制,并不是每次进入getInstance方法都需要同步,而是先不同步。当进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再检查实例是否存在。如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,创建过程只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。
在使用“双重检查加锁”机制实现时,需要使用关键字volatile,含义是被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多线程能够正确的处理该变量。JDK1.4及以前的版本,很多JVM对于volatile关键字的实现都存在问题,会导致“双重检查加锁”的失败,因此只能在JDK1.5及以上的版本使用。
示例代码
/**
* Created by duanxiangchao on 2016/7/26.
*/
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance = null;
private DoubleCheckSingleton(){}
public static DoubleCheckSingleton getInstance(){
//先检查实例是否存在,不存在则进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (DoubleCheckSingleton.class){
//再次检查是否存在实例,如果不存在,才真正创建实例
if(instance == null){
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
双重检查加锁和懒汉式对比
第一次访问,同时来了多个线程,但重加锁和双重加锁效果一样,此时instance为null,最先获得资源的进行加锁,其余的线程等待,二者效果相同。第二次、以后的访问,多个线程访问。对于双重检查加锁,instance不为null,多个线程不用等待,直接返回instance。单重检查,需要等其他线程检查完了才能获取锁进行判断检查,线程越多等待的时间会越长,并发量下降。所以双重检查的第一重检查是很必要的。
为什么还要第二重检查呢?这就是多线程问题
第一次访问时,多个线程同时访问。假如A线程和B线程同时访问,都判断instance为null,A线程获得对象锁开始创建;B线程等待A线程释放资源,等A线程创建完毕释放资源,这时B线程获得资源,但此时instance实例已经创建,如果不进行二次检查的,就会创建多个实例,就不再是单例,所以第二重检查也必不可少!
5.更好地实现方式
Java的类级内部类方式
采用静态初始化器的方式,可以由JVM来保证线程安全;采用类级内部类能够让类装载的时候不去初始化对象,在这个类级内部类里面去创建对象实例,这样只要不使用到这个类级内部类,那就不会创建对象实例。同时实现了延迟加载和线程安全。
/**
* Created by duanxiangchao on 2016/7/26.
* 类级内部类实例
*/
public class InnerClassSingleton {
/**
* 类级别的内部类,也就是静态的成员内部类,该内部类的实例和外部类的实例没有绑定关系,
* 而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder{
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static InnerClassSingleton instance = new InnerClassSingleton();
}
/**
* 私有化构造方法
*/
private InnerClassSingleton(){}
public static InnerClassSingleton getInstance(){
return SingletonHolder.instance;
}
}
如上代码,当getInstance()方法第一次被调用时,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个内部类在装载和初始化的时候,会初始化他的静态域,从而创建InnerClassSingleton的实例,由于是静态的域,因此只会被虚拟机在装在类的时候初始化一次,并有虚拟机来保证他的线程安全。
这种实现的优势在于,getInstance()方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问开销。
枚举式单例
单元素的枚举类型已经成为Singleton的最佳方法。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。Java枚举类型的基本思想是通过共有的静态final域为每个枚举常量导出实例的类。从某种角度来讲,枚举式单例的泛型化,本质上是单元素的枚举。
使用枚举实现单例控制,更加简洁,而且武昌地提供了序列化机制,并且由JVM从根本上提供保障,绝对防止多次实例化,是更加简洁、高效、安全的实现单例的方式。
示例代码
/**
* Created by duanxiangchao on 2016/7/26.
* 枚举式单例
*/
public enum EnumSingleton {
/**
* 单例实例
*/
INSTANCE;
/**
* 示例方法
*/
public void operate(){
System.out.println("单例操作......");
}
}
<pre name="code" class="java">/**
* Created by duanxiangchao on 2016/7/26.
*/
public class Test {
public static void main(String[] str){
//调用枚举单例的方法
EnumSingleton.INSTANCE.operate();
}
}
6.单例的优点、缺点、使用场景。
单例的优点
(1) 单例模式在内存中只有一个实例,减少了内存消耗,特别是一个对象频繁的创建、销毁,而且创建和销毁时性能又无法优化,这是采用单例模式的优势就非常明显。
(2) 单例模式只生成一个实例,减少了系统性能开销。当一个对象的产生需要较多资源时,如读取配置文件、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
(3) 单例模式可以在系统设置全局的访问点,优化共享资源的访问,例如可以设计一个单例类,负责所有映射表的映射处理。
单例的缺点
(1) 单例模式没有接口,扩展很困难。如果要扩展,除了修改代码,没有第二种途径可以实现。为什么不能增加接口?因为接口对单例模式没有任何意义,它要求“自行实例化”,并且提供单一实例,接口或抽象类是不能实例化的。
(2)单例模式对测试时不利的。在并行开发环境中,如果单例模式还没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
(3)单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例类,决定它是不是单例是环境决定的,单例模式把“要单例”和业务逻辑融合在一个类中了。
单例模式的使用场景
一个系统中,要求一个类有且只有一个对象实例,如果出现多个对象就会出现“不良反应”时,可以采用单例模式。
(1) 要求生成唯一序列号的环境。
(2)在整个项目中需要有访问一个共享访问点或共享数据,例如web上的计数器,可以不用每次刷新都计入到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
(3)创建一个对象需要消耗的资源过多,如访问I/O、访问数据库等资源。
(4)需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式。当然也可以直接声明为static的方式。
7.单例模式使用实例
单例获取properties文件中的配置信息
/**
* Created by duanxiangchao on 2016/7/26.
*/
public class ConfigSingleton {
private volatile static ConfigSingleton instance = null;
private ConfigSingleton(){}
public static ConfigSingleton getInstance(){
if(instance == null){
synchronized (ConfigSingleton.class){
if(instance == null){
instance = new ConfigSingleton();
}
}
}
return instance;
}
public String get(String key){
try {
InputStream inputStream =ClassLoader.getSystemResourceAsStream("config.propeties");
Properties properties = new Properties();
properties.load(inputStream);
return properties.get(key).toString();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}