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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式-单例模式(最全总结)相关的知识,希望对你有一定的参考价值。

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

饿汉式单例模式

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

代码示例
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​​只针对当前线程

以上是关于设计模式-单例模式(最全总结)的主要内容,如果未能解决你的问题,请参考以下文章

单例模式不是一件小事,快回来看看

最全23种设计模式之单例模式(Singleton)

史上最全的二十三种设计模式总结

史上最全单例模式的写法和破坏单例方式

高并发下线程安全的单例模式(最全最经典,值得收藏)

单例模式(史上最全)