跳至主要內容

单例模式

知识库结构与算法设计模式设计模式大约 10 分钟

1、简介

保证一个类只有一个实例,并提供一个全局访问点。当一个对象在整个系统中都可以用到时,单例模式就比较有用了。客户端不在考虑是否要实例化的问题,而把责任都交给应该负责的类去处理。他属性创建型设计模式。

单例模式旨在创建一个类的实例,创建一个类的实例我们用全局静态变量或者约定也能办到单例的作用。

1.1单例如何形成的

平常创建一个对象需要new对象,假如有一个对象ObjectClass我们实例化它。
new ObjectClass()
如果另外一个类要使用ObjectClass则可以再通过new来创建另外一个实例化,如果这个类是public 则我们可以在使用的时候多次实例化对象。

那我们怎么保证类不被其他类实例化,利用private关键字我们可以采用私有构造函数来阻止外部实例化该类。

public class ObjectClass
{
   private ObjectClass()
    {
    }
}

这样一来我们无法实例化ObjectClass则我们就无法使用它。那我们要怎么实例化呢?

由于私有构造方法我们只能在内部访问,所以我们可以用一个内部方法实例化ObjectClass,为了外部能够访问这个方法我们将这个方法设置成static。

这样做了之后确保返回对象始终是第一次创建的对象,我们用一个私有静态对象来存储实例化的对象,如果对象没创建我们则立即创建,如果已经创建就返回已经创建的对象。

public class ObjectClass
{
    private static ObjectClass singleton;
    private ObjectClass()
    {
    }

    public static ObjectClass GetSingletone()
    {
        if (singleton == null)
        {
            singleton = new ObjectClass();
        }
        return singleton;
    }
}
  • 单例模式:确保一个类只有一个实例,并提供一个全局访问点。

1.2 多线程导致单例模式问题

启用多线程测试单例返回对象

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; i++)
        {
            TestSingleton();
        }
        Console.ReadKey();
    }

    public static void TestSingleton()
    {
        Task.Factory.StartNew(new Action(() =>
        {
            var hc = ObjectClass.GetSingletone().GetHashCode();
            Console.WriteLine(hc);
        }));
    }
}

启了10个线程获得单例对象然后打印对象的HashCode。测试发现有HashCode不一致的情况,证明单例返回的对象并不是只有一个。

因为多线程运行的时候可能会同时进行if (singleton == null)的判断,如果此时singleton变量还没被实例化则可能有多个线程进入到实例化代码,以至于返回的实例化对象不是同一个。

1.3 解决多线程单例问题

由于多线程导致if检查变量问题,则争对检查问题我们可以有两类解决办法:

  1. "急切"创建实例,不用延迟实例化做法

急切实例化就是在静态初始化器中创建对象,这样就保证了程序运行阶段单例对象已经创建好,去除if判断。

public class ObjectClass
{
    private static ObjectClass singleton=new ObjectClass();
    private ObjectClass()
    {
    }

    public static  ObjectClass GetSingletone()
    {
        return singleton;
    }
}
  1. 加锁
    为了让创建对象只能有一个线程操作,则我们对创建对象代码进行加锁处理,再次改造GetSingletone方法。
public class ObjectClass
{
    private static ObjectClass singleton = new ObjectClass();
    private static object lockObj = new object();
    private ObjectClass()
    {
    }

    public static ObjectClass GetSingletone()
    {
        lock (lockObj)
        {
            if (singleton == null)
            {
                singleton = new ObjectClass();
            }
        }
        return singleton;
    }
}

加锁对性能有一定的损耗,如果你的系统对性能要求比较高,我们对于加锁的处理还有一种优化方式:双重检查加锁

public static ObjectClass GetSingletone()
   {
       if (singleton == null)
       {
           lock (lockObj)
           {
               if (singleton == null)
               {
                   singleton = new ObjectClass();
               }
           }
       }
       return singleton;
   }

使用双重检查加锁,则多线程在运行的时候如果已经创建了单例对象后就不会再进入到lock代码段以此减少锁带来的性能损耗。

然后我们再来测试一波,启用50个线程,可以看到输出的HashCode是一致的。

2、适用场景

  1. 确保在任何情况下都只要一个实例
  2. 想要可以简单的访问实例类
  3. 让类自己控制它的实例化
  4. 希望可以限制类的实例数
  5. 要求生成唯一序列化的环境
  6. 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据中,使用单例模式保持计数器的值,并确保是线程安全的。
  7. 需要定义大量的静态常量和静态方法(如工具类) 的环境,可以采用单例模式。(当然,也可以直接声明为static的方式)。

,如果出现多个可能导致程序的行为异常,资源使用过度,或者不一致的情况。

2.1 通用代码-线程安全

public class Singleton(){
    private static final Singleton singleton = new Singleton();
    //限制产生多个对象
    private Singletion(){
        
    }
    
    //通过该方法获取实例对象
    public static Singleton getSingleton(){
        return singleton;
    }
    
    //类中其他方法,尽量是static的
    public static void doSomething(){
        
    }
}

3、优点

1、只有一个实例,减少内存开销
2、对资源没有多重占用
3、设置全局访问点,严格控制访问

4、缺点

没有接口,扩展困难

5、存在问题

1、如果存在多个类加载器,那么就会有多个实例,解决:自行指定类加载器,并且是相同的加载器。
2、1.2之前垃圾收集器有个bug,会把单例对象回收,1.2之后这个bug已经解决了。
3、不适合作为父类。

6、结合其他模式

1、抽象工厂模式,建造者模式,原型模式,享元模式都可以使用单例模式
2、Facade对象都是一个实例,因为只需要一个Facade对象
3、状态对象通常也只需要一个实例

7、重要条件

1、
2、
3、

8、示例代码

8.1 懒加载单例

public class LazySingleton {
   private static LazySingleton lazySingleton;
   private LazySingleton(){
   }
   public static LazySingleton getInstance(){
       if(null == lazySingleton){
           lazySingleton = new LazySingleton();
       }
       return lazySingleton;
   }
}
public class SingletonTest {
    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

该模式在单线程下是没有问题的,但是在多线程的情况下,就不能保证只创建一个实例了。
我们来模拟多线程debug,看看输出的实例。

public class MyRunnable implements Runnable {
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
    }
}

8.2 懒加载多线程解决

public class LazyThreadSingleton {
    private static LazyThreadSingleton lazyThreadSingleton;
    private LazyThreadSingleton(){}
    public synchronized static LazyThreadSingleton getInstance(){
        if(null == lazyThreadSingleton){
            lazyThreadSingleton = new LazyThreadSingleton();
        }
        return lazyThreadSingleton;
    }
}


//在方法里面加synchronized,来控制多线程问题。debug查看只有一个线程可以进入getInstance()方法
//线程1执行完之后,线程2就可以执行了
//这种方式可以解决多线程的问题,但是对性能有很大的影响,synchronized是对整个类进行加锁。

//懒加载双重检查锁
//相比在方法中添加synchronized,双重检查锁的好处是:不用每次调用方法都需要加锁,只有在实例没有被创建的时候才会加锁处理。第2个的null判断是并发的标准判断:1锁2查3判断。这样才能保证第二个线程在进来之后不会在创建实例,因为已经创建了实例了。
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton;
    private LazyDoubleCheckSingleton() {
    }
    public static LazyDoubleCheckSingleton getInstance() {
        if (null == lazyDoubleCheckSingleton) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (null == lazyDoubleCheckSingleton) {
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

使用双重检查锁的性能要比之前在方法上加锁要好,但是也会有问题,会出现指令重排序问题。

此时线程2判断为null的时候,发现不为null,就会执行第4步,这样就出现问题了。
在变量中增加volatile来修饰,可以防止指令重排序。

8.4 静态内部类

为了解决指令重排序,可以使用静态内部类,让指令重排序对其他线程不可见,如:

代码示例:

public class StaticInnerSingleton {
    private StaticInnerSingleton(){
    }
    private static class InnerClass {
        private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }
    public static StaticInnerSingleton getInstance(){
        return InnerClass.staticInnerSingleton;
    }
}

8.5 饿汉式

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

因为类在加载的时候就被创建了,所以叫饿汉式。延迟加载是在类使用的时候才被创建,所以叫懒汉式。

8.6 序列化反序列化破坏单例

public class HungrySerializableSingleton implements Serializable {
    private static HungrySerializableSingleton hungrySingleton = new HungrySerializableSingleton();
    private HungrySerializableSingleton(){
    }
    public static HungrySerializableSingleton getInstance(){
        return hungrySingleton;
    }
}
 public static void main(String[] args) throws Exception {
        HungrySerializableSingleton instance = HungrySerializableSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("instance"));
        oos.writeObject(instance);
        File file = new File("instance");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySerializableSingleton newInstance = (HungrySerializableSingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

输出结果:

com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@3b9a45b3

false
发现经过反序列化之后,不是同一个类了。
在HungrySerializableSingleton中加入下面代码

private Object readResolve(){
        return hungrySingleton;
    }

输出结果:

com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
true

反序列出来的是同一个类,原因分析:
ObjectInputStream类中,如下代码:

if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}

hasReadResolveMethod该方法会先判断是否存在readResolve()方法,如果存在,那么会通过反射去调用类里面的readResolve()方法,反射调用主要是通过invokeReadResolve方法。

8.7 反射防御

public static void main(String[] args) throws Exception {
        Class objectClass = HungrySingleton.class;
        Constructor declaredConstructors = objectClass.getDeclaredConstructor();
        declaredConstructors.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) declaredConstructors.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

输出结果:

com.cimu.creational.singleton.HungrySingleton@1540e19d
com.cimu.creational.singleton.HungrySingleton@677327b6
false

通过反射创建出来两个实例,那么如何来防御呢?

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
        if(null != hungrySingleton){
            throw new RuntimeException("反射攻击");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

在私有构造函数中判断变量是否为空,如果不为空就抛出异常。但是在懒加载中是不起效果的。

8.8 枚举单例

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

可以看到如果是枚举的话,会根据name获取枚举的对象。可以通过jad反编译来查看枚举类,使用枚举来创建单例是比较推荐的做法。

8.9 容器单例

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> singletonMap = new HashMap<String, Object>();
    public static void putInstance(String key,Object object){
        if(null != key && !"".equals(key) && null != object){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,object);
            }
        }
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}
public static void main(String[] args) {     
ContainerSingleton.putInstance("object",EnumSingleton.getInstance());
 System.out.println(ContainerSingleton.getInstance("object"));
    }

可以通过该方法来存储一堆单例对象,但是存在反射和序列化的问题,可以使用ConcurrentHashMap来控制并发问题。

8.10 克隆破坏

第一不要实现Cloneable接口
第二,如果实现了,那么clone方法需要写成如下:

 @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

 @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

9、源码分析

9.1 jdk中应用

java.langRuntime类中也使用了单例,如代码:

private static Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
        return currentRuntime;
    }

9.2 MyBatis中应用

ErrorContext类使用了单例,这边使用了ThreadLocal<ErrorContext>的单例模式。

9.3 spring中应用

AbstractBeanFactory使用了单例,

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);

部分代码.......

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
  Object singletonObject = this.singletonObjects.get(beanName);
  if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
   synchronized (this.singletonObjects) {
    singletonObject = this.earlySingletonObjects.get(beanName);
    if (singletonObject == null && allowEarlyReference) {
     ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
     if (singletonFactory != null) {
      singletonObject = singletonFactory.getObject();
      this.earlySingletonObjects.put(beanName, singletonObject);
      this.singletonFactories.remove(beanName);
     }
    }
   }
  }
  return (singletonObject != NULL_OBJECT ? singletonObject : null);
 }

是把bean放到ConcurrentHashMap对象中。