单例模式学习和了解使用场景。
1.什么是单例模式?
确保一个类只能创建一个实例。
2.实现思路是什么?
不能让外界创建对象,所以构造器要私有化。
提供获得单例对象的方法。(所以这个方法是公开的,并且这个方法里New出了对象)
3.实例:
(1)根据上面的思路我们来写一个类,让其实现单例模式
public class Singleton{ private Singleton(){} private static Singleton instance =new Singleton(); //这里用static修饰 public static Singleton getInstance(){ return instance; } }
类加载时就创建对象,不管用不用,对象已经创建好了。线程安全(初始化就把对象创建好了,不会有多个线程创建多个对象的情况)。这种也称之为饿汉模式。
(2).上面的单例模式在初始化时就创建对象,并且是线程安全的。但是也有缺点,那就是在初始化时就创建了很多实例,并且很多实例可能我们都用不到,这样会占用内存。浪费内存资源(内存是很宝贵的资源)。我们可以考虑在需要这个实例时我们才去创建对象。
public class Singleton{ private Singleton(){} private static Singleton instance = null ; public static Singleton getInstance(){ //如果还没有这个实例,我们就去创建这个实例。 if(instance==null){ instance=new Singleton(); } return instance; } }
这种创建单例的方式叫懒汉模式(懒加载):用到的时候才创建。
(3).显然上面的第二个方法创建单例是有问题的。想一下,当两个线程同时走到 if(instance==null)这里时,那么就有可能创建了两个实例(其实还可能有更多线程同时走到这里)。那就不是单例的了。怎么办?这时我们可以考虑加锁。我们在获取对象这个方法上加锁,这样就不会有两个线程同时走到if(instance==null)这里了。
public class Singleton{ private Singleton(){} private static Singleton instance = null ; //对方法加锁, public static synchronized Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
(4).好像问题已经解决了,但是对方法加锁会有一个问题,那就是我们每次获取实例都需要同步(当已经有单例对象,别人再去获取这个单例时,同样需要等待锁资源),这样会降低程序的性能。其实我们只是想在创建对象的时候保证只有一个实例,那么我们可以把锁的范围缩小,在创建对象那里加锁。
public class Singleton{ private Singleton(){} private static Singleton instance =null ; public static Singleton getInstance(){ if(instance==null){ Synchronized(Singleton.class){ if(instance==null){ instance=new Singleton(); } } } return instance; } }
这样获取对象的时候,如果还没有被创建出来,只能有一个线程能创建,保证了单例。如果对象已经存在,那么我们获取对象时不用等待锁了。
想一想为什么锁里面会再次判断 if(instance==null) ?
当单例对象不存在,并且两个线程同时走到 synchronized(Singleton.class),这时第一个线程先拿到锁,第二线程会等待锁资源,当第一线程创建了对象并释放了锁资源时,第二个线程会拿到锁资源,如果没有再次判断,那么第二个线程会再次创建对象。所以必须用 if(instance==null) 再次判断。这种方式也叫双重检验锁
(5).通过注册表方式:
首先将需要单例的实例通过唯一键注册到注册表,然后通过键来获取单例
Public class RegisterSingleton{ static private HashMap registry=new HashMap(); //静态块,在类被加载时自动执行 Static{ RegisterSingleton rs=new RegisterSingleton(); registry.put(rs.getClass().getName(),rs); } //受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点 Protected RegSingleton(){} //静态工厂方法,返回此类的唯一实例 public static RegSingleton getInstance(String name){ if(name==null){ name=” RegSingleton”; } if(registry.get(name)==null){ registry.put(name,Class.forName(name).newInstance()); } Return (RegSingleton)registry.get(name); } }
上面的单例注册表的方式,其实就是把实例放到了一个map中,key是对象的名字,value就是这个对象。当我们获取这个对象时,根据对象的名字就可以从map获取这个对象了。如果没有这个对象,我们就先把这个对象放到map中。
问题1:为什么把对象放到map中就可以实现单例?
map中的key是唯一的,当我们根据key获取value时,获取的对象肯定是唯一的
问题2:为什么把单例放到map中?
其实这是用map这种数据结构存放我们实例化了的对象。我们知道new出来的对象都是存在堆栈中的。通过这种方式,我们把单例对象放到了map中,创建对象变成了往map中存数据,获取对象变成了从map中取数据。我们经常说的spring中的Ioc容器其实就是一个map。
4.应用
我们知道Spring里面是有单例的,如:<bean id="date" class="java.util.Date" singleton="true"/> 那么spring 里的单例是如何实现的呢?
本注册表实现了Spring接口“SingletonBeanRegistry”,该接口定义了操作共享的单例对象,Spring容器实现将实现此接口;
public class SingletonBeanRegister implements SingletonBeanRegistry { //单例Bean缓存池,此处不考虑并发 private final Map<String, Object> BEANS = new HashMap<String, Object>(); public boolean containsSingleton(String beanName) { return BEANS.containsKey(beanName); } public Object getSingleton(String beanName) { return BEANS.get(beanName); } @Override public int getSingletonCount() { return BEANS.size(); } @Override public String[] getSingletonNames() { return BEANS.keySet().toArray(new String[0]); } @Override public void registerSingleton(String beanName, Object bean) { if(BEANS.containsKey(beanName)) { throw new RuntimeException("[" + beanName + "] 已存在"); } BEANS.put(beanName, bean); } }
5.总结:
单例模式由于构造器是私有化的,所以不能被继承