Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

单例模式介绍

单例模式是整个设计中比较简单的模式,即使没有看过设计模式的相关资料,也会经常用在实际业务的编码开发中。因为在编程开发中经常会遇到这种场景—需要保证一个类只有一个实例,哪怕多线程同时访问,而且需要提供一个全局访问此实例的点。

单例模式主要解决的是一个全局使用的类,被频繁地创建与销毁,从而提升代码的整体性能。

其中单例模式的实现

单例模式的实现方式比较多,主要分为在实现上是否支持懒汉模式,是否支持在线程安全中运用各项技巧。当然,也有一些场景不需要考虑懒汉模式的情况,会直接使用static静态类或属性和方法的方式,供外部调用。

静态类使用

这种静态类方式在日常的业务开发中很常见,它可以在第一次运行时直接初始化,同时也不需要直到延迟加载再使用。在不需要维持任何状态的情况下,仅仅用于全局访问,使用静态类方式更加便捷。在需要被继承及维持一些特定状态的情况下,适合使用单例模式。

1
2
3
4
5
6
7
8
9
10
11
package com.bestrookie.singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author bestrookie
* @description 静态类的使用
* @date 2021/11/3 11:12 上午
*/
public class Singleton_00 {
public static Map<String,String> cache = new ConcurrentHashMap<String, String>();
}

懒汉模式

单例模式有一个特点就是不允许外部直接创建,也就是new Singleton_01(),因此这里在默认的构造函数上添加私有属性private。虽然采用此种方式的单例满足的懒汉模式,但是如果多个访问者同时获取对象实例,就会造成多个同样的实例并存,并没有达到单例的要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.bestrookie.singleton;
/**
* @author bestrookie
* @description 懒汉式(线程不安全)
* @date 2021/11/3 11:16 上午
*/
public class Singleton_01 {
private static Singleton_01 instance;
private Singleton_01(){

}
public Singleton_01 getInstance(){
if (instance != null){
return instance;
}else {
instance = new Singleton_01();
return instance;
}
}

}

懒汉模式(线程安全)

此种模式虽然是安全的,但由于把锁加到方法中,所有的访问因为需要锁占用,导致资源浪费,除非在特殊情况下,否则不建议采用此种方法实现单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.bestrookie.singleton;
/**
* @author bestrookie
* @description 懒汉模式(线程安全)
* @date 2021/11/3 11:24 上午
*/
public class Singleton_02 {
private static Singleton_02 instance;
private Singleton_02(){

}
public static synchronized Singleton_02 getInstance(){
if (instance == null) {
instance = new Singleton_02();
}
return instance;
}
}

饿汉模式(线程安全)

这种方式与开头实例化一个Map基本一致,在程序启动时直接运行加载,后续有外部需要使用时获取即可。这种方式并不是懒加载,也就是说无论程序中是否用到这样的类,都会在程序启动之初进行创建。这种方式造成的问题就像一款游戏软件,可能游戏地图还没有打开,但是程序已经将这些地图全部实例化。在手机上最明显的体验就是打开游戏提示内存满了,造成游戏卡顿。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.bestrookie.singleton;

/**
* @author bestrookie
* @description 饿汉式(线程安全)
* @date 2021/11/3 11:35 上午
*/
public class Singleton_03 {
private static Singleton_03 instance = new Singleton_03();
private Singleton_03(){

}
public static Singleton_03 getInstance(){
return instance;
}
}

使用内部类(线程安全)

使用类的静态内部类实现的单例模式,既保证了线程安全,又保证了懒汉模式,同时不会因为加锁而降低性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程的环境下可以被正确的加载。这也是推荐使用的一种单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.bestrookie.singleton;
/**
* @author bestrookie
* @description 内部类(线程安全)
* @date 2021/11/3 11:44 上午
*/
public class Singleton_04 {
private static class SingletonHolder{
private static Singleton_04 instance = new Singleton_04();
}
private Singleton_04(){

}
public Singleton_04 getInstance(){
return SingletonHolder.instance;
}
}

双重锁校验(线程安全)

双重锁模式使方法级锁的优化,减少了获取实例的耗时。同时,这种方法也满足的懒汉模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.bestrookie.singleton;
/**
* @author bestrookie
* @description 双重锁模式
* @date 2021/11/3 1:36 下午
*/
public class Singleton_05 {
private static Singleton_05 instance;
private Singleton_05(){

}
public static Singleton_05 getInstance(){
if (instance != null){
return instance;
}
synchronized (Singleton_05.class){
if (instance == null){
instance = new Singleton_05();
}
}
return instance;
}
}

CAS(线程安全)

Java并发库提供了很多原子类支持并发的数据安全性。使用CAS的好处是不需要使用传统的加锁方式,而是依赖CAS忙等算法、底层硬件的实现保证线程安全。相对于其他锁的实现,没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发。当然CAS也有一个缺点就是忙等,如果一直没有获取到,会陷于死循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.bestrookie.singleton;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author bestrookie
* @description CAS
* @date 2021/11/3 1:42 下午
*/
public class Singleton_06 {
private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();
private static Singleton_06 instance;
private Singleton_06(){

}
public static final Singleton_06 getInstance(){
for (;;){
Singleton_06 instance = INSTANCE.get();
if (null != instance){
return instance;
}
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}
}

枚举单例

此种方式可能是平时最少用到的。这种方式解决了最主要的线程安全、自有串行化和单一实例问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.bestrookie.singleton;
/**
* @author bestrookie
* @description 枚举单例
* @date 2021/11/3 1:58 下午
*/
public enum Singleton_07 {
/**
* 枚举单例
*/
INSTANCE;
public void test(){
System.out.println("hi~");
}
}

调用方式:

1
2
3
public static void main(String[] args) {
Singleton_07.INSTANCE.test();
}

这种写法虽然在功能上与共有域的方法接近,但是它更简洁。即使在面对负载的串行化或反射攻击时,也无偿地提供了串行化机制,绝对防止对此实例化。虽然这种方式还没有被广泛采用,但是单元素的枚举已经称为实现Singleton的最佳方法。

同时我们也要知道存在继承的场景下,此种方式是不可用的。

总结

虽然单例模式只是一个很平常的模式,但在各种的实现上却需要用到Java的基本功,包括懒汉模式、饿汉模式、线程是否安全、静态类、内部类、加锁和串行化等。在日常开发中,如果可以确保此类是全局唯一的,则不需要懒汉模式,那么直接创建并给外部调用即可。但如果有很多的类,有些需要在用户触发一定的条件后才显示,那么一定要用懒汉模式,对于线程安全,可以按照需要选择。

评论