首页 > 其他分享 >设计模式-单例模式(最全总结)

设计模式-单例模式(最全总结)

时间:2022-11-10 14:01:18浏览次数:40  
标签:null 最全 LazySingleton private INSTANCE 单例 设计模式 public

单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。

饿汉式单例模式

在类加载的时候就马上初始化了,此时还没到运行时只是将打包的代码加载到内存的时候就初始化(也就是线程还没有出现以前这个时间段初始化单例对象),所以此时一定是线程安全的,也就不可能存在访问安全问题

代码示例
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
//私有化构造方法防止外部调用
private HungrySingleton(){}

public static HungrySingleton getInstance(){
return INSTANCE;
}
}
优缺点

优点:

  1. 线程安全
  2. 执行效率高(因为在运行时根本不需要再去创建对象的)

缺点:

  1. 可能会造成内存浪费,为什么说可能呢,因为如果你的程序在运行过程中把所有的单例类都用上了那肯定是没有内存浪费的,但凡有一个类没有用上就会造成内存浪费,应为在运行前这个对象已经实例化了。所以饿汉式单例比较适合用在单例类比较少的情况下(一点点浪费我们就忽略掉用空间换时间)

懒汉式单例模式

上文说到了饿汉式可能会造成内存浪费,为了解决这个问题,也就出现了懒汉式单例模式。懒汉式单例模式的特点是,单例对象要在被使用的时候才会初始化

懒汉式简单代码实现
public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton(){}

public static LazySingleton getInstance(){
if(INSTANCE==null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
复制代码

咋一看好像没啥问题,但是如果是多线程的情况下就有可能会创建出多个单例对象如下图:

设计模式-单例模式(最全总结)_单例模式

所以上述实现方式的优缺点也很明显: 优点:

  1. 不会造成内存浪费

缺点:

  1. 线程不安全
懒汉式代码实现优化(加锁)

上文说到线程不安全,很多人可能想到加锁具体实现如下:

public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton(){}

public static synchronized LazySingleton getInstance(){
if(INSTANCE==null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}

也可以这样写:

public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton(){}

public static LazySingleton getInstance(){
synchronized(LazySingleton.class) {
if(INSTANCE==null){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}

}
}

这样写呢是将线程安全的问题解决了但是又带来了新的问题,就是以后所有的多线程都要排队访问如下图:

设计模式-单例模式(最全总结)_加载_02

性能问题又不好

懒汉式代码实现优化(加锁+双重if)
public class LazySingleton {
private static volatile LazySingleton INSTANCE = null;

private LazySingleton(){}

public static LazySingleton getInstance(){
if(INSTANCE==null){
synchronized(LazySingleton.class) {
if(INSTANCE == null){
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;

}
}

这里可能很多人会很奇怪为什么要两重if,如图:

设计模式-单例模式(最全总结)_单例类_03

需要注意的是​​INSTANCE​​变量需要添加​​volatile​​关键字,防止指令重排

这样就基本上解决了性能问题、内存浪费问题。

懒汉式代码实现优化(结合java语言特性优化)

其实上文双重if的优化基本上已经解决了我们的问题,但是总归还是要加锁。

我们可以从类的初始化角度来考虑如下代码:

public class LazySingleton {
private LazySingleton(){}
//默认是不加载的
private static final class InstanceHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
public static LazySingleton getInstance(){
//返回结果之前一定会先加载内部类
return InstanceHolder.INSTANCE;

}
}

乍一看以为有点像饿汉模式,但是区别于饿汉模式的是这里的内部类是不会加载的,不是说主类加载了内部类马上就会被加载而是等到使用的时候才会被加载,这样即解决了饿汉单例的内存浪费,也解决了懒汉模式的性能问题.

反射破坏单例

上文中说的所有单例的写法其实都能被反射破坏如下代码:

Class<?> clazz = LazySingleton.class;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton lazySingleton = (LazySingleton)constructor.newInstance();
LazySingleton lazySingleton2 = (LazySingleton)constructor.newInstance();
System.out.println(lazySingleton == lazySingleton2);
System.out.println(lazySingleton);
System.out.println(lazySingleton2);
} catch (Exception e) {
e.printStackTrace();
}

设计模式-单例模式(最全总结)_加载_04

这样就可以创建出两个对象。针对这种情况我们可以在构造方法里面做处理 [图片上传失败...(image-74726b-1668043900590)]

在构造方法中抛出异常

设计模式-单例模式(最全总结)_单例类_05

这样也就解决了反射破坏单例的问题。因

序列化破坏单例

上文中看似完美的单例但还是有可能被序列化破坏单例,有些对象是需要先序列化到磁盘,等到要使用的时候在反序列化转化成内存对象,但是反序列化就会为对象重新分配内存,也就是重新创建,所以又破坏了单例的初衷,如下代码:

LazySingleton lazySingleton = null;
LazySingleton lazySingleton2 = LazySingleton.getInstance();
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream("LazySingleton.obj");
ObjectOutputStream out = new ObjectOutputStream(fileOutputStream);
out.writeObject(lazySingleton2);
out.flush();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("LazySingleton.obj");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
Object o = inputStream.readObject();
lazySingleton = (LazySingleton)o;

System.out.println(lazySingleton == lazySingleton2);
System.out.println(lazySingleton);
System.out.println(lazySingleton2);
} catch (Exception e) {
e.printStackTrace();
}

设计模式-单例模式(最全总结)_单例类_06

发现又是两个对象所以破坏了单例

针对这种问题我们可以通过添加​​readResolve()​​方法解决这个问题如下代码:

public class LazySingleton implements Serializable {
private LazySingleton(){
// throw new RuntimeException("非法创建对象");
}
//默认是不加载的
private static final class InstanceHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
public static LazySingleton getInstance(){
//返回结果之前一定会先加载内部类
return InstanceHolder.INSTANCE;

}

private Object readResolve(){
return InstanceHolder.INSTANCE;
}
}

设计模式-单例模式(最全总结)_单例类_07

为什么重写​​readResolve()​​方法可以实现防止反序列化破坏单例呢不妨看一下​​ObjectInputStream.readObject()​​方法的源码:

设计模式-单例模式(最全总结)_单例类_08

设计模式-单例模式(最全总结)_单例类_09

在进入到​​readOrdinaryObject​​方法内发现

设计模式-单例模式(最全总结)_加载_10

设计模式-单例模式(最全总结)_加载_11

这里直接调用了​​readResolve​​方法返回返回了实例而不是重新创建。

从源码中也可以看出虽然能解决问题,但是其实内部还是将对象实例化了,只不过返回的对象不是新创建的对象而已,也存在者内存分配开销。所以上述的单例还不够完美。所以衍生出了注册师单例模式

注册师单例模式

注册式单例模式又被称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例,注册是单例模式有两种:枚举式单例模式、容器式单例模式

枚举式单例模式

具体写法是:

public enum EnumSingleton {
INSTANCE;

private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}
}

可以看一下编译后的文件(编译完成后反编译查看)

设计模式-单例模式(最全总结)_单例模式_12

发现其实枚举就是一个饿汉式单例模式在类加载的时候就实例化了。

我们也可以尝试用反射破坏实例化如下:

设计模式-单例模式(最全总结)_单例类_13

设计模式-单例模式(最全总结)_加载_14

发现会抛出异常。我们再看一下Enum的源码:

设计模式-单例模式(最全总结)_加载_15

发现只有一个有两个参数的构造方法:所以反射获取构造方法需要将参数填入如下:

Class<?> clazz = EnumSingleton.class;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton lazySingleton = (EnumSingleton)constructor.newInstance();
EnumSingleton lazySingleton2 = (EnumSingleton)constructor.newInstance();
System.out.println(lazySingleton == lazySingleton2);
System.out.println(lazySingleton);
System.out.println(lazySingleton2);
} catch (Exception e) {
e.printStackTrace();
}
复制代码

再看newInstance方法的源码:

设计模式-单例模式(最全总结)_加载_16

发现JDK源码中也是和我们的解决方法类似也就是判断如果是枚举类型就抛出异常。所以反射不能破坏枚举单例模式,

我们再尝试使用反序列化能否破坏单例

User user = new User();
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
EnumSingleton enumSingleton2 = null;
enumSingleton.setData(user);

FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream("LazySingleton.obj");
ObjectOutputStream out = new ObjectOutputStream(fileOutputStream);
out.writeObject(enumSingleton);
out.flush();
fileOutputStream.close();

FileInputStream fileInputStream = new FileInputStream("LazySingleton.obj");
ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
Object o = inputStream.readObject();
enumSingleton2 = (EnumSingleton)o;

System.out.println(enumSingleton.getData() == enumSingleton2.getData());
System.out.println(enumSingleton.getData());
System.out.println(enumSingleton2.getData());
} catch (Exception e) {
e.printStackTrace();
}

在打印发现:

设计模式-单例模式(最全总结)_单例模式_17

​User​​这个类还是单例的。

此时我们再看​​readObject​​源码:

设计模式-单例模式(最全总结)_加载_18

设计模式-单例模式(最全总结)_加载_19

发现底层并没有​​newInstance​​而是找到类然后在找到类变量​​INSTANCE​​指向的值然后返回。所以反序列化也不能破坏枚举单例模式

这里强调一下枚举单例模式也是比较推荐的单例模式,应为所有的逻辑处理都是JDK帮你实现的这也是最官方最权威最稳定的了所以推荐使用

容器式单例模式

枚举式单例虽然是推荐使用的写法上也是很优雅,但是仔细看上文可以知道单例类都是饿汉式单例,在类初始化的时候都会实例化对象,所以还是会有一点瑕疵就是可能会浪费内存,如果有大量的单例类就不太适合使用枚举式单例。 针对这个问题又出现了容器式单例代码如下:

public class ContainerSingleton {
private ContainerSingleton(){};
private static Map<String,Object> ioc = new HashMap<String,Object>();
public static Object getBean(String beanName){
synchronized (ContainerSingleton.class){
if(!ioc.containsKey(beanName)){
//不包含key 说明还没有创建
Object o = null;

try {
o = Class.forName(beanName).getDeclaredConstructor().newInstance();
ioc.put(beanName, o);
} catch (Exception e) {
e.printStackTrace();
}
}else{
return ioc.get(beanName);
}
}
return null;
}
}
复制代码

这个比较适合创建大量单例类的情况下,个人感觉其实就是上述几种单例的灵活运用组合的结果,当然上述容器式写法还是有很多可以优化的地方,这里就不多赘述,大家可以结合上面的文章自己尝试着优化一下这个容器式单例

这里可以看一下Spring的实现方式:

private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
synchronized(this.getSingletonMutex()) {
BeanWrapper bw = (BeanWrapper)this.factoryBeanInstanceCache.get(beanName);
if (bw != null) {
return (FactoryBean)bw.getWrappedInstance();
} else {
Object beanInstance = this.getSingleton(beanName, false);
if (beanInstance instanceof FactoryBean) {
return (FactoryBean)beanInstance;
} else if (!this.isSingletonCurrentlyInCreation(beanName) && (mbd.getFactoryBeanName() == null || !this.isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) {
Object instance;
label123: {
Object var8;
try {
try {
this.beforeSingletonCreation(beanName);
instance = this.resolveBeforeInstantiation(beanName, mbd);
if (instance == null) {
bw = this.createBeanInstance(beanName, mbd, (Object[])null);
instance = bw.getWrappedInstance();
}
break label123;
} catch (UnsatisfiedDependencyException var15) {
throw var15;
} catch (BeanCreationException var16) {
if (var16.contains(LinkageError.class)) {
throw var16;
}
}

if (this.logger.isDebugEnabled()) {
this.logger.debug("Bean creation exception on singleton FactoryBean type check: " + var16);
}

this.onSuppressedException(var16);
var8 = null;
} finally {
this.afterSingletonCreation(beanName);
}

return (FactoryBean)var8;
}

FactoryBean<?> fb = this.getFactoryBean(beanName, instance);
if (bw != null) {
this.factoryBeanInstanceCache.put(beanName, bw);
}

return fb;
} else {
return null;
}
}
}
}
复制代码

线程单例实现ThreadLocal

​ThreadLocal​​不能保证创建的对象是全局唯一,但是在单个线程中是唯一的,而且还不用考虑线程安全问题.

public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}

private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};

public static ThreadLocalSingleton getInstance(){
return threadLocal.get();
}
}
复制代码

执行结果:

设计模式-单例模式(最全总结)_单例模式_20

源码实现:

设计模式-单例模式(最全总结)_单例模式_21

发现底层也就是每个线程维护一个​​ThreadLocalMap​​,然后将实例化后的对象存在Map中,有点类似容器化单例只不过​​ThreadLocal​​只针对当前线程

标签:null,最全,LazySingleton,private,INSTANCE,单例,设计模式,public
From: https://blog.51cto.com/u_15773567/5840770

相关文章

  • 图解23种设计模式
    一、单一职责原则就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职......
  • 设计模式学习(八):桥接模式
    设计模式学习(八):桥接模式作者:Grey原文地址:博客园:设计模式学习(八):桥接模式CSDN:设计模式学习(八):桥接模式桥接模式桥接模式是一种结构型模式。它将抽象部分和实现部分分离,......
  • java 单例设计模式 懒汉式
    packagecom.tedu.test;/***单例设计模式懒汉式设计*优点:不会造成资源的浪费*缺点:会造成线程安全问题*/publicclasssingleonDemo{publicstatic......
  • java单例设计模式 饿汉式
    packagecom.tedu.test;/***单例设计模式的实现饿汉式*缺点:比较浪费资源*优点:饿汉式实现方式不会存在线程安全问题*单例设计模式的原则:实例唯一*/publi......
  • C++ 单例模式
    有时候用到单例模式,记录一下。另外,有时候也用静态函数。头文件:1#ifndefDATABASEMANAGER_H2#defineDATABASEMANAGER_H345#include"operatesqlite.h"......
  • 单例模式的5种实现方式
    publicclassTest{//饿汉式,线程安全,但提前加载,浪费内存privatestaticTestinstance=newTest();privatestaticTestgetInstance(){r......
  • 初识设计模式 - 访问者模式
    简介访问者设计模式(VisitorDesignPattern)的定义是,允许一个或多个操作应用到一组对象上,解耦操作和对象本身。在使用访问者模式的时候,被访问的元素通常不是单独存在的,它......
  • 浅谈PHP设计模式的迭代器模式
    简介:迭代器模式,是行为型的设计模式。提供一中方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。适用场景:除了学习,在PHP中几乎没有应用场景。优......
  • 设计模式---责任链模式
    简述将各个功能拆分后分别封装(各功能解耦),需要时可自由组合(包括执行顺序)话不多说,看个优化案例吧。优化案例最初版以下是模拟客户端想服务端发送请求的业务流程。......
  • 软件设计模式白话文系列(五)建造者模式
    1、描述将一个对象的构造过程进行封装,并按照一定顺序进行构造。通俗的讲,假如要创建电脑的对象,那么各个实例的属性不同,也就是电脑的配置各不相同.这个时候可以考虑用构......