Java设计模式之单例模式

在这里插入图片描述

单例模式

(1)单例模式定义

答案:
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡 的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个 Printer Spooler,以避免 两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。 总之,选择单例模式就是为了避免不一致状态。

(2)单例模式特点

答案:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。
  4. 单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。

(3)单例模式四大原则

答案:

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

(4)实现单例模式的方式

答案:
(1)饿汉式(立即加载);
(2)懒汉式(延迟加载);
(3)同步锁(解决线程安全问题):
(4)双重检查锁(提高同步锁的效率);
(5) 静态内部类;
(6)内部枚举类实现(防止反射攻击);

(5)单例模式常见应用场景

答案:

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式
  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  3. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作否则内容不好追加。
  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  7. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  8. Application 也是单例的典型应用(Servlet编程中会涉及到)
  9. 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  10. 在servlet编程中,每个Servlet也是单例
  11. 在spring MVC框架/struts1框架中,控制器对象也是单例
  12. 一个产品注册了一个商标,那么它就是单例的

(6) 单例模式的类型

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

(7)单例模式的优点

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

(8)常见的七种单例模式

1. 饿汉式

先创建后使用,线程安全,占用内存。代码如下:

/**
 * 饿汉式单例模式
 */
public class ClassA {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassA(){ }
    //2.在类的内部创建一个类的实例
    //类初始化时,立即加载这个对象(没有延时加载的优势)。加载类时,天然的是线程安全的!
    private static final ClassA instance = new ClassA();
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    //方法没有同步,调用效率高!
    public static ClassA  getInstance(){
        return instance;
    }

    //测试
    public static void main(String[] args) {
        ClassA a = ClassA.getInstance();
        ClassA b = ClassA.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述

2. 懒汉式

用的时候才创建,线程不安全,加锁会影响效率。资源利用率高了,但是,每次调用getInstance()方法都要同步,并发效率较低。代码如下:

/**
 * 懒汉式单例模式
 */
public class ClassB {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassB(){ }
    //2.在类的内部创建一个类的实例
    private static ClassB instance ;
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static synchronized ClassB  getInstance(){
        if(instance == null) {
            instance = new ClassB();
        }
        return instance;
    }

    //测试
    public static void main(String[] args) {
        ClassB a = ClassB.getInstance();
        ClassB b = ClassB.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述

3. 静态内部类方式

也是饿汉式和懒汉式的组合,调用getInstance()方法时才创建,达到了类似懒汉式的效果,同时又是线程安全的。代码如下:

/**
 * 使用静态内部类方式实现单例模式
 */
public class ClassC {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassC(){ }
    //2.在类的内部创建一个类的实例
    private static class Holder{
        private static ClassC instance = new ClassC();
    }
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static ClassC  getInstance(){
        return Holder.instance;
    }

    //测试
    public static void main(String[] args) {
        ClassC a = ClassC.getInstance();
        ClassC b = ClassC.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述

4. 枚举方法

线程安全,实现简单,调用效率高,不能延时加载。枚举本身就是单例模式,由JVM从根本上提供保障并且可以天然的防止反射和反序列化漏洞!需要继承的场景它就不适用了。枚举方式是Effective Java作者提倡的方式。代码如下:

/**
 * 使用枚举方法实现单例模式
 */
public enum ClassD {
    //定义一个枚举的元素,它就代表了Singleton的一个实例。
    INSTANCE;
    //对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public void otherMethod(){
        //功能处理
    }
    
    //测试
    public static void main(String[] args) {
        ClassD a = ClassD.INSTANCE;
        ClassD b = ClassD.INSTANCE;
        System.out.println(a==b);
    }
}

5.双重校验锁式

通常线程安全,加volatile的作用是禁止指令重排。(由于JVM底层内部模型原因,偶尔会出问题。不建议使用)代码如下:

/**
 * 使用双重校验锁来实现单例模式
 */
public class ClassE {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassE(){ }
    //2.在类的内部创建一个类的实例
    private volatile static ClassE instance; //volatile作用:保证多线程可以正确处理instance
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static ClassE  getInstance(){
        if(instance == null){ //检查实例,如果为空,就进入同步代码块
            synchronized (ClassE.class){
                if(instance == null){ //再检查一次,仍未空才创建实例
                    instance = new ClassE();
                }
            }
        }
        return instance;
    }

    //测试
    public static void main(String[] args) {
        ClassE a = ClassE.getInstance();
        ClassE b = ClassE.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述

6.使用ThreadLocal实现

线程安全,ThreadLocal采用以空间换时间的方式,为每一个线程都提供一份变量,因此可以同时访问而互不影响。代码如下:

/**
 * 使用ThreadLocal实现单例模式
 */
public class ClassF {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassF(){ }
    //2.在类的内部创建一个类的实例
    private static final ThreadLocal<ClassF> tls = new ThreadLocal<ClassF>(){
        @Override
        protected ClassF initialValue(){
            return new ClassF();
        }
    };
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static ClassF  getInstance(){
        return tls.get();
    }

    //测试
    public static void main(String[] args) {
        ClassF a = ClassF.getInstance();
        ClassF b = ClassF.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述

7.使用CAS锁来实现

(CAS锁(Compare and Swap):比较并交换,是一种有名的无锁算法,属于乐观锁)。用CAS锁来实现单例模式是线程安全的,代码如下:

/**
 * 使用CAS锁来实现单例模式
 */
public class ClassG {
    //1.私有化构造方法,使得在类的外部不能调用此方法,限制产生多个对象
    private ClassG(){ }
    //2.在类的内部创建一个类的实例
    private static final AtomicReference<ClassG> instance = new AtomicReference<ClassG>(); 
    //3.对外部提供调用方法:将创建的对象返回,只能通过类来调用
    public static final ClassG getInstance(){
        for(;;){
            ClassG current = instance.get();
            if(current != null){
                return current;
            }
            current = new ClassG();
            if(instance.compareAndSet(null,current)){
                return current;
            }
        }
    }

    //测试
    public static void main(String[] args) {
        ClassG a = ClassG.getInstance();
        ClassG b = ClassG.getInstance();
        System.out.println(a==b);
    }
}

控制台输出的结果如下图:
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值