用示例代码来帮你了解单例模式
对于“设计模式”这个词大家肯定都不陌生,很多框架也用到了设计模式,但是大部分的开发者应该是没有深入的了解过,我准备硬肝下这23设计模式作为专题文章的开端,一共23种设计模式,我尽量在<23天肝完。
为什么要学习设计模式:https://blog.csdn.net/kaituozhe_sh/article/details/107922339
在我大学四年,对设计模式也没有什么概念,写代码就想着能实现就可以了,不会有设计模式那样的思想,但是当学习到了框架的时候,对于设计模式才有了一些更深入的了解,使用设计模式的代码在扩展性上会比暴力的代码更容易维护,特别是当一个程序猿离职了后,你去接手它的代码,里面是一大堆if else,这样真的会崩溃,修改都不知道从何下手
硬肝系列目录
创建型模式
结构型模式
行为型模式
对于单例模式大家肯定都不陌生,但是凡是涉及到一个对象的创建过程,肯定涉及到线程安全,而设计模式中的单例模式就为我们提出了创建对象的几种方法,下面我来用实例来带大家来看看创建实例的方法有哪些
文章目录
饿汉式(线程安全)
先上代码:
package designModels.design_mode_05_SingletonMode;
//饿汉式、线程安全
public class Singleton_01 {
private static Singleton_01 INSTANCE = new Singleton_01();
private Singleton_01(){}
public Singleton_01 getInstance(){
return INSTANCE;
}
}
从上面的代码我们可以分析用饿汉式创造单例的优缺点
优点:在一开始就将类实例化好了,无论怎么请求都是那一个实例,所以它是线程安全的;
缺点:浪费空间、例如我打开一个游戏,一进去就将所有的东西都实例化好,比如所有游戏地图、道具,那得多占用空间啊,解决方法就是当触发事件的时候才去实例化它,这就出现了懒汉式
懒汉式(线程不安全)
兄弟们上代码:
package designModels.design_mode_05_SingletonMode;
//懒汉式、线程不安全
public class Singleton_02 {
private static Singleton_02 INSTANCE = null;
private Singleton_02(){}
public Singleton_02 getInstance(){
if (INSTANCE != null) return INSTANCE;
INSTANCE = new Singleton_02();
return INSTANCE;
}
}
从上面的代码我们可以看出,当调用x方法的时候才会实例化,就不会像饿汉式那样占用极大的空间,但是又引进了一个线程安全的问题,当多个线程同时去调用上面的x方法时,也是有概率造成创建多个实例的问题,然后我们又引进了一个线程安全的懒汉式
懒汉式(线程安全)
上代码
package designModels.design_mode_05_SingletonMode;
//懒汉式、线程安全
public class Singleton_03 {
private static Singleton_03 INSTANCE = null;
private Singleton_03(){}
public static synchronized Singleton_03 getINSTANCE() {
if (INSTANCE != null) return INSTANCE;
INSTANCE = new Singleton_03();
return INSTANCE;
}
}
从上面的代码我们可以看得出来,所谓的线程安全,就是在获取该实例的方法上加上synchronized关键字,也就是加锁,但是这又出现了一个问题,性能问题,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,也就是我们常说的线程上下文切换,代价比较高,耗费资源也比较多。这样太慢了,再然后我们又出现了基于双重锁的懒汉式,所以说不要怕解决不了,对于代码里的问题bugs,只要时间足够,我们就能解决,兄弟们,继续上代码冲冲冲!!
线程安全的懒汉式 基于双重锁(线程安全)
上代码
package designModels.design_mode_05_SingletonMode;
//双重锁校验、线程安全
public class Singleton_04 {
private static Singleton_04 INSTANCE = null;
private Singleton_04(){}
public static Singleton_04 getInstance(){
if (INSTANCE != null) return INSTANCE;
synchronized (Singleton_04.class){
if (INSTANCE == null){
INSTANCE = new Singleton_04();
}
}
return INSTANCE;
}
}
通过上面的代码我们可以看到,不是每次请求都需要获取锁,而是先判断一下该实例存不存在,不存在才往下面执行,然后这里我们运用了二次判空,为什么要加二次判空呢?举个例子吧,有A,B两个线程,这两个线程都通过了这句
if (INSTANCE != null) return INSTANCE;
现在是阻塞在了获取锁的步骤上,首先:
A获取锁~A实例化对象~A释放锁
然后:
B获取锁~B实例化对象
到这一步就大错特错了,违反了我们单例的设计原则,所以我们加了二次判空,当B拿到锁之后去判断该实例有没有去创建,如果创建则跳出来不用再去实例化对象了。但我们上面的代码还是有一些问题的,上面的代码在高并发的场景下有一定的可能引起jvm的指令重排,其实jvm的指令重排在一定的情况下能提升我们代码的执行效率,但是也会使代码执行出现问题,比如
INSTANCE = new Singleton_04();
上面这一段代码它并不是原子性的,它分为三步执行
1.在堆中给Singleton_04分配内存空间
2.初始化成员变量
3.将INSTANCE对象的引用指针指向堆中给Singleton_04的内存空间
到了第三步,INSTANCE中才可以调用Singleton_04里的方法
但是jvm就是觉得这么执行效率不高,把1-2-3的执行顺序给换了,改成了1-3-2,这样我INSTANCE的指向就指向了一块空的内存空间,而恰恰有一个线程过来过去取INSTANCE对象,将这个不为null但是为将成员变量初始化的代码给取到了,这样就会出现问题了,所以我们需要到一个volatile关键字,这个关键字大家肯定不陌生,他还有一个功能就是禁止指令重排序,这样就防止了,因为jvm的原因导致线程取到无用的对象。
通过静态内部类来实现单例模式(线程安全)
上代码
//静态内部类
public class Singleton_05 {
private static class Singleton{
private static Singleton_05 INSTANCE = new Singleton_05();
}
private Singleton_05(){}
public static Singleton_05 getInstance(){
return Singleton.INSTANCE;
}
}
为什么可以通过静态内部类来实现单例模式呢?这和饿汉式又有什么区别呢?
首先,静态内部类不会自己去加载,这样不会像饿汉式那样去占用空间,而是在外部类调用getInstance()方法时才去调用内部类创建外部类的实例。
这样不仅能保证线程安全,还能保证我们的单例原则,最后也保证了空间的合理使用
JVM会保证一个类的< clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果在一个类的< clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行< clinit>()方法后,其他线程唤醒之后不会再次进入< clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
< clinit>()方法的定义:
在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法< clinit>,它对静态变量、静态代码块进行初始化, 另一个是实例的初始化方法< init> 对非静态变量进行初始化。我个人认为可以简单的理解为由编译器自动收集类中的所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语气在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它后面的变量可以赋值但不能访问。
使用AtomicReference实现单例模式(线程安全)
首先介绍一下AtomicReference,它是java并发库中提供的一个原子类,java为什么要提供这些原子类呢?因为对于线程安全,能用一条语句执行的,就别用两条,原子性的操作是可以支持安全的高并发的访问数据,常见的还有AtomicInteger、AtomicBoolean等
上代码
package designModels.design_mode_05_SingletonMode;
import java.util.concurrent.atomic.AtomicReference;
public class Singleton_06 {
private static AtomicReference<Singleton_06> AR_INSTANCE = new AtomicReference<>();
private Singleton_06(){}
public static Singleton_06 getInstance(){
for( ; ;){
Singleton_06 INSTANCE = AR_INSTANCE.get();
if(INSTANCE != null) return INSTANCE;
AR_INSTANCE.compareAndSet(null,new Singleton_06());
return AR_INSTANCE.get();
}
}
}
通过上面的代码可以看出,该方式实现线程安全,不会像加锁那样耗费资源,而是通过一种CAS的方式实现,CAS是乐观锁,英文全称是Compare And Swap
但上面使用的是compareAndSet
,也就是先比较后设置,这么说可能有些同学不清楚
AR_INSTANCE.compareAndSet(null,new Singleton_06());
这段代码的意思就是说,如果AR_INSTANCE
的值为 null,则new Singleton_06()
,要不然不执行,它是一种乐观锁,通过不断的循环请求,不会像synchronized那样将线程阻塞,但是一直请求不到也是会导致线程处于死循环中
通过枚举实现单例模式(线程安全)
上代码
package designModels.design_mode_05_SingletonMode;
public enum Singleton_07 {
INSTANCE;
private Msg instance;
Singleton_07(){
instance = new Msg();
}
public Msg getInstance(){
return instance;
}
}
class Msg{
public String getMsg(){
return "I'm enumSingleton !!!";
}
}
最后说的这一种方式,是最推荐使用的,在枚举中,构造方法为私有,所有的枚举实例都为static final类型,final大家都知道,只要定义了就不能随便改变、因为有static所以只能被实例化一次,因为多线程的环境下,只会执行一次,其他的线程则会阻塞,所以我们通过这种方式,创造出来的单例都是唯一的,最主要的是,枚举还能防止经过反射attack、实例对象先序列化后再反序列化导致产生的对象与原对象不符的bug出现
让我们来看看《effective java》中:
“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”
我们使用双重锁创建单例模式来给大家验证一下:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton_04 singleton1 = Singleton_04.getInstance();
Constructor<Singleton_04> constructor = Singleton_04.class.getDeclaredConstructor();
//设置在使用构造器的时候不执行权限检查,没有了权限检查,也就没有了保障
constructor.setAccessible(true);
Singleton_04 reflect = constructor.newInstance();
System.out.println("通过反射得到的实例到底是不是同一个呢?");
System.out.println(reflect == singleton1?"yes" : "NOOOOOO");
}
执行结果:
通过反射得到的实例到底是不是同一个呢?
NOOOOOO
在这里我们的解决方案和上面说的使用枚举创建单例再加个异常处理即可
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, InstantiationException {
Singleton_07 singleton1 = Singleton_07.INSTANCE;
Constructor<Singleton_07> constructor = Singleton_07.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton_07 reflect = constructor.newInstance();
System.out.println("通过反射得到的实例到底是不是同一个呢?");
System.out.println(reflect == singleton1?"yes" : "NOOOOOO");
}
执行结果:
Exception in thread "main" java.lang.NoSuchMethodException: designModels.design_mode_05_SingletonMode.Singleton_07.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at designModels.design_mode_05_SingletonMode.Singleton_07.main(Singleton_07.java:19)
直接抛出异常
让我们来看看newInstance()方法的源码
就是关键的红色框框中的判断!!!
如果反射类为枚举,则抛出异常!!
接下来我们来看看枚举对于序列化的问题是如何解决的,其实就是在反序列化的过程中的一个方法
readObject()
就是上面的这个方法,导致的如果不使用枚举来创建单例,就会导致序列化后的与原本的对象不是同一个对象
又或者在单例类添加,下面这一段代码即可
private Object readResolve(){
return Singleton_04.getInstance();
}
这到底是为什么呢?让我们来看看readObject()的源码
进入到红框框中的方法readObject0(),让我们来重点看看它
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
// 如果是对象的反序列化,这里tc=115,即0x73,所以走下面的TC_OBJECT
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
如果是反序列化枚举类型的话,则进入到TC_ENUM,我们来看看源码:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
我给大家筛出来一段重要的
我们再接下去看valueOf()
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
通过下面这句关键的语句
T result = enumType.enumConstantDirectory().get(name);
我们获取到了枚举常量,而枚举常量为static final修饰,所以反序列化最终获取到的就是一开始我们实例化的那个值,如果不是枚举类呢?又会是怎么一回事?请听下回分解哈哈哈哈哈哈红红火火恍恍惚惚
等肝完设计模式了再给大家单独开一个阅读源码的专题,从里面能获取到开发者的设计思想也是一种不错的选择
所以最后我们可以得到一个结论,你知道的越多,不知道的也越多!!小小的一个单例模式就有这么多道道,加油加油!!!
完成:TO: 2021/3/18 23:26