设计模式之单例模式浅入浅出

文章目录

  • 浅谈单例模式
    • 定义:
    • 实现思路:
    • 单例的实现:
      • 饿汉模式
        • 饿汉模式的优缺点
      • 懒汉模式
        • 懒汉模式的优缺点:
    • 双重检查锁模式
    • 静态内部类单例模式
    • 枚举类实现单例模式
      • 破坏单例的方法以及解决方法

浅谈单例模式

摘要: 单例模式是Java中设计模式最简单的一种,只需要一个类就能实现到哪里模式,但是这模式也不能小看,在开发中会遇见很多的坑。

定义:

​ 单例模式在程序运行中只能实例化一次,建立一个全局唯一的对象,和静态变量像是,但是单例比静态变量要好,在项目启动的时候JVM就会加载静态变量,如果我们不使用这个变量的时候,就会造成资源的浪费,单例模式能够在使用实例的时候采取创建实例。类库中很多工具类都使用到了单例模式,缓存、日志对象、线程池等。如果这些不是一个实例的话,大概率会出现不可预知的问题,如处理的结果不一致等。

实现思路:

​ 静态化实例对象

​ 私有话构造方法,禁止外部进行实例化

​ 提供一个公共的方法来返回唯一的实例对象

单例的实现:

​ 单例的写法有恶寒、懒汉、双中检查锁、内部类单例、枚举单例物种方式,其中懒汉模式和双重检查锁模式两种方式,如果写法不合适的话,在多线程情况下一些异常问题。

饿汉模式

​ 饿汉模式实现的方法比较简单粗暴,定义静态属性,直接实例化对象,按照个人理解,饿汉模式就是太饿了,管他三七二十一,直接先吃了(创建对象)再说。代码如下:

public class HungryManPattern {
    //李忠静态变量来存储唯一实例
    private static final HungryManPattern hmp = new HungryManPattern();
    //私有化构造方法,防止外部进行实例化
    private HungryManPattern(){ }
    public static HungryManPattern getHungryManPattern(){
        return hmp;
    }
}

饿汉模式的优缺点

优点:

​ 因为使用了static关键字,JVM已经加载好了这个变量,保证在引用这个变量时候,关于这个变量的写入操作都完成,保证了线程的安全。

缺点:

​ 不能实现懒加载,不管我们能不能用到这个实例,都会记载这个类,我们如果长时间没有使用这个类,就会浪费一定的内存。

懒汉模式

​ 懒汉模式是一种相比饿汉比较懒的模式,在程序初始化的时候,并不会创建实例,只要在使用实例的时候才会创建,所以懒汉模式解决了饿汉的空间浪费,同时也引入了其他的问题,代码如下:

public class SlackerPattern {
    //定义一个静态变量,未初始化
    private static  SlackerPattern slackerPattern ;
    //私有化构造函数,防止外部实例化
    private SlackerPattern(){}

    public static SlackerPattern getSlackerPattern(){
        //使用时,先判断实例是否为null,如果实例为null,则创建对象
        if (slackerPattern ==null) slackerPattern = new SlackerPattern();
        return slackerPattern;
    }
}

上面是懒汉模式的实现方法,但是懒汉在多线程的情况下是不安全的,因为不能保证获取出来的对象内存地址是一样的,即获取的对象不是单例

        if (slackerPattern ==null) slackerPattern = new SlackerPattern();
        return slackerPattern;

​ 假如有两个线程都进入到if中,因为没有任何的锁,所以两个线程可以同时判断 slackerPattern == null ,所以都会去实例对象,就会出现多个对象的情况。

通过上面的分许,我指定了多份对象的原因,如果我们在创建实例的时候进行资源保护,是不是可以解决问题呢?我们给 getSlackerPattern() 方法加上synchronized 修饰,成为受保护的资源能够解决多分实例的问题,代码如下:

public class SlackerPattern {
    //定义一个静态变量,未初始化
    private static  SlackerPattern slackerPattern ;
    //私有化构造函数,防止外部实例化
    private SlackerPattern(){}

    public synchronized static SlackerPattern getSlackerPattern(){
        //使用时,先判断实例是否为null,如果实例为null,则创建对象
        if (slackerPattern ==null) slackerPattern = new SlackerPattern();
        return slackerPattern;
    }
}

​ 修改之后,我们解决了多个对象的问题,但是加入了 synchronized 进行修饰,就会引入新的问题,枷锁之后会使程序变成串行化,只要抢到锁的线程才回去执行这段代码,这是使性能大大降低。

懒汉模式的优缺点:

优点

​ 实现了懒加载,不浪费内存。

缺点

​ 在不加锁的情况下,线程不安全,会出现多个对象

​ 在枷锁的情况下,会有一定的性能问题

双重检查锁模式

​ 对于 getSlackerPattern() 函数来说,大部分操作是读操作,读奥做是安全的,所以我们没必要让每个线程必须只有锁次啊能调用该方法,我们需要调整锁的问题,所以就有了 双中检查锁模式,代码如下:

public class SlackerPattern {
    //定义一个静态变量,未初始化
    private static SlackerPattern slackerPattern ;
    //私有化构造函数,防止外部实例化
    private SlackerPattern(){}

    public  static SlackerPattern getSlackerPattern(){
        synchronized (SlackerPattern.class){
            //抢到锁后,判断是否为null
            if (slackerPattern ==null) slackerPattern = new SlackerPattern();
            return slackerPattern;
        }
    }
}

​ 双重检查锁模式是一个很好的单例模式,解决了单例、性能、线程安全问题,但是也存在一定的问题,在多线程的情况下,可能出现 空指针 ,出现问题的原因是JVM在实例化对象的时候会进行优化和指令的重排,什么是指令重排?我们来看一个例子。

    private SlackerPattern(){
        int a = 10;
        int b = 11;
        Object o = new Object();
    }

​ 上面的构造方法,我们编写的顺序是 a、b、o、JVM会对它进行指令重排,所以执行的顺序可能会有编号,但是最后都会保证所有的实例都完成实例化。如果构造函数中操作比较多时,为了提醒效率,JVM会在构造方法里的属性未全部完成实例化的时候,就返回对象。出现空指针问题的原因就在这里,当某一个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排的与纳音,其他线程获取的对象可能是一个不完整的一个对象,所以在使用实例的时候会出现空指针。要解决双重解检查锁模式带来的空指针异常的问题,只需要使用volatile关键字, volatile关键字机上之后,即在读操作之前,写操作必须全部完成,代码如下:

public class SlackerPattern {
    //定义一个静态变量,未初始化
    //添加volatile关键字,在读操作完成之前,写操作必须全部完成。
    private static volatile  SlackerPattern slackerPattern ;
    //私有化构造函数,防止外部实例化
    private SlackerPattern(){}

    public  static SlackerPattern getSlackerPattern(){
        synchronized (SlackerPattern.class){
            //抢到锁后,判断是否为null
            if (slackerPattern ==null) slackerPattern = new SlackerPattern();
            return slackerPattern;
        }
    }
}

​ 添加 volatile 关键字之后的双重检测锁模式是一种比较好的单例实现,能够保证多线程情况下的线程安全和性能问题。

静态内部类单例模式

​ 静态内部类单例模式,实例由内部类创建,由于JVM在加载外部类的过程中是不会加载静态内部类的,只要内部类的属性或者方法被使用的时候才会被加载,并初始化其静态属性。代码如下:

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton(){}

    //单例持有者
    private static class InnerClaa{
        private final static StaticInnerClassSingleton scsl = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getStaticInnerClassSingleton(){
        return InnerClaa.scsl;
    }
}

​ 内部类单例是一种优秀的单例模式,是开箱项目中比较常见的单例模式,保证了线程的啊取暖,也没有影响性能和空间的浪费

枚举类实现单例模式

​ 枚举类实现单例是线程安全的,充分利用了枚举的特性来实现单例模式,枚举的写法很简单,而且枚举类是所用单例中唯一一种不会被破坏的单例实现模式,代码如下:

public class EnunmSingleCase {
    //同样的私有化构造方法
    private EnunmSingleCase(){}

    private enum EnumSingClass{
       POJO;
       private final EnunmSingleCase enunmSingleCase;
        EnumSingClass(){
            enunmSingleCase = new EnunmSingleCase();
        }
        private EnunmSingleCase getEnunmSingleCase(){
            return enunmSingleCase;
        }
    }

    public static EnunmSingleCase getInstance(){
        return EnumSingClass.POJO.getEnunmSingleCase();
    }
}

破坏单例的方法以及解决方法

​ 1、除枚举方法之外,其他方法会通过反射的方式破坏单例,反射是通过调用构造方法生成新对对象,所以我们只要在构造方法中加一个判断即可,若已经有实例生成,则组织生成新的实例。

private Singleton(){
    if (instance !=null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
    }
}

2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,我们可以不实现序列化接口,如果非得实现序列化接口,重写反序列化方法readResolve(), 反序列化时直接返回单例对象。

  public Object readResolve() throws ObjectStreamException {
        return instance;
    }

相关代码地址:跳转Github

你可能感兴趣的:(设计模式)