简单定义
- 对于单例模式,各位开发同学们已经熟的不能在熟了吧,也是广泛应用在各类项目中,无论是使用什么高级编程语言,设计模式总是伴随其左右。
- 简单来说,单例模式就是确保一个类中只有一个实例化对象,然后提供一个全局可以访问点就ok了
单例模式的优缺点
- 主要优点:
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 主要缺点:
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
谈谈java和kotlin的实现方式
Kotlin中的几种单例模式
饿汉式单例
- 饿汉式单例模式是实现单例模式比较简单的一种方式,它有个特点就是不管需不需要该单例实例,该实例对象都会被实例化。
- 在kotlin中实现起来非常简单,只需要定义一个object对象表达式即可,无需手动去设置构造器私有化和提供全局访问点,这一点Kotlin编译器全给你做好了
object KSingleton : Serializable {//实现Serializable序列化接口,通过私有、被实例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
return KSingleton//由于反序列化时会调用readResolve这个钩子方法,只需要把当前的KSingleton对象返回而不是去创建一个新的对象
}
}
线程安全的懒汉式单例
- 当类加载的时候就去创建这个单例实例,当我们使用这个实例的时候才去初始化它
class KLazilySingleton private constructor() : Serializable {
fun doSomething() {
println("do some thing")
}
companion object {
private var mInstance: KLazilySingleton? = null
get() {
return field ?: KLazilySingleton()
}
@JvmStatic
@Synchronized//添加synchronized同步锁
fun getInstance(): KLazilySingleton {
return requireNotNull(mInstance)
}
}
//防止单例对象在反序列化时重新生成对象
private fun readResolve(): Any {
return KLazilySingleton.getInstance()
}
}
DCL(double check lock)改造懒汉式单例
- 线程安全的单例模式直接是使用synchronized同步锁,锁住getInstance方法,每一次调用该方法的时候都得获取锁,但是如果这个单例已经被初始化了,其实按道理就不需要申请同步锁了,直接返回这个单例类实例即可。于是就有了DCL实现单例方式
- 而且在kotlin中,可以支持线程安全DCL的单例,可以说也是非常非常简单,就仅仅3行代码左右,那就是Companion Object + lazy属性代理,一起来看下吧
class KLazilyDCLSingleton private constructor() : Serializable {//private constructor()构造器私有化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
return instance
}
companion object {
//通过@JvmStatic注解,使得在Java中调用instance直接是像调用静态函数一样,
//类似KLazilyDCLSingleton.getInstance(),如果不加注解,在Java中必须这样调用: KLazilyDCLSingleton.Companion.getInstance().
@JvmStatic
//使用lazy属性代理,并指定LazyThreadSafetyMode为SYNCHRONIZED模式保证线程安全
val instance: KLazilyDCLSingleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() }
}
}
静态内部类单例
- DCL虽然在一定程度上能解决资源消耗、多余synchronized同步、线程安全等问题,但是某些情况下还会存在DCL失效问题
- 在多线程环境下一般不推荐DCL的单例模式。所以引出静态内部类单例实现
class KOptimizeSingleton private constructor(): Serializable {//private constructor()构造器私有化
companion object {
@JvmStatic
fun getInstance(): KOptimizeSingleton {//全局访问点
return SingletonHolder.mInstance
}
}
fun doSomething() {
println("do some thing")
}
private object SingletonHolder {//静态内部类
val mInstance: KOptimizeSingleton = KOptimizeSingleton()
}
private fun readResolve(): Any {//防止单例对象在反序列化时重新生成对象
return SingletonHolder.mInstance
}
}
枚举单例
- 枚举单例实现,就是为了防止反序列化,因为我们都知道枚举类反序列化是不会创建新的对象实例的
- 枚举类型的序列化机制保证只会查找已经存在的枚举类型实例,而不是创建新的实例
enum class KEnumSingleton {
INSTANCE;
fun doSomeThing() {
println("do some thing")
}
}
Java中的几种单例模式
懒汉式(线程不安全)
//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
//私有的构造函数
private Singleton() {}
//私有的静态变量
private static Singleton single=null;
//暴露的公有静态方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
- 一般来说懒汉式分为三个部分,私有的构造方法,私有的全局静态变量,公有的静态方法
- 起到重要作用的是静态修饰符static关键字,我们知道在程序中,任何变量或者代码都是在编译时由系统自动分配内存来存储的,而所谓静态就是指在编译后所分配的内存会一直存在,直到程序退出内存才会释放这个空间,因此也就保证了单例类的实例一旦创建,便不会被系统回收,除非手动设置为null。
- 这种方式创建的缺点是存在线程不安全的问题,解决的办法就是使用synchronized 关键字,便是单例模式的第二种写法了。
懒汉式(线程安全)
public class Singleton {
//私有的静态变量
private static Singleton instance;
//私有的构造方法
private Singleton (){};
//公有的同步静态方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 这种单例实现方式的getInstance()方法中添加了synchronized 关键字,也就是告诉Java(JVM)getInstance是一个同步方法。
- 同步的意思是当两个并发线程访问同一个类中的这个synchronized同步方法时,一个时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完才能执行,因此同步方法使得线程安全,保证了单例只有唯一个实例。
- 但是它的缺点在于每次调用getInstance()都进行同步,造成了不必要的同步开销。这种模式一般不建议使用。
饿汉式(线程安全)
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton {
//static修饰的静态变量在内存中一旦创建,便永久存在
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
- 饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。其中instance=new Singleton()可以写成:
static {
instance = new Singleton();
}
- 属于变种的饿汉单例模式,也是基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化了。
DCL双重校验模式
public class Singleton {
private static Singleton singleton; //静态变量
private Singleton (){} //私有构造函数
public static Singleton getInstance() {
if (singleton == null) { //第一层校验
synchronized (Singleton.class) {
if (singleton == null) { //第二层校验
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 这种模式的特殊之处在于getInstance()方法上,其中对singleton进行了两次判断是否空,第一层判断是为了避免不必要的同步,第二层的判断是为了在null的情况下才创建实例。
具体我们来分析一下:假设线程A执行到了singleton = new Singleton(); 语句,这里看起来是一句代码,但是它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致会做三件事情:
- 给Singleton的实例分配内存
- 调用Singleton()的构造函数,初始化成员字段
- 将singleton对象指向分配的内存空间(即singleton不为空了)
但是在JDK1.5之后,官方给出了volatile关键字,将singleton定义的代码,为了解决DCL失效的问题。
private volatile static Singleton singleton; //使用volatile 关键字
静态内部类单例模式
public class Singleton {
private Singleton (){} ;//私有的构造函数
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//定义的静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton(); //创建实例的地方
}
}
- 第一次加载Singleton 类的时候并不会初始化INSTANCE ,只有第一次调用Singleton 的getInstance()方法时才会导致INSTANCE 被初始化。
- 因此,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder 类,这种方式不仅能够确保单例对象的唯一性,同时也延迟了单例的实例化。
枚举单例
前面的几种单例模式实现方式,一般都会稍显麻烦,或是在某些特定的情况下出现一些状况。下面介绍枚举单例模式的实现:
public enum Singleton { //enum枚举类
INSTANCE;
public void whateverMethod() {
}
}
枚举单例模式最大的优点就是写法简单,枚举在java中与普通的类是一样的,不仅能够有字段,还能够有自己的方法,最重要的是默认枚举实例是线程安全的,并且在任何情况下,它都是一个单例。即使是在反序列化的过程,枚举单例也不会重新生成新的实例。而其他几种方式,必须加入如下方法:
private Object readResolve() throws ObjectStreamException{
return INSTANCE;
}
这样的话,才能保证反序列化时不会生成新的方法
使用容器实现单例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();//使用HashMap作为缓存容器
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;//第一次是存入Map
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;//返回与key相对应的对象
}
}
- 在程序的初始,将多种单例模式注入到一个统一的管理类中,在使用时根据key获取对应类型的对象。
场景应用
1.那么什么时候需要考虑使用单例模式呢?
- 系统只需要一个实例对象,如系统要求提供一个唯一的* 序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
2.下面我们结合Android中一些源码来分析一下下
Android中常用的EventBus框架
- 我们可以看看EventBus中的如何使用单例模式的,主要是使用双重检查DCL
static volatile EventBus defaultInstance;
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
这样的话它的资源利用率会很高,并且第一次执行的时候,是单例对象才会被实例化,但是第一次加载的时候会稍慢,可以被接受
LayouInflater的单例模式实现
- 基本用法
LayoutInflater mInflater = LayoutInflater.from(this);
上边的写法估计没有人会陌生,获取LayoutInflater 的实例,我们一起看看具体的源码实现:
1.通过LayoutInflater.from(context)来获取LayoutInflater服务
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
2.看看context.getSystemService是怎么工作的,context的实现类是ContextImpl类,点进去看一下
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
3.进入到SystemServiceRegistry类中
/**
* Gets a system service from a given context.
*/
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
- 到这里了相信各位已经感觉这是上述说的使用容器实现单例模式了,对比一下,果然是
private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();
- 使用map通过键值对的方式保存系统服务。在调用registerService的时候注入。
/**
* Statically registers a system service with the context.
* This method must be called during static initialization only.
*/
private static <T> void registerService(String serviceName, Class<T> serviceClass,
ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
4.我们可以再看看这些服务都是在什么时候注册的
static {
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
}
- 很明显,这是在一个静态的代码块进行注册服务,第一次加载该类的时候执行,并且只执行一次,保证实例的唯一性
- 从这个过程中可以看出,系统将服务以键值对的形式存储在HashMap中,用户使用时只需要获取具体的服务对象,第一次获取时,调用getSystemService来获取具体的对象,在第一次调用时,会调用registerService通过map将对象缓存在一个列表中,下次再用时直接从容器中取出来就可以。避免重复创建对象,从而达到单例的效果。减少了资源消耗。
- 在Android源码中,APP启动的时候,虚拟机第一次加载该类时会注册各种ServiceFetcher,比如LayoutInflater Service。将这些服务以键值对的形式存储在一个HashMap中,用户使用时只需要根据key来获取到对应的ServiceFetcher,然后通过ServiceFetcher对象的getService函数获取具体的服务对象。当第一次获取时,会调用ServiceFetcher的creatService函数创建服务对象,然后将该对象缓存到一个列表中,下次再取时直接从缓存中获取,避免重复创建对象,从而达到单例的效果。Android中的系统核心服务以单例形式存在,减少了资源消耗。
ImageLoader图片加载框架
- 图片加载框架ImageLoader的实例创建就是使用了单例模式,因为这个ImageLoader中含有线程池、缓存系统、网络请求,很消耗资源,不应该创建多个对象,这时候就需要用到单例模式
ImageLoader.getInstance();// 在自己的Application中创建全局实例
.....
//getInstance()执行的源码
public static ImageLoader getInstance() {
if(instance == null) {//双重校验DCL单例模式
Class var0 = ImageLoader.class;
synchronized(ImageLoader.class) {//同步代码块
if(instance == null) {
instance = new ImageLoader();//创建一个新的实例
}
}
}
return instance;//返回一个实例
}
因此,如果我们创建一个对象需要消耗过多的资源的话,可以考虑单例模式
结语
以上都是同学我结合网上资料进行整理的单例模式的分析总结,简单来说,单例模式在设计模式中是非常常见的,它简单但同时又简单,值得我们继续深入探讨下去,下面我简单总结一下单例模式
- 一个核心原理就是私有构造,并且通过静态方法获取一个实例。
- 在这个过程中必须保证线程的安全性。
- 推荐使用静态内部内实现单例或加了Volatile关键字的双重检查单例