设计模式之单例模式

前言

设计模式一直都是我们程序语言中较为重要的代码优化套路
为了减少一些冗余代码,提高代码可复用性、可维护性、可读性、稳健性以及安全性。
设计是一种语法规范,是一种为了解决某种特点场景下的某个问题,而提出来的一系列方案。
设计模式一共分为23种,其中分为三种大类型;而我们今天所有讲的是创造型中的单例模式

什么是单例模式呢?

单例模式就是一个类中只存在一个实例对象,java中存在了多个单例的案例
比如:
一:ervlet就是一个单例模式,我们每次访问的时候都是同一个servlet,只会在第一次访问的时候才会生成
二: application也是一个单例文件,整个web中只存在这一个对象可以访问web.xml内容
三:我们使用的连接池也是一个单例模式
四:以及框架中的bean也是一个单例模式
     我们不难发现在java中多处使用到了单例模式,那它到底是有什么优点,才会让被多次使用到

单例模式的优点

一:整个类只存在一个对象,那么就代表单例模式减少了系统的消耗资源
二:由于只存在一个对象,所以它可以进行资源共享(比如application对象)

除了java中多处使用到单例模式之外,其实在我们的面试中呢,也会经常考到,基本都是要求手写一个单例模式出来,我相信大部分朋友都会写饿汉式或者懒汉式,但是在这两个模式现在已经烂大街的情况下,我们怎么样才能吸取面试官的眼光呢?下面我就来详细介绍一下单例模式的五种书写方法

单例模式注意点

一:整个类中只能创建一个对象,不能有其他因素去创建对象  ,所以构造方法需要私有化
二:当单例类中已经存在一个对象的时候,我们就不需要去创建对象,否则我们需要创建一个对象

单例模式的写法

一:饿汉式

    //直接创建对象
    private static SingletonType1 instance = new SingletonType1();
    //私有化构造器
    private SingletonType1(){
    }
    //提供公共方法访问
    public static SingletonType1 getInstance(){
        return instance;
    }
简要来说饿汉式就是一个饿汉,看到食物一上来就吃,但是在好久没吃东西的情况下,能直接狼吞虎咽吗?这肯定是由一点缺陷的

我们可以看到饿汉式一上来就创建了一个对象,那么什么时候会被创建出对象呢?
这就是我们类加载的多种场景了,比如,当调用类中方法的时候,反射,序列化等等情况都会引发类加载

那么我们在开发就可能无意识的导致了单例类被加载,这就会在无意识中消耗了系统资源,虽然一个对象对系统资源不会占用很多内存,但是这也是不可取的,我们希望的·是当我们需要使用的时候才让他创建对象,
这样会不会比较好呢?

饿汉式的缺点:
一:在无意识触发类加载的时候,创建了对象。且消耗了系统资源
二:无法做到延时加载 	
二:懒汉式
 //1.定义接受静态常量
    private static   SingletonType2  instanse = null;
    // 2.私有化构造器
    private SingletonType2(){}
    // 3.定义公共访问方法并在方法内部判断对象是否为null
    public static SingletonType2 getInstance()  {
        if(instanse == null){
                instanse = new SingletonType2();
                return instanse ;
        }
        return instanse;
    }
 

懒汉式和饿汉式的区别在于懒汉式需要在调用getInstance方法的时候才会创建对象
  优点:懒汉式做到的延时加载,且不会去无故浪费系统资源
  缺点:我们可以看到instanse 是一个共享变量,所以在单线程的情况下这样写法是没有一点问题的;
        但是在多线程的情况下毫无疑问会产生线程安全的问题

这是线程类中的run方法
class TestClass extends Thread{
    @Override
    public void run() {

        SingletonType2 instance = SingletonType2.getInstance();
        System.out.println(instance.hashCode());
    }
}
我创建了200个线程对象 并发访问
  for (int i = 0; i < 200; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            TestClass testClass = new TestClass();


            testClass.start();

        }
//在单例类中稍作了改变
 if(instanse == null){
            try {
                System.out.println(Thread.currentThread().getName());
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            instanse = new SingletonType2();
                return instanse ;
        }
        得出的结果是    	

述
这样的话无疑就破坏了我们单例模式的初衷

三:懒汉式同步

代码不变,在单例类中加了同步方法之后发现确实防止了单例,但是加同步之后却降低了效率,当每一个线程访问的时候,都需要等待,那么这样显然是不可行的;我们需要的是当实例对象生成之后,线程就不需要再进行同步了

缺点:虽然解决了线程并发所带来的的安全问题,但是大大降低了效率

  public static synchronized   SingletonType2 getInstance()  {

        if(instanse == null){
            try {
                System.out.println(Thread.currentThread().getName());
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            instanse = new SingletonType2();
                return instanse ;
        }
        return instanse;
    }
四:双重锁检式

双重锁优化了第三种单例方式,只有在第一次并发访问的时候需要进行同步,之后都不需要在进行同步

lass SingletonType3 {
    //1.定义接受静态常量
    private static volatile   SingletonType3 instanse = null;

    // 2.私有化构造器
    private SingletonType3() {
    }

    // 3.定义公共访问方法并在方法内部判断对象是否为null
    public static SingletonType3 getInstance() {
        //多线程情况下先进行第一步判断

        //第二次访问的时候不需要进行同步
        if (instanse == null) {
                //在第一次访问时可能多个线程执行到此行,这时候需要进行同步等待

                synchronized (SingletonType3.class) {
                    //只有第一个线程可以执行实例,其他线程都无法执行实例化

                    if (instanse == null) {

                        instanse = new SingletonType3();
                        return instanse;
                    }
                }
            }


        return instanse;
        }

    }
    优点:大大提高了效率
    缺点:代码太过繁琐
五:静态内部类单例模式
class SingletonType4 {
		  static class SingleClass{
		         private static final SingletonType4 instance=new SingletonType4();
		     }
		    private SingletonType4(){}
		    public static SingletonType4 getInstance(){
		        return SingleClass.instance;
		}

    }
  可以看到在外部类中存在了一个静态内部类,但是在加载外部类的时候内部类并不会被加载,除非在调用内部类中的静态方法
  优点一:实现了延迟加载
  优点二:不用担心线程安全问题,因为在类加载的时候是线程安全的 
 优点三:简化了代码的书写

注意: 虽然以上五种方式都可以实现单例模式,但是他们都有一个缺点
在特殊的场景中他们的单例都会被破坏掉
1.反射场景

//测试通过反射的对象
        Class<?> aClass = Class.forName("cn.itcast.singleton.SingletonType1");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Object o1 = declaredConstructor.newInstance(null);
        System.out.println("反射对象"+o1);
 //直接创建对象
    private static SingletonType1 instance = new SingletonType1();
    //私有化构造器
    private SingletonType1(){
     
    }
    //提供公共方法访问
    public static SingletonType1 getInstance(){
        return instance;
    }

在测试中发现生成的对象和反射后生成对象不一致,这就破坏了我们的单例、
解决办法:反射的时候,无非就是通过调用构造器来创建对象,那么我们可以再构造器中去写异常
判断了instance是否为空,如果不为空那就直接抛出异常,这样就解决了反射破坏单例的情况

 private SingletonType1(){
        //防止反射的破解单例
        if (instance!=null){
            throw new RuntimeException("对象已经实例化");
        }
    }

2.序列化场景
在我们使用反序列化的时候

 //测试反序列化的对象
        FileOutputStream fileOutputStream = new FileOutputStream("f://d.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance1);

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("f://d.txt"));
                objectOutputStream.writeObject(instance1);
        SingletonType1 o = (SingletonType1)objectInputStream.readObject();
        System.out.println("反序列化对象"+o);

结果
在这里插入图片描述
可以发现创建出来的单例对象与反序列的单例对象不一致

解决办法:单例类实现序列化接口
并定义一个方法
此方法是当文件在被反序列化的时候,直接将单例对象返回不需要再创建新对象 解决了反序列化破坏单列的问题

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

那这样呢就解决了反射和反序列化破坏单例的情况了,其实呢我们也可以不使用以上五种单例方式

六:枚举
public class Singleton5 {
    @Test
    public void  test() throws InterruptedException {
        SingletonType5 instance = SingletonType5.instance;
        SingletonType5 instance1 = SingletonType5.instance;


        System.out.println(instance==instance1);
    }
}
/*
  使用枚举也可以实现单例模式
    好处:枚举是天生的线程安全而且只会在枚举加载的时候加载
    注意:枚举不会被反射或者是反序列化破坏单例
 */
enum SingletonType5{
    instance;
}

由于枚举类实现单例,所以我们可以借由枚举类来完成单例
其次枚举类不会被反射,因为在反射检查中特地检查了枚举类型
第三
序列化一个枚举类的对象,调用的是继承的Enum的valueOf 方法T result = enumType.enumConstantDirectory().get(name);根据name去找存入的对象,所以不会生成多个对象。
所以我们也可以使用枚举

以上呢就是单例模式的几种方式,觉得博主写的有问题或者想要交流的童鞋们,可以评论与博主进行交流…
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值