Java设计模式-单例模式
单例模式定义:它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
单例模式主要分为两大类:饿汉式和懒汉式,懒汉式又分为 单线程下的普通懒汉式,多线程下的双重校验锁、静态内部类、枚举。
单例模式的应用场景举例:
配置管理器:当应用程序需要读取和维护配置信息时,通常会使用单例模式来创建一个配置管理器。这保证了整个应用中只有一份配置数据副本,所有组件都引用同一个配置对象,从而避免了数据不一致的问题。
日志记录器:单例模式确保在整个应用程序中只有一个日志记录器实例。这样,所有的日志消息都可以统一地被这个实例处理。
单例模式确保在整个应用程序中只有一个日志记录器实例。这样,所有的日志消息都可以统一地被这个实例处理。例如,在 Java 中,java.util.logging.Logger类可以通过单例模式来实现。开发人员可以通过Logger.getLogger(\”name\”)方法获取一个唯一的日志记录器实例,所有对该实例的调用都可以保证日志的一致性,比如记录不同模块的信息到同一个日志文件中。
缓存:缓存数据通常需要全局唯一的实例来管理,以便在不同的地方访问和修改缓存时保持一致性。
单例模式可以用于实现缓存。一个单例的缓存实例可以存储经常访问的数据,在整个应用程序享。例如,一个 Web 应用中的用户权限缓存。当用户登录后,系统会查询数据库获取用户的权限信息并存储在单例缓存中。在用户后续的操作中,如访问某个需要权限验证的页面时,系统可以直接从这个单例缓存中获取用户权限信息,而不是每次都去查询数据库,大大提高了系统的响应速度。
数据库连接池:
数据库连接是一个昂贵的操作,频繁地创建和销毁连接会消耗大量的系统资源。因此,通常会使用连接池来管理数据库连接,而连接池本身往往是一个单例,以确保在整个应用中只有一个连接池实例,优化数据库访问性能。
线程池:
当处理大量并发任务时,为每个任务创建一个新线程会导致系统资源耗尽。线程池可以预先创建一组线程,任务到来时,将任务分配给线程池中的线程来执行,任务执行完毕后线程可以被复用。
单例模式适用于线程池的实现。整个应用程序通常只需要一个线程池来管理线程资源。例如,在 Java 的ExecutorService框架中,通过Executors.newFixedThreadPool(n)创建一个固定大小的线程池,这个线程池可以作为一个单例存在于应用程序中。所有需要异步执行的任务都可以提交给这个单例线程池,它可以有效地管理线程的创建、分配和回收,提高系统的并发处理能力。
单例模式的优点:
- 控制实例数量:确保一个类只有一个实例,避免了实例的重复创建,节省系统资源。
- 全局访问点:提供一个全局访问点,使得访问该实例变得简单。
- 延迟加载:某些实现方式(如懒汉式单例)可以实现延迟加载,即在需要时才创建实例,从而提高系统性能。
- 资源共享和节省:单例模式确保在整个应用程序中只有一个实例存在。对于一些资源密集型的对象,如数据库连接池、线程池等,这可以避免创建多个相同的对象,从而节省系统资源。
(一)单例模式-饿汉式java样例
(二)单例模式-单线程下-懒汉式java样例
(三)单例模式-多线程下-双重校验锁java样例
(四)单例模式-静态内部类
(五)单例模式-枚举
Java单例模式详解
单例模式指的是一个类,在全局范围内(整个系统中)有且只能有一个实例存在。即该类本身负责提供一种访问其唯一对象的方式,不对外提供公共的构造函数(禁用默认公共构造函数),对于该类的实例化由它自己在类的内部进行维护。
Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)
单例模式关系图
Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())。
单例模式通用代码:
单例模式优缺点:
优点
● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
缺点
● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。
● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
单例模式的实用场景
在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:
● 要求生成唯一序列号的环境;
● 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
● 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
● 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
单例模式需要注意的两个地方:线程安全和对象复制。
线程安全问题,在高并发情况下创建单例的过程中,可以根据单例模式的基础代码可以看出来在返回对象之前先会判断这个对象是否已经被创建,当并发发生在return之前,判断条件(singleton == null)的值是true,这样就会生成两个对象,所以是线程不安全的。解决办法,可以在getSingleton方法前加synchronized关键字,也可以在getSingleton方法内增加synchronized来实现。
对象的复制情况。在Java中,如果调用Cloneable接口,并实现了clone方法,则可以直接通过对象复制方式创建一个新对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。解决该问题的最好方法就是单例类不要实现Cloneable接口,单例模式类复制的实现场景也是极少的。
单例模式也是最常见的一种设计模式,spring框架中Bean的管理默认是采用单例模式,由spring框架管理Bean的生命周期。
单例模式的实用方式
1、枚举(线程安全)
这是实现单例模式的最佳方法,更加简洁,自动支持序列化,杜绝防止多次实例化,非常高效!(强烈推荐使用)
2、内部静态类(线程安全)
这种方式可以达到跟双重校验锁一样的效果,但只适用于静态域的情况,双重校验锁可在实例域需要延迟初始化时使用。
3、饿汉模式(线程安全)
该方式虽然简单也安全,但是会造成再不需要实例时,产生垃圾对象,造成资源浪费,因此,一般不使用。
4、懒加载(线程不安全)
以上方式,如果存在多个线程同时访问getInstance()时,由于没有锁机制,会导致实例化出现两个实例的情况,因此,在多线程环境下时不安全的。
5、懒加载(线程安全)
在getInstance()方法上添加了同步锁。但是该方法虽然解决了线程安全的问题,但却也带来了另外的一个问题,就是每次获取对象时,都要先获取锁,并发性能很差,还需要继续优化!
6、双重校验(线程安全)
该方法将方法上的锁去掉了,避免了每次调用该方法都要获取锁的操作,从而提升了并发性能,同时在方法内部使用锁,进而解决了并发的问题,从而解决了上面并发安全+性能低效的问题,是个不错的实现单例的方式。
关于单例模式,不仅要懂得它的实现形式,也是需要学会手写单例的,很多公司在面试过程中很喜欢面试提问一些类似的问题,虽然在工作中需要自己注意单例创建的场景并不是很多,学会还是很有必要的。
7种创建方式,带你理解Java的单例模式
本文分享自华为云社区《》,作者:冰 河。
看几个单例对象的示例代码,其中有些代码是线程安全的,有些则不是线程安全的,需要大家细细品味,这些代码也是在高并发环境下测试验证过的。
- 代码一:SingletonExample1
这个类是懒汉模式,并且是线程不安全的
- 代码二:SingletonExample2
饿汉模式,单例实例在类装载的时候进行创建,是线程安全的
- 代码三:SingletonExample3
懒汉模式,单例实例在第一次使用的时候进行创建,这个类是线程安全的,但是这个写法不推荐
- 代码四:SingletonExample4
懒汉模式(双重锁同步锁单例模式),单例实例在第一次使用的时候进行创建,但是,这个类不是线程安全的!!!!!
线程不安全分析如下:
当执行instance = new SingletonExample4();这行代码时,CPU会执行如下指令:
1.memory = allocate() 分配对象的内存空间
2.ctorInstance() 初始化对象
3.instance = memory 设置instance指向刚分配的内存
单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。
指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。
如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:
1.memory = allocate() 分配对象的内存空间
3.instance = memory 设置instance指向刚分配的内存
2.ctorInstance() 初始化对象
假设目前有两个线程A和B同时执行getInstance()方法,A线程执行到instance = new SingletonExample4(); B线程刚执行到第一个 if (instance == null){处,如果按照1.3.2的顺序,假设线程A执行到3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;而实际上,线程A还未执行2.ctorInstance() 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。
- 代码五:SingletonExample5
懒汉模式(双重锁同步锁单例模式)单例实例在第一次使用的时候进行创建,这个类是线程安全的,使用的是 volatile + 双重检测机制来禁止指令重排达到线程安全
- 代码六:SingletonExample6
饿汉模式,单例实例在类装载的时候(使用静态代码块)进行创建,是线程安全的
- 代码七:SingletonExample7
枚举方式进行实例化,是线程安全的,此种方式也是线程最安全的
关注 点击下方,第一时间了解华为云新鲜技术~
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。