设计模式:关于单例模式的问题

关于单例模式的问题

    • 实现单例有多少种方式?
    • 懒汉式为什么要双锁检测?
    • 枚举内部实现原理, 枚举和序列化和反序列化
    • 框架提供了类A为单例模式, 如何打破单例模式? 你提供了类A为单例模式, 如何防止别人打破单例模式?

实现单例有多少种方式?

廖雪峰->单例

  • 饿汉式:在类加载的时候就加载。线程安全。
public class Singleton {
    // 静态字段引用唯一实例:
    private static final Singleton INSTANCE = new Singleton();

    // 通过静态方法返回实例:
    public static Singleton getInstance() {
        return INSTANCE;
    }

    // private构造方法保证外部无法实例化:
    private Singleton() {
    }
}
  • 懒汉式:即延迟加载,在调用方第一次调用getInstance()时才初始化全局唯一实例。
public class Singleton {
    private static Singleton INSTANCE = null;

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

    private Singleton() {
    }
}

但这种写法在多线程下是不行的,可能会创建多个实例对象。
此时,有两种方式:方法加锁或二次检查(DCL)
方法加锁:严重影响并发性能

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

二次检查/双重检查:由于Java的内存模型,双重检查在这里不成立。

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

因为JVM内存模型(JMM)允许‘无序写入’。即在JVM执行指令过程中,指令的执行顺序有可能是乱序的。如代码mInstance = new Singleton();其实这行代码做了3件事:

1,为单例对象分配内存空间
2,将mInstance引用变量指向刚分配好的内存空间(此时,mInstance已经是非null的了)
3,为单例对象通过mInstance调用其类的构造方法进行初始化。

在JVM执行过程中,2,3步的执行顺序是不确定的,可能是颠倒的。颠倒的情况下,在并发时,就有可能发生严重的错误,当线程一执行到步骤2而未执行步骤3时(实例未初始化,但引用变量mInstance已经非null),如果此时被线程2获取到了CPU的使用权,线程2在执行mInstance的非null判断时,将会认为mInstance为非null而直接返回引用,但其实此时是未初始化的,如果线程2使用这个mInstance引用,系统就会报错(因为mInstance未初始化却被使用了)。
在JDK版本较高(>1.5)的情况下,可以通过使用volatile关键字来避免这种情况

如果没有特殊的需求,使用Singleton模式的时候,最好不要延迟加载,这样会使代码更简单。

  • 枚举类单例:因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可
public enum World {
    // 唯一枚举:
	INSTANCE;

	private String name = "world";

	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:

String name = World.INSTANCE.getName();

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

懒汉式为什么要双锁检测?

避免直接在方法上加锁,降低锁的粒度,提高性能。第一次判断if (INSTANCE == null)可以检查是否存在实例,存在则直接返回实例对象,避免进入同步锁。

第二次判断if (INSTANCE == null)是为了避免在第一次判断到进入同步锁期间,有对象实例被创建出来,此时检查对象实例,如果依旧为null就创建实例,否则不创建直接返回。

枚举内部实现原理, 枚举和序列化和反序列化

我们定义一个枚举类:

public enum Color {
    RED, GREEN, BLUE;
}

编译器编译出的class大概如下:

public final class Color extends Enum { // 继承自Enum,标记为final class
    // 每个实例均为全局唯一:
    public static final Color RED = new Color();
    public static final Color GREEN = new Color();
    public static final Color BLUE = new Color();
    // private构造方法,确保外部无法调用new操作符:
    private Color() {}
}

枚举类其实就是一个特殊的类,它继承了Enum类,并被声明为final,表示其不能被继承。
类中的每个实例都是用static final声明的静态常量,是全局唯一的,且构造方法用private声明,确保外部无法调用new来创建实例对象。由于是static的所以所有的对象只会加载一次且线程安全,这就保证了实例的单例性。

单例有个问题就是一旦实现了 Serializable接口后,就不再是单例的了,因为每次反序列化调用readObject()时都会创建一个新对象。而枚举类为了保证其定义的枚举变量在JVM中是唯一的,对序列化和反序列化做了一些特殊处理。

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
枚举类的序列化问题

框架提供了类A为单例模式, 如何打破单例模式? 你提供了类A为单例模式, 如何防止别人打破单例模式?

  • 反射破坏单例:通过反射打开了构造器的权限(constructor.setAccessible(true)),进而调用了私有的构造函数,然后产生了一个新的对象。
public static void main(String[] args) throws Exception{

        Constructor constructor = SingleTest.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        SingleTest s1 = SingleTest.getSingleTest();
        SingleTest s2 = SingleTest.getSingleTest();
        SingleTest s3 = (SingleTest) constructor.newInstance();

        System.out.println("输出结果为:"+s1.hashCode()+"," +s2.hashCode()+","+s3.hashCode());
}

解决方法:
在单例中构建一个私有构造方法:

private static boolean flag = false;

private SingleTest(){
        synchronized(SingleTest.class){
            if(flag == false){
                flag = !flag;
            }else {
                throw new RuntimeException("单例模式被侵犯!");
            }
        }
 }
  • 序列化和反序列化:反序列话是通过readObject()方法重新构建新对象。

解决方法:

  1. 在单例中加入该方法:
 //在单例类中加入这个方法即可
    private Object readResolve(){
        return instance;
    }
  1. 使用enum类构建单例。

破坏单例及防护方法
反射破坏单例

你可能感兴趣的:(设计模式笔记,设计模式,java)