【设计模式】单例模式

  |   0 评论   |   0 浏览

简介

保证一个类只有一个实例,并提供一个全局访问点。当一个对象在整个系统中都可以用到时,单例模式就比较有用了。客户端不在考虑是否要实例化的问题,而把责任都交给应该负责的类去处理。他属性创建型设计模式。

适用场景

1、确保在任何情况下都只要一个实例
2、想要可以简单的访问实例类
3、让类自己控制它的实例化
4、希望可以限制类的实例数
5、像线程池,缓存,对话框等功能,如果出现多个可能导致程序的行为异常,资源使用过度,或者不一致的情况。

优点

1、只有一个实例,减少内存开销
2、对资源没有多重占用
3、设置全局访问点,严格控制访问

缺点

没有接口,扩展困难

存在问题

1、如果存在多个类加载器,那么就会有多个实例,解决:自行指定类加载器,并且是相同的加载器。
2、1.2之前垃圾收集器有个bug,会把单例对象回收,1.2之后这个bug已经解决了。
3、不适合作为父类。

结合其他模式

1、抽象工厂模式,建造者模式,原型模式,享元模式都可以使用单例模式
2、Facade对象都是一个实例,因为只需要一个Facade对象
3、状态对象通常也只需要一个实例

重要条件

1、单例模式就是让他本身来实例化对象,只实例化一次;
2、必须自行创建这个实例,即使用private的构造函数,确保其他对象不能实例化该对象;
3、必须自行向整个系统提供这个实例,即定义一个public static operation(getInstance())来获取一个实例,如Singleton.getInstance()

示例代码

懒加载单例

public class LazySingleton {
   private static LazySingleton lazySingleton;
   private LazySingleton(){
   }
   public static LazySingleton getInstance(){
       if(null == lazySingleton){
           lazySingleton = new LazySingleton();
       }
       return lazySingleton;
   }
}
public class SingletonTest {
    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}

该模式在单线程下是没有问题的,但是在多线程的情况下,就不能保证只创建一个实例了。
我们来模拟多线程debug,看看输出的实例。

public class MyRunnable implements Runnable {
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(lazySingleton);
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
    }
}

先写好多线程的代码,我这边使用的是idea,来对断点配置多线程debug,如图:
image.png
在断点处右键,就出来这个框了。接着一步一步debug。
image.png
可以在这里进行切换线程。
如图可以看到实例化了两个不同的对象:
image.png

懒加载多线程解决

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

在方法里面加synchronized,来控制多线程问题。debug查看只有一个线程可以进入getInstance()方法
image.png
线程1执行完之后,线程2就可以执行了
image.png
这种方式可以解决多线程的问题,但是对性能有很大的影响,synchronized是对整个类进行加锁。

懒加载双重检查锁

相比在方法中添加synchronized,双重检查锁的好处是:不用每次调用方法都需要加锁,只有在实例没有被创建的时候才会加锁处理。第2个的null判断是并发的标准判断:1锁2查3判断。这样才能保证第二个线程在进来之后不会在创建实例,因为已经创建了实例了。

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

使用双重检查锁的性能要比之前在方法上加锁要好,但是也会有问题,会出现指令重排序问题,如图:
image.png
步骤2、3进行了重排序,在单线程内是不会有问题的,但是在多线程里面就会出现问题,如图:
image.png
此时线程2判断为null的时候,发现不为null,就会执行第4步,这样就出现问题了。
在变量中增加volatile来修饰,可以防止指令重排序。

静态内部类

为了解决指令重排序,可以使用静态内部类,让指令重排序对其他线程不可见,如:
image.png
代码示例:

public class StaticInnerSingleton {
    private StaticInnerSingleton(){
    }
    private static class InnerClass {
        private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }
    public static StaticInnerSingleton getInstance(){
        return InnerClass.staticInnerSingleton;
    }
}

饿汉式

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

因为类在加载的时候就被创建了,所以叫饿汉式。延迟加载是在类使用的时候才被创建,所以叫懒汉式。

序列化反序列化破坏单例

public class HungrySerializableSingleton implements Serializable {
    private static HungrySerializableSingleton hungrySingleton = new HungrySerializableSingleton();
    private HungrySerializableSingleton(){
    }
    public static HungrySerializableSingleton getInstance(){
        return hungrySingleton;
    }
}
 public static void main(String[] args) throws Exception {
        HungrySerializableSingleton instance = HungrySerializableSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("instance"));
        oos.writeObject(instance);
        File file = new File("instance");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySerializableSingleton newInstance = (HungrySerializableSingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

输出结果:

com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@3b9a45b3
false

发现经过反序列化之后,不是同一个类了。
HungrySerializableSingleton中加入下面代码

private Object readResolve(){
        return hungrySingleton;
    }

输出结果:

com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
com.cimu.creational.singleton.HungrySerializableSingleton@135fbaa4
true

反序列出来的是同一个类,原因分析:
ObjectInputStream类中,如下代码:

if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

hasReadResolveMethod该方法会先判断是否存在readResolve()方法,如果存在,那么会通过反射去调用类里面的readResolve()方法,反射调用主要是通过invokeReadResolve方法。

反射防御

public static void main(String[] args) throws Exception {
        Class objectClass = HungrySingleton.class;
        Constructor declaredConstructors = objectClass.getDeclaredConstructor();
        declaredConstructors.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) declaredConstructors.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

输出结果:

com.cimu.creational.singleton.HungrySingleton@1540e19d
com.cimu.creational.singleton.HungrySingleton@677327b6
false

通过反射创建出来两个实例,那么如何来防御呢?

public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
        if(null != hungrySingleton){
            throw new RuntimeException("反射攻击");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

在私有构造函数中判断变量是否为空,如果不为空就抛出异常。但是在懒加载中是不起效果的。
image.png

枚举单例

public enum EnumSingleton {
    INSTANCE;
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

使用反射攻击,报以下错误
image.png
查看Constructor代码,发现
image.png
使用序列化和反序列化发现输出的值是相等的,
image.png
image.png
可以看到如果是枚举的话,会根据name获取枚举的对象。可以通过jad反编译来查看枚举类,使用枚举来创建单例是比较推荐的做法。

容器单例

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> singletonMap = new HashMap<String, Object>();
    public static void putInstance(String key,Object object){
        if(null != key && !"".equals(key) && null != object){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,object);
            }
        }
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}
public static void main(String[] args) {     
ContainerSingleton.putInstance("object",EnumSingleton.getInstance());
 System.out.println(ContainerSingleton.getInstance("object"));
    }

可以通过该方法来存储一堆单例对象,但是存在反射和序列化的问题,可以使用ConcurrentHashMap来控制并发问题。

克隆破坏

第一不要实现Cloneable接口
第二,如果实现了,那么clone方法需要写成如下:

 @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

上面示例源码

源码分析

jdk中应用

java.langRuntime类中也使用了单例,如代码:

private static Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
        return currentRuntime;
    }

mybatis中应用

ErrorContext类使用了单例,这边使用了ThreadLocal<ErrorContext>的单例模式,如:
image.png

spring中应用

AbstractBeanFactory使用了单例,

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
部分代码.......
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			synchronized (this.singletonObjects) {
				singletonObject = this.earlySingletonObjects.get(beanName);
				if (singletonObject == null && allowEarlyReference) {
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
						this.earlySingletonObjects.put(beanName, singletonObject);
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return (singletonObject != NULL_OBJECT ? singletonObject : null);
	}

是把bean放到ConcurrentHashMap对象中。

也可以关注我的公众号:程序之声
图片
关注公众号,领取更多资源

本文为博主原创文章,未经博主允许不得转载。