目录
引言
一、概述
1.概念:
2.单例模式有 3 个特点:
3.单例模式的优点和缺点:
单例模式的优点:
单例模式的缺点:
单例模式的应用场景:
单例模式的结构:
二、单例模式的实现
1.饿汉式单例
2.懒汉式单例
三、总结
引言
java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍两种:懒汉式单例、饿汉式单例。
一、概述
1.概念:
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
2.单例模式有 3 个特点:
-
单例类只有一个实例对象;
-
该单例对象必须由单例类自行创建;
-
单例类对外提供一个访问该单例的全局访问点。
3.单例模式的优点和缺点:
单例模式的优点:
单例模式的缺点:
-
单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
-
在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
-
单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
单例模式的应用场景:
对于 java来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。
-
需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
-
某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
-
某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
-
某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
-
频繁访问数据库或文件的对象。
-
对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
-
当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
单例模式的结构:
单例模式的主要角色如下。
结构如下:
二、单例模式的实现
Singleton 模式通常有两种实现形式。
1.饿汉式单例
饿汉式:在类加载时已经创建好该单例对象,等待被程序使用。
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
关于类加载,涉及到JVM的内容,我们目前可以简单认为在程序启动时,这个单例对象就已经创建好了。
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
代码如下:
public class HungrySingleton {private static final HungrySingleton instance = new HungrySingleton();private HungrySingleton() {}public static HungrySingleton getInstance() {return instance;}
}
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
2.懒汉式单例
懒汉式:在真正需要使用对象时才去创建该单例类对象。
懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。否则先执行实例化操作。
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:
public class LazySingleton {//使用volatile防止指令重排private static volatile LazySingleton instance = null; //保证 instance 在所有线程中同步private LazySingleton() {} //private 避免类在外部被实例化public static synchronized LazySingleton getInstance() {//使用synchronized上锁//getInstance 方法前加同步if (instance == null) {instance = new LazySingleton();}return instance;}
}
注意:如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
特别解释:
指令重排的问题。
创建一个对象,在JVM中会经过三步:
(1)为singleton分配内存空间
(2)初始化singleton对象
(3)将singleton指向分配好的内存空间
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。文字较为晦涩,可以看流程图:
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
三、总结
懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check(双重校验) + Lock(加锁),解决了并发安全和性能低下问题。
饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。