码猿技术专栏

微信公众号:码猿技术专栏

设计模式之单例模式

什么是单例模式

  • 该类只有一个实例
  • 构造方法是私有的
  • 有一个获取该类对象的静态方法getInstance()

应用场景

  • 一个国家只有一个主席
  • 如果此时的限定必须是抽象出来的类只能是一个对象,这个时候就需要使用单例模式

懒汉式

什么是懒汉式

  • 懒汉式是当用到这个对象的时候才会创建,即是在getInstance()方法创建这个单例对象

优缺点

  • 只有用到的时候才会创建这个对象,因此节省资源
  • 线程不安全
    • 我们知道一旦我们使用了懒汉式就是在getInstance()方法中创建这个单例对象,那么不可避免的就是线程安全问题

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 懒汉式的单例模式: 不是线程安全的
* 优点: 在使用的时候才会初始化,可以节省资源
*/
public class SignalLazy {
// 将默认的构造器设置为private类型的
private SignalLazy() {
}

// 静态的单例对象
private static SignalLazy instance;

//静态的获取单例的对象,其中有一个判断,如果没有初始化,那么就创建
public static SignalLazy getInstance() {
// 如果instance没有被初始化,那么就创建即可,这个是保证了单例,但是并不是线程安全的
if (instance == null) {
System.out.println("this is SignalLazy");
instance = new SignalLazy(); // 创建一 个对象
}
return instance; // 返回这个对象
}
}
  • 从上面的代码中我们可以知道一旦使用多线程创建对象,那么就会出现线程不安全,最后创建出来的就不是单例了

  • 测试代码如下

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

public static void main(String[] args) {
new Thread(new Runnable() {

@Override
public void run() {
//创建实例,并且输出其中的地址,如果地址相同, 那么就是同一个实例
System.out.println("this is"+ SignalLazy.getInstance());

}
}).start();

//主线程也是创建输出其中的地址,运行可以看出这两个地址是不一样的
System.out.println("this is"+SignalLazy.getInstance());

}

}

解决线程不安全

  • 线程同步锁(synchronized)
    • 我们知道每一个类都有一个把锁,我们可以使用线程同步锁来实现线程同步方法
    • 但是使用线程同步锁浪费资源,因为每次创建实例都需要请求同步锁,浪费资源
1
2
3
4
5
6
7
8
public synchronized static SignalLazy getInstance() {
// 如果instance没有被初始化,那么就创建即可,这个是保证了单例,但是并不是线程安全的
if (instance == null) {
System.out.println("this is SignalLazy");
instance = new SignalLazy(); // 创建一个对象
}
return instance; // 返回这个对象
}
  • 双重校验
    • 双重校验: 两次判断单例对象是否为 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
public class SignalTonDoubleCheck {
private volatile static SignalTonDoubleCheck instance = null;

private SignalTonDoubleCheck() {
}; // 将默认的构造方法设置私有

public static SignalTonDoubleCheck getInstance() {
if (instance == null) {
synchronized (SignalTonDoubleCheck.class) {
if (instance == null) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这个new 并不是原子操作,因此当多线程进行到这里需要及时刷新这个值,因此要设置为voliate
instance = new SignalTonDoubleCheck();
}
}
}
return instance;
}

}

  • 匿名内部类 (推荐使用)
    • 我们知道静态变量、静态代码块、静态方法都是在类加载的时候只加载一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SignalTonInnerHolder {
//私有构造函数
private SignalTonInnerHolder() {
}

/*
* 匿名内部类,其中利用了静态成员变量在类加载的时候初始化,并且只加载一次,因此保证了单例
*/
private static class InnerHolder {
private static SignalTonInnerHolder instance = new SignalTonInnerHolder();
}

public static SignalTonInnerHolder getInstance() {
return InnerHolder.instance; //加载类
}
}
  • 一旦加载SignalTonInnerHolder类的时候就会加载其中的静态类,随之加载的就是其中的创建对象语句,因此在类加载的时候就完成了创建,这个和我们后面说的饿汉式有点相同

饿汉式

什么是饿汉式

  • 在类加载的时候就创建单例对象,而不是在getInstance()方法创建
  • 所谓的饿汉式就是利用静态成员变量或者静态语句块在类加载的时候初始化,并且只初始化一次,因此这个是线程安全的,但是在没有用到的时候就初始化,那么是浪费资源

优缺点

  • 还没用到就创建,浪费资源
  • 类加载的时候就创建,线程安全

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 饿汉式:线程安全
*
*/
public class SignalHungry {
private SignalHungry() {
}

// 静态变量只有在类加载的时候初始化一次,因此这个是线程安全的
private static SignalHungry instance = new SignalHungry();

public static SignalHungry getInstance() {
return instance;
}

}

测试

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

public static void main(String[] args) {
new Thread(new Runnable() {

@Override
public void run() {
//创建实例,并且输出其中的地址,如果地址相同, 那么就是同一个实例
System.out.println("this is"+ SignalHungry.getInstance());

}
}).start();

//主线程也是创建输出其中的地址,运行可以看出这两个地址是不一样的
System.out.println("this is"+SignalHungry.getInstance());

}

}

总结

  • 饿汉式在类加载的时候就会创建单例对象,因此浪费资源
  • 懒汉式在用到的时候才创建,节省资源,但是线程不安全,但是我们可以使用匿名内部类的方式使其线程安全
  • 一般在使用的时候会使用懒汉式的匿名内部类的实现和饿汉式的创建方式

笔者有话说

  • 最近建了一个微信交流群,提供给大家一个交流的平台,扫描下方笔者的微信二维码,备注【交流】,我会把大家拉进群

欢迎关注我的其它发布渠道