【设计模式】单例模式

懒汉不安全

class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

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

        return instance;
    }
}

多线程下由于指令重排序,可能导致instance分配了空间但还没完成初始化,另一个线程就插入了,此时instance已经不为null了,但实际上还没有完成初始化,此时return的可能一个null。

懒汉安全

class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {

    }

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

        return instance;
    }
}

简单粗暴的synchronized,并发性比较低,不建议。

懒汉双重检查(重要)

class Singleton4 {
    private static volatile Singleton4 instance;

    private Singleton4() {

    }

    public static Singleton4 getInstance() {
        if (instance == null) {//第一次判断,如果不为null,可以省去synchronized的开销。
            synchronized (Singleton4.class) {
                if (instance == null) {//第二次判断,排除多个线程进入创建多个实例的可能。
                    instance = new Singleton4();
                }
            }
        }

        return instance;
    }

}

如果不给instance变量加volatile关键字,那么第一次判断null时,instance变量可能已经完成了地址分配,但是还没有完成初始化,最终造成空指针异常。
对象创建有三条指令:分配内存空间、初始化对象、设置instance指向分配的内存空间。
这个问题是指令重排序造成的,可能导致还没有发生初始化,指针就完成了指向。因此必须加上volatile,禁止“初始化对象”和“设置instance指向分配的地址”两条指令的重排序。

对于两次instance的是否为空的判断解释:

  1. 为何在synchronization外面的判断?

    为了提高性能!如果拿掉这次的判断那么在行的时候就会直接的运行synchronization,所以这会使每个getInstance()都会得到一个静态内部锁,这样的话锁的获得以及释放的开销(包括上下文切换,内存同步等)都不可避免,降低了效率。所以在synchronization前面再加一次判断是否为空,则会大大降低synchronization块的执行次数。

  2. 为何在synchronization内部还要执行一次呢?

    因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

饿汉安全

class Singleton2 {
    private static Singleton2 instance = new Singleton2();//new对象由jvm的机制保证一定线程安全

    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        return instance;
    }
}

由JVM保证天然的线程安全,但是还是不太好,因为我们还是希望懒加载。

静态内部类(重要)

class Singleton5 {
    private Singleton5() {

    }

    private static class SingletonHolder {
        private static final Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

推荐用法,由JVM保证天然的线程安全,并且实现了懒加载。

枚举(重要)

参考 https://blog.csdn.net/moakun/article/details/80688851

public enum Singleton {  
    INSTANCE;  
    private Resource instance;
    Singleton () {
        instance = new Resource();
    }
    public Resource getInstance() {
        return instance;
    }
}  

Singleton.INSTANCE.getInstance() 即可获得所要实例。
使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同事通过static来定义的。如:

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后代码为:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

枚举可避免反序列化破坏单例前面我们提到过,使用“双重校验锁”实现的单例其实是存在一定问题的,就是这种单例有可能被序列化所破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不做更加详细的说明了。

以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。

图片说明

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

        public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。

所以,JVM对序列化有保证。

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

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。这部分内容在《深度分析Java的枚举类型—-枚举的线程安全性及序列化问题》中也有更加详细的介绍,还展示了部分代码,感兴趣的朋友可以前往阅读。

全部评论

相关推荐

头像
11-06 10:58
已编辑
门头沟学院 嵌入式工程师
双非25想找富婆不想打工:哦,这该死的伦敦腔,我敢打赌,你简直是个天才,如果我有offer的话,我一定用offer狠狠的打在你的脸上
点赞 评论 收藏
分享
投票
我要狠拿offer:如果不是必须去成都绝对选九院呀,九院在四川top1研究所了吧
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务