1 单例模式

2018-10-10  本文已影响17人  6cc89d7ec09f

1 单例模式的优点:


1 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
2 避免对资源的多重占用(比如写文件操作)。

2 饿汉式的写法 缺点 以及解决办法


//code 1
public class Singleton {
    //在类内部实例化一个实例
    private static Singleton instance = new Singleton();
    //私有的构造函数,外部无法访问
    private Singleton() {
    }
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        return instance;
    }
}

//code 3 饿汉式变种
public class Singleton2 {
    //在类内部定义
    private static Singleton2 instance;
    static {
        //实例化该实例
        instance = new Singleton2();
    }
    //私有的构造函数,外部无法访问
    private Singleton2() {
    }
    //对外提供获取实例的静态方法
    public static Singleton2 getInstance() {
        return instance;
    }
}

1 第一次使用时就已经被初始化了.当需要到数仓查询一些数据到内存中时,希望项目启动的时候就可以把数据加载完成,而且数仓查询缓慢,懒汉式会导致第一次查询耗时很长.
2 该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。

1 不能延迟加载。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。
2 如果这个类被多次加载的话也会造成多次实例化。

1 通过静态内部类的方法
2 懒汉式(下文有介绍)

//code 4
public class Singleton{
    //在静态内部类中初始化实例对象
    private static class SingletonHolder {
        private static final SingletonINSTANCE = new Singleton();
    }
    //私有的构造方法
    private Singleton() {
    }
    //对外提供获取实例的静态方法
    public static final SingletongetInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。

3 懒汉式的写法 缺点 以及解决办法


//code 5
public class Singleton {
    //定义实例
    private static Singleton instance;
    //私有构造方法
    private Singleton(){}
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

1 实例化延迟到第一次被引用的时候。防止不必要的浪费

1 在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。

//code 6
public class SynchronizedSingleton {
    //定义实例
    private static SynchronizedSingleton instance;
    //私有构造方法
    private SynchronizedSingleton(){}
    //对外提供获取实例的静态方法,对该方法加锁
    public static synchronized SynchronizedSingleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

1 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的延迟加载,
2 效率很低,因为99%情况下不需要同步。(因为上面的synchronized的加锁范围是整个方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance。)

//code 7
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

1 通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。

2 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。(指令重排)
线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有传到B使用的内存(缓存一致性)),程序很可能会崩溃。

//code 8
public class VolatileSingleton {
    private static volatile VolatileSingleton singleton;

    private VolatileSingleton() {
    }

    public static VolatileSingleton getSingleton() {
        if (singleton == null) {
            synchronized (VolatileSingleton.class) {
                if (singleton == null) {
                    singleton = new VolatileSingleton();
                }
            }
        }
        return singleton;
    }
}

1 上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。
2 即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候

//code 11
package com.hollis;
import java.io.Serializable;

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
}

在jdk中ObjectInputStream的类中有readUnshared()方法,如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”(我也不明白),然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。

4 单例懒汉式如何使用final

//code 9
class FinalWrapper<T> {
    public final T value;

    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class FinalSingleton {
    private FinalWrapper<FinalSingleton> helperWrapper = null;

    public FinalSingleton getHelper() {
        FinalWrapper<FinalSingleton> wrapper = helperWrapper;

        if (wrapper == null) {
            synchronized (this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
                }
                wrapper = helperWrapper;
            }
        }
        return wrapper.value;
    }
}

5 为什么饿汉式会线程安全,枚举类创建的单例会线程安全吗?

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)

6 怎样破坏单例?为什么?


1 反射可以破坏单例,这个是毋庸置疑的
2 序列化也可以破坏单例

对象的序列化过程通过ObjectOutputStreamObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStreamreadObject 该方法通过反射的方式调用无参构造方法新建一个对象。

6 为什么枚举可避免反序列化破坏单例

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
可以参考:ObjectInputputStreamreadObject的代码

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题

上一篇下一篇

猜你喜欢

热点阅读