设计模式学习(1)单例模式

单例模式是指这个类只能产生一个实例,而且自行实例化并向整个系统提供这个实例
为了避免在类外部被创建其他实例,这个类的构造函数被私有化了,外部无法通过构造函数new 实例
单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
————————————————
版权声明:本文为CSDN博主「炸斯特」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jason0539/article/details/23297037

单例的四大原则:

1.构造私有。
2.以静态方法或者枚举返回实例。
3.确保实例只有一个,尤其是多线程环境。
4.确保反序列换时不会重新构建对象。

我们常用的单例模式有:
饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式

饿汉模式

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,
恶汉模式,就是生怕没有,提前先创建好实例

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要判断了,节省了运行时间。

/**
 * Created by 此生辽阔 on 2021/4/11 12:20
 */
public class Singleton {
    private static final Singleton singleton=new Singleton();
    private Singleton() {//构造函数私有化,防止外部创建类的实例
    }
    public  static Singleton  getInstance()//用static修饰,这样外部就能通过类名来调用 getInstance()函数得到预先创建的类的实例
    {
        return singleton;
    }
    private void say()//用static 修饰可以直接用类名来调用
    {
        System.out.println("我是单例"); //通过实例singleton调用该方法
    }
}

懒汉模式

只有当调用getInstance的时候,才会去初始化这个单例。
懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

class Singleton2{
    private static  Singleton2 singleton2=null;//注意不能加final关键字,不然那后面不能再给singleton2赋值
    private Singleton2() {
    }
    public static Singleton2 getInstance()
    {
        if(singleton2==null)
        {
            singleton2=new Singleton2();
        }
        return singleton2;
    }
}

使用静态内部类实现单例模式

public class Singleton {
	private static class SingletonHoler {
		/**
		 * 静态初始化器,由JVM来保证线程安全
		 */
		private static Singleton instance = new Singleton();
	}
 
	private Singleton() {
	}
 
	public static Singleton getInstance() {
		return SingletonHoler.instance;
	} 
}

当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
————————————————
版权声明:本文为CSDN博主「IT_搬运工」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/fd2025/article/details/79711198

静态内部类的单例模式与饿汉式单例模式有点类似,唯一的不同在于,饿汉式单例模式中单例对象是随着Single2类加载而生成,而静态内部类单例模式则通过静态内部类产生单例对象,其利用静态内部类不会随着外部类的加载而加载的特性使得当getInstance方法被调用后Single4Inner类才会被加载,从而生成SINGLE4对象,同时用static和final修饰,保证只会生成一个SINGLE4对象,保证了其线程的安全。
————————————————
版权声明:本文为CSDN博主「Starry-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/nobody_1/article/details/85415892

枚举实现的单例模式

嗯MMMM,先知道有这种方式吧,有空再深究

枚举单例模式

public enum Singleton {
    INSTANCE;
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上

枚举类隐藏了私有的构造器。
枚举类的域 是相应类型的一个实例对象
那么枚举类型日常用例是这样子的:

public enum Singleton  {
    INSTANCE 
 
    //doSomething 该实例支持的行为
      
    //可以省略此方法,通过Singleton.INSTANCE进行操作
    public static Singleton getInstance() {
        return Singleton.INSTANCE;
    }
}

枚举单例模式在《Effective Java》中推荐的单例模式之一。但枚举实例在日常开发是很少使用的,就是很简单以导致可读性较差。
静态内部类单例模式,即保证线程安全又保证唯一性。
众所周知,单例模式是创建型模式,都会新建一个实例。那么一个重要的问题就是反序列化。当实例被写入到文件到反序列化成实例时,我们需要重写readResolve方法,以让实例唯一。

private Object readResolve() throws ObjectStreamException{
        return singleton;
}

作者:GitCode8
链接:https://www.jianshu.com/p/3bfd916f2bb2
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

利用这种方式实现的单例模式,不仅可以解决线程的同步问题,还可以防止反序列化。
Java的反射机制是可以把Class文件载入到内存,然后new出一个对象的,这也就意味着,在其他七种方式中,其他方法得到了实例对象后,就可以根据其Class,反序列化得到另外的对象。
而枚举类则不同,Java里的枚举类是没有构造方法的,所以无法根据枚举单例new出其他的对象。
原文链接:https://blog.csdn.net/Jarvenman/article/details/100136562

饿汉式以及懒汉式中的双重检查式、静态内部类式都无法避免被反序列化和反射生成多个实例。而枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反序列化和反射的破坏。

破坏单例模式的三种方式

  • 反射
  • 序列化
  • 克隆

解决方案如下:
1、防止反射
定义一个全局变量,当第二次创建的时候抛出异常
2、防止克隆破坏
重写clone(),直接返回单例对象
3、防止序列化破坏
添加readResolve(),返回Object对象

单例模式的应用

单例模式的优点

  • 单例模式在内存中只有一个实例,减少了内存开支
  • 通过在应用启动时产生一个单例对象,然后用永久驻留内存的方式解决一个对象的产生需要比较多的资源(读取配置,产生其他依赖对象)的问题,减少系统性能开销
  • 单例模式可以在系统设置全局访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理

单例模式的缺点

  • 单例模式一般没有接口,因为单例模式要求自行实例化,而接口和抽象类是不能实例化的,所以单例模式扩展比较困难
  • 单例模式对设计不利,在并行开发环境中,如果单例模式没有完成,是不能进行测试的,,没有接口也不能使用mock方式虚拟一个对象
  • 单例模式与单一职责原则冲突,一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是单例要取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中

单例模式的使用场景

单例模式只应在有真正的 “单一实例” 的需求时才可使用,比如

  • 要求生成唯一序列号的环境
  • 在整个项目中需要一个共享访问点或共享数据,比如,web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的
  • 创建一个对象需要消耗的资源过多,如要访问I/O和数据库等资源
  • 需要定义大量的静态常量和静态方法(比如工具类)
  1. 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
  3. 单例模式 使用的场景:需要 频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级
    对象),但又经常用到的对象、 工具类对象、频繁访问数据库或文件的对象(比如 数据源、session 工厂等)
    ————————————————
    版权声明:本文为CSDN博主「IT_搬运工」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/fd2025/article/details/79711198

单例模式的注意事项

高并发情况下,懒汉模式的单例容易出现线程同步的问题,容易产生多个实例

比如线程A访问时,发现singleton2为null,于是它执行singleton2=new Singleton2();但是对象初始化是需要时间的,现在A还没有获得对象,B线程又来访问,此时singleton2依旧为null,于是会创建一个singleton2,这样,线程A和线程B都获得了一个对象,在内存中就出现了这个类的两个实例

什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
————————————————
版权声明:本文为CSDN博主「炸斯特」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jason0539/article/details/23297037

解决线程不安全的方法

  • 给getInstance()加上synchronized关键字
    在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的

public static synchronized Singleton getInstance() {
         if (single == null) {  
             single = new Singleton();
         }  
        return single;
        }
  • 在getInstance()内部加上synchronized关键字

双重检查锁定
在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

public static Singleton getInstance() {
        if (singleton == null) {  
            synchronized (Singleton.class) {  
               if (singleton == null) {  
                  singleton = new Singleton(); 
               }  
            }  
        }  
        return singleton; 
    }

思考:为什么要判断两次null?

如果没有外层的check,相当于给整个getInstance()方法加上了synchronized关键字,也就是每次获取单例对象都要获取class对象的monitor,monitor是粒度较大的的锁,开销较大。所以外层的判断目的是:第一次获取单例对象后,再次获取该单例对象无需进行同步
如果没有内层的check,假如有两个线程,线程1和线程2同时进入外层判断,即第8步,线程1获得对象锁,进入同步代码块并初始化对象后,释放对象锁,返回单例对象结束了,线程1获取对象锁进入同步代码块后又再次初始化了instance对象,导致多线程下单例模式的非线程安全;
原文链接:https://sg.jianshu.io/p/502e07012750

注意:单例对象需要加volatile

 private static  volatile Singleton2 singleton2=null;

volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢?

假设没有关键字volatile的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行instance = new Instance(),该构造方法是一个非原子操作,编译后生成多条字节码指令,由于JAVA的指令重排序,可能会先执行instance的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后instance便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B进入,就会看到一个不为空的但是不完整(没有完成初始化)的Instance对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例
————————————————
版权声明:本文为CSDN博主「炸斯特」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jason0539/article/details/23297037

在程序里new一个对象的时候,其实经历了几个不同的步骤。下面的图展示了Object o = new Object()编译为class文件的几条指令:
在这里插入图片描述
可以看到,哪怕是简单的一个Object o = new Object(),在jvm底层也是分为好几条不同的指令来运行的,其中大致包括这么三个步骤:
1:申请一块内存,其中成员变量赋默认值
2: 调用类的构造方法,给成员变量赋初始值
3: 将这块内存区域赋值给栈的相应变量,简单理解就是把new出来的这个Object赋值给o
但是如果就这么运行下去,按理来说也是不会有问题的,问题就出在jvm底层在某些时候存在“指令重排”的问题,也就是说,这三个步骤的2,3两步是可能调换的。
如果2,3两步发生指令重排,在高并发的情况下就会出现这样的情况
线程A在new Singleton()的时候指令重排,给成员变量赋默认值后直接将Singleton对象赋值给了INSTANCE,正在准备执行第二步,即调用类的构造方法,给成员变量赋初始值
此时,线程B调用getInstance()方法,判断INSTANCE == null,由于此时Object已经赋值给了o,所以INSTANCE不为空,直接使用, 然而其实此时INSTANCE才仅是半初始化状态而已,如果线程B拿着这个实例对象,显然是会出现问题的
下面说回volatile关键字,volatile关键字主要有两个作用,第一是保证变量线程之间的可见性,这一点不用多说,第二点就是防止指令重排,在private static /volatile/ Singleton INSTANCE;这句话中加上volatile关键字,可以防止上述现象的出现,从而保证线程安全。
————————————————
版权声明:本文为CSDN博主「炖冻豆腐」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Jarvenman/article/details/103969667

  • 使用饿汉式单例模式
  • 不实现Cloneable接口(若实现了Cloneable接口,并实现了clone方法,则可以直接通过对象复制的方式创建一个新的对象,对象复制不是调用构造函数,因此单例模式中,构造器虽然私有化了,对象仍可以被复制)

单例模式的扩展(有上限的多例模式)

应用场景:读取文件,在内存中启动固定数量的reader实例,染后在需要读取文件时就可以快速响应
要求一个类只能产生2个或者3个实例怎么实现?

一个可以产生两个实例的皇上类

class Emperor{
    //定义最多能产生的实例数量
    private  static int maxNumOfEmperor=2;
    //每个皇帝都有名字,使用一个ArrayList来容纳,用static修饰,作为类变量
    private  static ArrayList<String>nameList=new ArrayList<>();
    //定义一个列表,容纳所有的皇帝实例
    private static ArrayList<Emperor>emperorList=new ArrayList<>();
    //当前皇帝序列号
    private static int countNumOfEmperor=0;

    private Emperor(String name) {
        nameList.add(name);
    }

    //产生所有的对象,静态代码块会比构造函数先加载
    static {
        for (int i = 0; i < maxNumOfEmperor; i++) {
            emperorList.add(new Emperor("皇帝" + i));
        }
    }
     //随机获得一个皇帝对象
     public static Emperor getEmperor()
        {
            Random random=new Random();
            countNumOfEmperor=random.nextInt(maxNumOfEmperor);
            return emperorList.get(countNumOfEmperor);
        }

        public static void say()
        {
            System.out.println(nameList.get(countNumOfEmperor));
        }
    }

臣子类访问皇帝类

class Minister{

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Emperor emperor=Emperor.getEmperor();
            System.out.print("第"+(i+1)+"个大臣访问的是");
            emperor.say();
        }
    }
}

在这里插入图片描述

也可以不用ArrayList存放名字,就用一个name属性也行的,如果把name设置成私有的,就需要提供get方法

class Emperor{
    //定义最多能产生的实例数量
    private  static int maxNumOfEmperor=2;
    //每个皇帝都有名字
    private String name;
    //定义一个列表,容纳所有的皇帝实例
    private static ArrayList<Emperor>emperorList=new ArrayList<>();
    private Emperor(String name) {    
        this.name=name;
    }

    public String getName() {
        return name;
    }
    //产生所有的对象,静态代码块会比构造函数先加载
    static {
        for (int i = 0; i < maxNumOfEmperor; i++) {
            emperorList.add(new Emperor("皇帝" + i));
        }
    }
     //随机获得一个皇帝对象
     public static Emperor getEmperor()
        {
            Random random=new Random();
            return emperorList.get(random.nextInt(maxNumOfEmperor));
        }
    }
class Minister{
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Emperor emperor=Emperor.getEmperor();
            System.out.println("第"+(i+1)+"个大臣访问的是"+emperor.getName());
        }
    }
}

在这里插入图片描述

单例模式的最佳实践

在Spring中,每个Bean默认就是单例的,优点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建,什么时候销毁,销毁的时候要如何处理
如果采用非到哪里模式(Prototype类型)则Bean初始化后管理交给J2EE容器,Spring容器不再跟踪Bean的生命周期

推荐阅读

《设计模式之禅》
JAVA设计模式之单例模式
The “Double-Checked Locking is Broken” Declaration
为什么单例模式中的Double Check要加volatile

Java设计模式——单例模式(Singleton)

volatile在double check中的意义

java 单例模式的几种实现方式

深入理解单例模式:静态内部类单例原理
设计模式之单例模式
你知道吗?枚举单例模式是世界上最好的单例模式!!!
菜鸟教程单例模式
深入理解设计模式(一):单例模式
漫画:什么是单例模式?
单例模式五种实现

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值