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