不断的学习,我们才能不断的前进
一个好的程序员是那种过单行线马路都要往两边看的人

单例模式

1. 简介

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

  • 单例的构造函数为私有
  • 提供一个自身的静态私有成员变量;
  • 提供一个公有的静态工厂方法

2. 优缺点

  1. 优点:
    由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
    提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  2. 缺点:
    由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
    滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

3. 实现的几种方式

3.1 饿汉式

类一加载就完成初始化

public class HungerSingleton {
   public HungerSingleton(){

   }
   private static final HungerSingleton singleton=new HungerSingleton();

   public static HungerSingleton getInstance() {
      return singleton;
   }
   @Test
   public void test(){
      HungerSingleton hungerSingleton= HungerSingleton.getInstance();
      HungerSingleton hungerSingleton1= HungerSingleton.getInstance();
      System.out.println(hungerSingleton.equals(hungerSingleton1));// true
   }
}

缺点:太浪费内存空间了

3.2 懒汉式

public class LazySingleton {
	private LazySingleton(){
		System.out.println(Thread.currentThread().getName()+"ok");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	private static LazySingleton lazySingleton;

	public static LazySingleton getInstance(){
		if (lazySingleton == null) {
			lazySingleton = new LazySingleton();
		}
		return lazySingleton;
	}
	@Test
	public void test(){
		for(int i=0;i<10;i++){
			new Thread(()->{
				LazySingleton.getInstance();
			}).start();
		}
	}
}

懒汉式,在需要使用的时候才完成加载;但是这个是线程不安全的,多线程的情况下有问题

3.3 懒汉式加锁+双重检测

public class LazySingletonLock {
   private LazySingletonLock(){
      System.out.println(Thread.currentThread().getName()+"ok");
      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   private volatile static LazySingletonLock lazySingleton;
   // private static LazySingletonLock lazySingleton;
   public static LazySingletonLock getInstance(){
      if (lazySingleton == null) {
         synchronized (LazySingleton.class){
            if(lazySingleton==null){
               lazySingleton = new LazySingletonLock();
            }
         }
      }
      return lazySingleton;
   }
   @Test
   public void test(){
      for(int i=0;i<10;i++){
         new Thread(()->{
          LazySingletonLock.getInstance();
         }).start();
      }
   }
}

线程安全、延迟加载、加锁后效率较低

LazySingletonLock实例需要加锁volatile关键字,避免指令重排,否则还是有可能会出错

3.4 静态内部类实现

public class InnerclassSingleton {
   private InnerclassSingleton() { }

   private static class InnerclassSingletonInstance {
      private static final InnerclassSingleton INSTANCE = new InnerclassSingleton();
   }

   public static InnerclassSingleton getInstance() {
      return InnerclassSingletonInstance.INSTANCE;
   }
}

使用类装载的机制来保证初始化实例时只有一个线程。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
只有在使用的时候(getInstance())才会实例化。

9.5 反射破坏单例模式

public class LazySingletonLock {
   private LazySingletonLock(){
      System.out.println(Thread.currentThread().getName()+"ok");
      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   private static LazySingletonLock lazySingleton;
   public static LazySingletonLock getInstance(){
      if (lazySingleton == null) {
         synchronized (LazySingleton.class){
            if(lazySingleton==null){
               lazySingleton = new LazySingletonLock();
            }
         }
      }
      return lazySingleton;
   }
   @Test
   public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
      LazySingletonLock instance = LazySingletonLock.getInstance();
      Constructor<LazySingletonLock> declaredConstructor = LazySingletonLock.class.getDeclaredConstructor();
      declaredConstructor.setAccessible(true);//  可以防伪私有变量
      LazySingletonLock instance1 = declaredConstructor.newInstance();
      System.out.println(instance);
      System.out.println(instance1);
   }
}

私有变量是不安全的,可以通过反射来破坏单例性

9.6 避免反射破坏

public class LazySingletonLock {
   private LazySingletonLock(){
      synchronized (LazySingleton.class){
         if(lazySingleton!=null){
            throw new RuntimeException("不要用反射来破环单例");
         }
      }
      System.out.println(Thread.currentThread().getName()+"ok");
      try {
         TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   private static LazySingletonLock lazySingleton;
   public static LazySingletonLock getInstance(){
      if (lazySingleton == null) {
         synchronized (LazySingleton.class){
            if(lazySingleton==null){
               lazySingleton = new LazySingletonLock();
            }
         }
      }
      return lazySingleton;
   }
   @Test
   public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
      LazySingletonLock instance = LazySingletonLock.getInstance();
      Constructor<LazySingletonLock> declaredConstructor = LazySingletonLock.class.getDeclaredConstructor();
      declaredConstructor.setAccessible(true);//  可以防伪私有变量
      LazySingletonLock instance1 = declaredConstructor.newInstance();
      System.out.println(instance);
      System.out.println(instance1);
     
     // 通过反射创建两个实例,还是会破坏单例
      LazySingletonLock instance3 = declaredConstructor.newInstance();
		LazySingletonLock instance4 = declaredConstructor.newInstance();
		System.out.println(instance);
		System.out.println(instance1); // 两个实例是不同的
   }
}

还是不能避免反射的破话,如果直接从反射获取两个实例,还是会出问题,这时候可以通过一个标志位来判断是否创建了实例;但是这个标识位通过反编译还是可以获取到。

9.7 通过枚举来实现单例

枚举是不能通过反射来创建的。

enum SingletonEnum {
	INSTANCE("name","ljh"),
	;
	private  String key;
	private  String value;
	SingletonEnum(String key,String value){
		this.key=key;
		this.value=value;
	}
	// 声明单例对象
	private SingletonEnum getInstance() {
		return INSTANCE;
	}
}
public class EnumSingleton {
   // 枚举类型是线程安全的,并且只会装载一次
   @Test
   public void test() throws Exception {
      SingletonEnum instance = SingletonEnum.INSTANCE;
      SingletonEnum instance1 = SingletonEnum.INSTANCE;
      System.out.println(instance);
      System.out.println(instance1);
      //  尝试反射破解
      // 查看输出的class文件发现是无参数构造器, java.lang.NoSuchMethodException:
      // 不是期望的异常,用命令行javap反编译这个类发现还是无参数构造器
      // 用jad工具反编译,反现有两个参数string,int; ava.lang.IllegalArgumentException: Cannot reflectively create enum objects
      // 得到想要的异常。
      Constructor<SingletonEnum> declaredConstructor = SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
      declaredConstructor.setAccessible(true);
      SingletonEnum singletonEnum = declaredConstructor.newInstance();
      SingletonEnum singletonEnum1 = declaredConstructor.newInstance();
      System.out.println(singletonEnum);
      System.out.println(singletonEnum1);
   }
}
  1. 为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
    volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton(); 这个操作不是原子的,所以需要加锁。 创建实例的三步操作:
  • 为 singleton 分配内存空间
  • 初始化 singleton
  • 将 singleton 指向分配的内存地址

因为JVM会进行指令重排,所以这三步的顺序可能会被打乱。

  1. 第一次检查singleton 为空后为什么内部还进行第二次检查?
    A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次

目录