一起学设计模式 - 单例模式

文章目录
  1. 1. 概述
  2. 2. 解锁姿势
  3. 3. - 说点什么

单例设计模式(Singleton Pattern)是最简单且常见的设计模式之一,主要作用是提供一个全局访问且只实例化一次的对象,避免多实例对象的情况下引起逻辑性错误(实例化数量可控)

概述

Java中,单例模式主要分三种:懒汉式单例、饿汉式单例、登记式单例三种。

  • 懒汉:非线程安全,需要用一定的风骚操作控制,装逼失败有可能导致看一周的海绵宝宝
  • 饿汉:天生线程安全,ClassLoad的时候就已经实例化好,该操作过于风骚会造成资源浪费
  • 单例注册表:Spring初始化Bean的时候,默认单例用的就是该方式

特点

  • 私有构造方法,只能有一个实例。
  • 私有静态引用指向自己实例,必须是自己在内部创建的唯一实例。
  • 单例类给其它对象提供的都是自己创建的唯一实例

案例

  • 在计算机系统中,内存、线程、CPU等使用情况都可以再任务管理器中看到,但始终只能打开一个任务管理器,它在Windows操作系统中是具备唯一性的,因为弹多个框多次采集数据浪费性能不说,采集数据存在误差那就有点逗比了不是么…
  • 每台电脑只有一个打印机后台处理程序
  • 线程池的设计一般也是采用单例模式,方便对池中的线程进行控制

注意事项

  • 实现方式种类较多,有的非线程安全方式的创建需要特别注意,且在使用的时候尽量根据场景选取较优的,线程安全了还需要去考虑性能问题。
  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 没有抽象层,扩展有困难。
  • 职责过重,在一定程度上违背了单一职责原则
  • 使用时不能用反射模式创建单例,否则会实例化一个新的对象

解锁姿势

第一种:单一检查(懒汉)非线程安全

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
public class LazyLoadBalancer {

private static LazyLoadBalancer loadBalancer;
private List<String> servers = null;

private LazyLoadBalancer() {
servers = new ArrayList<>();
}

public void addServer(String server) {
servers.add(server);
}

public String getServer() {
Random random = new Random();
int i = random.nextInt(servers.size());
return servers.get(i);
}

public static LazyLoadBalancer getInstance() {
// 第一步:假设T1,T2两个线程同时进来且满足 loadBalancer == null
if (loadBalancer == null) {
// 第二步:那么 loadBalancer 即会被实例化2次
loadBalancer = new LazyLoadBalancer();
}
return loadBalancer;
}

public static void main(String[] args) {
LazyLoadBalancer balancer1 = LazyLoadBalancer.getInstance();
LazyLoadBalancer balancer2 = LazyLoadBalancer.getInstance();
System.out.println("hashCode:"+balancer1.hashCode());
System.out.println("hashCode:"+balancer2.hashCode());
balancer1.addServer("Server 1");
balancer2.addServer("Server 2");
IntStream.range(0, 5).forEach(i -> System.out.println("转发至:" + balancer1.getServer()));
}
}

日志

1
2
3
4
5
6
7
hashCode:460141958
hashCode:460141958
转发至:Server 2
转发至:Server 2
转发至:Server 2
转发至:Server 1
转发至:Server 2

分析: 在单线程环境一切正常,balancer1balancer2两个对象的hashCode一模一样,由此可以判断出堆栈中只有一份内容,不过该代码块中存在线程安全隐患,因为缺乏竞争条件,多线程环境资源竞争的时候就显得不太乐观了,请看上文代码注释内容

第二种:无脑上锁(懒汉)线程安全,性能较差,第一种升级版

1
2
3
4
5
6
public synchronized static LazyLoadBalancer getInstance() {
if (loadBalancer == null) {
loadBalancer = new LazyLoadBalancer();
}
return loadBalancer;
}

分析: 毫无疑问,知道synchronized关键字的都知道,同步方法在锁没释放之前,其它线程都在排队候着呢,想不安全都不行啊,但在安全的同时,性能方面就显得短板了,我就初始化一次,你丫的每次来都上个锁,不累的吗(没关系,它是为了第三种做铺垫的)..

第三种:双重检查锁(DCL),完全就是前两种的结合体啊,有木有,只是将同步方法升级成了同步代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
//划重点了 **volatile**
private volatile static LazyLoadBalancer loadBalancer;

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

1.假设new LazyLoadBalancer()加载内容过多
2.因重排而导致loadBalancer提前不为空
3.正好被其它线程观察到对象非空直接返回使用

1
2
3
mem = allocate();                  //LazyLoadBalancer 分配内存
instance = mem; //注意当前实例已经不为空了
initByLoadBalancer(instance); //但是还有其它实例未初始化

存在问题: 首先我们一定要清楚,DCL是不能保证线程安全的,稍微了解过JVM的就清楚,对比C/C++它始终缺少一个正式的内存模型,所以为了提升性能,它还会做一次指令重排操作,这个时候就会导致loadBalancer提前不为空,正好被其它线程观察到对象非空直接返回使用(但实际还有部分内容没加载完成)

解决方案:volatile修饰loadBalancer,因为volatile修饰的成员变量可以确保多个线程都能够顺序处理,它会屏蔽JVM指令重排带来的性能优化

volatile详解:http://blog.battcn.com/2017/10/18/java/thread/thread-volatile/

第四种:Demand Holder (懒汉)线程安全,推荐使用

1
2
3
4
5
6
7
8
9
10
private LazyLoadBalancer() {}

private static class LoadBalancerHolder {
//在JVM中 final 对象只会被实例化一次,无法修改
private final static LazyLoadBalancer INSTANCE = new LazyLoadBalancer();
}

public static LazyLoadBalancer getInstance() {
return LoadBalancerHolder.INSTANCE;
}

分析:Demand Holder中,我们在LazyLoadBalancer里增加一个静态(static)内部类,在该内部类中创建单例对象,再将
该单例对象通过getInstance()方法返回给外部使用,由于静态单例对象没有作为LazyLoadBalancer的成员变量直接实例化,类加载时并不会实例化LoadBalancerHolder,因此既可以实现延迟加载,又可以保证线程安全,不影响系统性能(居家旅行必备良药啊)

第五种:枚举特性(懒汉)线程安全,推荐使用

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Lazy {
INSTANCE;
private LazyLoadBalancer loadBalancer;

//枚举的特性,在JVM中只会被实例化一次
Lazy() {
loadBalancer = new LazyLoadBalancer();
}

public LazyLoadBalancer getInstance() {
return loadBalancer;
}
}

分析: 相比上一种,该方式同样是用到了JAVA特性:枚举类保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)

第六种:饿汉单例(天生线程安全),

1
2
3
4
5
6
7
8
9
public class EagerLoadBalancer {
private final static EagerLoadBalancer INSTANCE = new EagerLoadBalancer();

private EagerLoadBalancer() {}

public static EagerLoadBalancer getInstance() {
return INSTANCE;
}
}

分析: 利用ClassLoad机制,在加载时进行实例化,同时静态方法只在编译期间执行一次初始化,也就只有一个对象。使用的时候已被初始化完毕可以直接调用,但是相比懒汉模式,它在使用的时候速度最快,但这玩意就像自己挖的坑哭着也得跳,你不用也得初始化一份在内存中占个坑…

- 说点什么

全文代码:https://gitee.com/battcn/design-pattern/tree/master/Chapter2/battcn-singleton

  • 个人QQ:1837307557
  • battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)