创建型设计模式(1)-- 单例模式

一、名称:

      单例模式

 

二、概念:

     一个类有且仅有一个实例,并且自行实例化向整个系统提供;

     根据上面设计模式的概念,我们可以看出,单例设计模式有以下三个特点:

   1、单例的类在应用中只有一个实例,其他类不能随便创建该类的实例;

        这个可以通过构造函数私有化来实现,当然这个方法只能防止别人通过new的方式来创建对象,对于不走寻常路的,比如反射或者克隆还是无法完全避免的。如果某个对象已经被设计为单例模式,那就要做好措施,防止通过反射或者克隆来生成新的对象。反射可以通过在构造方法里面添加逻辑来破坏反射的调用而实现,克隆可以通过重写clone方法来破坏克隆生成新的对象。

   2、这个实例由单例的类自行创建;

       创建实例很容易,但是创建的时机在具体的实现中可能会各有不同,比如可以在项目一启动的时候就创建好,也可以采用懒加载的模式,在该类被第一次调用的时候才创建,目前主流都比较倾向于后者,毕竟有可能这个类很久很久一直都使用不到啊。当然对于那种频繁使用的对象,其实区别并不是很大。创建这里特别要强调的是,多线程环境下单个实例的创建问题,毕竟如果控制不当,一不小心,单例就单不成了。

   3、单例的类需要提供一个对外的可以获取该实例的方法。

        这个对外提供的获取实例的方法,会被设计为静态方法,这样在获取该实例的时候,通过类名就可以直接调用。如果不这样做,就会引起鸡生蛋,还是蛋生鸡的问题了。

三、适用场景

   1、对照实际业务场景,现实业务当中只有一个对象的内容可以考虑为单例,比如应用系统中的某个配置文件,文件只有一个,且应用一旦启动,就无需理会改变,可以考虑用单例模式来对照;

   2、对象本身是无状态的,创建一个,创建一千个对象,这些对象之间的区别不大;

   3、对象比较大,或者比较复杂,创建和回收都会比较消耗性能,这时候可以思考,是否能将这种对象只创建一个;

四、具体实现

     我们自己系统里面,有个字典项功能,这些字典项只能通过每次部署的时候,上脚本来实现增改删,在不登录数据库手动修改的情况下,应用一旦启动,这些字典项基本可以说都是固定值。为此,我将这些字典值设计为静态缓存,所有的数据放到一个单例类里面,每次使用的时候,直接获取。

   在实现这个功能的时候,我认真比对了目前主流的几种单例模式的实现方式,其实主要可以分为两大类,饿汉模式和懒汉模式,他们各自有各自的优缺点,还是得根据具体的业务场景以及应用的客观情况来觉得使用哪种,在不严格的情况下,或者两者皆可。

      1、饿汉模式

            饿汗模式用最为通俗易懂的方式来解释,指的是,应用一启动的时候,就创建好对象放在内存,以防止系统“饥饿”。

       优点呢,主要有两点

          a、应用启动可用,每次的访问速度基本一致,不会出现懒汉模式那种第一次使用会比较缓慢的情况。

          b、应用启动即创建,可以应用自行保证只创建一个,不会在创建的时候,出现线程竞争的情况。

        缺点也比较明显

          a、有些对象可能很久也使用不到,但是却一直存放在内存中,占用资源。一个应用中,最好不要出现大量的饿汉单例。

 

         (一)恶汉模式实现一:

public class FormalSingleton {
	
	//创建对象
	private static final FormalSingleton instance = new FormalSingleton();
	
	//私有化构造方法
	private FormalSingleton() {}
	
	//获取实例的方法
	public static FormalSingleton getInstance() {
		 return instance;
	}

}

      这种一种最普通的实现方法,重点主要用以下四点;

       (1) 类被初始化好,不管是直接等号后面new一个对象,还是放在静态代码块里面等方式皆可,不管何种形式,甚至还有更五花八门的写法,宗旨就是对象必须在项目启动的时候就被初始化

     (2)初始话的对象,被标记为static的,主要是方便后面的静态方法调用,至于是否需要final修饰,则根据具体需求而定

     (3)构造方法私有化,没解决反反射的问题,这个留到下一个饿汉模式再说

     (4)公开的私有方法,返回实例,这个也不再解释

 

     (二)恶汉模式实现二:

/**
 * 枚举单例
 * @author 
 */
public enum SingletonEnum {
     
	    INSTANCE;
	
	public static SingletonEnum getInstance() {
		return INSTANCE;
	}
	    
}

    这个是利用枚举实现的单例,完美的解决了单例的所面临的两大难题,比如反射,克隆,下面就具体分析下,枚举模式到底是怎么做到的。

   枚举模式首先定义了一个枚举值INSTANCE,当我定义枚举值的时候,实际上java做了什么?枚举预先创建了一个名为INSTANCE的对象,并将以INSTANCE为key将其放入一个存放对象的map中

 //源码片段1:
 public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        //每个枚举类都有一个enumConstantDirectory枚举常量字典,用来存放
        // 每个初始化的枚举值,根据枚举的名称可以获取到具体的枚举值,
        // 这个枚举值存放在Class类中
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }


//源码片段2:
//这个方法不是在Enum类下,而是在Class类下
Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    

   枚举模式又是怎么做到反反射和反克隆的呢,这个就要从enum的源码中寻找答案了

    /**
     * Enum里面重写的克隆方法,直接抛了异常
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

   /**
    * Constructor里面不支持枚举类型的反射,也是抛异常
    */
   @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

   通过上面的代码可以看出来,枚举类型是通过抛异常的方式禁用这两个方法的,其他任何类型的单例模式,我觉得都可以借鉴,后面再讨论其他类型的单例模式,就不再讨论反反射和反克隆方式了,处理方式其实都是一致的。

 

      2、懒汉模式

           懒汉模式则正好相反,系统应用启动的时候,不初始化对象,而是等到该对象第一次被请求的时候才会去创建,本质上就是一种懒加载的思想。

         懒汉的优点和缺点则正好与饿汉相反

         优点:

         a、按需创建,不会滞留无用对象在内存中。

         缺点:

         a、初次访问速度会比较慢,对客户体验有那么一点的影响(从长久来看,其实可以忽略不计)。

         b、首次创建的时候需要解决好并发问题,保证只创建一个。

     (一)懒汉模式实现一:

/**
 * 同步懒汉模式
 *
 */
public class LazySingleton {
    
	 private static volatile LazySingleton instance = null;
     
      private LazySingleton() {}
	 
	 public synchronized static LazySingleton getInstance() {
		   if(null == INSTANCE) {
			   INSTANCE = new LazySingleton();
		   }
		   return INSTANCE;
	 }
}

   这种方式是懒汉模式实现最容易想到的方法,重点在于

  (1) 对象开始是null, 并用volatile 修饰(保证新建的对象可见,并限制编译器和处理器对代码做的重排序);

  (2)获取对象的方法用synchronized修饰或者方法内部整个包裹在synchronized代码块内,同步判null 和创建、返回的逻辑;

  这样做,功能上面是可以实现的,但所有的操作都放在synchronized里面,大家排队执行,所有的操作都变成了串行的,可以完全保证线程的安全性,但是同样性能就会大大下降。

    (二)懒汉模式实现二

/**
 * 双重检查单例
 */
public class DoubleCheckSingleton {
	
     private static volatile DoubleCheckSingleton instance = null;
     
     private DoubleCheckSingleton() {}
     
     public static DoubleCheckSingleton getInstance() {
    	    if(null == instance) {   	 
    	    	synchronized(DoubleCheckSingleton.class) {
    	    		if(null == instance) {
    	    			instance = new DoubleCheckSingleton();
        	    	}
    	    	}   	    	
    	    }
    	    return instance;
     }
}

      这种方式是懒汉模式实现一的进化版,主要是为了解决其的性能问题,重点代码在获取实例的方法中,可以看到代码中判断了两次instance是否为null,一次带锁,一次不带锁,为什么要这样写呢。

    在单例模式中,并发引起的线程不安全,主要发生在该对象的前几次获取的时候,可能会出现几个线程同时判断是否为空,同时创建对象,这时候需要加锁,保证线程安全,而后面当对象已经创建,不需要再创建对象的时候,就不会再出现线程安全问题,判断不为空,直接获取对象即可,没必要再加锁。

  在第一次判断instance是否为null的时候,没有加锁,所有的线程可以一起判断,如果instance已经创建,那所有的线程就都可以直接返回了,这个主要是为了方便后期对象已经创建后的方法调用。如果是在方法初期,instance实例还没有创建,这时候就继续下走,再挨个排队校验一次是否为空,安全的创建对象。

  这种方式的优点是既实现了懒加载单例,也在一定程度上面优化了性能问题,缺点是,写法上面不是特别的好理解,看着有那么点奇怪,用文艺的话来说,不那么优雅。

 (三)懒汉模式实现三

/**
 * 静态内部类
 */
public class StaticInnerClassSingleton {
      
	private StaticInnerClassSingleton() {}
	
	private static class LazyHolder{
		private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
	}
	   
	public static StaticInnerClassSingleton getInstance() {
		return LazyHolder.INSTANCE;
	}
}

    以上写法被称为静态内部类的单例实现方式,在单例类的内部创建一个静态内部类,在静态内部类的内部实例化当前类(或者说将当前类的实例化后的对象作为静态内部类的一个属性)。由于是静态内部类,在外部类被初始化的时候,静态内部类是不会初始化的,相应的,静态内部类里面的外部类对象也就不会被创建,只有当调用到静态内部类的时候,才会初始化、创建外部类对象,且只会初始化一次,即只会创建一个外部类对象。

  这种方式代码中无需加锁等操作,是利用的静态内部类的特性,由jvm保证对象创建的懒加载以及仅创建一次,如果感兴趣可以自行研究下《虚拟机规范》,《深入java虚拟机》等内容,此处不做展开。

  这种方式是不是就是完美了的呢?如果创建的对象需要传参的话,就无法使用这种方式,除此之外,静态内部类可以说是个接近完美的方式。

 

 讲到这里,单例模式常见几种创建方式,基本都已经说完了,但是除了以上的内容外,其实还有一种创建方式,那就是容器模式,有时间感兴趣的可以研究下spring的源码,看看spring是如何实现单例bean的初始化的。

 

五、优点

    (1)只有一个实例对象,方便管理,控制住了这一个就控制住了所有

    (2)节约内存空间,减少创建销毁的损耗,减轻了垃圾回收的压力

六、缺点

     (1)需要花费力气保证整个系统只有一个实例

     (2)不方便扩展

七、注意点

       不要滥用

八、使用概率

        高

    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值