单例模式

30 阅读7分钟

单例模式

单例模式就是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

1.饿汉式(静态常量)

  1. 构造器私有化(防止new)
  2. 类的内部创建对象
  3. 向外暴露一个静态的公共方法(getInstance())
public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {

    }

    /**
     * 2.本类内部创建对象实例
     */
    private final static Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载的时候就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到Lazy Loading的效果,可能会造成内存的浪费

2.饿汉式(静态代码块)

这种方式和上一个方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点与上一个方式相同,同样可能造成内存浪费。

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    /**
     * 1.构造器私有化,外部不能new
     */
    private Singleton() {

    }

    private static Singleton INSTANCE;

    static {
        INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

3.懒汉式(线程不安全)

懒汉式的方式的确起到了Lazy Loading的效果,但是只能在单线程下使用;如果在多线程下,多个线程操作同一个临界资源,这时便会产生多个实例,所以在多线程环境下不能使用这种方式。在实际开发中,不要使用这种方式。

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

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

4.懒汉式(线程安全,同步方法)

这种方式的确解决了线程不安全问题,但是带来了效率问题,每个线程在想要获取类的实例时,执行getInstance方法都要进行同步。
其实这个方法只执行一次实例化代码就够了,后面的线程想要获取该类的实例,直接return就行了,方法进行同步效率太低了,在实际开发中,不推荐使用这种方式

public class SingletonTest01 {

    public static void main(String[] args) {
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

5.懒汉式(线程安全,同步代码块)

这种方式本意是想对上一种方式进行改进,但是这种同步并不能起到线程同步的作用,可能在多线程下产生多个实例,因此在实际开发中,不能使用这种方式。

public class SingletonTest01 {

    public static void main(String[] args) {
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    private static Singleton instance;

    private Singleton() {

    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}

6.双重检查(Double-Check)

解决了线程安全问题,同时解决了懒加载问题,同时保证效率。在实际开发中强烈推荐使用。

volatile的作用是防止在创建实例的时候,还没初始化完,有其他线程进来,此时实例已经不为空,多线程拿到的实例数据就不一样,加了该关键字后,先初始化完成后再赋值给实例。

volatile相对于synchronized来说是一个轻量级的锁,只能修饰变量。

通俗点解释就是: 多线程环境下一个线程修改了volatile修饰的变量时,别的线程能立马就读到这个新值。

public class SingletonTest01 {

    public static void main(String[] args) {
        /*HashMap<String, Integer> hm = new HashMap<>();
        Runnable task = () -> {
            Singleton s = Singleton.getInstance();
            String sCode = s.hashCode() + "";
            hm.merge(sCode, 1, (a, b) -> a + b);
        };
        // 模拟多线程环境下使用 Singleton 类获得对象
        for (int i = 0; i < 1000; i++) {
            new Thread(task).start();
        }

        //输出结果
        for (Map.Entry<String, Integer> entry : hm.entrySet()) {
            System.out.println(entry.getKey() + "\t" + entry.getValue());
        }*/
        Singleton singleton01 = Singleton.getInstance();
        Singleton singleton02 = Singleton.getInstance();
        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);
        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }
}

class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

7.静态内部类

这种方式采用类装载的机制来保证初始化实例时只有一个线程

  1. 当类被装载的时候,其静态内部类不会被装载;
  2. 当调用类的静态方法时,其静态内部类会被装载;
  3. 当类被装载的时候,线程是安全;

类的静态属性只会在第一次加载类的时候初始化,所以JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程无法进入。

静态内部类也是非常好地实现单例模式的方式

public class StaticInnerSingleton {
    private StaticInnerSingleton() {
    }

    private static class Holder {
        static StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance() {
        return Holder.instance;
    }
}

7.1 通过反射获取破坏静态内部类的单例模式

public class StaticInnerReflectTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = StaticInnerSingleton.class;
            Constructor<?> c = clazz.getDeclaredConstructor((Class<?>[]) null);
            c.setAccessible(true);

            Object o1 = c.newInstance();

            StaticInnerSingleton o2 = StaticInnerSingleton.getInstance();

            System.out.println(o1 == o2 ? "相等" : "不等");
        } catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

防止这种情况的发生, 可以在类的构造方法中判断实例是否为空, 如果不为空抛出异常, 这样反射就无法执行

7.2 通过序列化破坏静态内部类的单例模式

8.枚举

借助枚举方式实现单例模式,不仅能避免多线程同步问题,而且能防止反序列化重新创建新的对象;

Josh Bloch 推荐使用的方式

public class SingletonTest {
    public static void main(String[] args) {
        Singleton s1 = Singleton.INSTANCE;
        Singleton s2 = Singleton.INSTANCE;

        System.out.println(s1 == s2);

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

        s1.sayHello();
    }
}

enum Singleton {
    /**
     * 实例
     */
    INSTANCE;

    public void sayHello() {
        System.out.println("hello");
    }
}

9.单例注册表(Spring的单例模式实现)

解决单例类不能继承的痛点

package com.hoki.singleton;

import java.util.HashMap;  
public class RegSingleton {

    public static void main(String[] args) {
        RegSingleton singleton01 = RegSingleton.getInstance(null);
        RegSingleton singleton02 = RegSingleton.getInstance(null);

        //两个实例的地址相同,表示它们是同一对象实例
        System.out.println(singleton01 == singleton02);

        //两个实例的hashcode也相同,也表示它们是同一对象实例
        System.out.println(singleton01.hashCode());
        System.out.println(singleton02.hashCode());
    }

    static private HashMap<String, Object> registry = new HashMap<>();

    //静态块,在类被加载时自动执行
    static {
        RegSingleton rs = new RegSingleton();
        registry.put(rs.getClass().getName(), rs);
    }

    /**
     * 受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
     */
    protected RegSingleton() {
    }

    /**
     * 静态工厂方法,返回此类的唯一实例
     *
     * @param name
     * @return
     */
    public static RegSingleton getInstance(String name) {
        if (name == null) {
            name = "com.hoki.singleton.RegSingleton";
        }
        if (registry.get(name) == null) {
            try {
                registry.put(name, Class.forName(name).newInstance());
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return (RegSingleton) registry.get(name);
    }
}    

Spring框架源码实现如下:

public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{  
       /** 
        * 充当了Bean实例的缓存,实现方式和单例注册表相同 
        */  
       private final Map singletonCache=new HashMap();  
       public Object getBean(String name)throws BeansException{  
           return getBean(name,null,null);  
       }  
    ...  
       public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{  
          //对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)  
          String beanName=transformedBeanName(name);  
          Object bean=null;  
          //手工检测单例注册表  
          Object sharedInstance=null;  
          //使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高  
          synchronized(this.singletonCache){  
             sharedInstance=this.singletonCache.get(beanName);  
           }  
          if(sharedInstance!=null){  
             ...  
             //返回合适的缓存Bean实例  
             bean=getObjectForSharedInstance(name,sharedInstance);  
          }else{  
            ...  
            //取得Bean的定义  
            RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);  
             ...  
            //根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关  
            //<bean id="date" class="java.util.Date" scope="singleton"/>  
            //如果是单例,做如下处理  
            if(mergedBeanDefinition.isSingleton()){  
               synchronized(this.singletonCache){  
                //再次检测单例注册表  
                 sharedInstance=this.singletonCache.get(beanName);  
                 if(sharedInstance==null){  
                    ...  
                   try {  
                      //真正创建Bean实例  
                      sharedInstance=createBean(beanName,mergedBeanDefinition,args);  
                      //向单例注册表注册Bean实例  
                       addSingleton(beanName,sharedInstance);  
                   }catch (Exception ex) {  
                      ...  
                   }finally{  
                      ...  
                  }  
                 }  
               }  
              bean=getObjectForSharedInstance(name,sharedInstance);  
            }  
           //如果是非单例,即prototpye,每次都要新创建一个Bean实例  
           //<bean id="date" class="java.util.Date" scope="prototype"/>  
           else{  
              bean=createBean(beanName,mergedBeanDefinition,args);  
           }  
    }  
    ...  
       return bean;  
    }  
}

10.JDK中Runtime的实现

这里使用的就是饿汉式,因为我们肯定会用到它,所以在内存不浪费的前提下,使用饿汉式可以保证线程安全。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    //剩下的代码省略    
}

11.单例模式的使用场景

需要频繁地进行对象的创建和销毁,创建对象时耗时过多或耗费资源过多(即重量级对象),但又经常用到的对象如: 工具类对象,频繁访问数据库或文件的对象(数据源、session工厂等)。