单例模式
什么是单例模式
类的单例设计模式,就是采取一定的方法保证在整个软件系统中,某个类只能存在一个对象实例,并且这个类会提供一个获取对象实例的方法。
思路:如果让一个类在一个虚拟机里面只能产生一个对象,就必须将构造器的访问权限设置为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.class
public 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 枚举饿汉式单例