设计模式之单例模式

单例模式


单例模式在面试中比较常见的问题,大佬写的比较好,分享给大家单例模式

1.什么是单例模式?

单例模式是一种常用的软件设计模式,它定义是单例对象的类只能允许一个实例存在。确保一个类只有一个实例,并提供该实例的全局访问点。

该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。
单例模式的特点:

  1. 类构造器私有
  2. 持有自己类的引用 (自己创建自己唯一的实例)
  3. 对外提供获取实例的静态方法

类图:
设计模式之单例模式
举一个简单单例模式的例子

public class SimpleSingleton {
    //自己持有自己的引用,自己创建自己唯一的示例
    private static final SimpleSingleton INSTANCE=new SimpleSingleton();
    //私有构造函数
    private SimpleSingleton(){

    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance(){
        return INSTANCE;
    }
}

做一个小测试,通过getInstance()获取两次示例,输出他们哈希值,看看结果是什么?

public class SimpleSingletonTest {
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

输出结果:

1554874502
1554874502

两次得到实例的哈希值相同,说明两个实例是同一个实例。

2.饿汉模式和懒汉模式

饿汉模式懒汉模式是实现单例模式常用的两种方式。

2.1.饿汉模式

实例在初始化的时候就已经建好了,之后就不会在实例化不管你有没有用到,先建好了再说。具体代码如下:

public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的构造方法
    private SimpleSingleton() {
    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

还有另一种变种方式

public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE;
	//静态代码块实例化
	static{
		INSTANCE=new SimpleSingleton();
	}
    //私有的构造方法
    private SimpleSingleton() {
    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

由于饿汉模式只是最开始初始化的时候实例化,之后不会再被实例化,所有饿汉模式是线程安全的,但是也带来了缺点,一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

2.2懒汉模式

顾名思义就是实例在用到的时候才去创建,需要用的时候才去检查有没有实例,如果有则返回,没有则新建。具体代码如下:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

懒汉模式虽然解决了资源浪费的问题,但是它缺带来了线程安全问题。
例如:有两个线程,他们同时调用getInstance()方法,同时走到if (INSTANCE == null),同时判断INSTANCE == null成立, INSTANCE会被实例化两次。这样就违背了单例模式的定义了。
解决办法:
利用synchronized关键字修饰共有的静态方法getInstance(),在getInstance方法上加synchronized关键字,对该方法加锁,保证在并发(多线程)的情况下,只有一个线程进入该方法创建INSTANCE对象的实例。
代码:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static synchronized SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

但是利用synchronized关键字修饰公有的静态方法getInstance()会降低getInstance()方法的性能,因为,如果一个线程进入getInstance()方法后,其他的线程必须等待。举个例子:如果INSTANCE已经被实例化了,当一个在线程进入了getInstance()方法,虽然此时INSTANCE!=null,其他线程也需要等待。

3.双重检查锁

3.1.如何实现?

加锁操作只需要对实例化那部分的代码进行,只有当 INSTANCE 没有被实例化时,才需要进行加锁。双重校验锁先判断 INSTANCE 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁,加锁时候在检查一次INSTANCE 是否为空
代码:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

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

在加锁之前判断是否为空,可以确保INSTANCE不为空的情况下,不用加锁,可以直接返回。
为什么在加锁之后,还需要判断INSTANCE是否为空呢?

答:是为了防止在多线程并发的情况下,实例化多个对象。

**比如:线程a和线程b同时调用getInstance方法,假如同时判断INSTANCE都为空,这时会同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次INSTANCE是否为空,则可能会重复创建实例。所以需要在synchronized前后两次判断。

3.2.volatile关键字

我们写好的程序是这样的。

public static SimpleSingleton4 getInstance() {
      if (INSTANCE == null) {//1
          synchronized (SimpleSingleton4.class) {//2
              if (INSTANCE == null) {//3
                  INSTANCE = new SimpleSingleton4();//4
              }
          }
      }
      return INSTANCE;//5
  }

注意第4处, INSTANCE = new SimpleSingleton4(),这条语句不具备原子性,new关键字在创建一个对象的示例时分为三步:

  1. 分配内存
  2. 调用构造函数,初始化
  3. 将对象引用赋值给变量(将对象的地址分配给INSTANCE
    在JVM中,java 虚拟机会对上述指令优化重排,可能改变部分指令的顺序,
    设计模式之单例模式

上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2刚好进入,由于此时对象已经不为 Null,所以线程 2 可以*访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

volatile关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止JVM指令重排。

4.静态内部类

4.1.静态内部类如何实现单例

具体代码如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

我们看到在SimpleSingleton5类中定义了一个静态的内部类Inner。在SimpleSingleton5类的getInstance方法中,返回的是内部类Inner的实例INSTANCE对象。
只有在程序第一次调用getInstance方法时,虚拟机才加载Inner并实例化INSTANCE对象。
java内部机制保证了,只有一个线程可以获得对象锁,其他的线程必须等待,保证对象的唯一性。

4.2.反射漏洞

代码:

    @Test
    public void test1(){
        Class<SimpleSingleton4> simpleSingleton4Class=SimpleSingleton4.class;
        try {
            Constructor<SimpleSingleton4> declaredConstructor = simpleSingleton4Class.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            SimpleSingleton4 newInstance = declaredConstructor.newInstance();
            System.out.println("newInstance == SimpleSingleton4.getInstance():"+(newInstance == SimpleSingleton4.getInstance()));
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

输出:

newInstance == SimpleSingleton4.getInstance():false

由此看出,通过反射创建的对象,跟通过getInstance方法获取的对象,并非同一个对象,也就是说,这个漏洞会导致SimpleSingleton4非单例。
那么,要如何防止这个漏洞呢?
这就需要在无参构造方式中判断,如果非空,则抛出异常了。

  private SimpleSingleton4(){
        if(Inner.INSTANCE != null) {
            throw new RuntimeException("不能支持重复实例化");
        }

    }

4.3.序列化漏洞

众所周知,java中的类通过实现Serializable接口,可以实现序列化。
我们可以把类的对象先保存到内存,或者某个文件当中。后面在某个时刻,再恢复成原始对象。
但是在反序列化的时候会创建新的实例,打破了单例模式对象唯一的要求。

    @Test
    public void test2() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(SimpleSingleton4.getInstance());
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        SimpleSingleton4 newInstance = (SimpleSingleton4) ois.readObject();
        //判断是否是同一个对象
        System.out.println("newInstance == SimpleSingleton4.getInstance():"+(newInstance==SimpleSingleton4.getInstance()));
    }

输出:

newInstance == SimpleSingleton4.getInstance():false

那么,如何解决这个问题呢?

答:重新readResolve方法。

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

再一次运行结果:

newInstance == SimpleSingleton4.getInstance():true

程序在反序列化获取对象时,会去寻找readResolve()方法。

  1. 如果该方法不存在,则直接返回新对象。
  2. 如果该方法存在,则按该方法的内容返回对象。
  3. 如果我们之前没有实例化单例对象,则会返回null。

5.枚举

其实在java中枚举就是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。
在枚举对象唯一性的这个特性,还能创建其他的单例对象,例如:

package job.designpattern;
public enum SimpleSingleton5 {
    INSTANCE;

    private Student instance;

    SimpleSingleton5(){
        instance=new Student();
    }

    public Student getInstance(){
        return instance;
    }

}
class Student{
}

jvm保证了枚举是天然的单例,并且不存在线程安全问题,此外,还支持序列化

6.多例模式

单例模式,只会产生一个实例。但它其实还有一个变种。
多例模式,顾名思义,它允许创建多个实例。但它的初衷是为了控制实例的个数,其他的跟单例模式差不多。
具体实现代码如下:

public class SimpleMultiPattern {
    //持有自己类的引用
    private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
    private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();

    //私有的构造方法
    private SimpleMultiPattern() {
    }
    //对外提供获取实例的静态方法
    public static SimpleMultiPattern getInstance(int type) {
        if(type == 1) {
          return INSTANCE1;
        }
        return INSTANCE2;
    }
}

有些朋友可能会说:既然多例模式也是为了控制实例数量,那我们常见的池技术,比如:数据库连接池,是不是通过多例模式实现的?
答:不,它是通过享元模式实现的。
那么,多例模式和享元模式有什么区别?
多例模式:跟单例模式一样,纯粹是为了控制实例数量,使用这种模式的类,通常是作为程序某个模块的入口。
享元模式:它的侧重点是对象之间的衔接。它把动态的、会变化的状态剥离出来,共享不变的东西。

7.应用场景

7.1 .Runtime

jdk提供了Runtime类,我们可以通过这个类获取系统的运行状态。

7.2.LogFactory

mybatis提供LogFactory类是为了创建日志对象,根据引入的jar包,决定使用哪种方式打印日志.

7.3. spring的单例

以前在spring中要定义一个bean,需要在xml文件中做如下配置:

<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">

在bean标签上有个scope属性,我们可以通过指定该属性控制bean实例是单例的,还是多例的。如果值为singleton,代表是单例的。当然如果该参数不指定,默认也是单例的。如果值为prototype,则代表是多例的。

上一篇:01-Java内存模型


下一篇:单例模式 详解