设计模式--单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供全局访问点。单例模式是创建型模式。J2EE标准中ServletContext、ServletContextConfig等,Spring框架应用中ApplicationContext、数据库的连接池等也都是单例形式。

饿汉单例模式在类加载的时候就立即初始化,并且创建单例对象。它是线程安全的,在线程还没出现以前就实例化了。

class HungrySingleton{
    //先静态、后动态
    //先属性,后方法
    //先上后下
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    private HungrySingleton(){
    }
    
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

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

class HungrySingleton{
    //先静态、后动态
    //先属性,后方法
    //先上后下
    private static final HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }

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

缺点:所有对象类加载的时候就实例化了,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费

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

/**
 * 懒汉式单例模式 在需要使用的时候在进行实例化
 */
class LazySimpleSingleton{
    private LazySimpleSingleton(){}
    
    public static LazySimpleSingleton lazy = null;
    public static LazySimpleSingleton getInstance(){
        if(lazy==null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
    
}

但是这种写法在多线程环境下,就会出现线程安全问题。

可以给getInstance方法加上synchronized关键字,使这个方法变成线程同步方法:

class LazySimpleSingleton{
    private LazySimpleSingleton(){}

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

}

synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批线程阻塞,从而导致程序性能下降。

双重检查锁的单例模式(double-checked locking):

class LazySimpleSingleton{
    private LazySimpleSingleton(){}

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

双重检查锁需要加上volatile关键字,

  • 禁止指令重排序:在没有volatile修饰的情况下,处理器可能会对指令进行重排序以提高性能。这可能导致某些线程在第一次条件判断之前看到的是一个非空但尚未完全初始化的对象,从而违反了单例模式的规则。使用volatile关键字可以防止这种情况的发生,确保每个线程看到的对象状态是一致的。
  • 确保可见性:volatile关键字强制线程从主内存读取变量的最新值,而不是依赖本地的缓存副本。这样做的目的是确保不同线程之间对共享数据的更新和读取是可见的。
  • 确保有序性:volatile同样有助于维护程序的执行顺序,尽管它本身不能保证原子性。在双重检查锁的场景中,volatile的使用有助于确保特定操作的顺序,如首先判断单例对象是否为空,然后再尝试创建新对象。

用到synchronized关键字总归要上锁,对程序性能还是存在一定的影响的。我们可以从类初始化的角度来考虑,采用静态内部类的方式

/**
 *  采用静态内部类的方式
 */
class LazyInnerClassSingleton{
    //使用LazyInnerClassSingleton的时候,默认会先初始化内部类
    //如果没有使用,则内部类不加载
    private LazyInnerClassSingleton(){};
    public static final LazyInnerClassSingleton getInstance(){
        //返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }
    //默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

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

反射破坏单例

上面介绍的单例模式的构造方法除了加上private关键字,没有做任何处理。如果我们使用反射来调用其构造方法,在调用getInstance()方法,应该有两个不同的实例。以LazyInnerClassSingleton位例:

public class Test {
    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 = c.newInstance();

            System.out.println(o1==o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

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

class LazyInnerClassSingleton{
    //使用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();
    }
}

序列化破坏

一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化的对象会重新分配内存,即重新创建。即相当于违背了单例模式的初衷,如下:

/**
 * 反序列化导致破坏单例模式
 */
class SeriableSingleton implements Serializable{
    //序列化,就是将内存中的状态通过转换成字节码的形式
    //从而写入其他地方(可以是磁盘,网络I/O)
    
    //反序列化就是将 通过I/O流的读取,进而将读取的内容转换成Java对象
    //在转换的过程中会重新创建对象 new
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

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


public class Test {
    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();
        }
    }
}

反序列化时,首先获取序列化的类 : desc( 可理解为单例类的class类,但它和JVM加载到内存中的单例class类有不同)因为如果我们的单例类在构造方法中通过实例不为空则抛出异常防止了反射破坏单例,那单例类是不允许再实例化的。而desc类却依然可以实例化。(当我们反序列化一个对象时,永远不会调用其类的构造函数,反序列化后的实例变量与序列化之前的实例变量相同,类变量与当前的类变量相同,如果反序列化时类未被加载则类变量为默认值。)

当我们通过反序列化readObject()方法获取对象时会去寻找readResolve()方法,如果该方法不存在则直接返回新对象,如果该方法存在则按该方法的内容返回对象,以确保如果我们之前实例化了单例对象,就返回该对象如果我们之前没有实例化单例对象,则会返回null

如何保证在序列化的情况下也能够实现单例模式呢?需要增加readResolve()方法。代码如下:

/**
 * 反序列化导致破坏单例模式
 */
class SeriableSingleton implements Serializable{
    //序列化,就是将内存中的状态通过转换成字节码的形式
    //从而写入其他地方(可以是磁盘,网络I/O)

    //反序列化就是将 通过I/O流的读取,进而将读取的内容转换成Java对象
    //在转换的过程中会重新创建对象 new
    public final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    @Serial
    private Object readResolve(){
        return INSTANCE;
    }
}

 从JDK源码角度,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回

注册式单例模式

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

  • 枚举式单例模式
/**
 * 枚举式单例模式
 */
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 Test {
    public static void main(String[] args) {
        EnumSingleton s1 = null;
        EnumSingleton s2 = EnumSingleton.getInstance();
        s2.setData(new Object());

        FileOutputStream fos = null;

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

            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(s1.getData());


            System.out.println(s2.getData());
            System.out.println(s1.getData()==s2.getData());

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

 通过反编译工具Jad反编译EnumSingleton在是静态变量,是饿汉单例模式的实现。在反序列化中readObject()调用了readObject0()->readEnum()。

public static final EnumSingleton INSTANCE = new EnumSingleton("INSTANCE", 0);

 枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

如果通过反射破坏枚举单例模式,汇报java.lang.NoSuchMethodException异常(找不到无参的构造方法),测试代码:

public class Test {
    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c =  clazz.getDeclaredConstructor();
            c.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

 反编译后,EnumSingleton的只有一个构造方法:

 通过有参构造器,报java.lang.IllegalArgumentException异常,即不能用反射来创建枚举类型。在newInstance方法中做了强制判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。测试反射代码如下:

public class Test {
    public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c =  clazz.getDeclaredConstructor(String.class, int.class);
            c.setAccessible(true);
            c.newInstance("Tom",666);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

  • 容器式单例模式

 其实枚举式单例模式,虽然写法优雅,但是也会有一些问题,因为它在类加载之时就将所有的对象初始化防在类内存中,这其实跟饿汉式并无差异,不适合大量创建单例对象的场景。注册式单例模式的另一种写法,即容器式单例模式:

class ContainerSingleton{
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).getDeclaredConstructor().newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }else{
                return ioc.get(className);
            }
        }
    }
}

容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。

Spring中容器式单例模式的实现代码:

线程单例实现ThreadLocal 

ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,是线程安全的。ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是通过线程空间来实现隔离的。

/**
 * 线程单例实现 ThreadLocal
 */
class ThreadLocalSingleton{
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

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

public class Test {
    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        System.out.println("End");
    }
}

JDK中一个经典的单例模式的应用,Runtime类,如下:

总结 

单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木羊子羽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值