侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

JAVA设计模式之单例模式(6种实现)

孔子说JAVA
2019-11-11 / 0 评论 / 0 点赞 / 121 阅读 / 3,881 字 / 正在检测是否收录...

1、单例模式(Singleton)的定义

单例对象的类只能允许一个实例存在。

  • 许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

单例的实现主要是通过以下两个步骤:

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
  2. 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引。

2、模式(Singleton)优缺点

单例模式是一种创建型设计模式。其主要优缺点如下:

优点:

  1. 在内存中只有一个对象,节省内存空间;
  2. 避免频繁的创建销毁对象,可以提高性能;
  3. 避免对共享资源的多重占用,简化访问;
  4. 为整个系统提供一个全局访问点。

缺点:

  1. 不适用于变化频繁的对象;
  2. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  3. 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

3、模式(Singleton)适用环境

  1. 需要生成唯一序列的环境
  2. 需要频繁实例化然后销毁的对象。

4、模式(Singleton)的结构

image-1649414161935

单例模式的主要角色如下:

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

5、模式(Singleton)的应用实例

5.1 静态内部类实现单例模式(Holder模式)

public class Singleton {
    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
     * 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
     */
    private static class SingletonHolder{
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static Singleton instance = new Singleton();
    }
    /**
     * 私有化构造方法
     */
    private Singleton(){
    }
    public static  Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

优点: 将懒加载和线程安全完美结合的一种方式(无锁),内部类由JVM保证线程安全性。

5.2 饿汉模式

原理:创建好一个静态变量,每次要用时直接返回。

// 类加载的方式是按需加载,且加载一次。。因此,在该单例类被加载时,就会实例化
// 一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会
// 被加载一次,因此只会创建一个实例,即能够充分保证单例。
public class SingletonHungry {
    private static SingletonHungry instance = new SingletonHungry();
    private SingletonHungry() {
    }
 
    public static SingletonHungry getInstance() {
        return instance;
    }
}

优点: 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点: 启动时即创建实例,启动慢,有可能造成资源浪费。

  • 在类装载的时候就完成实例化,没有达到Lazy Loading的效果,在类加载时进行创建会导致初始化时间变长。如果从始至终从未使用过这个实例,则会造成内存的浪费。

5.3 懒汉模式(饱汉模式) - 非线程安全

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

优点: 懒加载启动快,资源占用小,使用时才实例化,无锁。

缺点: 非线程安全(UnThreadSafe)。

5.4 懒汉模式(饱汉模式) - 线程安全

原理:延迟创建,在第一次用时才创建,之后每次用到就返回创建好的。

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

缺点: 由于synchronized的存在,多线程时效率很低。synchronized 为独占排他锁,并发性能差。即使在创建成功以后,获取实例仍然是串行化操作。

5.5 双重校验锁懒汉模式DCL(Double Check Lock)

原理:在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。

public class Singleton {
    // 使用valatile,使该变量能被所有线程可见
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){//初始化时,加锁创建
                // 为避免在加锁过程中被其他线程创建了,再作一次非空校验
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

优点: 线程安全;延迟加载;效率较高。

  • 注:实例必须有 volatile 关键字修饰,其保证初始化完全。

Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

  • 使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率。

5.6 枚举模式实现单例——将单例维护在枚举类中作为唯一实例

原理:定义枚举类型,里面只维护一个实例,以此保证单例。每次取用时都从枚举中取,而不会取到其他实例。

public enum  SingletonEnum {
    INSTANCE;

    public void anyFunction() {
    	System.out.println("enum singleton");
    }

    public static void main(String[] args) {
        Singleton.INSTANCE.anyFunction();
    }
}

优点:

  1. 使用SingletonEnum.INSTANCE进行访问,无需再定义getInstance方法和调用该方法。
  2. JVM对枚举实例的唯一性,避免了上面提到的反序列化和反射机制破环单例的情况出现:每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

原因:

  • 枚举类型在序列化时仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器禁止重写枚举类型的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

6、前5种模式(Singleton)的缺点

6.1 反序列化对象时会破环单例

反序列化对象时不会调用getXX()方法,于是绕过了确保单例的逻辑,直接生成了一个新的对象,破环了单例。

解决办法: 重写类的反序列化方法,在反序列化方法中返回单例而不是创建一个新的对象。

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     
 
   protected Singleton() {     
   }  
 
   //反序列时直接返回当前INSTANCE
   private Object readResolve() {     
            return INSTANCE;     
   }    
}

6.2 反射机制会破环单例

在代码中通过反射机制,直接调用类的私有构造函数创建新的对象,会破环单例。

解决办法: 维护一个volatile的标志变量在第一次创建实例时置为false;重写构造函数,根据标志变量决定是否允许创建。

private static volatile  boolean  flag = true;
private Singleton(){
    if(flag){
    flag = false;   //第一次创建时,改变标志
    }else{
        throw new RuntimeException("The instance  already exists !");
    }
}

7、总结

  • 尽量减少同步块的作用域;
  • 尽量使用细粒度的锁
0

评论区