嘻哈说:设计模式之单例模式

1、嘻哈说

首先,请您欣赏单例模式的原创歌曲

嘻哈说:单例模式
作曲:懒人
作词:懒人
Rapper:懒人

某个类只有一个实例
并自行实例化向整个系统提供这个实例
需要私有构造方法毋庸置疑
自行实例化各有各的依据
提供单一实例则大体一致
饿汉静态变量初始化实例
懒汉初始为空
获取实例为空才创建一次
方法加上锁弄成线程安全的例子
DCL双重检查锁两次判空加锁让并发不是难事
创建对象并不是原子操作因为处理器乱序
volatile的关键字开始用武之地
静态内部类中有一个单例对象的静态的实例
枚举天生单例
容器管理多个单例
复制代码

试听请点击这里

闲来无事听听曲,知识已填脑中去;

学习复习新方式,头戴耳机不小觑。

番茄课堂,学习也要酷。

2、定义

在Java设计模式中,单例模式相对来说算是比较简单的一种创建型模式。

什么是创建型模式?

创建型模式是设计模式的一种分类。

设计模式可以分为三类:创建型模式、结构型模式、行为型模式。

创建型模式:提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。

结构型模式:关注类和对象的组合,用继承的概念来组合接口和定义组合对象获得新功能的方式。

行为型模式:关注对象之间的通信。

我们来看一下单例模式的定义。

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例

也就是,保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式在懒人眼中就是,注孤生,悲惨世界

3、特性

从定义中,我们可以分析出一些特性来:

单例类只能有一个实例

确保某一个类只有一个实例,must be 呀。

单例类必须自行创建自己的唯一的实例

自行实例化。

单例类必须给所有其他对象提供这一实例 。 向整个系统提供这个实例。

内存中会长期持有单例实例,如果不是对所有对象提供访问,例如只对包内类提供访问权限,存在的意义就不大了。

4、套路

怎样确保某一个类只有一个实例?

套路1:私有化空构造方法,避免多处实例化。

套路2:自行实例化,保证实例化在内存中只存在一份。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

套路1与套路3是固定的套路,基本不会有变。

套路2则有很多灵活的实现方式,只要保证只实例化一次就是可以的。

OK,那我开始撸代码。

5、代码

1、饿汉模式

package com.fanqiekt.singleton;

/**
 * 饿汉单例模式
 *
 * @author 番茄课堂-懒人
 */
public class EHanSingleton {

	private static EHanSingleton sInstance = new EHanSingleton();

	//私有化空构造方法
	private EHanSingleton() {}

	//静态方法返回单例类对象
	public static EHanSingleton getInstance() {
		return sInstance;
	}

	//其他业务方法
	public void otherMethods(){
		System.out.println("饿汉模式的其他方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证实例化在内存中只存在一份

实现方式:静态实例变量的初始化

实现原理:类加载时就会初始化单例对象,并且只初始化一次。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

为什么叫饿汉?

因为饿汉很饿,需要尽早初始化来喂饱自己。

从线程安全,优缺点总结一下。

线程安全:利用类加载器的机制,肯定是线程安全的。

为什么这么说呢?

ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

优点:类加载时会初始化单例对象,首次调用速度变快。

缺点:类加载时会初始化单例对象,容易产生垃圾。

2、懒汉模式

package com.fanqiekt.singleton;

/**
 * 懒汉模式
 *
 * @author 番茄课堂-懒人
 */
public class LazySingleton {

	private static LazySingleton sInstance;

	//私有化空构造方法
	private LazySingleton() {}

	//静态方法返回单例类对象
	public static LazySingleton getInstance() {
		//懒加载
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}

	//其他业务方法
	public void otherMethods(){
		System.out.println("懒汉模式的其他方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证实例化在内存中只存在一份

实现方式:getInstance()里进行实例判空

实现原理:为空则创建实例;不为空,则直接返回实例。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

为什么叫懒汉?

因为懒汉懒惰,懒得初始化,用到了才开始初始化。

线程安全吗?

很明显,不是线程安全的,因为getInstance()方法没有做任何的同步处理。

怎么办?

给getInstance()加锁。

//静态方法返回单例类对象,加锁
	public static synchronized LazySingleton getInstance() {
		//懒加载
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}
复制代码

这样就变成线程安全的懒汉模式了。

懒汉模式有什么优缺点呢?

优点:第一次使用时才会初始化,节省资源

缺点:第一次使用时需要进行初始化,所以会变慢。给getInstance()加锁后,getInstance()调用也会变慢。

那有没有办法可以去掉getInstance()锁后还线程安全呢?

3、DCL

package com.fanqiekt.singleton;

/**
 * Double Check Lock 单例
 *
 * @author 番茄课堂-懒人
 */
public class DCLSingleton {

	private static DCLSingleton sInstance;

	//私有化空构造方法
	private DCLSingleton() {}

	//静态方法返回单例类对象
	public static DCLSingleton getInstance() {
		//两次判空
		if(sInstance == null) {
			synchronized(DCLSingleton.class) {
				if(sInstance == null) {
					sInstance = new DCLSingleton();
					return sInstance;
				}
			}
		}
		return sInstance;
	}

	//其他业务方法
	public void otherMethods(){
		System.out.println("DCL模式的其他方法");
	}
}
复制代码

与懒汉模式的区别在于:

去掉getInstance()方法上的锁,在方法内部实例为空后再进行加锁。

好处:只有当实例没有初始化的情况下才会同步锁,避免了给getInstance()整个方法加锁的情况。

dcl的全称是Double Check Lock,双重检查锁。所谓的双重检查就是两次判空。

为什么要进行第二次判空,这不是脱裤子放屁,多此一举嘛。

可能觉得它只是个屁,但其实是窜稀,所以,脱裤子也是有必要的。

有这样一种情况,线程1、2同时判断第一次为空,在加锁的地方的阻塞了,如果没有第二次判空,那么线程1执行完毕后线程2就会再次执行,这样就初始化了两次,就存在问题了。

两次判空后,DCL就安全多了,一般不会存在问题。但当并发量特别大的时候,还是会存在风险的。

在哪里呢?

sInstance = new DCLSingleton()这里。

是不是很奇怪,这句很普通的创建实例的语句怎么会有风险。

情况是这样的:

sInstance = new DCLSingleton()并不是一个原子操作,它转换成了多条汇编指令,大致做了3件事情:

第一步:分配内存。

第二步:调用构造方法初始化。

第三步:将sInstanc对象指向分配空间。

由于Java编译器允许处理器乱序执行,所以这三步顺序不定,如果依次执行肯定没问题,但如果执行完第一步和第三步后,其他的线程使用sInstanc就会报错。

那如何解决呢?

这里就需要用到关键字volatile了。

volatile有什么用呢?

第一个:实现可见性。

什么意思呢?

在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。

这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

volatile在这个时候就派上用场了。

读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程重新拷贝一份,这样就保证子线程的会跟主线程的一致。

写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。

第二个:防止处理器乱序执行。

volatile变量初始化的时候,就只能第一步、第二步、第三步这样的顺序执行了。

所以我们可以把sInstance的变量声明的代码更改下。

private volatile static DCLSingleton sInstance;
复制代码

不过,由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

感觉实现起来有点复杂,那有没有一样优秀还更简单点的单例模式?

4、静态内部类

package com.fanqiekt.singleton;

/**
 * 静态内部类单例模式
 *
 * @author 番茄课堂-懒人
 */
public class StaticSingleton {

	//私有静态单例对象
	private StaticSingleton() {}

	//静态方法返回单例类对象
	public static StaticSingleton getInstance() {
		return SingleHolder.INSTANCE;
	}

	//单例类中存在一个静态内部类
	private static class SingleHolder {
		//静态类中存在静态单例声明与初始化
		private static final StaticSingleton INSTANCE = new StaticSingleton();
	}

	//其他业务方法
	public void otherMethods(){
		System.out.println("静态内部类的其他方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证实例化在内存中只存在一份

实现方式:声明一个静态内部类,静态内部类中有个单例对象的静态实例,getInstance()返回静态内部类的静态单例对象

实现原理:内部类不会在其外部类被加载的时候被加载,只有当内部类被使用的时候才会被使用。这样就避免了类加载的时候就被初始化,属于懒加载。

静态内部类中的静态变量是通过类加载器初始化的,也就是在内存中是唯一的,保证了单例。

线程安全:利用了类加载器的机制,肯线程安全

静态内部类简单,线程安全,懒加载,所以,强烈推荐

还有一个大家可能想象不到的实现方式,那就是枚举。

5、枚举

package com.fanqiekt.singleton;

/**
 * 枚举单例模式
 *
 * @Author: 番茄课堂-懒人
 */
public enum EnumSingleton {
    INSTANCE;

    //其他业务方法
    public void otherMethods(){
        System.out.println("枚举模式的其他方法");
    }
}
复制代码

枚举的特点:

保证只有一个实例。

线程安全。

自由序列化。

可以说枚举就是一个天生的单例,而且还可以自由序列化,反序列化后也是单例的。

而上边几种单例方式反序列化后是会重新再生成对象的,这就是枚举的强大之处。 那枚举的原理是什么呢?

我们可以看一下生成的枚举反编译一下,我在这里只粘贴下核心部分。

public final class EnumSingleton extends Enum{
    private EnumSingleton(){}

    static {
        INSTANCE = new EnumSingleton();
    }
}
复制代码

Enum就是一个普通的类,它继承自java.lang.Enum类。所以,枚举具有类的所有功能。

他的实现方式优点类似于饿汉模式。

而且,代码还做了一些其他的事情,例如:重写了readResolve方法并将单一实例返回,因此反序列化也会返回同一个实例。

6、容器

package com.fanqiekt.singleton;

import java.util.HashMap;
import java.util.Map;

/**
 * 容器单例模式
 *
 * @Author: 番茄课堂-懒人
 */
public class SingletonManager {

    private static Map<String, Object> objectMap = new HashMap<>();

    //私有化空构造方法
    private SingletonManager(){}

    //将单例的对象注册到容器中
    public static void registerService(String key, Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key, instance);
        }
    }

    //从容器中获得单例对象
    public static Object getService(String key){
        return objectMap.get(key);
    }
}

复制代码

实现方式:一个静态的Map,一个将对象放到map的方法,一个获取map中对象的方法

实现原理:根据key存对象,如果map中已经存在key,则不放入map;不存在key,则放入map,这样可以保证每个key对应的对象为单一实例。

容器单例的最大好处是,可以管理多个单例。

Android源码中就用到了这种方式,通过Context获取系统级别的服务(context.getSystemService(key))。

6、END

单例模式实现的方式虽然有很多,但都是为了让某一个类只有一个实例

今天就先说到这里,下次是建造者模式,感谢大家。

    原文作者:算法小白
    原文地址: https://juejin.im/post/5ba22f7bf265da0ab915bfff
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞