当前位置: 首页 > ds >正文

单例模式详解

本文主要分享的内容是单例模式的应用场景、常见的单例模式写法、保证线程安全的单例模式策略、反射暴力攻击单例解决方案及原理分析、序列化破坏单例的原理及解决方案。

一、单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。

二、常见的单例模式写法

2.1 饿汉式单例

这种单例是在类加载的时候就马上初始化了,同时创建单例对象。这种方式不加任何锁,执行效率高,绝对的线程安全,但是缺点就是一开始就已经加载了,不管这个类最后有没有用到都会占用空间,浪费内存。饿汉式适用在单例对象较少的情况。Spring 中 IOC 容器 ApplicationContext 就是典型的饿汉式单例。下面是饿汉式单例的写法

/*** 饿汉式单例*/
public class HungrySingleton {private static final HungrySingleton hungrySingleton =new HungrySingleton();private HungrySingleton(){}public static HungrySingleton getInstance(){return hungrySingleton;}}

利用静态代码块加载的写法如下:

public class HungryStaticSingleton {private static final HungryStaticSingleton hungryStaticSingleton;static{hungryStaticSingleton = new HungryStaticSingleton();}private HungryStaticSingleton(){}public static HungryStaticSingleton getInstance(){return hungryStaticSingleton;}
}

2.2 懒汉式单例

懒汉式单例的特点就是在被外部类调用的时候才会加载,相比饿汉式,这样可以减少内存空间的浪费,做到“按需加载”。

public class LazySimpleSingleton {private LazySimpleSingleton(){}private static LazySimpleSingleton lazy = null;public static LazySimpleSingleton getInstance(){if(lazy == null){lazy = new LazySimpleSingleton();}return lazy;}
}

但是上面这种写法会带来一定的线程安全问题,当同时运行多个线程环境下 LazySimpleSingleton被实例化了多次。有时,我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了。所以我们要给 getInstance()加上 synchronized 关键字,使这个方法变成线程同步方法。

public class LazySimpleSingleton {private LazySimpleSingleton(){}private static LazySimpleSingleton lazy = null;public synchronized static LazySimpleSingleton getInstance(){if(lazy == null){lazy = new LazySimpleSingleton();}return lazy;}
}

在以上这种写法中,我们将其中一个线程执行并调用 getInstance()方法时,另一个线程再次调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance()
方法,所以这个时候线程安全的问题便解决了。但是,用synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。所以这个时候就需要用到双重检查锁的单例模式

//双重检查式单例
public class LazyDoubleCheckSingleton {//防止指令重排序private volatile static LazyDoubleCheckSingleton lazy = null;private LazyDoubleCheckSingleton(){}public static LazyDoubleCheckSingleton getInstance(){if(lazy == null){synchronized (LazyDoubleCheckSingleton.class){if(lazy == null){lazy = new LazyDoubleCheckSingleton();}}}return lazy;}}

当第一个线程调用 getInstance()方法时,第二个线程也可以调用 getInstance()。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在getInstance()方法内部阻塞。但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。

所以我们最好的方式就是使用静态内部类的方式,内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。

//内部类的方式实现
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
public class LazyInnerClassSingleton {private static class LazyHolder{private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}private  LazyInnerClassSingleton(){};public static final LazyInnerClassSingleton getInstance(){return LazyHolder.LAZY;}
}

但是,以上这种内部类的写法,当我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会
两个不同的实例。我们通过使用以下代码对这个内部类单例进行暴力破解:

public class LazyInnerClassSingletonTest {public static void main(String[] args) {try{//对单例进行破坏Class<?> clazz = LazyInnerClassSingleton.class;//通过反射拿到私有的构造方法Constructor c = clazz.getDeclaredConstructor(null);c.setAccessible(true);//暴力初始化,调用两次构造方法,相当于new了两次Object o1 = c.newInstance();Object o2 = c.newInstance();//对比两个对象是否相同System.out.println(o1);System.out.println(o2);System.out.println(o1 ==o2);}catch (Exception e){e.printStackTrace();}}
}

运行结果如下:

cn.tf.pattern.singleton.lazy.LazyInnerClassSingleton@4b67cf4d
cn.tf.pattern.singleton.lazy.LazyInnerClassSingleton@7ea987ac
false
 

从运行结果,我们可以看出这里创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多
次重复创建,则直接抛出异常。来看优化后的代码:

//内部类的方式实现
//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
public class LazyInnerClassSingleton {private static class LazyHolder{private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();}private  LazyInnerClassSingleton(){if(LazyHolder.LAZY!=null){throw  new RuntimeException("不允许创建多个实例");}};public static final LazyInnerClassSingleton getInstance(){return LazyHolder.LAZY;}
}

当再次去暴力破解后就会提示“不允许创建多个实例”,从而保证单例的实现。

2.3 序列化单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。

//序列化单例
public class SeriableSingleton implements Serializable {public final static SeriableSingleton INSTANCE = new SeriableSingleton();private SeriableSingleton(){};public static SeriableSingleton getInstance(){return INSTANCE;}
}

对于这种写法的单例,我们可以通过以下暴力破解的方式来破坏这个单例:

/反序列化时导致单例破坏
public class SeriableSingletonTest {public static void main(String[] args) {SeriableSingleton s1=null;SeriableSingleton s2 = SeriableSingleton.getInstance();FileOutputStream fos = null;try{//将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。fos = new FileOutputStream("SeriableSingleton.obj");ObjectOutputStream os = new ObjectOutputStream(fos);os.writeObject(s2);os.flush();os.close();FileInputStream fis = new FileInputStream("SeriableSingleton.obj");ObjectInputStream is = new ObjectInputStream(fis);s1 = (SeriableSingleton) is.readObject();is.close();//反序列化后的对象会重新分配内存,//即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当//于破坏了单例System.out.println(s1);System.out.println(s2);System.out.println(s1==s2);}catch (Exception e){e.printStackTrace();}}
}

运行结果如下:
 cn.tf.pattern.singleton.seriable.SeriableSingleton@7b23ec81
cn.tf.pattern.singleton.seriable.SeriableSingleton@6e0be858
 false

可以看出,反序列化后的对象和手动创建的对象是不一致的,在这里被实例化了两次,此时我们为了保证序列化的情况下也能够实现单例,需要将序列化单例优化成如下所示:

//序列化单例
public class SeriableSingleton implements Serializable {public final static SeriableSingleton INSTANCE = new SeriableSingleton();private SeriableSingleton(){};public static SeriableSingleton getInstance(){return INSTANCE;}//防止被序列化破坏private Object readResolve(){return INSTANCE;}}

对于增加的这个readResolve方法,我们可以在JDK源码中的ObjectInputStream中的readObject()方法中逐步找到,在readObject()中有一个readObject0()方法,进入readObject0()之后可以看到TC_OBJECTD 中判断,调用了 ObjectInputStream 的 readOrdinaryObject()方法,

private Object readObject0(boolean unshared) throws IOException {...case TC_OBJECT:return checkResolve(readOrdinaryObject(unshared));...
}

isInstantiable()里面的代码如下:

 boolean isInstantiable() {requireInitialized();return (cons != null);}

判断一下构造方法是否为空,构造方法不为空就返回 true。所以从这里可以看出,如果是有无参构造方法就会去实例化。在判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法,就是判断 readResolveMethod 是否为空,不为空就返回 true。

boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}

通过全局查找找到了赋值代码在私有方法
ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码

readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);

我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单例被破坏的问题。但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两次,只不过新创建的对象没有被返回而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。

2.4 ThreadLocal 线程单例

ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

public class ThreadLocalSingleton {private static final ThreadLocal<ThreadLocalSingleton> threadLocal =new ThreadLocal<ThreadLocalSingleton>(){@Overrideprotected ThreadLocalSingleton initialValue() {return new ThreadLocalSingleton();}};private ThreadLocalSingleton(){};public static ThreadLocalSingleton getInstance(){return threadLocal.get();}}

 

2.5 注册式单例

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

2.4.1 容器式单例

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。写法如下:

/*** 容器式单例,容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。*/
public class ContainerSingleton {private ContainerSingleton(){}private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();public static Object getInstance(String className){synchronized (ioc){if(!ioc.containsKey(className)){Object obj = null;try{obj = Class.forName(className).newInstance();ioc.put(className,obj);}catch (Exception e){e.printStackTrace();}return obj;}else{return ioc.get(className);}}}}

2.4.2 枚举登记式单例

枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。

public enum EnumSingleton {INSTANCE;private Object data;public Object getData(){return data;}public void setData(Object data){this.data = data;}public static EnumSingleton getInstance(){return INSTANCE;}
}

序列化不能破坏枚举式单例,因为枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。

 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);}}

 

同时反射也不能破坏枚举式单例,因为在JDK 源码中,进入 Constructor 的newInstance()方法,在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,将会直接抛出异常。

@CallerSensitivepublic T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{if (!override) {if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {Class<?> caller = Reflection.getCallerClass();checkAccess(caller, clazz, null, modifiers);}}if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");ConstructorAccessor ca = constructorAccessor;   // read volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(initargs);return inst;}

3、总结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用;可以通过设置全局访问点,严格控制访问。单例模式的缺点就是没有接口,扩展困难,如果要扩展单例对象,只有修改代码。单例模式的重点是私有化构造器保证线程安全、延迟加载、防止序列化和反序列化破坏单例、防御反射攻击单例。

文中提到的代码下载地址:https://github.com/sdksdk0/pattern_design/tree/master/src/main/java/cn/tf/pattern/singleton

http://www.xdnf.cn/news/11793.html

相关文章:

  • Java图像编程之:Graphics
  • sudo命令及权限管理命令
  • UEDITOR1.4.3.3整合教程
  • ACM竞赛及备战建议
  • 对IOC和DI的通俗理解
  • JAVA环境变量配置详解(全网最新详细教程)
  • 用RStudiod的界面来读取数据
  • Thread、ThreadLocal、ThreadLocalMap
  • 一文带你详细了解JVM运行时内存
  • POM及POM设计原理
  • Umi安装教程
  • 沉没成本
  • 整合StarRocks主键表全部知识点
  • iframe标签常见问题和解决方法(持续更新)
  • 我们公司用了7年的Dubbo架构技术栈(稳的一批),这套架构真绝了!
  • VNC虚拟网络控制台(概述、windows系统连接linux系统演示)
  • Spring Boot核心原理
  • 《康熙王朝》剧情分集介绍【全】
  • 新鲜出炉的JSON,拿走不谢!
  • 充分且简单!使用 Express 打造专属“轻量代理神器”
  • Java小白一文简单普及Java中的String关键字
  • VS Code :下载、安装、常用插件、工作空间概念、运行html
  • VMware虚拟机、VMware Tools、常用设置、资源分配和卡顿问题、其他常见问题
  • 使用BASE64实现编码和解码
  • php 能否制作图形,PHP制作 3D图形之自定义图形及矢量图[多图]
  • Wi-Fi 6(802.11ax)解析24:802.11ax中MU-MIMO和OFDMA的区别
  • 10个可爱的外国互动型网站
  • 免费3D模型网站大盘点,你推荐哪一个?
  • 俺常用的资源网站
  • 搭建 PHP 开发环境:详细步骤和示例代码