Contents

软件设计模式之单例模式

单例模式是软件设计中非常常见的模式,但真正用好也用对的好像还有很多路要走。一起来研究一下。

单例,最最起码得有这些吧

  • 私有的构造方案
  • 一个 static 的实例
  • 一个 static 的外部访问入口

具体代码应该是这样的(懒汉模式)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

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

当然了,看着好像是那么回事,完成了以上操作,我们来测试一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SingletonTest {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            //test1
            Singleton singleton = Singleton.getInstance();
            System.out.println("running at thread" + Thread.currentThread().getName() + ">>" + singleton);
        }
    }
}

输出(每次运行输出是不一样的)

1
2
3
running at threadmain>>cn.zhucongqi.singleton.Singleton@49476842
running at threadmain>>cn.zhucongqi.singleton.Singleton@49476842
running at threadmain>>cn.zhucongqi.singleton.Singleton@49476842

单例完成了!?

我们在多线程下测试一下,改造一下

 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 SingletonTest {

    public static void main(String[] args) {
//        for (int i = 0; i < 3; i++) {
//            //test1
//            Singleton singleton = Singleton.getInstance();
//            System.out.println("running at thread" + Thread.currentThread().getName() + ">>" + singleton);
//        }


        //test2
        Runnable r = new Runnable() {
            @Override
            public void run() {
                Singleton singleton = Singleton.getInstance();
                System.out.println("running at thread" + Thread.currentThread().getName() + ">>" + singleton);
            }
        };
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
    }
}

输出(每次运行输出是不一样的)

1
2
3
4
5
6
running at threadThread-2>>cn.zhucongqi.singleton.Singleton@a1a1c96
running at threadThread-5>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-4>>cn.zhucongqi.singleton.Singleton@a1a1c96
running at threadThread-3>>cn.zhucongqi.singleton.Singleton@a1a1c96
running at threadThread-0>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-1>>cn.zhucongqi.singleton.Singleton@b0545bc

出现问题了!!!怎么在多线程下出现了不同实例了???是不是我们的设计有问题呢?我们在图示位置打个断点,按照 Thread 来处理

/images/singleton-breakpoint.png#mid

然后 Debug 一下 /images/singleton-breakpoint-running.png#mid

我们先来看看 Thread0 的情况 /images/singleton-breakpoint-t0-running.png#mid /images/singleton-breakpoint-t0-over.png#mid

看到此时instance为{Singleton@475}。我们换到 Thread1 再看看 /images/singleton-breakpoint-t1-running.png#mid 看到此时instance也是{Singleton@475}。往下走 /images/singleton-breakpoint-t1-over.png#mid

额等等,怎么instance变成{Singleton@476}了,被覆盖咯??? 看来我们的设计有问题哦!

怎么改造呢?

从 Debug 来看,是getInstance()时出现了线程安全问题,我们用synchronized是不是可以解决问题呢?我们再改造一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

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

再 Debug 一下。 /images/singleton-breakpoint-monitor.png#mid 看到就一个 Thread 是 running 的,其他的是 Monitor 的,是不是可以了。我们往下继续走。 /images/singleton-breakpoint-monitor-t0-over.png#mid 看到,Thread0 走出来之后,Thread4 才进入到 Running 状态。也就是加入synchronized把整个class都锁住了。目的达到,但是性能不好。我们再改造一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

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

再Debug 看看 /images/singleton-breakpoint-running-2.png#mid 现在所有 Thread 都是 Running 的了。我们继续走,先看看 Thread0 /images/singleton-breakpoint-t0-running-2.png#mid 再看看 Thread3 /images/singleton-breakpoint-t3-running.png#mid 看样子又要被覆盖咯!!!看来还是不行哦!是不是我们再加一把锁哦?!

我们再改造一下

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

    private static Singleton instance = null;

    private Singleton(){}

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

这次我们先 Run 一下看看结果,输出

1
2
3
4
5
running at threadThread-2>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-0>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-1>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-3>>cn.zhucongqi.singleton.Singleton@b0545bc
running at threadThread-4>>cn.zhucongqi.singleton.Singleton@b0545bc

结果对咯!为什么呢?

其实,刚才的效果和不断的改造,我们把最初的Singleton改造成了DoubleCheck 的懒汉式Singleton了。

当然了,上面这种方式只是一种方式罢了。下面直接上其他的方式Code。

2020.2.20更新

在Doublecheck的第一次check的时候,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。主要原因是重排序,**所以只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。**因为被volatile关键字修饰的变量是被禁止重排序的。

1
private static volatile Singleton instance = null;

EnumSingleton 枚举方式实现的单例

1
2
3
4
5
6
7
8
9
// Enum Singleton
public enum EnumSingleton {

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

InnerClassSingleton 内部类方式实现的单例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Inner class Singleton
public class InnerSingleton {

    private InnerSingleton(){
        if (null != InnerHolder.INSTANCE) {
            throw new RuntimeException("Just only one Instance can be create!");
        }
    }

    //Serializable
    private Object readResolve() {
        return InnerHolder.INSTANCE;
    }

    public static InnerSingleton getInstance() {
        return InnerHolder.INSTANCE;
    }

    private static class InnerHolder {
        private static final InnerSingleton INSTANCE = new InnerSingleton();
    }
}

特别说明

  • 为了避免序列化出现的 Singleton 实例不唯一的情况,覆盖 Object 的readResolve方法。
1
2
3
private Object readResolve() {
        return InnerHolder.INSTANCE;
    }
  • 为了避免,通过反射错误的使用 Singleton,在构造中做再处理
1
2
3
4
5
 private InnerSingleton(){
        if (null != InnerHolder.INSTANCE) {
            throw new RuntimeException("Just only one Instance can be create!");
        }
}