Java单例模式

1、单例模式概述

单例模式是一种常用的软件设计模式,也是著名的GoF23种设计模式之一,是指是单例对象的类只能允许一个实例存在。单例模式在多线程情况下保证实例唯一性的解决方案。

2、单例模式实现方式

1.饿汉式

public class Singleton {
	private static Singleton singleton = new Singleton();
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		return sinleton;	
	}
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。singleton作为类变量,在类初始化过程中,会被收集进()方法中,该方法会保障同步,从而避免了线程同步问题。

缺点:在类装载的时候就完成实例化,若从未使用过这个实例,则会造成内存的浪费。

2.懒汉式

public class Singleton {
	private static Singleton singleton;
	
	private Singleton(){}
	
	public static Singleton getInstance(){
		if(singleton == null) {
			singleton = new Singleton();
		}	
		return singleton;
	}
}

优点:延时加载。没用到该类时,不会去初始化对象,也就不会造成内存的浪费。
缺点:线程不安全

3.静态内部类

public class Singleton {
	private Singleton(){}

	private  static class SingletonHolder{
		private static Singleton instance = new Singleton();
	}

	public static Singleton getInstance() {
		return SignletonHolder.instance();
	}
}

4.枚举类

public enum SingletonEnum {
    INSTANCE;
    public void doSomething(){
        System.out.println("do sth.");
    }
}

单例模式的优缺点和应用场景

优点:
1.防止大量创建对象,减少内存开支
2.加快对象访问速度,提升了代码性能

缺点:
无法扩展
职责过大

应用场景
在Spring中创建的Bean实例默认都是单例模式存在的。
数据库连接池

单例模式的线程安全问题

1.懒汉式改造

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

对方法上锁,虽然保证了线程安全,但效率低。

2.懒汉式 双检锁

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

看上去上锁了,但不能保证线程安全。因为java的指令重排序。java中一个对象的创建有三个步骤:
1.申请内存空间
2.创建对象
3.将内存空间地址赋给变量
java中的指令重排序问题就可能导致第3步比第2步先执行。这就会导致其他线程在判断singleton == null 时为false。因为singleton变量此时不为null,它指向了内存空间了。所以此时其他线程调用getInstance就会放回一个还没实例化完全的对象。

3.使用volatile的双重检查

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

volatile禁止了指令重排序,保证了线程安全。

单例模式的破坏问题

1.克隆

以volatile的双重检查为例,

public class Singleton implements  Cloneable  {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance(){
        if (singleton == null){
            synchronized (Singleton.class){
                if (singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

实现 Cloneable 接口,尽管构造函数是私有,但还会创建一个对象。因为 clone 方法不会调用构造函数,会直接从内存中 copy 内存区域。所以单例模式的类是切记不要实现 Cloneable 接口。

public static void main(String[] args) throws CloneNotSupportedException {
    Singleton singleton = getInstance();
    Singleton singleton1 = (Singleton) singleton.clone();
    Singleton singleton2 = getInstance();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton2.hashCode());
}

自己运行一下,hash 值不一样,所以克隆成功了,生成了一个新对象。单例模式被成功破坏!
如何解决

/**
 * 防止克隆攻击
 * @return
* @throws CloneNotSupportedException
 */
@Override
protected Object clone() throws CloneNotSupportedException {
    return getInstance();
}

就是重写 clone 方法,调用 getInstance() 方法,返回已有的实例即可!

2.序列化

现在我们再来看序列化是如何破坏单例模式的。现在假设你的单例模式,实现了 Serializable 接口。看我下面反序列化的案例!

public static void main(String[] args) throws IOException, ClassNotFoundException {
    Singleton singleton = getSingleton();
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\xttblog.obj"));
    oos.writeObject(singleton);
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\xttblog.obj")));
    Singleton singleton1 = (Singleton) ois.readObject();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton == singleton1);
}

执行之后,hash 值不一样了,获取的对象非同一对象。结论,单例模式又被破坏了!

如何解决
很简单,自定义实现对象的 readResolve() 方法。

private Object readResolve() {
    return getSingleton();
}

为什么实现对象的 readResolve() 方法就可以了呢?这个你可以自己 debug 一下,上面反序列化的代码。其中有一个 readOrdinaryObject 方法在做怪!

Class cl = desc.forClass();//获取SingletonIn的Class
if (cl == String.class || cl == Class.class
       || cl == ObjectStreamClass.class) {
    throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {//如果所代表的类是可序列化或者自定义序列化并且可以序列化runtime时候被实例化,则返回true
    obj = desc.isInstantiable() ? desc.newInstance() : null;
//debug在这里通过反射创建了对象
} catch (Exception ex) {
    //省略
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
    handles.markException(passHandle, resolveEx);
}
//是否实现了自定义序列化接口
if (desc.isExternalizable()) {
    readExternalData((Externalizable) obj, desc);
} else {
    readSerialData(obj, desc);//读取持久化的数据并写入对象
}
handles.finish(passHandle);
if (obj != null & handles.lookupException(passHandle) == null &&
    desc.hasReadResolveMethod())
//如果所代表对象实现了可序列化或者自定义序列化接口,并且定义了readResolve方法,则返回true
{
    //获取readResolve方法中的对象,并替换obj
    Object rep = desc.invokeReadResolve(obj);
    if (unshared && rep.getClass().isArray()) {
        rep = cloneArray(rep);
    }
    if (rep != obj) {
        handles.setObject(passHandle, obj = rep);
    }
}
return obj;//返回返序列化对象

3.反射

public static void main(String[] args)
        throws ReflectiveOperationException {
    Class cls = Singleton.class;
    Constructor constructor = cls.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton singleton = constructor.newInstance();
    Singleton singleton1 = getSingletonIn();
    System.out.println(singleton.hashCode());
    System.out.println(singleton1.hashCode());
    System.out.println(singleton == singleton1);
}

执行之后,hash 值不一样了,获取的对象非同一对象。结论,单例模式又被破坏了!

如何解决

private Singleton() {
    if (null != instance) {
        throw new RuntimeException();
    }
}

因为执行反射会调用无参构造函数,所以上面的判断就可以起作用了!

总结

综上所述,单例模式需要考虑,线程安全问题,效率问题,防止反射、反序列化、克隆。要不然,就有可能被黑客利用!

看到这里,有些人可能会问,这也太麻烦了,有没有更简便的方法呢?有,枚举模式。枚举类型是绝对单例的,可以无责任使用。

public enum Singleton implements Cloneable, Serializable{
    Instance;
    private Singleton() {
        init();
    }
    public void init() {
        //省略
    }
}

一个枚举,就算实现双接口,也是无论如何都无法被破坏的。

  1. 枚举无法克隆。因为Enum类重写了clone方法,直接抛出异常。
protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
  1. 利用反射的话,在调用constructor.newInstance方法,newInstance内部会判断是否是枚举类,如果是直接抛异常。
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
	 throw new IllegalArgumentException("Cannot reflectively create enum objects");
  1. 对于反序列化 Java 仅仅是将枚举对象的 name 属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。
private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

所以,枚举才是实现单例模式的最好方式!

你可能感兴趣的:(java)