设计模式の单列模式
所谓单列模式
单列模式是指确保一个类在任何情况下都绝对只有一个实例,并对外提供一个全局的访问点
比如:ServletContext、SeevletContextConfig、ApplicationContext、数据库连接池 ......
但创建单列的方式有很多种,下面我们一一来学习
饿汉式单列
饿汉式适用在单例对象较少的情况,Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例
懒汉式单列
在上面的图中,我们说他的缺点,只能在单线程中使用,
-
下面我们就用手动控制多线程运行进度的方式来破解这种单列
道高一尺,魔高一丈,居然大家都知道这种破解方法了,那就只有进化了:synchronized
-
要想使得懒汉式在多线程环境下保证自己的单列,那就只有让那个判断变为线程同步方法了
我们再次开启线程调试模式进行debug
-
我们发现当两个线程都被我们同步控制到 getInstance()方法时,一个线程显示为running,一个线程状态显示为monitor(阻塞)
-
直到我们第一个县城执行完,返回的时候,第二个线程状态才变更为running,此时再判断singleton3 == null 明显就是false了,保证了单列
-
但是如果我们的服务的线程并发情况比较严重,那么获得该实列的成本就高了起来,线程阻塞情况逐渐严重
-
完美版本,兼顾多线程环境,性能得到保证:双重检查锁
-
第一次判断,并发线程都可以进入,即使全部判断为true
-
其中一个线程获得块级锁synchronized执行权,进行执行,进入其中,再次判断也为true,创建对象并返回
-
其他同步并发线程依次进入块级锁synchronized,再次进行判断,结果为false,直接返回
-
除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题
-
这其中涉及到一个关键字:volatile
为什么要加引用修饰符:volatile
-
singleton3 = new Singleton3(); 这行代码对于JVM而言可以依次分解为三步
-
分配内存空间
-
初始化对象
-
将对象引用指向刚分配的内存空间
-
-
为了提高程序执行性能,编译器和处理器会对指令的处理过程进行:重排序机制
-
上面依次执行三步,2和3就变换了位置,这或许就是真正的懒吧
-
分配内存空间
-
将对象引用指向刚分配的内存空间
-
初始化对象
-
-
如果我们的对象引用 singleton3 没有加 volatile 关键字,这个单列可能就被破坏了
-
试想
-
A、B两个线程同时进入第一次 if (singleton3 == null) ,均拿到为true的结果
-
A分配到了CPU执行权,进入块级锁,
-
这时发生了重排序机制,初始化对象这一步变成了第三步 , 还没执行完,方法返回了,释放锁
-
B线程进入块级锁,再次判断,if (singleton3 == null) ,也得到结果为:true,再次创建对象返回
-
单列模式被破坏,GG
-
-
-
经过volatile修饰的变量,如果一个线程修改了该变量的值,会立刻刷新到主内存区域,其他线程要读该变量的值,必须要写完之后。
-
也就是B线程在第二次判断if (singleton3 == null)时,必须等到线程A对该变量写完初始化成功后再读取
-
于是 if (singleton3 == null) 就会得到结果为false,直接返回线程A创建的对象实列
-
静态内部类单列
反射破解单列
就拿我们目前为止觉得很OK的静态内部类单列,我们来破解他的单列
可以发现我们是通过强制访问 Singleton7 无参构造来实现的暴力初始化,为了防止他暴力访问无参方法创建实例,咱也来写一把牛逼的代码
-
我们在私有的构造方法中加点颜色,防止他暴力访问
序列化破解单列
当我们将一个单例对象创建好,然后序列化为字符串,然后再将字符串反序列化为对象
反序列化后的对象会重新分配内存, 即重新创建,单列又被破坏了哦
序列化的方式分为很多种,上面我们使用三方库的方式将其序列化为字符串,然后再反序列化为对象,可见单列已经被破坏
网上还有一种序列化方式是可以防止单列被破坏的:下面我们来大概看一下
将创建好的单列对象通过IO流,刷盘到磁盘中,然后再通过IO流去读取
-
至于原因是什么,大家可以去看看ObjectInputStream的readObject()方法的源码
-
JDK中,通过明文指定名为readResolve属性,反射得到该方法,如果该方法存在,则返回该方法返回的实列作为反序列化的引用
-
如果该方法不存在,则会从新创建一个新的对象,并完成分配内存、初始化对象、将对象引用指向刚分配的内存空间的过程
-
注册式单列(枚举)
注册式单例有两种写法:一种为枚举登记,一种为容器缓存
此外,当我们使用序列化的方式尝试破坏枚举的单列时,是不行的,通过反射的方式爱破坏也是不行的
枚举的方式是一种推荐的单列模式的实现方式
注册式单列(容器缓存)
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的
public class ContainerSingleton { private ContainerSingleton(){} private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>(); public static Object getBean(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); } } } }
ThreadLocal线程单列
ThreadLocal 不能保证其 创建的对象是全局唯一,但是能保证在单个线程中是唯一的
那么ThreadLocal又是如何保证线程隔离的呢
ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的
总结
单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。