单例模式设计模式(详细,易懂)

单例模式

单例模式一定安全吗?哪些方式可以破坏单例模式?

单例模式不一定安全,以下几种方式可以破坏单例模式:

  1. 多线程环境下的线程安全性:
  • 如果多个线程同时访问单例对象的获取方法,可能会导致多次创建实例或出现竞态条件,从而破坏单例模式。
  • 解决方法:可以通过加锁(如使用synchronized关键字或ReentrantLock)来确保在多线程环境下只有一个线程可以创建实例,来保证单例。
  1. 序列化和反序列化:
  • 当单例类实现了Serializable接口时,单例对象可以被序列化和反序列化。在反序列化时,会创建一个新的对象,从而破坏了单例模式。
  • 解决方法:可以通过重写readResolve()方法,在反序列化时返回单例对象,确保反序列化后仍然是同一个单例对象。
  1. 反射:
  • 通过反射机制可以调用类的私有构造方法来创建对象,从文破坏单例模式。
  • 解决方法:可以在构造方法中添加逻辑,防止多次实例化;或这在创建第二个实例时抛出异常。
  1. 类加载器:
  • 在一些特殊情况下,可能会存在多个类加载器加载同一个类,导致创建多个单例对象的情况。
  • 解决方法:可以通过控制类加载器的方式来确保只有一个类加载器加载单例类。
  1. 多个实例:
  • 单例模式的实现可能并不完全符合单例模式的定义,例如通过静态变量或静态方法来实现单例,但在多个类加载器的情况下可能会创建多个单例对象。
  • 解决方法:确保单例模式的实现符合单例模式的定义,同时考虑类加载器的影响。
  1. 拷贝可以破坏单例
定义

确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

饿汉式

饿汉式单例模式就是在类加载的时候就立即初始化,并且创建单例对象。
不管你有没有用到,都先建好了再说。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

优点

线程安全,没有加任何锁、执行效率比较高。

缺点

类加载的时候就初始化,不管后期用不用都占着空间,浪费内存,不推荐这样使用单例。

代码示例
/**
 * 单例模式
 * 饿汉式:类加载时就立即初始化,并且创建单例对象。
 * 线程安全,占用资源
 */
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 static LazySimpleSingleton lazySimpleSingleton = null;

    //构造函数私有
    private LazySimpleSingleton() {
    }

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

上面这种写法不是线程安全的:
在多线程情况下,如果两个线程同时走到if (lazySimpleSingleton == null)这个判断,因为lazy还没有实例化过,所以两个线程判断的结果都是true,然后同时进入代码块创建了多个实例,返回不是同一个对象,线程不安全。

/**
 * 单例模式
 * 懒汉式:实例在用到时才去检查有没有实例,没有则创建,有则返回。
 */
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    //构造函数私有
    private LazySimpleSingleton() {
    }

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

可以加锁synchronized修饰解决线程安全问题,但是在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。所以为了兼顾性能和线程安全问题,可以使用双重检查锁方式来创建懒汉式单例:

/**
 * 单例模式
 * 懒汉式:实例在用到时才去检查有没有实例,没有则创建,有则返回。
 */
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton = null;

    //构造函数私有
    private LazySimpleSingleton() {
    }

    public static LazySimpleSingleton getInstance() {
        if (lazySimpleSingleton == null) {
            synchronized (LazySimpleSingleton.class) {
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }
}

使用synchronized关键字总归要上锁,对程序性能还是存在一定影响。

/**
 * 静态内部类方式
 * 兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
 */
public class LazyInnerClassSingleton {

    //使用LazyInnerClassSingleton的时候,默认会先初始化内部类
    //如果没有使用,则内部类是不加载的。
    private LazyInnerClassSingleton() {
    }

    //static是为了使单例的空间共享,保证这个方法不会被重写、重载
    public static final LazyInnerClassSingleton getInstance() {
        //在返回结果以前,一定会先加载内部类。
        return LazyHolder.LAZY;
    }

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

这种方式兼顾了饿汉式单例的内存浪费问题和synchronized的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

反射破坏单例

饿汉式和懒汉式单例都是将构造函数私有,用来防止通过new来创建对象实例。

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) throws Exception {
        Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
        //通过反射获取私有的构造函数
        Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
        //强制访问
        c.setAccessible(true);
        //暴力初始化
        LazyInnerClassSingleton singleton = c.newInstance();
        //调用两次构造方法,相当于new了两次,违反单例原则。
        LazyInnerClassSingleton instance = singleton.getInstance();
        System.out.println(singleton == instance);//false
    }
}
/**
 * 静态内部类方式
 * 兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
 */
public class LazyInnerClassSingleton {

    //使用LazyInnerClassSingleton的时候,默认会先初始化内部类
    //如果没有使用,则内部类是不加载的。
    private LazyInnerClassSingleton() {
        //防止反射破坏单例
        if (LazyHolder.LAZY != null) {
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    //static是为了使单例的空间共享,保证这个方法不会被重写、重载
    public static final LazyInnerClassSingleton getInstance() {
        //在返回结果以前,一定会先加载内部类。
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}
序列化破坏单例

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,会重新分配内存,也就是重新创建对象。就会破坏单例。

/**
 * 反序列化破坏单例
 */
public class SeriableSingleton implements Serializable {
    //序列化就是把内存中的状态通过转换成字节码的形式
    //从而转换一个I/O流,写入磁盘或网络I/O
    //内存中的状态会永久保存下来

    //反序列化就是将已经持久化的字节码内容转化为I/O流
    //通过I/O流的读取,进而将读物的内容转换为Java对象
    //在转换过程中会重新创建对象new
    private static final SeriableSingleton INSTANCE = new SeriableSingleton();

    private SeriableSingleton() {
    }

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }
}
public class SeriableSingletonTest {
    public static void main(String[] args) {
        SeriableSingleton instance = SeriableSingleton.getInstance();
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            //序列化
            FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
            oos = new ObjectOutputStream(fos);
            oos.writeObject(instance);
            //反序列化
            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ois = new ObjectInputStream(fis);
            SeriableSingleton instance2 = (SeriableSingleton) ois.readObject();
            System.out.println(instance2 == instance);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
                if (oos != null) {
                    oos.close();
                }
            } catch (Exception e) {

            }
        }
    }
}

结果为false,可知反序列化的对象和原始对象不是同一个,打破了单例。
解决方案:只需增加readResolve()方法即可。

public class SeriableSingleton implements Serializable {

    //序列化就是把内存中的状态通过转换成字节码的形式
    //从而转换一个I/O流,写入磁盘或网络I/O
    //内存中的状态会永久保存下来

    //发序列化就是将已经持久化的字节码内容转化为I/O流
    //通过I/O流的读取,进而将读物的内容转换为Java对象
    //在转换过程中会重新创建对象new

    private static final SeriableSingleton INSTANCE = new SeriableSingleton();

    private SeriableSingleton() {
    }

    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

    //解决反射导致破坏单例的问题
    private Object readResolve(){
        return INSTANCE;
    }
}

通过源码可知底层是通过反射来找到一个无参的readResolve()方法,并返回该方法返回值。
虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。

注册式单例模式

注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。
注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。

枚举式单例模式
public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

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

序列化和反序列化不会破坏枚举式单例。
通过反射也不能破坏枚举式单例,会报错找不到无参构造器,无法使用反射创建枚举。

容器式单例
/**
 * 容器式单例
 */
public class ContainerSingleton {
    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (ioc.containsKey(className)) {
                return ioc.get(className);
            } else {
                Object obj = null;
                try {
                    //通过类加载器来创建对象
                    obj = Class.forName(className);
                    ioc.put(className, obj);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                return obj;
            }
        }
    }
}

容器式单例模式适用于实例非常多的情况,便于管理。但是它是非线程安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值