一文拿下单例模式的七种写法

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式(线程不安全、加锁同步、双重检查)、静态内部类、枚举以及登记式单例五种实现方式。分析了各种方式的优缺点,推荐使用双重检查和静态内部类实现,同时也讨论了单例模式在分布式环境下的问题及其解决方案,并提供了JDK中单例模式的应用实例。此外,文章还提醒了单例模式在项目开发中的适用场景和注意事项。
摘要由CSDN通过智能技术生成

单例模式简介

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
比如Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够,这是就会使用到单例模式。

这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
  单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。

  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
在讲单例之前,要做一次基础知识的科普行动。大家都知道Java类加载器加载内容的顺序:
1、从上往下(Java的变量需要先声明才能使用)
2、静态后动态(对象实例化)(静态块和static关键字修饰在实例化以前分配内存空间)
3、先属性后方法(成员变量不能定义在方法中,只能定义在class下)
4. 类被加载器加载时时线程安全的,这个是jvm类加载器保证的

核心

我们在写项目的时候 单例我们其实用的很多甚至说是无处不在,但是我们为啥很多同学并不是很熟悉,一方面是因为太多了平时不怎么不关注,更重要的是被面试官吊打过,对他有心理阴影,感觉这个东西很难,甚至很深,今天看了我的这一篇文章让你解开单例的面纱,再也不怕面试官吊打
不要看这篇文章很长下面的三句话记住绝对很简单:
1. 只有private构造方法,确保外部无法实例化;
2. 通过private static变量持有唯一实例,保证全局唯一性;
3. 通过public static方法返回此唯一实例,使外部调用方能获取到实例。

当你理解了上面的三句话还管他集中写法吗? 爱几种几种,只是不同的变种而已,

public class Singleton {
    //1.构造函数私有化
    private Singleton(){}
    //2. 静态字段引用唯一实例:
    private static final Singleton INSTANCE=new Singleton();
}

这是完成了上面的的三步中的前两步,但是我们如何给外部提供对象那,你说使用一个静态方法可以吗?或者吧INSTANCE变量直接privat修改成public暴露出去可以吗完全没问题只要你能提供就可以了
在这里插入图片描述
完全没问题吧,但是我们项目中还是更多的是使用静态方法的方式比较多写,就是一个习惯,或者说这个实例并不是很容易创建,所以还是方法比较合适;

public class Singleton {
    //1.构造函数私有化
    private Singleton(){}
    //2. 静态字段引用唯一实例:
    private static final Singleton INSTANCE=new Singleton();
    //3.通过静态方法来获取
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

class test1{
    public static void main(String[] args) {
//        System.out.println(Singleton.INSTANCE.hashCode());
//        System.out.println(Singleton.INSTANCE.hashCode());
//        System.out.println(Singleton.INSTANCE.hashCode());
//        System.out.println(Singleton.INSTANCE.hashCode());
        System.out.println(Singleton.getInstance().hashCode());
        System.out.println(Singleton.getInstance().hashCode());
        System.out.println(Singleton.getInstance().hashCode());
        System.out.println(Singleton.getInstance().hashCode());
    }
}

项目当中就使用这种方式即可,到现在很多同学还不行估计,可能认为你不会就别瞎忽悠了,不会说不会的吧…我现在有一百个不服气不信就写写看看几个方式

饿汉

上面的写法就是平时说的饿汉,不管你是通过静态变量还是静态方法都算一种吧.既然说有那么多种写法我们就得对比这说是优缺点吧

优缺点说明:

  1. 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
  2. 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  3. 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类
    装载,这时候初始化instance就没有达到lazy loading的效果
  4. 结论:这种单例模式可用,可能造成内存浪费

简单说: 线程安全,内存有可能浪费
我们写的代码程序一次都不用的概率不高,不然不白写了,这也是我推荐的写法的理由,简单,安全安全

懒汉四种写法

饿汉就是针对懒汉内存有可能浪费进行的改良,啥意思,就是类初始化的时候不进行创建,在第一次使用的时候进行创建,比较简单:

第一种写法: 错误的写法不能使用

public class Singleton1 {
    private Singleton1() {
    }
    public static Singleton1 singleton1 = null;

    public static Singleton1 getInstance(){
            singleton1=new Singleton1();
        }
        return singleton1;
    }
}

单线程是完全没问题的
在这里插入图片描述
从下图可以看出来多线程不安全,因为不是单例的
在这里插入图片描述
那怎么着啊,那就改成线程安全的吧,不能因为这个小问题就把我们难住吧,走起最简单的就是加一个synchronized

第二种写法 在getInstance方法上加同步synchronized,效率低不推荐使用

public class Singleton2 {

    // 1. 构造方法私有化
    private Singleton2() {
    }
//2、然后声明一个静态变量保存单例的引用
    public static Singleton2 singleton1 = null;

    //3.静态方法获取实例
    不安全的
    public static synchronized Singleton2 getInstance(){
        if (singleton1==null){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singleton1=new Singleton2();
        }
        return singleton1;
    }
}

多线程也是安全的了吧,但是吧我们都知道synchronized是重量锁,这样吞吐量又不高了,我们怎么解决这个吞吐量的问题
在这里插入图片描述

  1. 解决了线程不安全问题
  2. 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低
  3. 结论:在实际开发中,不推荐使用这种方式

第三种 双重检查,推荐使用

我们知道可以使用静态块解决上面每次都进行同步的过程,

public class Singleton3 {
    private Singleton3() {
    }
//2、然后声明一个静态变量保存单例的引用
    public static Singleton3 singleton1 = null;

    //3.静态方法获取实例
    不安全的
    public static volatile  Singleton3 getInstance(){
        if (singleton1==null){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Singleton3.class){
                  singleton1=new Singleton3();
            }

        }
        return singleton1;
    }
}

在这里插入图片描述
多线程下我们发现并不是单例模式,

  1. 这种方式,本意是想对上面实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块
  2. 但是这种同步并不能起到线程同步的作用。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,
    另一个线程也通过了这个判断语句,这时便会产生多个实例
  3. 结论:在实际开发中,不能使用这种方式
    说白了这个就是错误的写法我们改良成下面的方案:
public class Singleton3 {

    // 1. 构造方法私有化
    private Singleton3() {
    }
//2、然后声明一个静态变量保存单例的引用
    public static  volatile Singleton3 singleton1 = null;

    //3.静态方法获取实例
    不安全的
    public static  Singleton3 getInstance(){
        if (singleton1==null){
            synchronized (Singleton3.class){
                if (singleton1==null){
                    singleton1=new Singleton3();
                }
            }
        }
        return singleton1;
    }
}

优缺点说明:

  1. Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两
    次if (singleton == null)检查,这样就可以保证线程安全了。
  2. 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),
    直接return实例化对象,也避免的反复进行方法同步.
  3. 线程安全;延迟加载;效率较高
  4. 结论:在实际开发中,推荐使用这种单例设计模式

第四种 静态内部类应用实例 推荐使用

public class Singleton4 {
    private Singleton4() {
    }

    public static final Singleton4 getInstance(){
           return SingletonInstance.INSTANCE;

    }

    static  class  SingletonInstance{
        public static final Singleton4 INSTANCE=new Singleton4();
    }
}

优缺点说明:

  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  2. 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  4. 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
  5. 结论:推荐使用.

枚举 (没见过)

实现Singleton的方式是利用Java的enum,因为Java保证枚举类的每个枚举都是单例,所以我们只需要编写一个只有一个枚举的类即可:

public enum  Singleton5{
     // 唯一枚举:
    INSTANCE;

    private String singleton5;

     public String getSingleton5() {
         return singleton5;
     }

     public void setSingleton5(String singleton5) {
         this.singleton5 = singleton5;
     }
 }

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。
这种方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我从来没这么写过,也没见过这么写的代码

登记式单例(1种写法,不常用)

public class Singleton6 {
    private static Map<String,Singleton6> map = new HashMap<String,Singleton6>();
    static{
        Singleton6 single = new Singleton6();
        map.put(single.getClass().getName(), single);
    }
    //保护的默认构造子
    protected Singleton6(){}
    //静态工厂方法,返还此类惟一的实例
    public static Singleton6 getInstance(String name) {
        if(name == null) {
            name = Singleton6.class.getName();
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton6) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
}

登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

总结

懒汉式单例跟饿汉式单例的根本区别
从名字上来说,饿汉和懒汉,
饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,
而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
另外从以下两点再区分以下这两种方式:
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
至于1、2、3这三种实现又有些区别,
第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,
第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗
第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

分布式环境下的单例

有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
对第一个问题修复的办法是:

private static Class getClass(String classname) throws ClassNotFoundException {   
	ClassLoader classLoader = Thread.currentThread().getContextClassLoader();   
	
	if(classLoader == null){
		classLoader = Singleton.class.getClassLoader();   
	}
	return (classLoader.loadClass(classname));   
}

对第二个问题修复的办法是:

public class Singleton implements java.io.Serializable {   
	   public static Singleton INSTANCE = new Singleton();   
	   protected Singleton() {  }   
	   private Object readResolve() {   
		   return INSTANCE;   
	   }
}

单例模式在JDK 应用的源码分析

在这里插入图片描述
jdk也是直接使用的饿汉,我非常推荐的也是这个简单好用

单例模式注意事项和细节说明

  1. 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需
    要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使
    用new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或
    耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数
    据库或文件的对象(比如数据源、session工厂等)

我们在web项目中更多的是使用spring,将类依赖spring容器管理,这样获取到的都是单例的

@Component // 表示一个单例组件

从上面看过了,除了不能用的,不常用的,不推荐的之外好像也没多少比较好用的了,平时我在项目中使用的比较多的就是懒汉,和静态内部类,同学们你都明白了,你的项目中使用多哪几种方式,欢迎评论区说下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值