详解单例模式的(7种实现)
作者:互联网
目录
详解单例模式的(7种实现)
单例模式的实现基本都是要基于private的构造函数
1. 饿汉式
顾名思义,就是很饿,一遇到就要创建。
// 饿汉式 ,很饿,一上来就能接受
// 问题:可能浪费空间,比如下面的四个数组
public class Hungry {
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
2. 懒汉式
基础的懒汉式,先不创建,第一次getInstance的时候创建。
多线程下存在问题,因为在第一个实例创建的过程中但还未创建完成,第二个实例开始创建,就会得到两个甚至多个实例。
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
// 多线程下有问题,
if (lazyMan == null){
lazyMan = new LazyMan(); // 不是一个原子性操作,指令重排
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
// 使用十个线程创建对象 此时会输出多个实例
System.out.println(LazyMan.getInstance());
}).start();
}
}
}
3. 懒汉式加锁
只需要在2懒汉式的基础上,对getInstance方法加锁,锁住Class模板,就可以防止刚才那个问题了。
这个看似多线程下正确的解法,据侯捷老师说曾经难住了当时当时的计算机界,最后发现原来时指令重排,下一个方法详细说。
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
// 多线程下有问题,
if (lazyMan == null){
lazyMan = new LazyMan(); // 不是一个原子性操作,指令重排
}
return lazyMan;
}
public static synchronized void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
// 使用十个线程创建对象 此时会输出多个实例
System.out.println(LazyMan.getInstance());
}).start();
}
}
}
4. 懒汉式+双锁检查
在一个对象的创建过程种,我们只写一句new Singleton,但其实包括三个小过程,1. 内存空间的开辟,2. 对象的初始化,3. 将对象指向内存地址,cpu指令重排若重排2,3,第一个实例已经不是null了,第二次调用的时候直接返回了这个对象,但实际上这个对象还没有真正创建完成,未完成初始化的对象逃逸了,这在多线程编程中时很糟糕的。
解决方法就是著名的双重检查锁
public class LazyMan {
private static boolean lihe = false;
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
// 多线程下有问题,
// 解决:著名的双重检测锁模式的懒汉式单例 DCL懒汉式
if (lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null){
lazyMan = new LazyMan(); // 不是一个原子性操作,指令重排
/*
* 1. 分配内存空间
* 2. 执行构造方法,初始化对象
* 3. 对象指向空间
*/
// 会出现先执行3,然后另一个线程进来认为已经不是null了,未定义对象逃逸
}
}
}
return lazyMan;
}
}
说明:双锁机制的出现是为了解决前面同步问题和性能问题,看上面的代码,简单分析下确实是解决了多线程并行进来不会出现重复new对象,而且也实现了懒加载,但是当我们静下来并结合java虚拟机的类加载过程我们就会发现问题出来了,对于JVM加载类过程不熟悉的,这里我简单介绍下,熟悉的跳过这段(当然,既然你熟悉就自然会知道双锁的弊端了)。
jvm加载一个类大体分为三个步骤:
加载阶段:就是在硬盘上寻找java文件对应的class文件,并将class文件中的二进制数据加载到内存中,将其放在运行期数据区的方法区中去,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构;
连接阶段:这个阶段分为三个步骤,步骤一:验证,验证什么呢?当然是验证这个class文件里面的二进制数据是否符合java规范咯;步骤二:准备,为该类的静态变量分配内存空间,并将变量赋一个默认值,比如int的默认值为0;步骤三:解析,这个阶段就不好解释了,将符号引用转化为直接引用,涉及到指针,这里不做多的解释;
初始化阶段:当我们主动调用该类的时候,将该类的变量赋于正确的值(这里不要和第二阶段的准备混淆了),举个例子说明下两个区别,比如一个类里有private static int i = 5; 这个静态变量在"准备"阶段会被分配一个内存空间并且被赋予一个默认值0,当道到初始化阶段的时候会将这个变量赋予正确的值即5,了解了吧!
好了,上面大体介绍了jvm类加载过程,回到我们的双锁机制上来分析下问题出在了哪里?假如有两个并发线程a、b,a线程主动调用了静态方法getInstance(),这时开始加载和初始化该类的静态变量,b线程调用getInstance()并等待获得同步锁,当a线程初始化对象过程中,到了第二阶段即连接阶段的准备步骤时,静态变量doubleKey 被赋予了一个默认值,但是这时还没有进行初始化,这时当a线程释放锁后,b线程判断doubleKey != null,则直接返回了一个没有初始化的doubleKey 对象,问题就出现在这里了,b线程拿到的是一个被赋予了默认值但是未初始化的对象,刚刚可以通过锁的检索!
饿汉式由于一开始就初始化了,所以不存在这个问题。
5. 静态内部类
静态内部类的方式效果类似双检锁,但实现更简单。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
// 静态内部类
public class Holder {
private Holder(){
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
}
6. 登记式
登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。
这里我对登记式单例标记了可忽略,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。
//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
static{
Singleton3 single = new Singleton3();
map.put(single.getClass().getName(), single);
}
//保护的默认构造子
protected Singleton3(){}
//静态工厂方法,返还此类惟一的实例
public static Singleton3 getInstance(String name) {
if(name == null) {
name = Singleton3.class.getName();
System.out.println("name == null"+"--->name="+name);
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton3) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一个示意性的方法
public String about() {
return "Hello, I am RegSingleton.";
}
public static void main(String[] args) {
Singleton3 single3 = Singleton3.getInstance(null);
System.out.println(single3.about());
}
}
7. 枚举
枚举时Java天然的内部类,而且值得注意的是,Java的反射机制完全可以破坏以上六种单例模式。但却无法破坏枚举,这就是亲儿子的好处。原因倒不是在于枚举有什么厉害的,而是反射本身规定了不能破坏枚举单例。
// 枚举 emun是什么,本身也是一个Class类
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
下面是反射的newInstance源码,可以清晰地看到"Cannot reflectively create enum objects"。
@CallerSensitive
public 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 volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
谈谈怎样破坏单例模式
刚才说到枚举不能被破坏,前面都可以被破坏。其实就是利用反射机制,将private设置成accessible
// 反射破解单例
LazyMan instance = LazyMan.getInstance();
Field lihe = LazyMan.class.getDeclaredField("lihe");
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance == instance2);
输出false,这样就获得了两个实例。
或许还可以通过设置别人不知道的私有标志变量去限制上面这种破解,但是若是能获得反编译的源码,就只有亲儿子枚举可以做到不被破解了。
标签:模式,LazyMan,详解,static,private,单例,new,public 来源: https://blog.csdn.net/weixin_42227763/article/details/115102153