由简入繁详解单例模式

目录

1. 概念

2. 分类

2.1 饿汉式单例模式

2.2 懒汉式单例模式

2.3 兼顾懒汉式和饿汉式

2.4 注册式单例模式


1. 概念

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点

2. 分类

2.1 饿汉式单例模式

饿汉式单例模式在类加载的时候就初始化并创建单例对象,它是绝对的线程安全,没有加任何锁、执行效率也比较高,但不管用或不用都必须占用一定空间,Spring的IOC容器ApplicationContext就是饿汉式的。

它的代码如下,很好看懂:

/**
 * 写法一:静态代码块初始化
 */
public class Singleton2 {
    private static final Singleton2 singleton2;

    static {
        singleton2 = new Singleton2();
    }
    private Singleton2() {
    }

    public static Singleton2 getSingleton2(){
        return singleton2;
    }
}

/**
 * 写法二:直接初始化
 */
public class Singleton {

    private static final Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton(){
        return singleton;
    }
}

2.2 懒汉式单例模式

懒汉式单例在被外部调用时内部类才会被加载,我们的研究也是主要针对懒汉式开展。

2.2.1 存在线程安全隐患的懒汉式

先看一个单例实现:

public class One {

    private One(){}
    private static One one = null;

    public static One getOne(){
        if (one == null){
            return one = new One();
        }
        return one;
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExectorThread(), "one");
        Thread thread2 = new Thread(new ExectorThread(), "two");
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

class ExectorThread implements Runnable{
    public void run() {
        One one = One.getOne();
        System.out.println(Thread.currentThread().getName()+":"+one);
    }
}

大家可能已经看出来了,这个程序两个线程可能输出不同one对象,这个情况我们可以用线程模式的debug模拟出来:

首先左键打三个断点:

由简入繁详解单例模式_第1张图片

右键断点勾选Thread(三个都选):

由简入繁详解单例模式_第2张图片

然后以debug模式运行:

由简入繁详解单例模式_第3张图片

切换到one线程使用step into调试:

使one线程执行到以下程序语句:

由简入繁详解单例模式_第4张图片

继续切换到two线程使之执行完毕后发现控制台打印:

由简入繁详解单例模式_第5张图片

切换到one线程使之执行完发现控制台打印:

由简入繁详解单例模式_第6张图片

此时就模拟出了此种单例模式的bug情况。我们可以通过synchronized关键字来解决这种情况。

2.2.2 线程安全但性能堪忧的懒汉式

代码如下:

/**
 * 无安全隐患的单例
 *      但高并发下程序性能很差
 */
public class Two {

    private Two(){}
    private static Two two = null;

    public synchronized static Two getTwo(){
        if (two == null){
            two = new Two();
        }
        return two;
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExectorThread1(), "one");
        Thread thread2 = new Thread(new ExectorThread1(), "two");
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

class ExectorThread1 implements Runnable{
    public void run() {
        Two two = Two.getTwo();
        System.out.println(Thread.currentThread().getName()+":"+two);
    }
}

 我们可以再次照着上次的模拟方式测试此种单例模式,发现没有持有synchronized锁的线程在访问getTwo()时会进入MONITOR状态:

由简入繁详解单例模式_第7张图片

当持有synchronized锁的线程运行完getTwo()方法后,另一个线程才会进入RUNNING状态继续运行,这样就保证了单例的唯一性,但是在高并发的情况下,不管单例存在不存在,每一个线程都需要等待锁的持有,所以对程序性能造成很大麻烦。

2.2.3 双重检查锁(DCL)的懒汉式

代码如下:

public class Three {

    private Three(){}
    private static volatile Three three = null;

    public static Three getThree(){
        if (three == null){
            synchronized (Three.class){    //因为单例模式是全局的,所以使用类锁
                if (three == null){
                    three = new Three();
                }
            }
        }
        return three;
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(new ExectorThread2(), "one");
        Thread thread2 = new Thread(new ExectorThread2(), "two");
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

class ExectorThread2 implements Runnable{
    public void run() {
        Three three = Three.getThree();
        System.out.println(Thread.currentThread().getName()+":"+three);
    }
}

使用了双重检查锁后,如果该对象已经存在则不需要进行锁的争用,极大的降低了高并发下程序性能损耗。

2.3 兼顾懒汉式和饿汉式

代码如下:

public class Four {
    private Four(){}

    public static final Four getFour(){
        return LazyHolder.Lazy;
    }

    public static class LazyHolder{
        private static final Four Lazy = new Four();
    }

    public static void main(String[] args) {
        Four.getFour();
    }
}

当Four类加载时,代码中的LazyHolder静态内部类默认是不加载的(JVM类加载机制),只有调用getFour()时才会new一个Four()对象,而static和final保证了单例的唯一性和不变性。

但此代码还有一个问题如果使用反射强行创建多个对象,这个单例的唯一性还是会被破坏,如下:

    public static void main(String[] args) {
        try {
            Class fourClass = Four.class;

            Constructor declaredConstructor = fourClass.getDeclaredConstructor(null);

            declaredConstructor.setAccessible(true);

            //强制创建了两次对象
            Four four1 = declaredConstructor.newInstance();
            Four four2 = declaredConstructor.newInstance();

            System.out.println(four1);
            System.out.println(four2);

        }catch (Exception e){
            e.printStackTrace();
        }
    }

运行发现:

由简入繁详解单例模式_第8张图片

创建了两个不同的实例对象,那么如何避免?可以改良一下私有的构造方法:

public class Four {
    private Four(){
        if(LazyHolder != null){
            throw new RuntimeException("不允许创建多个Four单例");
        }
    }

    public static final Four getFour(){
        return LazyHolder.Lazy;
    }

    public static class LazyHolder{
        private static final Four Lazy = new Four();
    }

    public static void main(String[] args) {
        Four.getFour();
    }
}

详细说说这个过程,先看一段代码(只是上面的加上了一些输出断点):

public class Four {
    private Four(){
        System.out.println("内部类加载前:"+System.currentTimeMillis());
        System.out.println(LazyHolder.Lazy+":"+System.currentTimeMillis());
        if (LazyHolder.Lazy != null){
            throw new RuntimeException("不允许创建多个Four实例");
        }
        System.out.println("创建了一个单例实例");
    }

    public static final Four getFour(){
        return LazyHolder.Lazy;
    }

    public static class LazyHolder{
        static {
            System.out.println("内部类加载:"+System.currentTimeMillis());
        }
        private static final Four Lazy = new Four();
    }

    public static void main(String[] args) {
        try {
            Class fourClass = Four.class;
            Constructor declaredConstructor = fourClass.getDeclaredConstructor(null);
            declaredConstructor.setAccessible(true);
            System.out.println("第一次创建对象:"+System.currentTimeMillis());
            Four four1 = declaredConstructor.newInstance();
            System.out.println("第二次创建对象:"+System.currentTimeMillis());
            Four four2 = declaredConstructor.newInstance();
            System.out.println(four1);
            System.out.println(four2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 首先打两个断点:

由简入繁详解单例模式_第9张图片

debug运行,使用step over(折线)达到如下所示:

由简入繁详解单例模式_第10张图片

使用step into(指向下的箭头)进入Four的构造方法:

由简入繁详解单例模式_第11张图片

step over两次,此时到达第8行并输出:

由简入繁详解单例模式_第12张图片

再一次step over,此时输出:

由简入繁详解单例模式_第13张图片

由于输出语句调用了LazyHolder.Lazy触发了静态内部类的类加载,所以静态变量Lazy的初始化也就是Four的构造方法又执行了一次,所以会有第二句"内部类加载前"的语句输出,此时判断LazyHolder.Lazy是否为空,由于已经处于类加载期间,所以直接判断Lazy为null,结束创建过程,返回到原先输出Lazy的语句(输出刚才创建的Lazy对象)。发现Lazy不为空,再step over之后就抛出了"不允许创建多个Four实例"异常:

由简入繁详解单例模式_第14张图片

2.4 注册式单例模式

当对象序列化后写入磁盘,再反序列化将其转化为内存对象时,反序列化的对象会重新分配内存并重新创建,当这个对象是单例对象的话,以上所述的单例模式就保证不了单例的唯一性了。

测试代码如下:

public class Five implements Serializable {
    private Five(){}
    private final static Five five = new Five();

    public static Five getFive(){
        return five;
    }

    public static void main(String[] args) {
        Five f1 = Five.getFive();
        Five f2 = null;

        try {
            FileOutputStream fos = new FileOutputStream("Five.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(f1);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("Five.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            f2 = (Five) ois.readObject();
            ois.close();

            System.out.println(f1);
            System.out.println(f2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

输出:

由简入繁详解单例模式_第15张图片

其实增加一个readResolve方法就可以解决这个问题:

如下:

public class Five implements Serializable {
    private Five(){}
    private final static Five five = new Five();

    public static Five getFive(){
        return five;
    }

    private Object readResolve(){
        return five;
    }
}

输出:

由简入繁详解单例模式_第16张图片

原因是在JDK的ObjectStreamClass源码中有这样一个赋值代码,它就是通过反射找到无参的readResolve方法:

readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);

而ObjectInputStream中判断了如果readResolveMethod这个方法存在则调用invokeReadResolve:

if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
        ......

invokeReadResolve就通过反射调用了readResolve方法:

Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException{
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
        ......

虽然解决了问题,但事实上还是创建了两个对象,只不过调用了readResolve方法返回的是同一个对象。注册式单例可以解决这个问题。

注册式单例模式就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例,它分为枚举式单例模式和容器式单例模式。

2.4.1 枚举式单例模式

先看枚举式单例模式的写法:

public enum Six {
    SIX;
    private Object data;
    public Object getData(){
        return data;
    }

    public void setData(Object data){
        this.data = data;
    }

    public static Six getSix(){
        return SIX;
    }

    public static void main(String[] args) {
        Six s1 = Six.getSix();
        s1.setData(new Object());
        Six s2 = null;

        try {
            FileOutputStream fos = new FileOutputStream("Six.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("Six.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (Six) ois.readObject();
            ois.close();

            System.out.println(s1.getData());
            System.out.println(s2.getData());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

运行后:

由简入繁详解单例模式_第17张图片

双击Target中的class文件:

由简入繁详解单例模式_第18张图片

点击View->show ByteCode可以看到Six反编译的字节码,其中有:

  // access flags 0x8
  static ()V
   L0
    LINENUMBER 12 L0
    NEW cn/xupt/设计模式/单例模式/注册式/Six
    DUP
    LDC "SIX"
    ICONST_0
    INVOKESPECIAL cn/xupt/设计模式/单例模式/注册式/Six. (Ljava/lang/String;I)V
    PUTSTATIC cn/xupt/设计模式/单例模式/注册式/Six.SIX : Lcn/xupt/设计模式/单例模式/注册式/Six;

表示枚举式单例模式在静态代码块中就给SIX进行了赋值,是饿汉式的实现,那它如何不被序列化影响,答案是readObject0()方法中的readEnum()方法:

    private Object readObject0(boolean unshared) throws IOException {
        ......
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
        ......
    }

readEnum():

    private Enum readEnum(boolean unshared) throws IOException {
        
        ......    

        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum en = Enum.valueOf((Class)cl, name);//*********
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

枚举类型是通过类名和类对象找到一个唯一的枚举对象,这就解释了它为什么不会被序列化影响。

2.4.2 容器化单例模式

public class ContainerSingleton {
    private static Map ioc = new ConcurrentHashMap();
    private ContainerSingleton(){}
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try{
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);
        }
    }
}

容器式单例模式适用于单例对象非常多的情况,使用它可以便于管理。

感谢观看。

你可能感兴趣的:(设计模式)