设计模式学习笔记|单例模式 Singleton
单例模式是设计模式中比较经常听说的设计模式,也是比较容易掌握的设计模式。基本上接触过设计模式的人别的模式不一定能说出来,但是一般“单例模式”和“工厂模式”是都能说出来的。
很多时候,我们都会以为单例模式是比较好掌握的,但是后来在我的学习当中,我发现还是有很多问题是没有考虑到的,甚至是想象不到的。
单例模式是要使类的实例在内存中只有一份。听起来挺容易的,但是这个还真是没有想象的那么简单。我的代码使用 Java 来进行描述。
通常情况下,在使用 Java 来完成 单例模式 的时候,都知道存在两种写法,一种是饿汉模式,另一种是懒汉模式。所谓饿汉模式,就是在类加载入内存之后,直接实例化一个对象出来;懒汉模式是在需要的时候再去实例化一个对象出来。
为什么有饿汉模式和懒汉模式呢?这得从它们的加载时机来考虑。很多人认为,饿汉模式在类进入内存就实例化一个对象有些不妥,因为没有使用,为什么要着急实例化呢,所以就出现了懒汉模式。懒汉模式是在需要的时候才去实例化类的对象,但是懒汉模式会因为多线程的问题,会导致实例化多个对象出来,而此时就需要解决多线程同步的问题。解决多线程同步的问题,就需要用到锁,那么就又带来了效率上的问题。
说了这么多,那么来看看,到底如何来使用 Java 语言完成一个 单例模式。
饿汉模式
先来看看饿汉模式的代码:
public class Singleton01 {
private static final Singleton01 INSTANCE = new Singleton01();
// 构造函数为 private
private Singleton01() {}
public static Singleton01 getInstance() { return INSTANCE; }
// 此处模拟类中处理业务的方法
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Singleton01 s1 = Singleton01.getInstance();
Singleton01 s2 = Singleton01.getInstance();
// 两个引用指向的是一个对象
System.out.println(s1 == s2);
}
}
```
单例模式的第一步就是将 构造方法 的访问修饰符设置为 private,使得外部无法直接实例化。然后在类中定义一个静态的 getInstance 方法用来获取实例。
使用饿汉模式的单例,在类加载到内存后,静态变量只实例化一次,JVM 保证其线程的安全。
其缺点是,不管该类是否要使用,都会马上得到一个实例。因此,这就有了懒汉模式。
#### 懒汉模式
懒汉模式的单例的代码:
public class Singleton06 {
private static volatile Singleton06 INSTANCE;
private Singleton06() {}
public static Singleton06 getInstance() {
if (INSTANCE == null) {
// 双重检查
synchronized (Singleton06.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i ++) {
new Thread(()->{
System.out.println(Singleton06.getInstance().hashCode());
}).start();
}
}
}
```
以上就是懒汉模式的代码。
在懒汉模式中,使用了 synchronized 来解决方法“不可重入”的问题,其中使用 Thread.sleep 来让线程休息一下,从而让出线程所占用的 CPU 而产生线程的切换。可以把第一个 if 判断和 synchronized 两行删掉,只留下最里面的 if 语句块的内容,就会发现会实例化多个对象了。
实例化多个对象
在 Java 中提供了反射的机制,即使使用单例模式,仍然可以实例化出多个对象。无论是上面的饿汉模式,还是懒汉模式,都可以实例化多个实例。
这里使用第一个饿汉模式的代码进行测试,测试代码如下:
Class<?> aClass = Class.();
Singleton01 s3 = (Singleton01) aClass.newInstance();
代码很简单,只有上面两句,但是这样就已经实例化出了一个对象,且通过 s3 可以调用该类中的方法。
因此这样,就可以实例化对象出来了,内存中就有了一个类的多个实例了。
枚举类的单例
枚举在很多语言中都有,一般情况就是定义一些有限的常量。其实,枚举类中可以定义方法。看一下枚举类的单例代码,代码如下:
public enum Singleton08 {
INSTANCE;
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i < 100; i ++) {
new Thread(()->{
System.out.println(Singleton08.INSTANCE.hashCode());
}).start();
}
}
}
以上代码,仍然通过 new Thread 多线程来得到其实例。
但是,通过输出可以看出,其 hashCode 始终是一样的。
接着使用上面的反射来获取枚举类的实例,代码如下:
Class<?> aClass = ;
{
aClass = Class.();
Singleton01 s3 = (Singleton01) aClass.newInstance();
然后代码执行到 newInstance 方法时会报错,提示访问异常。
因为枚举类没有定义构造函数,因此无法实例化。
也就是说使用枚举类,即可以保证线程的安全,也可以防止反射来实例化。算是一种完美的方法。
最后
看似简单的单例模式,其中竟然也蕴含着这么多的知识点,学完真是受益非浅。虽然只是一个单例模式,掌握了一种设计模式,但是从各种实现中,又学到了很多其他的知识。比如,类实例化的时机,多线程方法的不可重入,枚举类的另类用法等。
所以,知识如果能够串联起来,那么才能把学到的知识融会贯通,真正掌握和吸收。
这就是我关于设计模式中单例模式的一篇笔记。
微信中搜索 “码农UP2U” 关注我的公众号吧!!!