设计模式(一):单例模式(Singleton Pattern)

单例模式:自己创建自己的唯一实例,在内存中只有这一个对象,不允许其它类自由创建该类的对象,给所有其它类提供这唯一实例

单例设计模式

  1. 私有(private)化该类的空参构造器,禁止其他类从外部来创建本类的对象。
  2. 定义一个本类的对象,指向自己实例的引用,privatefinal修饰,其他类不可改变此引用变量的值。
1
Singleton instance;
  1. 提供以自己实例为返回值的静态的公共的方法。
1
2
3
public static Singleton getInstance(){
return instance
}

单例设计模式的实现方式主要有饿汉式、懒汉式、静态内部类、枚举、双重检查锁

单例设计实现

饿汉式

饿汉式:是在不管你用的用不上,在类创建的同时就创建一个静态的对象供系统使用,可以确保对象是唯一的,以后不再改变,线程安全。但有个算不上缺点的弱点,就是提前创建静态对象如果比较多的话又不一定全都有的上,会浪费内存。

  1. 方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @name: Singleton_Eager1
* @desc: 饿汉式:线程安全,类在装载时即初始化实例,实例若没用到则浪费内存)
*/
public class Singleton_Eager1 {

//1. 创建本类对象,不想让外部修改成员变量的值,私有
private static Singleton_Eager1 instance = new Singleton_Eager1();

//2. 私有构造方法,其他类不能访问该构造方法,也就不能通过构造方法来创建对象
private Singleton_Eager1() {
}

//3. 返回实例
public static Singleton_Eager1 getInstance() {
return instance;
}
}
  1. 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @name: Singleton_Hungry2
* @desc: 饿汉式变种:引用变量使用final修饰,其它类不可以修改此变量的值
*/
public class Singleton_Eager2 {

//1.引用变量使用 final修饰,不可改变值
public static final Singleton_Eager2 instance = new Singleton_Eager2();

//2.私有构造方法,其他类不能访问该构造方法,也就不能通过构造方法来创建对象
private Singleton_Eager2() {
}

// 返回实体
public static Singleton_Eager2 getInstance() {
return instance;
}
}
  1. 方式三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @name: Singleton_Hungry3
* @desc: 饿汉式变种:静态代码块的方式创建实例,与 Hungry1_Singleton 类似
**/
public class Singleton_Eager3 {

private static Singleton_Eager3 instance = null;

//静态代码块的方式
static {
instance = new Singleton_Eager3();
}

private Singleton_Eager3() {
}

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

懒汉式

懒汉式:在需要使用的时候才去建这个单例对象;但在多线程访问时如果不做锁(synchronized),存在创建多个对象的可能,也就达不到单例设计的要求,是线程不安全的。申明一个指向自己的对象引用,但不创建对象,通过一个公共静态方法来创建对象,创建对象之前先判断是否存在,不存在则创建,存在则直接返回对象。

  1. 方式一:线程不安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @name: Singleton_Lazy1
* @desc: 懒汉式:先判断实例是否存在,不存在则创建,线程不安全
**/
public class Singleton_Lazy1 {

// 1.声明一个引用,不创建对象
private static Singleton_Lazy1 instance;

// 2.私有构造方法,其他类不能访问该构造方法,也就不能通过构造方法来创建对象
private Singleton_Lazy1() {
}

// 3.返回实例
public static Singleton_Lazy1 getInstance() {
// 每次都得判断,浪费时间
if (instance == null) {
// 在多线程的情况下,线程执行权可能被抢走,
// 线程1等待,线程2执行创建对象;线程1醒来创建对象,就不是单例了
instance = new Singleton_Lazy1();
}
return instance;
}
}
  1. 方式二:线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @name: Singleton_Lazy1_Safe
* @desc: 懒汉式:使用synchronized同步方法解决线程安全问题
**/
public class Singleton_Lazy1_Safe {

private static Singleton_Lazy1_Safe instance;

private Singleton_Lazy1_Safe() {
}

// 方法加锁,但效率低
public static synchronized Singleton_Lazy1_Safe getInstance() {
if (instance == null) {
instance = new Singleton_Lazy1_Safe();
}
return instance;
}
}

饿汉式懒汉式两者区别: 饿汉式是空间换时间,如果该类较多的话,会加大系统内存压力;懒汉式是时间换空间,不使用时不初始化,节省内存,但存在线程安全问题。

静态内部类

利用类加载机制来初始化实例确保线程安全,内部类在被调用时才会被装载,内部类的静态方法才会被初始化。
内部类的加载:不管是静态或非静态内部类,只有在第一次使用时才会被加载; 外部类不调用getInstance()方法时,内部类不会加载,达到了懒汉的效果。
当调用方法时内部类被加载初始化实例,加载过程不会有多线程的问题,类在第一次加载成功之后会被缓存起来,而且一般一个类不会加载多次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @name: Singleton_StaticInner
* @desc: 静态内部类:饿汉式和懒汉式综合模式,即保证了初始化,又保证了同步(线程安全)
**/
public class Singleton_StaticInner {

//内部类在第一次使用时才会被加载。
private static class InstanceHandle {
public static Singleton_StaticInner instance = new Singleton_StaticInner();
}

private Singleton_StaticInner() {
}

public static Singleton_StaticInner getInstance() {
return InstanceHandle.instance;
}
}

枚举方式

枚举方式创建单例是 Effective Java作者Josh Bloch 提倡的方式,它利用了枚举类天生的线程安全性来确保单例的安全。

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
/**
* @name: Singleton_Enum
* @desc: 枚举类实现单例
* 调用:Singleton_Enum.getInstance(); 或 Singleton_Enum.INSTANCE;
**/
public enum Singleton_Enum {
INSTANCE;

// instance vars, constructor
private final Connection connection;

Singleton_Enum() {
// Initialize the connection
connection = new Connection();
}

// Static getter
public static Singleton_Enum getInstance() {
return INSTANCE;
}

public Connection getConnection() {
return connection;
}
}

public class Connection {

public Connection() {
}
}

双重检查锁

使用volatile变量禁止指令重排序(编译器对指令优化会造成指令执行顺序调换),让 DCL(Double-Check Locking) 生效。
volatile 确保本条指令不会因编译器的优化而省略,且要求每次直接读值(变量修改可见性)。

双重检查锁 只是在实例未被创建的时候再加锁处理,不用让线程每次都加锁,可以保证线程安全性。

在并发多线程情况下,如果都通过第一重 instance == null 的判断,则进入 synchronized 机制,只有一个线程进入并创建实例,其它线程排队等候,第一个线程出来后,第二个线程进入,执行第二重的 instance == null 判断,因第一个线程已创建实例,直接返回。若没有第二重判断,则第二个线程及后续线程都会创建新的实例,就会失去单例的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @name: Singleton_DoubleSyn
* @desc: 双重校验锁
**/
public class Singleton_DoubleSyn {

private static volatile Singleton_DoubleSyn instance;

private Singleton_DoubleSyn() {
}

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

登记式维护

  1. 由于懒汉式和饿汉式都把构造方法私有化了,所以它不能被继承,登记式可以解决这个问题,也是线程安全的。
  2. 该模式类似于Spring Bean容器里面单例管理的用法,将类名注册并放到 Map 集合里,下次要用的时候直接取。该模式是对一组单例模式进行的维护,主要是在数量上的扩展(类似容器),通过 map 我们把单例存进去,这样在调用时,先判断该单例是否已经创建,是的话直接返回,不是的话创建一个登记到 map 中,再返回。
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
37
38
39
40
/**
* @name: Singleton_Reg
* @desc: 登记式单模式, 是对一组单例进行维护
**/
public class Singleton_Reg {

private Singleton_Reg() {
}

private static Map<String, Singleton_Reg> singletonMap = new HashMap<>();

// 静态代码块
static {
Singleton_Reg instance = new Singleton_Reg();
singletonMap.put(instance.getClass().getName(), instance);
}

// 静态工厂方法,返回唯一登记,有则取出,没有则存入
public static Singleton_Reg getInstance(String className) {
if (className == null) {
className = "com.demo.SingletonReg";
}
if (!singletonMap.containsKey(className)) {
try {
singletonMap.put(className, (Singleton_Reg) Class.forName(className).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
return singletonMap.get(className);
}
}

public class Singleton_Main {

public static void main(String[] args) throws Exception {
Singleton_Reg instance = Singleton_Reg.getInstance(null);
System.out.println(instance == Singleton_Reg.getInstance(null));
}
}

该模式对于数量又分为固定数量和不固定数量的。上面采用的是不固定数量的方式,在getInstance()方法中加上参数(string className),然后通过子类继承,重写这个方法将 className 传进去。

单例应用场景

单例设计模式适用于大家共用唯一一个实例的场景,如果各自创建需要用到的实例存在无必要的资源浪费则适用单例设计模式, 如:连接池、容器、统一计数器等,数据库连接池或线程池只需要一个实例,在池中维护多条连接, Spring IOC容器是个单例容器,记录网站登录人数的计数器是单例。

其它参考

单例模式的七种写法
聊一聊Java的单例
双重检查锁定(double-checked locking)与单例模式

设计模式(一):单例模式(Singleton Pattern)

http://blog.gxitsky.com/2018/01/05/DesignPatterns-01-singleton/

作者

光星

发布于

2018-01-05

更新于

2022-06-17

许可协议

评论