单例模式是一种常用的软件设计模式,也是著名的GoF23种设计模式之一,是指是单例对象的类只能允许一个实例存在。单例模式在多线程情况下保证实例唯一性的解决方案。
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return sinleton;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。singleton作为类变量,在类初始化过程中,会被收集进()方法中,该方法会保障同步,从而避免了线程同步问题。
缺点:在类装载的时候就完成实例化,若从未使用过这个实例,则会造成内存的浪费。
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点:延时加载。没用到该类时,不会去初始化对象,也就不会造成内存的浪费。
缺点:线程不安全
public class Singleton {
private Singleton(){}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SignletonHolder.instance();
}
}
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禁止了指令重排序,保证了线程安全。
以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() 方法,返回已有的实例即可!
现在我们再来看序列化是如何破坏单例模式的。现在假设你的单例模式,实现了 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;//返回返序列化对象
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() {
//省略
}
}
一个枚举,就算实现双接口,也是无论如何都无法被破坏的。
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
constructor.newInstance
方法,newInstance
内部会判断是否是枚举类,如果是直接抛异常。if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
所以,枚举才是实现单例模式的最好方式!