设计模式之单例模式

173 阅读5分钟

在java中单例模式是经常被使用的,比如spring中所创建的bean都是单例的

那么单例模式有什么好处呢?

  1. 单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  2. 因为类控制了实例化过程,所以类可以灵活更改实例化过程。

简单的来说就是向外暴露唯一的对象由开发者进行操作,同时有有利于开发者更改创建对象的过程

那么当我们需要使用单例模式的时候自己怎么去实现单例模式呢?经过前人的摸索大概有以下的几种方法。

  • 饿汉式

    public class Mrg01 {
    
        private static final Mrg01 MRG_01 = new Mrg01();
    
        private Mrg01(){}
    
        public static Mrg01 getMrg01(){
            return MRG_01;
        }
    }
    

    饿汉式的原理是将构造函数变成私有的,保证在外部不能够实例化此对象,然后将创建对象的过程设置成静态的,当类加载到内存之后就会实例化一个实例,且jvm会保证这个过程的线程安全。

    但是他的缺点也是很明显的,那就是无论这个对象使用不使用他都会不实例化,这就造成了一定的资源浪费,

    所以在在这个基础上就提出了懒汉式单例

  • 懒汉式

    public class Mrg02 {
        private static  Mrg02 MRG_02 ;
        private Mrg02(){}
    
        public  static Mrg02 getMrg02(){
            if (MRG_02==null){
                MRG_02 = new Mrg02();
            }
            return MRG_02;
        }
    }
    

    懒汉式就是将实例化对象的时机改成在使用的时候实例化,如果不使用就不实例化,这样就不会造成资源的浪费了。但是这种写法有很大的问题,比如在这个时候使用多线程获取单例对象的时候,就可能会造成对象不是单例,如下图打印出来的对象的地址并不是同一个对象

    public class Mrg02 {
        private static  Mrg02 MRG_02 ;
        private Mrg02(){}
    
        public  static Mrg02 getMrg02(){
            if (MRG_02==null){
            	// 让这个线程沉睡10ms可以看的更清晰下
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 在一个线程判断对象存在的同时 第二个线程也进入到了这里
                // 如果第二个对象争夺到了资源的使用权那么他会优先创建对象
                // 然后等另外一个获取锁之后会再次创建对象
                MRG_02 = new Mrg02();
            }
            return MRG_02;
        }
    
        public static void main(String[] args){
            for (int i=0;i<100;i++){
                new Thread(()->{
                    System.out.println(Mrg02.getMrg02());
                }).start();
            }
        }
    }
    

    当然我们可以在方法上添加关键字synchronized 来解决这个问题,但是同时带来的还有效率下降。

    除了在方法上添加synchronized 关键字之外,还可以通过双重检查来解决线程的问题,但是要注意的是

    需要在private static Mrg02 MRG_02 ;上加上volatile,原因是防止代码重排序导致对象未初始化便已经分配好内存空间,导致返回的对象在使用的时候报空指针异常

    public class Mrg02 {
        private static volatile  Mrg02 MRG_02 ;
        private Mrg02(){}
    
        public  static Mrg02 getMrg02(){
            if (MRG_02==null){
                synchronized(Mrg02.class){
                    if (MRG_02 == null){
                        MRG_02 = new Mrg02();
                    }
                }
            }
            return MRG_02;
        }
        public static void main(String[] args){
            for (int i=0;i<100;i++){
                new Thread(()->{
                    System.out.println(Mrg02.getMrg02());
                }).start();
            }
        }
    }
    

    如果需要彻底解决效率问题,那就可以使用静态内部类的方式创建单例

  • 静态内部类方式

    public class Mrg03 {
    
        private Mrg03(){
        }
    
        private static class Mrg03Holder{
            private final static Mrg03 MRG_03 = new Mrg03();
        }
    
        public static Mrg03 getInstance(){
            return Mrg03Holder.MRG_03;
        }
    
    
        public static void main(String[] args){
            for (int i=0;i<100;i++){
                new Thread(()->{
                    System.out.println(Mrg03.getInstance());
                }).start();
            }
        }
    }
    

    静态内部类创建单例的方式是最完美的方法,它不仅可以解决资源问题还可以解决线程问题。如果需要在代码中实现单例的化我推荐使用此方式。

  • 枚举方式

    public enum Mrg04 {
        INSTANCE;
    
        public static void main(String[] args){
            for (int i=0;i<100;i++){
                new Thread(()->{
                    System.out.println(Mrg04.INSTANCE.hashCode());
                }).start();
            }
        }
    }
    

    也可以通过枚举的方式去实现单例模式,而且这种方式也是effect Java中推荐的方式,effect Java中是对于这种方法这样说:这种方法在功能上与公有域方法相似,但是更加简洁,无偿的提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候,虽然这种方式还没有广泛采用,但是氮元素的枚举类型经常成为时间singleton的最佳方法。注意,如果singleton必须扩展一个超类,而不是扩展enum的时候,则不宜使用这个方法。

    其实这种方式就是为了更好的防止反序列化和反射攻击,因为通过反序列化和反射可以产生当前对象的副本,可以通过副本去继续操作程序。

  • 枚举方式外的防止反序列化攻击的方式

    我们可以提供一个readResolve方法,此方法允许我们使用readObjec创建实例来代替另一个实例,当反序列化之后,新建对象上的readResolve就会被调用,然后该方法返回的对像引用将被返回,取代新建的对象。

    private Object readResolve(){
        return INSTANCE;
    }