设计模式实现---单例模式

单例模式可能是最最经常使用的模式之一了(另一个竞争对手是工厂模式)。 可能你并不知道单例模式是什么,但是

无论是工作还是面试,你一定会听到这个词汇。

概念

单例模式是设计模式的一种,属于创建型模式。单例模式是指在一次程序的运行中,相同类的实例有且最多只有一个。这样看来,用static修饰的方法好像满足条件(因为每次都是调用的同一个类而不是用new关键字创建出来的类)。sorry,并不是这样的,单例模式由他自己的 style:

  • 单例模式只能有一个实例
  • 单例类必须自己创建自己的唯一实例(我生我自己~)
  • 单例类必须给所有其他对象提供这一实例(换句话说就是类似于写个get方法这种的~)

要满足上面的要求,我们可以设想下需要进行以下操作:

  • 因为只有一个实例,所以我们需要将修饰词为public的构造方法改为private(只需州官放火,不许百姓点灯)
  • 自己创建自己,所以我们可以用static修饰一个对象并且初始化

  • 能给其他对象提供实例,也就是说我们需要写一个public修饰的get方法,方法返回实例内存的那个唯一对象

实现

单例模式的实现依照构建方式分类都能分成两个大类:懒汉式饿汉式;其中因为需要考虑线程安全(因为单例模式只有一个实例,所以在多线程环境下创建的时候可能会出现重复创建的情况)的原因,实现细节又有些许改变

懒汉式

懒汉式是指只在第一次使用到这个类的时候构建实例,如果不用则不构建。相比于下面的饿汉式,更加节省内存空间。

普通实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author bestsort
*/
public class Singleton {
private int a;
private static Singleton instance;
/**
* 改为private,禁止从外部调用构造方法创建新的实例
*/
private Singleton(){}
/**
* get方法用于获取唯一实例
*/
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

这种实现非常简单,但是会出现线程安全问题。如果是多个线程同时访问,此时由于未加锁,会 new 出来多个Singleton对象.

加锁实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author bestsort
*/
public class Singleton {
private int a;
private static Singleton instance;
private Singleton(){}
public synchronized static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

相较于上面的不加锁,虽然在get方法上加上synchronized关键字能够保证其线程安全,但是锁粒度太大了,会影响效率,毕竟大多数情况下并不需要同步。

饿汉式

饱汉式是指不管用没有用到这个类,都会在类装载的时候构建一个唯一实例出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author bestsort
*/
public class Singleton {
private int a;
/**
* 默认创建一个实例
*/
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){return instance;}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

可以通过Singleton.getInstance()形式获取 Singleton 实例。这是最简单的一种实现,但是容易产生垃圾对象(因为new出来的instance实例不一定会用到)。而且因为是基于static关键字,所以初始化的时候不会产生线程安全问题。

其他实现

双重校验锁实现

双重校验锁会进行两次检查,第一次是当实例为null的时候,如果需要创建实例对象,则会用synchronized关键字锁住响应对象头,然后再次检查对象是否为空(因为从检查到上锁前这段时间里实例对象可能已经被其他线程创建出来了,只有上锁后才能唯一确定其状态是【null】还是【已创建】)。而且相较于其他方法,双重校验锁能够在多线程的情况下依旧保持较高的性能。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* @author bestsort
*/
public class Singleton {
private int a;
/**
* 这里使用 volatile 防止指令重排,因为new并不是一个原子操作
* 不加 volatile 的话可能其他线程会访问到未初始化的对象
* 可以参见 java.util.concurrent.atomic 下的类,里面的value同样用volatile修饰过
*/
private volatile static Singleton instance;
private Singleton(){}
/**
* 双重检查锁(double-checked locking)
*/
public static Singleton getInstance(){
if (instance == null){
//这里用synchronized锁住了该对象,保证有且只有一个线程创建Singleton实例
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

静态内部类实现

静态内部类的好处是通过classloader的特性,可以天生实现懒加载和线程安全。只有当调用getInstance时,classloader才会去装在SingletonHolder,从而实例化INSTANCE。如果要实现懒加载特性,推荐使用之这种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author bestsort
*/
public class Singleton {
private int a;
private static Singleton instance;
private static class SingletonHolder{
private static Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

枚举

枚举类实现单例模式有很多优点:

  • 枚举类默认有private的无参构造方法

  • 自带线程安全

  • 枚举类天生支持序列化,也能防止反序列化重新创建新的对象

  • 枚举类编译后方法修饰词默认是final,能够防止反射攻击。此点存疑,笔者通过Fernflower decompiler反编译后并未看到final关键字,但是StackOverflow上时这么说的。这里因为笔者水平限制,所以给出相关链接由读者自行鉴别:StackOverflow相关问题问题中提到的外链CSDN博文对此解释.

    相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
public enum Singleton{
// 实例
INSTANCE;
int a;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
}

调用的时候可以通过

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
System.out.println(singleton.getA());
}
}

进行调用。

单例模式使用场景

个人认为,绝大多数用于数据处理的类都应该通过单例模式创建。Spring 就是这么做的,默认组件就是通过单例模式创建,有且仅有一个实例。还有就是频繁创建/销毁的对象,因为创建/销毁通常需要更多的GC次数,所以可以通过单例实现对象的复用。

觉得文章不错的话可以请我喝一杯茶哟~
  • 本文作者: bestsort
  • 本文链接: https://bestsort.cn/2020/01/28/128/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-SA 许可协议。转载请注明出处!并保留本声明。感谢您的阅读和支持!
-------------本文结束感谢您的阅读-------------