常见的几种单例模式

还未入行的Java小白,帮自己以及其他小白(估计没有)整理下所学到的东西当然也有一部分原因是怕自己记不住
如有大神有幸翻到此文章,也希望能看一看,若此文中内容有误,还望能纠个错,提个意见.

什么是单例模式?

在整个程序中只有一个实例,这个类负责创建自己的对象并确保只有一个对象被创建.

为什么需要单例模式?

防止全局使用或消耗大量系统资源的类频繁的创建销毁

哪些地方可以用到单例?

数据库连接池,线程池,日志…

怎么实现单例?

实现要点如下:

a)私有构造器
b)设立静态变量
c)公开获取实例的静态方法

写法一:饿汉式
public class Singleton1 {
    //静态变量
    private static Singleton1 singleton = new Singleton1();
    //私有构造器
    private Singleton1() {
    	System.out.println("load");
    }
    //公开获取实例的静态方法
    public static Singleton1 getSingleton() {
        return singleton;
    }
}

写好后可以测试,这里试了下两种状态

public static void main(String[] args) throws Exception {
	demo();
	reflexDemo();
	threadDemo();
}

private static void demo() {
	System.out.println("普通");
	Singleton1 s1 = Singleton1.getSingleton();
	Singleton1 s2 = Singleton1.getSingleton();
	System.out.println(s1 == s2);
}

private static void reflexDemo() throws Exception {
	System.out.println("反射");
	Class<Singleton1> clazz = Singleton1.class;
	Constructor<Singleton1> constructor = clazz.getDeclaredConstructor();
	constructor.setAccessible(true);
	Singleton1 s1 = Singleton1.getSingleton();
	Singleton1 s2 = constructor.newInstance();
	System.out.println(s1 == s2);
}

private static void threadDemo() {
	System.out.println("多线程");
	for (int i = 0; i < 5; i++) {
		new Thread() {
			@Override
			public void run() {
				System.out.println(Singleton1.getSingleton());
			}
		} .start();
	}
}

分别测试结果

load
普通
true
load
反射
false
load
多线程
singleton.Singleton1@d001520
singleton.Singleton1@d001520
singleton.Singleton1@d001520
singleton.Singleton1@d001520
singleton.Singleton1@d001520

从输出结果可以看出饿汉式是线程安全,反射不安全.且在类初始化的时候就已经创建了实例,如果一直都没有使用的话,那就白嫖了内存资源.

写法二:登记式
public class Singleton2 {
    private static class SingletonHolder {
        private static Singleton2 singleton = new Singleton2();
    }

    private Singleton2() {
        System.out.println("load");
    }

    public static Singleton2 getSingleton() {
        return SingletonHolder.singleton;
    }
}

测试方法,还是一样分三种情况测试

private static void demo() {
	System.out.println("普通");
	Singleton2 s1 = Singleton2.getSingleton();
	Singleton2 s2 = Singleton2.getSingleton();
	System.out.println(s1 == s2);
}

private static void reflexDemo() throws Exception {
	System.out.println("反射");
	Class<Singleton2> clazz = Singleton2.class;
	Constructor<Singleton2> constructor = clazz.getDeclaredConstructor();
	constructor.setAccessible(true);
	Singleton2 s1 = Singleton2.getSingleton();
	Singleton2 s2 = constructor.newInstance();
	System.out.println(s1 == s2);
}

private static void threadDemo() {
	System.out.println("多线程");
	for (int i = 0; i < 5; i++) {
		new Thread(() -> {
			System.out.println(Singleton2.getSingleton());
		}).start();
	}
}

测试结果

普通
load
true
反射
load
load
false
多线程
load
singleton.Singleton2@3d797c69
singleton.Singleton2@3d797c69
singleton.Singleton2@3d797c69
singleton.Singleton2@3d797c69
singleton.Singleton2@3d797c69

从输出结果可以看出登记式可以说是饿汉式pro,不同点是登记式可以延迟加载,不会浪费内存资源.其他与饿汉式一样线程安全,反射不安全.

写法三:懒汉式
public class Singleton3 {
    private static Singleton3 singleton3 = null;

    private Singleton3() {
        System.out.println("load");
    }

    public static Singleton3 getSingleton() {
        if (singleton3 == null)
            singleton3 = new Singleton3();
        return singleton3;
    }
}

测试方法

@Test
void demo() {
	System.out.println("普通");
	Singleton3 s1 = Singleton3.getSingleton();
	Singleton3 s2 = Singleton3.getSingleton();
	System.out.println(s1 == s2);
}
@Test
void reflexDemo() throws Exception {
	System.out.println("反射");
	Class<Singleton3> clazz = Singleton3.class;
	Constructor<Singleton3> constructor = clazz.getDeclaredConstructor();
	constructor.setAccessible(true);
	Singleton3 s1 = Singleton3.getSingleton();
	Singleton3 s2 = constructor.newInstance();
	System.out.println(s1 == s2);
}
@Test
void threadDemo() {
	System.out.println("多线程");
	for (int i = 0; i < 5; i++) {
		new Thread(() -> {
			System.out.println(Singleton3.getSingleton());
		}).start();
	}
}

测试结果

普通
load
true
反射
load
false
多线程
load
singleton.Singleton3@7839e252
singleton.Singleton3@806b040
load
singleton.Singleton3@7839e252
singleton.Singleton3@7839e252
singleton.Singleton3@7839e252

经过了将近十多分钟的不断重试,终于得出了可以有代表性的结果.
可以从上面的结果得出懒汉式可以延迟加载,反射不安全,线程不安全.
不过线程问题,可以通过修改代码来实现控制

public static Singleton3 getSingleton() {
	if (singleton3 == null) {
		synchronized (Singleton3.class) {
			if (singleton3 == null) {
				singleton3 = new Singleton3();
				System.out.println("load");
			}
		}
	}
	return singleton3;

因为懒的再做好几分钟测试所以直接说结论
多次测试后观察结果说明已经可以做到线程安全,但是这就不会出现问题了么?
还是有一种隐患存在,如果发生指令重排的话,就不一定能保证了.

注:以下为个人看法,若有误还望及时告知补正

众所周知指令重排是cpu和编译器为了提高程序的执行效率会按照一定的规则允许指令优化.

那么至于会怎么进行优化,可能会优化成什么样子?需要先知道我们的程序理论是按照什么逻辑顺序执行.
我们在编译后的Singleton3.class的文件目录中,用cmd输入如下指令,将.class文件进行反编译

javap -c Singleton3.class > Singleton3.txt

打开Singleton3.txt后里面的内容如下

Compiled from "Singleton3.java"
public class singleton.Singleton3 {
  public static singleton.Singleton3 getSingleton();
    Code:
       0: getstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
       3: ifnonnull     45
       6: ldc           #3                  // class singleton/Singleton3
       8: dup
       9: astore_0
      10: monitorenter
      11: getstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
      14: ifnonnull     35
      17: new           #3                  // class singleton/Singleton3
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
      24: putstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
      27: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      30: ldc           #6                  // String load
      32: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      35: aload_0
      36: monitorexit
      37: goto          45
      40: astore_1
      41: aload_0
      42: monitorexit
      43: aload_1
      44: athrow
      45: getstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
      48: areturn
    Exception table:
       from    to  target type
          11    37    40   any
          40    43    40   any

  static {};
    Code:
       0: aconst_null
       1: putstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
       4: return
}

对照Oracle Help中的描述以及翻译和百度的支持,主要的内容在如下几行

17: new           #3                  // class singleton/Singleton3
20: dup
21: invokespecial #4                  // Method "<init>":()V
24: putstatic     #2                  // Field singleton3:Lsingleton/Singleton3;
27: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
30: ldc           #6                  // String load
32: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
35: aload_0

这几行的作用大致如下

操作码描述
new创建一个对象,并且其引用进栈
dup复制栈顶数值,并且复制值进栈
invokespecial用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和超类构造方法
putstatic为指定的类的静态域赋值
getstatic获取指定类的静态域,并将其值压入栈顶
ldc将常量值从常量池中推送至栈顶
invokespecial用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和超类构造方法
aload_index当前frame的局部变量数组中下标为index的引用型局部变量进栈

分析这几行后简单来说就是下面这几个顺序
a)创建(new Singleton3())对象
b)获得对象地址
c)将singleton3引用指向对象地址

从这个顺序上来看正常是a->b->c.
那先得到一个地址,将singleton3引用指向地址,然后再在这个指定的地址上创建对象,仿佛这么做也没毛病,即b->c->a的顺序
但若是多线程的情况下情况可能就要出问题了
如果在线程A执行完b->c,而线程B去进行是否为null的判断,那此时singleton3不为空,B不用抢锁直接把还未初始化好的singleton3返回执行其他操作,可能就会报空指针异常了.

注:以上推测由于无法通过测试证实,若有误还望有哪位大神可以告知真正的原因

既然是因为指令重排的问题导致的隐患,那么就很简单了,只要加上volatile修饰符,让singleton3变量的操作不会进行指令重排就可以了

private static volatile Singleton3 singleton3 = null;
写法四:枚举式
public enum Singleton4 {
   INSTANCE{
       @Override
       void print() {
           System.out.println("Singleton4.print");
       }
   };
   abstract void print();
}

测试方法

@Test
void demo() {
	Singleton4 s1 = Singleton4.INSTANCE;
	Singleton4 s2 = Singleton4.INSTANCE;
	System.out.println(s1 == s2);
}
@Test
void reflexDemo() throws Exception {
	Class<Singleton4> clazz = Singleton4.class;
	Constructor<Singleton4> constructor = clazz.getDeclaredConstructor();
	constructor.setAccessible(true);
	Singleton4 s1 = constructor.newInstance();
}

测试结果

普通
true
反射
java.lang.NoSuchMethodException: singleton.Singleton4.<init>()

	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at org.demo.singleton.Singleton4Tests.ReflexDemo(Singleton4Tests.java:24)
	...

枚举优势很明显,代码相对简单,创建枚举时默认线程安全,而且天然的防止反射,而其他几种需要手动添加判断和报错的逻辑代码.目前好像也是比较推荐使用枚举单例.
不过必然也存在缺点,如果是需要有继承的情况,那枚举就不太合适了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值