单例模式-图文详解

概念

全世界就只要一个---在整个java程序中,只有这个类的一个实例

比如Student a = new Student(); 就是Student类只创建这一个实例,只能有这一个对象存在

主要解决:一个全局使用的类频繁地创建与销毁。在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)

缺点

没有接口,不能继承,与单一 职责原则冲突,一 个类应该只关心内部逻辑, 而不关心外面怎么样来实例化。

特点

单例模式有以下特点:

(1)单例类只能有一个实例;

(2)单例类必须自己创建自己的唯一实例;(例如 反射创建的实例就会破坏单例模式)

(3)单例类必须给所有其他对象提供这一实例。

关键代码

构造函数是私有的。

1、为了保证只有一个对象,不能new对象,所以设置构造方法私有。

2、只能通过方法或者属性获取对象,如果通过属性获取,这个属性是可以修改的,所以属性只能

是私有的。所以只能通过方法获取。

3、由于我们不能new对象,所以获取对象的方法定是静态的。属性也得是静态的,因为不是静态的,静态的方法访问不到

使用场景

· 1、要求生产唯一序列号。

· 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

· 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

枚举:对象固定,私有构造器

何时使用

当您想控制实例数目,节省系统资源的时候。

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

实现方式

单例模式的写法有好几种,主要有三种:懒汉式单例、饿汉式单例、登记式单例。

饿汉式和懒汉式区别:

(1)初始化时机与首次调用:

  • 饿汉式是在类加载时,就将单例初始化完成,保证获取实例的时候,单例是已经存在的了。所以在第一次调用时速度也会更快,因为其资源已经初始化完成。
  • 懒汉式会延迟加载,只有在首次调用时才会实例化单例,如果初始化所需要的工作比较多,那么首次访问性能上会有些延迟,不过之后就和饿汉式一样了。

(2)线程安全方面:饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,需要通过额外的机制保证线程安全

线程安全

访问速度

性能

饿汉式

安全

懒汉式

不安全

1.饿汉式

特点

  1. 在类加载后就提前创建好了唯一实例, 并不是用到时才创建,而是提前创建
  2. 因为 唯一实例是在静态代码块里面,静态代码的执行处于类生命周期中的初始化阶段,由虚拟机保证其原子且安全执行。所以不用考虑这里的线程不安全问题,所以,饿汉式 线程安全。
  3. 就是占内存,有性能损耗
public class Singleton1 implements Serializable {

    //1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
    private Singleton1() {
        //防止 反射强制获取到私有构造器 创建了实例对象 破坏了单例模式
        if(INSTANCE != null){
            throw new RuntimeException("单例对象不能重复创建");
        }
    }
    //2.静态的成员变量:用私有构造器创建出的唯一实例
    private static final Singleton1 INSTANCE = new Singleton1();

    //3.公共的静态方法:获得实例对象
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

反射会破坏掉单例模式

虽然你的构造方法是私有的,但是反射可以强制获取私有的构造方法,然后再用构造方法创建实例,这样实例就不是唯一的了,单例模式就被破坏了

解决方法

在构造器中加一个判断

        //防止 反射强制获取到私有构造器 创建了实例对象 破坏了单例模式
        if(INSTANCE != null){
            throw new RuntimeException("单例对象不能重复创建");
        }

 

2.懒汉式

需要的时候才会去创建对象

  1. 好处节省内存
  2. 坏处用的时候才创建稍微有点慢

例如下面代码,就是在调用getInstance()方法时才会创建

public class Singleton2 {
    //1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
    private Singleton2() {}
    //2.静态的成员变量:用私有构造器创建出的唯一实例
    //懒汉式 先设置null,调用的时候再创建
    private static Singleton2 single=null;
    
    //3.公共的静态方法:获得实例对象
    public static synchronized Singleton2 getInstance() {
        if (single == null) {
            single = new Singleton2();
        }
        return single;
    }
}

线程不安全

当创建了100个线程,调用getInstance方法时,可能会有多个线程同时看到没有new,就会执行多次new,调用多次构造方法(也就是会进行多次初始化)

解决方法

线程安全 给getInstance方法加synchronize锁

3.双重校验锁DCL--安全懒汉式

之前的 懒汉式单例中,为了解决线程不安全的问题,我们选择加锁,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的。

我们可以选择不给getInstance方法加synchronize锁,而是在这个方法里面去进行加synchronize锁,因为方法锁的范围太广,其他线程阻塞的范围就大,时间就长。

并且,我们加两个锁,在synchronized块外面加一个,里面加一个

之前的方式:锁加在方法上

    public static synchronized Singleton2 getInstance() {
        if (single == null) {
            single = new Singleton2();
        }
        return single;
    }

现在的方式:锁加在方法内部

public class Singleton3 {
    //1.私有的构造器:构造器不是私有的话,其他类会调用你的构造器,来创建实例对象,那就有可能有多个实例了,就不是单例模式了
    private Singleton3() {}
    //2.静态的成员变量:用私有构造器创建出的唯一实例
    private static volatile Singleton3 single=null;
    
    //3.公共的静态方法:获得实例对象    
    public static  Singleton3 getInstance() {
        if (single == null) {
            synchronized (Singleton3.class){
                if (single == null){
                    single = new Singleton3();
                }
            }
        }
        return single;
    }
}

为什么需要使用两个 if 进行判断呢?

我们加了两个锁,在synchronized块外面加一个,里面加一个

假设高并发下,线程A、B 都通过了第一个 if 条件。若A先抢到锁,new 了一个对象,释放锁,然后线程B再抢到锁,此时如果不做第二个 if 判断,B线程将会再 new 一个对象。使用两个 if 判断,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗。

为什么两个if判断null:

场景:有可能多个线程同时进入了第一个if,都读到对象null了,如果第一个线程加上锁创建了对象之后,释放锁之后,如果不进行再次判断null的话,就会再次进行创建对象(以第一个判断null为准),两次就是为了防止多次创建对象

第一个if:对象为null的时候才进入下面if进行创建对象,如果不为null就return

第二个if:判断在第一个为null的多线程情况下阻止多创建对象的。

示例:如果只有一个if,也就是上面的那个

 

 

 

为什么要加 volatile?

        volatile 解决共享变量的可见性问题、有序性问题,在这里主要是解决有序性问题

        原因:可能导致对象还没创建成功(只分配了空间地址,没有数据)就返回句柄,空指针异常)、static(不必创建对象)

        加了volatile,会使在赋值语句之后加上一个内存屏障,阻止之前的一些操作越过屏障,可以阻止代码或者说指令的重排序

        volatile 的作用主要是禁止指定重排序。假设在不使用 volatile 的情况下,两个线程A、B,都是第一次调用该单例方法,线程A先执行 singleton = new Singleton(),但由于构造方法不是一个原子操作,编译后会生成多条字节码指令,由于 JAVA的 指令重排序,可能会先执行 singleton 的赋值操作,该操作实际只是在内存中开辟一片存储对象的区域后直接返回内存的引用,之后 singleton 便不为空了,但是实际的初始化操作却还没有执行。如果此时线程B进入,就会拿到一个不为空的但是没有完成初始化的singleton 对象,所以需要加入volatile关键字,禁止指令重排序优化,从而安全的实现单例。

4.静态内部类懒汉式

两全其美的方式:既有 饿汉式 线程安全 的特点,又有 懒汉式 没有性能损耗的 特点

只要方法静态代码块里面,线程就是安全的,所以我们想办法将创建实例的操作放到静态代码块里面

内部类的特点

  1. 内部类可以访问外部类的私有变量和方法
  2. 内部类 是在静态代码块里面的,态代码的执行处于类生命周期中的初始化阶段,由虚拟机保证其原子且安全执行,也就是有饿汉式的优点:线程安全
  3. 并且内部类,你不用的时候不会加载,所以也有懒汉式的优点:性能高

利用了类加载机制来保证初始化 instance 时只有一个线程,所以也是线程安全的,同时没有性能损耗,这种比上面的方法都好一些,既实现了线程安全,又避免了同步带来的性能影响。

public class Singleton {  
    //静态内部类
    private static class LazyHolder {  
       private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
       return LazyHolder.INSTANCE;  
    }  
}  

JDK中的单例模式

这里参考CSDN博主「哪 吒」的原创文章

原文链接:【源码分析设计模式 1】JDK中的单例模式_jdk中单例模式_哪 吒的博客-CSDN博客

1、Runtime

Runtime类封装了Java运行时环境。每一个Java程序实际上都启动了一个JVM进程,那么每个JVM进程都对应一个Runtime实例,此实例由JVM为其实例化。每个Java应用程序都有一个Runtime实例,使应用程序能够与其运行的环境相连接。

由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。

2、java.awt.Toolkit

懒汉式单例。不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。因为很多时候并不常用Java的GUI和其中的对象。如果使用饿汉单例的话会影响JVM的启动速度。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值