常见的设计模式之单例模式

概念

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

常见写法

饿汉式单例

在单例类首次加载时就创建单例

public class HungerSingleton{
    
    
 	private static final HungerSingleton instance =new HungerSingleton();
    private HungerSingleton(){
    
    }
    public static HungerSingleton getInstance(){
    
    
        return instance;
    }
}

缺点

  • 浪费内存空间,不适合大量创建单例对象的场景

懒汉式单例

为了解决饿汉式单例可能带来的内存浪费的问题,提出了懒汉式单例。懒汉式单例的特点是:单例对象要在被使用时才会初始化。

线程不安全

public class LazySingleton{
    
    
    private static LazySingleton instance =null;
    private LazySingleton(){
    
    }
    public static LazySingleton getInstance(){
    
    
        if(instance==null){
    
    
            return new LazySingleton();
        }
        return instance;
    }
}

缺点

  • 线程不安全

线程安全

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

缺点

  • 必须加锁 synchronized 才能保证单例,但加锁会影响效率。

双重检查锁

既能解决线程安全又能提升程序性能,提出了双重检查锁

class SynchronizedSingleton{
    
    
    private static volatile SynchronizedSingleton instance =null;
    private SynchronizedSingleton(){
    
    }
    private static SynchronizedSingleton getInstance(){
    
    
        if (instance==null){
    
    
            synchronized (SynchronizedSingleton.class){
    
    
                if (instance==null){
    
    
                    return new SynchronizedSingleton();
                }
            }
        }
        return instance;
    }
}

缺点

  • 总归还是用到了synchronized 加锁操作,影响性能

内部类实现

利用了内部类的加载方式:外部类初始化的时候内部类不会加载,只要在调用内部类的方法时才会初始化内部类。
是性能最优的且线程安全的一种懒汉式单例写法

class LazyInnerClassSingleton{
    
    
    private LazyInnerClassSingleton(){
    
    }

    public static final LazyInnerClassSingleton getInstance() {
    
    
        return LazyHoder.instance;
    }

    //延迟加载
    private static class LazyHoder{
    
    
        private static final LazyInnerClassSingleton instance=new LazyInnerClassSingleton();
    }
}

反射破坏单例

但是,上面这种方法就是完美的么?我们先来看一个测试例子

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();
             Object o2=LazyInnerClassSingleton.getInstance();
            System.out.println(o1==o2);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述
可以看到,我们通过反射来调用其构造方法,和调用getInstance方法,出现了两个不同的实例,这种情况怎么解决呢?

class LazyInnerClassSingleton{
    
    
    private LazyInnerClassSingleton(){
    
    
        if(LazyHoder.instance!=null){
    
    
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
    
    
        return LazyHoder.instance;
    }

    //延迟加载
    private static class LazyHoder{
    
    
        private static final LazyInnerClassSingleton instance=new LazyInnerClassSingleton();
    }
}

我们通过构造方法的判断和异常,让调用者只能通过getInstance去拿实例

序列化破坏单例

一个对象创建完成,有时需要将对象序列化后写入磁盘,下次使用再从磁盘读取对象并进行反序列化,将其转成内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的对象是单例对象,就会破坏单例

public class HungerSingleton implements Serializable{
    
    
    private static final HungerSingleton instance =new HungerSingleton();
    private HungerSingleton(){
    
    }
    public static HungerSingleton getInstance(){
    
    
        return instance;
    }
    public static HungerSingleton deepClone(HungerSingleton hs){
    
    
        HungerSingleton i =null;
        try {
    
    
            //序列化对象
            FileOutputStream fo=new FileOutputStream("HungerSingleton.obj");
            ObjectOutputStream os=new ObjectOutputStream(fo);
            os.writeObject(hs);

            os.flush();
            os.close();

            //反序列化对象
            FileInputStream fi=new FileInputStream("HungerSingleton.obj");
            ObjectInputStream ois=new ObjectInputStream(fi);
            i=(HungerSingleton)ois.readObject();
            ois.close();

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

    public static void main(String[] args) {
    
    
        HungerSingleton i=HungerSingleton.getInstance();
        HungerSingleton ii=deepClone(i);
        System.out.println(i==ii);
    }
}

运行结果:
在这里插入图片描述
很明显,序列化后的对象和手动创建的对象是不一致的,实例化了两次。那么,怎么去解决这个问题呢?很简单,只需要增加readResolve方法就可以了

public class HungerSingleton implements Serializable{
    
    
    private static final HungerSingleton instance =new HungerSingleton();
    private HungerSingleton(){
    
    }
    public static HungerSingleton getInstance(){
    
    
        return instance;
    }
     //序列化破坏单例的解决方案:
    //重写readResolve方法
    private Object readResolve(){
    
    
        return instance;
    }
}

再来看结果:
在这里插入图片描述

注册式单例

注册式单例又被称为登记式单例,就是讲每一个实例都登记到某一个地方,使用唯一标识获取单例

枚举式单例

enum EnumSingleton{
    
    
    INSTANCE;
    private Object obj;

    private Object getObj(){
    
    
        return obj;
    }
    public static EnumSingleton getInstance(){
    
    
        return INSTANCE;
    }

    public static void main(String[] args) {
    
    
        EnumSingleton e1=EnumSingleton.getInstance();
        EnumSingleton e2=EnumSingleton.getInstance();
        System.out.println(e1==e2);

    }
}

枚举法单例模式是《Effective Java》书中推荐的一种单例模式实现写法

缺点

  • 在类加载之时就将所有的对象初始化放在内存中,与饿汉式一样,不适合大量创建单例对象的场景

ThreadLocal单例

前面我们也讲过ThreadLocal,它是单个线程唯一的,天生就是线程安全的

class ThreadLocalSingleton{
    
    
    private ThreadLocalSingleton(){
    
    }

    private static final ThreadLocal<ThreadLocalSingleton> instance=new ThreadLocal<ThreadLocalSingleton>(){
    
    
        @Override
        protected ThreadLocalSingleton initialValue() {
    
    
            return new ThreadLocalSingleton();
        }
    };

    private static ThreadLocalSingleton getInstance(){
    
    
        return instance.get();
    }
}

ThreadLocal单例也算是一种特殊的单例模式吧,对ThreadLocal原理感兴趣的可以去看之前的文章深入理解ThreadLocal

总结

一般建议使用饿汉式单例。只有在要明确实现 lazy loading 效果时,才会使用内部类实现的单例模式。如果涉及到反序列化创建对象时,可以尝试使用第枚举方式。如果有其他特殊的需求,可以考虑使用双重检锁方式。

猜你喜欢

转载自blog.csdn.net/xzw12138/article/details/106620163