设计模式之单例模式

一、单例模式:
  • 核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

    • 例如:Windows的任务管理器(Task Manager),回收站(Recycle Bin),项目中读取配置文件的类,网站计数器,数据库连接池的设计,操作系统的文件系统,Spring中每个Bean默认是单例的,servlet中每个servlet也是单例,spring MVC/Struts中的控制器对象
  • 优点:

    • 只有一个实例,系统开销小。可以在系统设置全局的访问点,优化对共享资源的访问
  • 常见的实现方式:

    • 主要:

      1. 饿汉式–线程安全,调用效率高。但不能延时加载。

        package com.wzg.singleton;
        /**
         * 饿汉式单例模式
         * @author wang
         * 问题:
         * 		如果只是加载本类而不是要调用getInstance(),甚至永远也没有调用,,会造成资源浪费。
         */
        public class SingletonDemo1 {
        	// 类初始化时立即加载这个对象(没有延时加载的优势)。由于加载类时天然的线程安全的。
        	private static SingletonDemo1 instance = new SingletonDemo1();  
        	// 私有化构造器
        	private SingletonDemo1() {}  
        	// 方法没有同步调用效率高
        	public static SingletonDemo1 getInstance() {
        		return instance;
        	}
        }
        
      2. 懒汉式–线程安全,调用效率不高。但可以延时加载。

        package com.wzg.singleton;
        /**
         * 懒汉式单例模式--真正用的时候才加载
         * @author wang
         * 问题:
         * 		资源利用率高了。但是每次调用getInstance()都要同步,并发效率低。
         */
        public class SingletonDemo2 {
        	// 类初始化时,不能初始化这个对象(延时加载,真正用的时候才创建)
        	private static SingletonDemo2 instance;  
        	// 私有化构造器
        	private SingletonDemo2() {}  
        	// 方法同步,调用效率低
        	public static synchronized SingletonDemo2 getInstance() {
        		if (instance == null) {
        			instance = new SingletonDemo2();
        		}
        		return instance;
        	}
        }
        
    • 其他方法:

      1. 双重检测锁式–由于JVM底层内部模型原因,偶尔会出现问题。

        package com.wzg.singleton;
        /**
         * 双重检测锁式实现
         * @author wang
         * 这个模式将同步内容下放到if内部,提高了执行的效率,
         * 不必每次获取对象时都进行同步,只有第一次才同步,创建以后就没必要了。	
         */
        public class SingletonDemo3 {
        	private static SingletonDemo3 instance = null;  
        	// 私有化构造器
        	private SingletonDemo3() {}  
        	// 将同步内容下放到if内部
        	public static SingletonDemo3 getInstance() {
        		if (instance == null) {
        			SingletonDemo3 sc;
        			synchronized (SingletonDemo3.class) {
        				sc = instance;
        				if(sc == null) {
        					synchronized (SingletonDemo3.class) {
        						if(sc == null) {
        							sc = new SingletonDemo3();
        						}
        					}
        					instance = sc;
        				}
        			}
        		}
        		return instance;
        	}
        }
        
        
      2. 静态内部类式–线程安全,调用效率高。可以延时加载。也是一种懒加载方式。

        package com.wzg.singleton;
        /**
         * 静态内部类实现
         * @author wang
         * 	要点:
         * 		外部类没有static属性,则不会像饿汉式那样立即加载对象。
         * 		只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的。
         * 	instance是static final类型,保证了内存中只有一个这样的实例存在,而且只能被赋值一次,
         * 	从而保证了线程的安全性。
         * 		兼备了并发高效调用和延迟加载优势
         */
        public class SingletonDemo4 {
        	private static class SingletonClassInstance{
        		private static final SingletonDemo4 instance = new SingletonDemo4();
        	}
        	// 私有化构造器
        	private SingletonDemo4() {}  
        	// 
        	public static SingletonDemo4 getInstance() {
        		return SingletonClassInstance.instance;
        	}
        }
        
        
      3. 枚举单例–线程安全,调用效率高,不能延时加载。

        package com.wzg.singleton;
        /**
         * 使用枚举实现单例模式
         * @author wang
         * 实现简单。枚举本身就是单例模式。由JVM从根本上提供保障,避免通过反射和反序列化的漏洞。
         * 缺点:无延时加载
         */
        public enum SingletonDemo5 {
        	// 这个枚举元素本身就是单例对象
        	INSTANCE;
        	// 添加实际需要的操作
        	public void singletonOperation() {
        		
        	}
        }
        
  • 如何选用

    • 单例对象 占用资源少,不需要延时加载:枚举式 好于 饿汉式
    • 单例对象 占用资源大, 需要延时加载:静态内部类式 好于 懒汉式
  • 问题:

    • 反射可以破解上面集中实现方式(不包含枚举)(可以再构造方法中手动抛出异常控制)

      例如针对上面的懒汉式单例模式(SingletonDemo2类)的破解:

      package com.wzg.singleton;
      
      import java.lang.reflect.Constructor;
      
      /**
       * 测试反射破解
       * @author wang
       *
       */
      public class Client {
      	
      	public static void main(String[] args) throws Exception{
      		SingletonDemo2 s1 = SingletonDemo2.getInstance();
      		SingletonDemo2 s2 = SingletonDemo2.getInstance();
      		// 下面两行输出显示为同一个对象
      		System.out.println(s1);
      		System.out.println(s2);
      		
      		// 通过反射直接调用私有构造器
      		Class<SingletonDemo2> clazz = (Class<SingletonDemo2>) Class.forName("com.wzg.singleton.SingletonDemo2");
      		Constructor<SingletonDemo2> c = clazz.getDeclaredConstructor(null);
      		// 跳过权限检查,访问私有属性
      		c.setAccessible(true);
      		SingletonDemo2 s3 = c.newInstance();
      		SingletonDemo2 s4 = c.newInstance();
      		// 下面两行输出显示两个不同的新对象
      		System.out.println(s3);
      		System.out.println(s4);
      	}
      	
      }
      

      防止反射漏洞的方法:在私有构造器里设置:

      public class SingletonDemo2 {
      	......
              
      	// 私有化构造器
      	private SingletonDemo6() {
      		// 防止反射漏洞
      		if(instance!=null) {
      			throw new RuntimeException();
      		}
      	}  
      	......
      }
      
      
    • 反序列化可以破解上面几种实现方式(不包含枚举)

      例如针对上面的懒汉式单例模式(SingletonDemo2类)的破解:

      对SingletonDemo2类修改,添加接口

      public class SingletonDemo2 implements Serializable{
          ......
      }
      
      package com.wzg.singleton;
      
      import java.io.FileInputStream;
      import java.io.FileOutputStream;
      import java.io.ObjectInputStream;
      import java.io.ObjectOutputStream;
      import java.lang.reflect.Constructor;
      
      /**
       * 测试反序列化破解
       * @author wang
       *
       */
      public class Client2 {
      	
      	public static void main(String[] args) throws Exception{
      		SingletonDemo2 s1 = SingletonDemo2.getInstance();
      		SingletonDemo2 s2 = SingletonDemo2.getInstance();
      		// 下面两行输出显示为同一个对象
      		System.out.println(s1);
      		System.out.println(s2);
      		
      		//通过反序列化的方式构造多个对象
      		FileOutputStream fos = new FileOutputStream("result.txt");
      		ObjectOutputStream oos = new ObjectOutputStream(fos);
      		oos.writeObject(s1);
      		oos.close();
      		fos.close();
      		
      		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("result.txt"));
      		SingletonDemo2 s3 = (SingletonDemo2) ois.readObject();
      		// 下面输出显示一个新对象
      		System.out.println(s3);
      	}
      	
      }
      
      

      防止反序列化的方法,在原函数SingletonDemo2中添加一个方法:

          // 反序列化时,如果定义了readResolve()方法则直接返回此方法指定的对象,而不需要单独再创建新对象。
      	// 实际上是一种回调
      	private Object readResolve() throws ObjectStreamException {
      		return instance;
      	}
      

      这时再执行破解类Client2时,3个对象相同。

  • 单例模式各种实现方式的效率测试:

    利用多线程的测试环境:

    • 同步辅助类CountDownLatch,在完成一组或其他线程中执行的操作之前,它允许一个或多个线程一直等待
      • countDown() 当前线程调用此方法,则计数减一(建议放在finally里执行)
      • await() 调用此方法会一直阻塞当前线程,直到计时器的值为0
    package com.wzg.singleton;
    
    import java.util.concurrent.CountDownLatch;
    
    /**
     * 单例模式五种实现方式的效率测试
     * @author wang
     *
     */
    public class Client3 {
    	
    	public static void main(String[] args) throws Exception{
    		
    		long start = System.currentTimeMillis();
            // 开启的线程数
    		int threadNum = 10;
    		final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
    		
    		for(int k=0;k<10;k++) {
    			new Thread(new Runnable() {
    				
    				@Override
    				public void run() {
    					// TODO Auto-generated method stub
    					for(int i=0;i<10000;i++) {
    						Object o = SingletonDemo1.getInstance();
    //						Object o = SingletonDemo2.getInstance();
    //						Object o = SingletonDemo3.getInstance();
    //						Object o = SingletonDemo4.getInstance();
    //						Object o = SingletonDemo5.INSTANCE;
    					}
    					
    					countDownLatch.countDown();
    				}
    			}).start();
    		}
    		countDownLatch.await();// main线程阻塞,直到计数器变为0,才会向下执行。
    		
    		long end = System.currentTimeMillis();
    		System.out.println("总耗时"+(end-start)+"ms");
    	}
    	
    }
    

    我的测试结果:

    实现方式耗时
    饿汉式7ms
    懒汉式15ms
    双重检测锁式11ms
    静态内部类式9ms
    枚举式5ms
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值