什么是单例模式

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

img

单例模式应用场景

举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。

继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。

再举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制

同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。

从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点:

适用场景:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 4.方便资源相互通信的环境

单例模式的优缺点

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

单例模式实现

饿汉式

jvm加载类的的时候就把单例对象生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 饿汉式单例
public class Singleton1 {

// 指向自己实例的私有静态引用,主动创建
private static Singleton1 instance = new Singleton1();

// 私有的构造方法
private Singleton1(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getInstance(){
return instance;
}
}

我们知道,类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,jvm底层保证了单例

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

懒汉式

jvm加载类的的时候不生成单例对象,而是需要的时候才第一次创建,线程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 懒汉式单例
public class Singleton2 {

// 指向自己实例的私有静态引用
private static Singleton2 instance;

// 私有的构造方法
private Singleton2(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton2 getInstance(){
// 被动创建,在真正需要使用时才去创建
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}

们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (instance== null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

线程安全饿汉式之全锁

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton3 {

private static Singleton3 instance;

public static synchronized Singleton3 getInstance() {
// 创建单例的具体业务逻辑.....省略
if (instance == null) {
instance = new Singleton3();
}
// 创建单例的具体业务逻辑.....省略
return instance;
}
}

优点:保证了线程安全和懒加载

缺点:效率低。这种模式保证的线程安全,但是效率特别低,在高并发情况下,每次调用getInstance方法都需要加锁,显然不符合高并发情况的效率。

线程安全饿汉式之部分锁+双重判空检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton4 {

private static Singleton4 instance; // 不使用volatile关键字
// 双重锁检验
public static Singleton4 getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton4.class) {
//这里必须要在使用一次判空检查,在多线程情况下,可能多个线程都判断为空,进入了synchronized
if (instance == null) {
instance = new Singleton4(); // 第10行
}
}
}
return instance;
}
}

如果发生了重排序(在高并发情况下小概率出现,但是确实会出现):

1
2
3
4
5
6
7
8
9
10
11
instance = new Singleton4(); // 第10行

// 可以分解为以下三个步骤
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址

// 上述三个步骤可能会被重排序为 1-3-2,也就是:
1 memory=allocate();// 分配内存 相当于c的malloc
3 s=memory //设置s指向刚分配的地址
2 ctorInstanc(memory) //初始化对象

而一旦假设发生了这样的重排序,比如线程A在第10行执行了步骤1和步骤3,但是步骤2还没有执行完。这个时候另一个线程B执行到了第7行,它会判定instance不为空,然后直接返回了一个未初始化完成的instance!

优点:保证了加锁的效率,线程安全和懒加载

缺点:在高并发情况下,小概率出现实例为空(或者一个默认值)的情况

线程安全饿汉式之双重锁检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton5 {

private static volatile Singleton5 instance;

// 双重锁检验
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
//这里必须要在使用一次判空检查,在多线程情况下,可能多个线程都判断为空,进入了synchronized
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}

这里使用volatile禁止了instance = new Singleton5(); 中发生的指令重排序,保证了线程安全。

静态内部类

利用了jvm底层保证只能创建一次,保证了线程安全

类的装载机制保证初始化实例的时候只有一个线程,静态内部类在Singleton被装载时不会立即初始化,而是在调用getInstance时才会装在静态内部类,从而完成Singleton的实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton6 {
public Singleton() {
}
public void doAction(){
//TODO 实现你需要做的事
}
private static class SingletonInstance{
private final static Singleton SINGLETON = new Singleton();
}
public static Singleton getInstance(){
return SingletonInstance.SINGLETON;
}
}

优点:

  1. 利用JVM加载静态内部类的机制保证多线程安全
  2. 实现Lazy loading效果
  3. 效率高

枚举实现单例模式

在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。而且枚举还防止了序列化。

利用序列化也就是反射,能直接更改一个类的任意属性,但是如果使用枚举类,可以避免这种情况。因为枚举类在编译后是abstract class,是没有构造方法的,所有无法通过一个构造方法去创建一个对象,所以即使序列化后的对象也是之前的单例对象。

1
2
3
4
5
6
7
public enum Singleton7 {
INSTANCE;

public void doSomething() {
System.out.println("doSomething");
}
}

调用方法:

1
2
3
4
5
6
7
8
public class Main {

public static void main(String[] args) {
Singleton7.INSTANCE.doSomething();
}

}

优点:

  1. 线程安全(枚举实例的创建默认就是线程安全的)
  2. 不会因为序列化而产生新实例
  3. 防止反射攻击

单例防止反射

以双重检测方式为例测试反射,序列化,克隆是否能破环单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Singleton5 implements Serializable, Cloneable   {

private static volatile Singleton5 instance;

// 双重锁检验
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
//这里必须要在使用一次判空检查,在多线程情况下,可能多个线程都判断为空,进入了synchronized
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

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
34
35
36
/**
* @author lvxiaoyi
* @date 2021/10/11 9:20
*/
public class Singleton5Reflect {
public static void main(String[] args) throws Exception{
// 通过getInstance()获取
Singleton5 singleton = Singleton5.getInstance();
System.out.println("singleton的hashCode:"+singleton.hashCode());

// 通过反射获取
Constructor<Singleton5> constructor = Singleton5.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton5 reflex = constructor.newInstance();
System.out.println("reflex的hashCode:"+reflex.hashCode());

// 通过克隆获取
Singleton5 clob = (Singleton5)Singleton5.getInstance().clone();
System.out.println("clob的hashCode:"+clob.hashCode());

//通过序列化,反序列化获取
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(Singleton5.getInstance());

ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Singleton5 serialize = (Singleton5)ois.readObject();
ois.close();
bis.close();
oos.close();
bos.close();
System.out.println("serialize的hashCode:"+serialize.hashCode());
}
}

1
2
3
4
singleton的hashCode:1163157884
reflex的hashCode:1956725890
clob的hashCode:356573597
serialize的hashCode:2074407503

防止反射可以通过一个变量来控制,但是反射也能修改变量,如果先修改变量在创建实例,还是不能防止反射,所以到现在为止最好的方法就是使用枚举来创建防止反射的单例模式

参考文章:https://www.cnblogs.com/call-me-pengye/p/11169051.html