单例模式
作为对象的创建模式,单例模式确保某一个类只有一个实例,且自行实例化并向系统提供这个实例,这个类称为单例类。
单例模式的特点:
1. 单例类只有一个实例
2. 单例类必须自己创建自己的唯一实例
3. 单例类必须给所有对象提供这一实例
饿汉式
1 class Instance1 {
2 private static Instance1 instance1 = new Instance1();
3
4 private Instance1() {
5
6 }
7
8 public static Instance1 getInstance() {
9 return instance1;
10 }
11 }
饿汉式,就是使用类的时候不管用的是不是类的单例部分,都重新创建一个单例类。这种写法不会造成竞争,线程安全。
为什么饿汉式单例天生就是线程安全的?
类的加载方式是按需加载,且只会加载一次。因此,单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说就是,在线程访问单例对象之前,就已经创建好了对象。再加上一个类在整个生命周期中,只会被加载一次,所以只会创建一个实例。线程每次只能也必定能拿到这个唯一的对象。所以说是线程安全的。
懒汉式(非线程安全)
1 class Instance2 {
2 private static Instance2 instance = null;
3
4 private Instance2() {
5
6 }
7
8 public static Instance2 getInstance() {
9 if (instance == null) {
10 instance = new Instance2();
11 }
12 return instance;
13 }
14
15 }
懒汉式,就是只有当单例类用到时,才会创建。这种写法基本不用,因为它是线程不安全的。多个线程在第9行判断是否为空时,就会出现问题。
双重检查
1 class Instance3 {
2 private volatile static Instance3 instance = null;
3
4 private Instance3() {
5
6 }
7
8 public static Instance3 getInstance() {
9 if (instance == null) {
10 synchronized (Instance3.class) {
11 if (instance == null) {
12 instance = new Instance3();
13 }
14 }
15 }
16 return instance;
17 }
18 }
最简单的方法是直接给getInstance()方法加锁,但这样效率低。线程调用getInstance()方法都会等待获取锁。事实上,只有第一次创建对象时需要加锁,其他时候都不需要。基于这个考虑,所以引用了双检锁(double checked lock)的写法。
但是上面的instance变量如果没有加volatile关键字,就会出现问题。
首先我们要明白的是new Instance3()是一个非原子操作,分为3步:
1. 给对象分配内存空间
2. 初始化对象
3. 将变量指向刚分配的内存地址
但实际上,这个过程可能会发生无序写入(指令重排序),也就是说上面的3条指令可能会因为指令重排序而先执行步骤3再执行步骤2。
这样可能会发生如下步骤:
- 线程1进入getInstance()方法,判断instance==null,进入synchronized代码块
- 线程1被线程2预占,因为instance为null,也想进入synchronized代码块,但线程1持有锁对象,线程2只能阻塞
- 线程1执行,new对象,退出同步代码块,并返回实例instance
- 线程2获得了锁对象,执行到第二个instance==null时,因为已经不等于null了,所以直接退出,返回instance(但可能该实例还没有初始化或完成初始化,导致线程2使用该实例而出错)
所以volatile关键字是必须的
静态内部类
1 class Instance4 {
2
3 private Instance4() {
4
5 }
6
7 private static class InstanceHolder {
8 private static Instance4 instance = new Instance4();
9 }
10
11 public static Instance4 getInstance() {
12 return InstanceHolder.instance;
13 }
14 }
这种方式依然使用classloder机制,使得单例类仍然只会被加载一次,不同的是只有当调用getInstance方法时,才会显示装在类InstanceHolder,从而实例化Instance4。
枚举
1 enum Instance5 {
2 instance;//实例
3
4 public void method() {
5 ...6 }
7
8 }
直接用Instance5.instance.method()调用方法。使用枚举不仅能避免多线程同步问题,还能防止反序列化重新创建对象。
单例模式的好处
1. 控制资源的使用,通过多线程同步来控制资源的并发访问
2. 控制实例的产生,从而节约资源
3. 控制数据的共享,在不建立直接关联的情况下,让多个不相关的进程或线程实现通信