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
单例模式实现的方式虽然有很多,但都是为了让某一个类只有一个实例。
今天就先说到这里,下次是建造者模式,感谢大家。