设计模式-单例模式

214 阅读6分钟

单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、ServletContextConfig等;在Spring框架应用中ApplicationContext;数据库的连接池也都是单例形式

饿汉式单例

它是在类加载的时候就立即初始化,并且创建单例对象 优点:

  • 没有加任何的锁、执行效率比较高
  • 在用户体验上来说,比懒汉式更好

缺点:

  • 类加载的时候就初始化,不管你用还是不用,我都占着空间
  • 浪费了内存,有可能占着茅坑不拉屎
  • 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题

我们来看一下代码怎么写

public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return  hungrySingleton;
    }

}

还有另外一种写法,利用静态代码块的机制:

public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return  hungrySingleton;
    }
}

懒汉式单例

懒汉式单例的特点是:被外部类调用的时候内部类才会加载

下面看懒汉式单例的简单

public class LazySimpleSingleton {

    private LazySimpleSingleton(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;

    public  static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

然后写一个线程类ExectorThread类:

public class ExectorThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + singleton);
    }
}

测试类:

public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("结束");
    }
}

运行结果:

End
Thread-1:com.xiaoxin.singleton.threadlocal.ThreadLocalSingleton@6cff0eaa
Thread-0:com.xiaoxin.singleton.threadlocal.ThreadLocalSingleton@410272d9

从结果看有一定几率出现创建两个不同结果的情况,意味着上面的单例存在线程安全隐患。

那么,我们如何来优化代码,使得懒汉式单例在线程环境下安全呢?来看下面的代码,给getInstance()加 上synchronized关键字,是这个方法变成线程同步方法:

public class LazySimpleSingleton {

    private LazySimpleSingleton(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton lazy = null;

    public synchronized static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}

我们再看运行结果

End
Thread-1:com.xiaoxin.singleton.lazy.LazySimpleSingleton@55f9807a
Thread-0:com.xiaoxin.singleton.lazy.LazySimpleSingleton@55f9807a

多次运行发现两个对象都是一样的,可以说线程安全的问题算是解决了。

但是,用synchronized加锁,在线程数量比较多情况下,如果CPU 分配压力上升,会导致大批 量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既 兼顾线程安全又提升程序性能呢?答案是肯定的。

我们来看双重检查锁的单例模式:

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy = null;

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

这种双重检查锁的单例从synchronized基于整个类的阻塞到了方法内部阻塞性能上大大的提高了。

但是,用到synchronized关键字,总归是要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的代码,采用静态内部类的方式:

public class LazyInnerClassSingleton {

    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }
    
    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new   		LazyInnerClassSingleton();
    }
}

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。这也可以说是史上最好的单例模式的实现方式

反射破坏单例

上面介绍的单例模式的构造方法除了加上private以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用getInstance()方法,应该就会两个不同的实例

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) {
        try{
            //很无聊的情况下,进行破坏
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通过反射拿到私有的构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //强制访问
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            //调用了两次构造方法,相当于new了两次
            //犯了原则性问题,
            Object o2 = LazyInnerClassSingleton.getInstance();
            System.out.println(o1 == o2);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行结果如下:

false

Process finished with exit code 0

显然,是创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码

public class LazyInnerClassSingleton {
    
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }
    
    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new   		LazyInnerClassSingleton();
    }
}

运行结果

java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.xiaoxin.singleton.LazyInnerClassSingletonTest.main(LazyInnerClassSingletonTest.java:26)
Caused by: java.lang.RuntimeException: 不允许创建多个实例
	at com.xiaoxin.singleton.lazy.LazyInnerClassSingleton.<init>(LazyInnerClassSingleton.java:17)
	... 5 more

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:

public class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

}

测试代码:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();
            
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

com.xiaoxin.singleton.seriable.SeriableSingleton@5010be6
com.xiaoxin.singleton.seriable.SeriableSingleton@3e3abc88
false

运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加readResolve()方法即可。来看优化代码:

public class SeriableSingleton implements Serializable {

    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

再看运行结果:

com.xiaoxin.singleton.seriable.SeriableSingleton@3e3abc88
com.xiaoxin.singleton.seriable.SeriableSingleton@3e3abc88
true

破坏单例模式的方式有哪些?

破坏单例模式方式 解决方案
反射机制 构造器中进行处理,可以抛出异常
序列化和反序列化 声明readReslove()方法,返回单例

总结

1.私有化构造器

2.保证线程安全

3.延迟加载

4.防止序列化和反序列化破坏单例

5.防御反射攻击单例