内容简介:首先,请您欣赏闲来无事听听曲,知识已填脑中去;
首先,请您欣赏 单例模式的原创歌曲 。
嘻哈说:单例模式 作曲:懒人 作词:懒人 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))。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 设计模式——订阅模式(观察者模式)
- 设计模式-简单工厂、工厂方法模式、抽象工厂模式
- java23种设计模式-门面模式(外观模式)
- 设计模式-享元设计模式
- Java 设计模式之工厂方法模式与抽象工厂模式
- JAVA设计模式之模板方法模式和建造者模式
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。