单例模式
什么是单例模式
类的单例设计模式,就是采取一定的方法保证在整个软件系统中,某个类只能存在一个对象实例,并且这个类会提供一个获取对象实例的方法。
思路:如果让一个类在一个虚拟机里面只能产生一个对象,就必须将构造器的访问权限设置为private,这样在外部,就不能new这个对象了,只能在内部实例化这个对象。因为外部 不能获取,所以可以通过调用一个静态方法,返回类的内部创建的静态对象。
两大种实现方式
饿汉式
饿汉式实现举例
package com.designPattren;/*** 饿汉式单例模式*/
public class SingleHungry {private static final SingleHungry SINGLE_HUNGRY_INSTANCE = new SingleHungry();/*** 防止反射破坏单例*/private SingleHungry() {if (SINGLE_HUNGRY_INSTANCE != null) {throw new RuntimeException("不能重复创建单例");}System.out.println("singleton");}public static SingleHungry getInstance() {return SINGLE_HUNGRY_INSTANCE;}/*** 重写readResolve方法,防止反序列化破坏单例* @return*/public Object readResolve() {return SINGLE_HUNGRY_INSTANCE;}/*** 测试方法,懒汉还是饿汉式*/public static void otherMethod() {System.out.println("aaaaaa");}}
注意:如果构造方法不对单例对象是否实现进行判断,单例就可能被反射破坏;
readResolve方法的重写是防止反序列化破坏单例,重写这个方法之后反序列化就不返回字节数组了,返回这个单例对象。
使用这种方式创建单例对象,是不能方式Unsafe破坏单例的
枚举式饿汉:
/*** 枚举类创建单例*/
public enum SingletonHungryBuEnum {INSTANCE;SingletonHungryBuEnum() {System.out.println("singleton");}public SingletonHungryBuEnum getInstance() {return INSTANCE;}public void otherMethod() {System.out.println("aaaaa");}}
枚举饿汉式能天然防止反射、反序列化破坏单例
懒汉式
懒汉式不同于饿汉式,懒汉式单例只有调用这个单例对象的时候才创建对象。
public class Singleton3 implements Serializable {private Singleton3() {System.out.println("private Singleton3()");}private static Singleton3 INSTANCE = null;// Singleton3.classpublic static synchronized Singleton3 getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton3();return INSTANCE;}
//这里为了测试类初始化的时候,是否调用构造方法,如果调用了构造方法,也就意味着对象被初始化了public static void otherMethod() {System.out.println("otherMethod()");}}
这样锁粒度太大了。实际上需要同步的时候只是在首次创建单例对象的时候。所以可以改进一下
双检锁懒汉式
public class Singleton4 implements Serializable {private Singleton4() {System.out.println("private Singleton4()");}private static volatile Singleton4 INSTANCE = null; // 可见性,有序性public static Singleton4 getInstance() {if (INSTANCE == null) {synchronized (Singleton4.class) {if (INSTANCE == null) {INSTANCE = new Singleton4();}}}return INSTANCE;}public static void otherMethod() {System.out.println("otherMethod()");}
}
前文我们加锁是在整个getInstance方法上加锁,但是粒度太大了。我们只需要在创建新对象的时候加锁就可以了。但是如果只有第一层
INSTANCE == null
判断,那么可能会出现下面的问题:A线程占有资源B进程挂起,但是if判断B线程是可以进去的,A释放资源之后B又创建新对象了,为了避免这种问题,我们在同步代码快里面再加一个判断。双检机制解决了重复创建对象的问题。
但是这并不是完全安全的,在整个对象创建的过程,其实是分三步的,创建对象,调用构造方法,给静态变量赋值。创建对象毋庸置疑是第一步执行的,但是后面两步,它实际上先后执行的顺序是不确定的。JVM可能 先putstatic,在执行构造方法init对象。如果先putstatic,那么当前单例对象就不是空了,其他线程在这个时候如果来获取单例对象,就可能获取到未被构造方法初始化的对象。为了避免这种问题,在单例对象上加volatile关键字。保证单例对象的有序性。加关键字之后,执行构造方法这一步骤就会加上内存屏障,putstatic只能在构造方法init对象之后才能执行。避免了指令重排序。解决了上述的问题。
内部类懒汉式
内部类相较于上述实现方法,实现比较简单,利用静态内部类的特性。只有在首次使用静态内部类的时候静态内部类才会加载。这样我们在实现单例的时候只要把单例对象定义在内部类里,当调用getInstance的时候,静态内部类的静态变量就实例化。静态变量的初始化是在静态代码块里面的,天然的避免了线程安全。
public class Test {public static void main(String[] args) {otherMethod();System.out.println("-----------------------");/*Test instance = new Test().getInstance();System.out.println(instance == null ? "1" : "0");*/}Test() {System.out.println("go To init");}static class a{private static final Test INSTANCE = new Test();} public static Test getInstance() {return a.INSTANCE;}/*** 测试是否调用构造方法,看这个变量有没有先创建。*/public static void otherMethod() {System.out.println("om");}}
在JDK中的体现。
java.lang.RunTime 典型的饿汉式单例创建
java.io.Console 双捡锁懒汉式单例模式
-
Collections 中的 EmptyNavigableSet 内部类懒汉式单例
-
ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
-
Comparators.NaturalOrderComparator.INSTANCE 枚举饿汉式单例