设计模式之单例模式

设计模式之单例模式

(详细代码在后面)

单例模式的意思就是只有一个实例。

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。

在应用这个模式时,单例对象的类必须保证只有一个实例存在。

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

单例模式属于创建型模式的一种,应用于保证一个类仅有一个实例的场景下,并且提供了一个访问它的全局访问点,如spring中的全局访问点BeanFactory,spring下所有的bean都是单例。

单例模式有以下特点: 

1、单例类只能有一个实例。  a.私有构造方法 

2、单例类必须自己创建自己的唯一实例。  b.私有静态引用指向自己实例 ,私有静态全局变量

3、单例类必须给所有其他对象提供这一实例。  c.以自己实例为返回值的公有静态方法 ,公有静态方法

  

实现单例模式的思路是: 

  1. 一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);
  2. 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
  3. 同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。 

 

需要注意的地方: 

  • 单例模式在多线程的 应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例, 这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。

      解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。 

  • 只要有反射,任何类都不安全,任何private都是纸老虎,反射可以破坏单例。通过类名.class获得反射对象,

    通过 Constructor<类名> declaredConstructor = 类名.class.getDeclaredConstructor(null);获得空的构造函数,再通过declaredConstructor.setAccessible(true) 暴力破解私有权限,然后再类名 instance = declaredConstructor.newInstance() 创建实例。

       可以通过创建枚举类型的类来解决反射破坏单例的问题。

  • 成员属性和成员方法都必须定义成static为了保证在自身类中获取自身对象

  • getInstance() 方法 必须定义成punblic为了保证对外公开

  • 饿汉式,为什么线程安全? 

        单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。也就是说,线程每次都只能也必定只可以拿到这个唯一的对象饿汉式单例天生就是线程安全的。

  • 类被加载时就会实例化一个对象?

      因为静态资源在类被加载的时候就会加载

  • 为什么必须把构造函数设为私有函数?

       不能在别的类中来获取该类的对象,只能在类自身中得到自己的对象

  • 单例模式跟全局变量有什么区别,似乎也可以提供一个public的类变量和一个private的构造函数实现类似的功能?

     全局变量是一个变量,存在于某个具体的类中,而单例是一个对象,可以存在于整个应用。

  • instance为什么一定要是static的?

  1. 通过静态的类方法(getInstance) 获取instance,该方法是静态方法,instance由该方法返回(被该方法使用),如果instance非静态,无法被getInstance调用;
  2. instance需要在调用getInstance时候被初始化,只有static的成员才能在没有创建对象时进行初始化。且类的静态成员在类第一次被使用时初始化后就不会再被初始化,保证了单例;
  3. static类型的instance存在静态存储区,每次调用时,都指向的同一个对象。其实存放在静态区中的是引用,而不是对象。而对象是存放在堆中的。

  • 单例模式的构造方法为什么私有?
  1. 设置private以后,每次new对象的时候都要调用构造方法。而private的权限是当前类,那么其他类new对象的时候一定会失败。
  2. 设置成private是考虑封装性,防止在外部类中进行初始化,也就不是单例了。

单例模式VS静态类(静态属性/方法)

把类中所有属性/方法定义成静态也可以实现"单例"。 静态类不用实例化就可以使用,虽然使用比较方便,但失去了面向对象的一些优点,适用于一些过程简单且固定、不需要扩展变化、不需要维护任何状态的类方法,如java.lang.Math,里面每种计算方法基本都是固定不变的。那为什么需要用"NEW"单例模式,而不把类中所有属性/方法定义成静态的?
单例模式保证一个类对象实例的唯一性,有面向对象的特性,虽然扩展不容易,但还是可以被继承(protected权限的构造方法)、重写方法等。

单例模式中的单例对象会不会被垃圾回收

对于JDK1.2后的JVM HotSpot来说,判断对象可以回收需要经过可达性分析,由于单例对象被其类中的静态变量引用,所以JVM认为对象是可达的,不会被回收。
另外,对于JVM方法区回收,由堆中存在单例对象,所以单例类也不会被卸载,其静态变量引用也不会失效。

Spring(IOC框架)实现的单例

Spring的一个核心功能控制反转(IOC)或称依赖注入(DI):
高层模块通过接口编程,然后通过配置Spring的XML文件或注解来注入具体的实现类(Bean)。
这样的好处的很容易扩展,想要更换其他实现类时,只需要修改配置就可以了。通过IOC容器来实现,其默认生成的Bean是单例的(在整个应用中(一般只用一个IOC容器),只创建Bean的一个实例,多次注入同一具体类时都是注入同一个实例)
IOC容器来实现过程简述如下:
当需要注入Bean时,IOC容器首先解析配置找到具体类,然后判断其作用域(@Scope注解);
如果是默认的单例@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON),则查找容器中之前有没有为其创建了Bean实例;
如果有则直接注入该Bean实例,如果没有生成一个放到容器中保存(ConcurrentHashMap – map.put(bean_id, bean)),再注入。
注:其中解析配置查找具体类、生成Bean实例和注入过程都是通过Java反射机制实现的。

从上面可以了解到,Spring实现的单例和我们所说的单例设计模式不是一个概念:
前者是IOC容器通过Java反射机制实现,后者只是一种编程方法(套路)。
但总的来说,它们都可以实现“单例”。

优点: 

  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例 。
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  3. 提供了对唯一实例的受控访问。 
  4. 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  5. 允许可变数目的实例。
  6. 避免对共享资源的多重占用。 

缺点: 

  1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。 
  2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。 
  3. 单例类的职责过重,在一定程度上违背了“单一职责原则”。 
  4. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。 

适用场景: 

    单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:    

  1. 需要频繁实例化然后销毁的对象。 
  2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  3. 有状态的工具类对象。
  4. 频繁访问数据库或文件的对象。 

使用场景: 

  1. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
  2. 控制资源的情况下,方便资源之间的互相通信。如线程池等。 

应用场景举例: 

  1. 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 
  2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~ 
  3.  windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。 
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。 
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 
  6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 
  7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。 
  8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。 
  9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。 
  10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例. 

实现单利模式的原则和过程: 

单例模式原则:确保一个类只有一个实例,自行实例化并向系统提供这个实例 

单例模式分类:

  1. 饿单例模式(类加载时实例化一个对象给自己的引用),
  2. 懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式) 

单例模式要素: 

  1. 私有构造方法   ★★★核心要素
  2. 私有静态引用指向自己实例 
  3. 以自己实例为返回值的公有静态方法 

java静态资源(静态方法,静态属性)是程序一运行就加载到jvm中,还是当被调用的时候才进行加载呢?

java静态资源(静态方法,静态属性)是程序一运行就加载到jvm中的。

  1. 类中的静态属性会被加入到类对象(也可以叫做类的模板,是类的描述) 的构造器中,静态方法也会被加入到类对象中。
  2. 当第一次使用类时,JVM会通过类加载器,加载类对象,从而初始化静态属性,并装入类的方法,包括静态方法和实例方法(方法不会被调用,只是加载,从这个意义上来说,静态方法和实例方法是类似的)。
  3. 当创建类的实例对象时,JVM会调用类的构造器,从而初始化类的属性。

扩展资料:JVM 类加载机制JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

1、加载加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

2、验证这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3、准备准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

4、解析解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:CONSTANT_Class_infoCONSTANT_Field_infoCONSTANT_Method_info等类型的常量。

5、初始化初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。初始化阶段是执行类构造器<client>方法的过程。

一、饿汉式

1、普通饿汉式

该模式在类被加载时就会实例化一个对象。

该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)

/**
 * 1.饿汉式:线程安全,耗费资源
 */
class HugerSingleton1 {
    //该对象的引用不可修改
    private static final HugerSingleton1 ourInstance = new HugerSingleton1();
    public static HugerSingleton1 getInstance() {
        return ourInstance;
    }
    private HugerSingleton1() {}
}

优点 

    1.线程安全 

    2.在类加载的同时已经创建好一个静态对象,调用时反应速度快 

缺点 

    资源效率不高,可能getInstance()永远不会执行到,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化 

测试:

        HugerSingleton1 single = HugerSingleton1.getInstance();
        HugerSingleton1 single1 = HugerSingleton1.getInstance();
        System.out.println(single);
        System.out.println(single1);
        System.out.println(single.hashCode());
        System.out.println(single.hashCode());

输出:

返回相同的对象实例,hashcode值都是一样的,说明是同一个实例对象

为什么说饿汉式单例是线程安全的?

类加载的方式是按需加载,且只加载一次

因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。单例就是该类只能返回一个实例。

换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。

也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

2、饿汉式,在静态代码块实例对象

/**
 * 2.饿汉式:在静态代码块实例对象
 */
class HugerSingleton2 {
    private static HugerSingleton2 ourInstance;
    static {
        ourInstance = new HugerSingleton2();
    }
    public static HugerSingleton2 getInstance() {
        return ourInstance;
    }
    private HugerSingleton2() {}
}

java 静态代码块 静态方法区别

一般情况下,如果有些代码必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的,需要在项目启动的时候就初始化

在不创建对象的情况下,其他程序来调用的时候,需要使用静态方法,这种代码是被动执行的,静态方法在类加载的时候 就已经加载,可以用类名直接调用
比如main方法就必须是静态的 这是程序入口
两者的区别就是:静态代码块是自动执行的;
静态方法是被调用的时候才执行的

所以此处将对象实例,放在静态代码块中也可以

二、懒汉式

3、普通懒汉式

类加载时不初始化,该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)

/**
 * 3.懒汉式:非线程安全
 */
class Singleton1 {
    private static Singleton1 ourInstance;
    public static Singleton1 getInstance() {
        if (null == ourInstance) {
            ourInstance = new Singleton1();
        }
        return ourInstance;
    }
    private Singleton1() {
        // 可写可不写,只是为了测试懒汉式线程不安全的情况,输出线程的名字
        System.out.println(Thread.currentThread().getName() + "__ok");
    }
}

优点: 

避免了饿汉式的那种在没有用到的情况下创建事例,资源利用率高,不执行getInstance()就不会被实例,可以执行该类的其他静态方法。 

缺点: 

懒汉式在单个线程中没有问题,但多个线程同时访问的时候就可能同时创建多个实例,而且这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但是还是会存在拿到不同对象的情况。 假设当前有N个线程同时调用getInstance()方法,由于当前还没有对象生成,所以一部分同时都进入step 2,那么就会由多个线程创建多个多个user对象。

解决这个问题的办法就是加锁synchonized,第一次加载时不够快,多线程使用不必要的同步开销大。

测试:

//开10个线程去测试,懒汉式线程不安全的情况
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                Singleton1.getInstance();
            }).start();
        }

输出:

可见多个线程同时访问的时候,可能同时创建多个实例

解决办法:使用synchronized关键字。

经改造上面代码展示如下:

4、线程安全的懒汉式:加锁

/**
 * 4.线程安全的懒汉式:给方法加锁
 */
class Singleton2 {
    private static Singleton2 ourInstance;
    public synchronized static Singleton2 getInstance() {
        if (null == ourInstance) {
            ourInstance = new Singleton2();
        }
        return ourInstance;
    }
    private Singleton2() {}
}

公开访问点getInstance: public 和  synchronized

public保证对外公开,synchronized保证多线程时的正确性(因为类变量不是在加载时初始化的)

5、线程安全的懒汉式:DCL懒汉式,双重检查锁

/**
 * 5.线程安全的懒汉式:双重检查锁(同步代码块)
 * DCL懒汉式单例
 */
class Singleton3 {
    private volatile static Singleton3 ourInstance;
    public static Singleton3 getInstance() {
        if (null == ourInstance) {
            synchronized (Singleton3.class) {
                if (null == ourInstance) {
                    //不是一个原子性操作
                    //原子性是指一个操作是不可中断的. 即使是在多个线程一起执行的时候,
                    ourInstance = new Singleton3();
                    /**
                     *  1. 分配内存空间
                     *  2. 执行构造函数,初始化对象
                     *  3. 把这个对象指向这个空间
                     *
                     *  123   期望的情况
                     *  132   可能的情况,分配完内存空间后,先占用空间,再去构造函数,初始化对象
                     *  第一个线程A进来时,如果走成了132,是没有问题的
                     *  第二个线程B进来时,由于先执行3已经占用了这个空间,会认为ourInstance!=null,
                     *          直接返回已有的对象,但是因为还没有经过2,此时没有完成构造,返回的对象实例是一片虚无
                     *   这就是指令重排现象
                     *   保证ourInstance 避免CPU指令重排,必须要加volatile
                     *   volitile保证不发生指令重排,不保证原子性
                     */
                }
            }
        }
        return ourInstance;
    }

优点 

    资源利用率高,不执行getInstance()就不被实例,可以执行该类其他静态方法 

缺点 

    第一次加载时反应不快,由于java内存模型一些原因偶尔失败 ,如上注释

6、线程安全的懒汉式:静态内部类

/**
 * 6.线程安全的懒汉式:静态内部类(推荐)
 */
class Singleton4 {
    private Singleton4() {
    }
    public static Singleton4 getInstance() {
        return SingletonHolder.ourInstance;
    }
    private static class SingletonHolder {
        private static Singleton4 ourInstance = new Singleton4();
    }

}

优点 

    资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法 

缺点 

    第一次加载时反应不够快 

总结: 

    一般采用饿汉式,若对资源十分在意可以采用静态内部类,不建议采用懒汉式及双重检测

但是,即使加锁再安全,面对反射,也是不安全的

测试:

//破坏 Singleton2,3,4
        
         // 创建普通单例对象
         Singleton2 instance = com.so.Singleton2.getInstance();
         
         //通过反射破坏单例,通过反射创建实例对象
         //获得构造函数,因为是无参构造,所以传入null
         Constructor<Singleton2> declaredConstructor =  Singleton2.class.getDeclaredConstructor(null);
         //暴力破坏私有权限
         declaredConstructor.setAccessible(true);
         //创建实例对象
         Singleton2 instance2 = declaredConstructor.newInstance();

         System.out.println(instance);
         System.out.println(instance2);

输出:

输出hashCode

System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());

输出:

可见通过反射,确实创建了两个不同的实例对象

那么如何解决这个问题呢?

我们找到反射中,创建实例的方法

查看它的源码

发现,源码里面有讲,用getModifiers判断字段的修饰符,如果是public且类为enum就抛出异常。

也就是说,我们使用枚举创建单例模式的话,就不会被反射破坏

三、枚举

7、枚举

//  enum是一个什么,本身也是一个java类
enum Singleton5{
    INSTANCE;
    public Singleton5 getInstance(){
        return  INSTANCE;
    }
}

测试:

        Singleton5 instance1  = Singleton5.INSTANCE;
        Singleton5 instance2  = Singleton5.INSTANCE;
        System.out.println(instance1);
        System.out.println(instance2);

输出:

没问题,返回的确实是同一个对象

那么枚举到底能不能被反射破坏呢,我们来试试

测试:

        Singleton5 instance3  = Singleton5.INSTANCE;
        Constructor<Singleton5> declaredConstructor1 =  Singleton5.class.getDeclaredConstructor(null);
        declaredConstructor1.setAccessible(true);
        Singleton5 instance4 = declaredConstructor1.newInstance();

        System.out.println(instance3);
        System.out.println(instance4);

输出:

发现怎么输出的错误,不和我们源码里看到的一样呢??

错误里面说,没有一个空参的构造器

我们去target目录中,查看IDEA为我们生成的源码

注:target目录是:intellij IDEA默认的编译路径,就是存放class或者bai包文件的地方,是IDE自动生成的

发现没有问题啊,也是无参构造函数,所以传入null没毛病啊。这是为什么呢

我们真正执行后的结果是不会骗我们的,所以只要是出现空参构造了,就是在骗我们,那么这里IDEA骗了我们

我们通过反编译工具,将class文件反编译

依旧可以看到还是有一个空参构造

说明还是在骗我们。。。

我们继续将class文件再反编译回java文件

这次可以清楚的看到我们的源码的执行过程

所以说,最终执行是有参的。

那么我们根据源码,传入 string.class 和 int.class 获得有参的构造器

测试:

        Singleton5 instance3  = Singleton5.INSTANCE;
        Constructor<Singleton5> declaredConstructor1 =  Singleton5.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor1.setAccessible(true);
        Singleton5 instance4 = declaredConstructor1.newInstance();

        System.out.println(instance3);
        System.out.println(instance4);

输出:

成功!

参考如下:

为什么说单例模式的饿汉式是线程安全的?_jike11231的博客-CSDN博客_饿汉模式为什么是线程安全的

B站,某神视频

个人博客:wggz.top

个人微信公众号:后端开发充电宝

欢迎关注👏

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值