【设计模式1_单例模式】

本文深入探讨了单例模式的不同实现方式,包括饿汉式、懒汉式、静态内部类及枚举单例模式,并分析了每种模式的特点及适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于重量级、无状态的对象,对象的重复创建和销毁是有巨大开销的,在系统中可以保持唯一实例以支持重复使用。
受Spring管理的对象默认都是单例,另外自己创建的线程池、连接池也应该保证单例。

如何创建:私有化构造器,并提供一个全局唯一的访问入口以获取单例对象。

饿汉式单例模式

利用类加载机制创建出对象,由jvm保证对象唯一。
在类加载的 ‘准备’阶段时就创建好了对象,等第一次通过getInstance()方法获取对象时就可以直接拿到,所以叫饿汉。
缺点是对象过早创建,即使它并不一定会被使用,造成资源浪费。

/**
 * 饿汉单例模式
 */
public class EagerSingleton {
 
    //私有化构造器
    private EagerSingleton(){
    }
    
	// 利用类加载保证只执行一次
	// 注意final 修饰才能在类加载的 ‘准备’阶段直接赋初始值,也就是直接执行了new EagerSingleton()
    private static final EagerSingleton eagerSingleton = new EagerSingleton(); 
 
 	// 提供获取单例对象的访问
    public static EagerSingleton getInstance(){
        return eagerSingleton;
    }
}

懒汉式单例模式

懒汉单例模式-基础版本
/**
 * 懒汉单例模式-基础版本
 * 当getInstance()第一次被调用时创建出对象,后续再调用getInstance()时判断对象已有创建,则直接return
 * 
 * 在多线程下,可能会创建出多实例
 */
public class LazySingleton { 

	private LazySingleton() {}

	private static LazySingleton instance; 

	public static LazySingleton getInstance() {
		if(instance == null) {
			instance = new LazySingleton();
		}

		return instance; //对象已有创建,则直接return
	}
}
懒汉单例模式-双重校验锁
/**
 * 懒汉单例模式-双重校验锁
 * 第一个if(instance == null) 的作用:
 * 		判断对象已存在则不用加锁,降低锁粒度;
 * 第二个if(instance == null) 的作用:
 * 		仍然可能多个线程都通过了第一个if,未抢到锁的线程将被阻塞,再重新获得synchronized 锁后,
 * 	会继续执行synchronized中的内容,没有第二个if仍将创建出多实例,需要再判断一次。
 * 
 * volatile 的作用:禁止指令重排
 * new LazySingleton()只有一句代码,但在字节码层面是三个步骤:
 * 		1. 分配堆内存空间
 * 		2. 在堆上初始化这个对象
 *  	3. 堆堆引用地址赋值给变量
 * 	  2和3都依赖于1,但2和3之间没有依赖关系,可能发生指令重排序为 1->3->2  当引用赋值后,instance 不再等于 null,
 * 其他线程可直接返回instance ,而实际上第2步,对象初始化并未完成,那么拿到instance 的线程将发生空指针。
 */
public class LazySingleton { 

	private LazySingleton() {}

	private static volatile LazySingleton instance;  // volatile 禁止指令重排

	public static LazySingleton getInstance() {
		if(instance == null) {
			synchronized (LazySingleton.class) {
				if(instance == null) {
					instance = new LazySingleton();
				}
			}
		}
		return instance; //对象已有创建,则直接return
	}
}

静态内部类单例模式

public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHolder{
     private static SingleTon instance = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHolder.instance;
  }
}

结合类加载的知识。
外部类和内部类一样,经过加载、连接后并不立刻初始化。(此时内部类的instance 只会赋默认值null)

只有当调用外部类:LazySingleton.getInstance() 并返回调用 内部类 静态instance属性时,触发内部类初始化,为instance 类变量赋初始值。这个初始值就是 new LazySingleton();

为instance 类变量赋初始值这个过程只会发生一次,且由JVM保证这个过程是单线程的。new LazySingleton() 的过程也是instance 赋初始值的过程,而赋初始值的过程是单线程的,就保证了new 对象期间不会发生doubleCheck中那样线程上下文切换。


以上的写法都是自己刚好就是需要被保证单例的资源类;但其实也可以是仅仅作为一个util类,对第三方的资源类提供一个获取单例的入口,而不涉及对这个资源类的代码改动。

利用反射创建出多实例

以上饿汉、懒汉、内部类创建的单例模式其实不能阻止利用反射创建出多个实例;

    SingleTon st1=  SingleTon.getInstance();
  
	Constructor<SingleTon> constructor = SingleTon.class.getDeclaredConstructor(); // 获取无参构造器
    constructor.setAccessible(true);  // 改变私有属性
    SingleTon st2= constructor.newInstance();  
    
    System.out.println(st1== st2); // false

解决办法是:在私有化的构造器中再加上判断实例如果已存在,则抛出异常RuntimeException

public class SingleTon{

  private SingleTon(){
      if (SingleTonHolder.instance != null){
          throw new RuntimeException("不能创建多个实例");
      }
  }
 
  private static class SingleTonHolder{
     private static SingleTon instance = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHolder.instance;
  }

}

利用反序列化创建出多实例

防反序列化,需要通过提供readResolve方法,返回instance。

枚举单例模式

外部通过 SomeThing.INSTANCE.getInstance() 就可以访问单实例。

public enum SomeThing {
    INSTANCE;
    
    private Resource instance;
   
    private SomeThing() {
        instance = new Resource();
    }
    public Resource getInstance() {
        return instance;
    }
}


class Resource{
	// Resource 是我们要单保证例的资源,网络连接,数据库连接,线程池等等...
}

// 有个疑问。虽然能保证通过SomeThing.getInstance() 拿到的Resource对象是单例,但并不能防止在其他地方直接 new Resource()...
// 这个写法就是针对的依赖包,只保证通过我提供的方式获取到单例。
// 例如项目中线程池、连接池,能保证从池里拿到都是同一个实例,但没有限制不能自己创建链接、自行new Thread()

枚举类型单例,写在枚举容器,也不会对原资源做改造,可以防被反序列化和反射生成多个实例。


如果需要被保证单实例的资源类本就是自行创建,可以换一个写法:
(这个Resource 就不能是第三方jar包里的类了,如果是我们自己创建的类,并且需要保证单例,推荐这个内部枚举的方式)

public class Resource { 
		// Resource 是我们要单保证例的资源,网络连接,数据库连接,线程池等等...
		private  Resource {
		}
		
		private enum SomeThing {
    		INSTANCE;
    		
			private final Resource instance;
			
			private SomeThing() {
       				 instance = new Resource();
 			 }
			
			public Resource getInstance() {
       				 return instance;
  			 }
		}
		
		public static Resource getInstance() {
			SomeThing.INSTANCE.getInstance();
		}
}

使用反射declaredConstructor.newInstance()的源码中,会判断如果当前是枚举类型,则抛出异常,也就意味着如果通过枚举创建单例模式可以防止反射创建多实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值