单例模式的N种写法

夙愿已清 提交于 2019-12-09 12:14:05

鲁迅在《孔乙己》说孔乙己会写N种不同的回字, 回、囘、囬…… 。那今天我们来写几种常见的单例模式。单例模式在所有的设计模式书籍里都是作为第一个模式来讲解,因为它给人简单易于理解的感觉,但是真正写好一个单例模式,并能分析出不同写法的利弊还是需要花点功夫。一般单例模式实现的主要思想是把类的构造函数私有化,使之不能进行new。

单例模式一般主要看两个方面:

  • 对象是否延迟初始化
  • 初始化过程是否是线程安全的

这里我们在写单例之前先把验证上述的两个方面的代码先贴一下,下面的单例我们都通过下面代码来验证:
类加载是是否进行初始化,我们在单例的构造函数里打印一句话

        // 测试延迟初始化代码
        //类全路径名,记得测试时替换掉
        Class.forName("com.yao.single.xx.Singleton");
        System.out.println("singleton has been loaded");
        TimeUnit.SECONDS.sleep(3);
        Singleton singleton=Singleton.getInstance();
        singleton.sayHello();

测试初始化线程安全代码:这里仅仅是打印出每个线程拿到的对象是否是同一个对象,思路是通过设置CountDownLatch,来同时去获取实例,打印出来实例toString,来判断对象是否是同一个,有可能会hash冲突,打印出相同的字符串,但是可能性极小(有好的办法可以留言)。

        //测试并发代码
        final CountDownLatch latch=new CountDownLatch(1);
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<10;i++) {
            executorService.submit(new Runnable() {
                public void run() {
                    try {
                        latch.await();
                        //Singleton single = Singleton.INSTANCE;
                        Singleton single = Singleton.getInstance();
                        System.out.println(single);
                        single.sayHello();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
        }

        latch.countDown();

下边我们来介绍一下单例模式的五种写法。

最简单的一种

/**
 * Created by robin .
 */
public class Singleton {
    private final static Singleton INSTANCE=new Singleton();
    private Singleton() {
        System.out.println("init----");
    }
    public static Singleton getInstance(){
        return INSTANCE;
    }
    public void sayHello(){
        System.out.println("hello!");
    }
}

这种方式实现单例实现简单,易于理解。但是它有两个缺点:

  • 不能实现单例对象的延迟加载
  • 可以通过反射破坏单例

第一个缺点不用解释,这里简单解释下第二个缺点,如何通过反射破坏单例。

        Singleton one=    Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        //设置构造函数的访问性
        constructor.setAccessible(true);
        Singleton two= constructor.newInstance();

        System.out.println(one==two);

我们只需要通过反射拿到构造函数,然后设置其访问性为true,即可实例化出一个新的对象出来。本种实现虽然有以上缺点但是是线程安全的。

枚举实现

第一种方法我们写的单例可以通过反射就破坏掉,那么我们这次写一个反射破坏不掉的单例模式,那就是借助java里的enum来实现,因为enum本质上和类是一样的,只不过它需要事先初始化好它所需的实例,其实它本身是个多例模式。

/**
 * Created by robin.
 */
public enum Singleton {
    INSTANCE;
    private Singleton() {
        System.out.println("init----");
    }
    public void sayHello(){
        System.out.println("hello!");
    }

}

这里通过enum来实现的单例模式,它是线程安全的这毋庸置疑,但仍旧没有避免对象实例的延迟初始化。

双重检查锁定

前面介绍的两个都没有实现对象延迟的初始化,那这里我们就来写一个能够延迟初始化的单例模式。这里我们借助双重检查锁定的思想来实现。

public class Singleton {
    private  static Singleton INSTANCE;
    private Singleton() {
         System.out.println("init----");
    }
    public static Singleton getInstance(){
        if (INSTANCE==null){
            synchronized (Singleton.class){
                if (INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }
    public void sayHello(){
        System.out.println("hello!");
    }

}

从上面的代码里,我们在getInstance中为了避免过多的锁开销,我们采用synchroinzed块模式,先判断一遍INSTANCE是否为空,如果为空才进行synchroinzed 锁定,为了避免在锁定过程中,其他线程捷足先登初始化了实例,所以需要在锁定后重新判断一遍对象是否为空,如果不为空则进行初始化操作。

这个看似完美的延迟初始化在多线程环境中仍然是有问题的。因为在INSTANCE=new Singleton();这一步其实包含了三个过程:

  • 1 分配对象内存空间
  • 2 初始化对象
  • 3 INSTANCE指向内存地址

这三步其实在JIT编译器过程会发生重排序,也就是在1 分配完内存空间后 直接进行 3 赋值操作,而此时对象可能还没有被2初始化,但是其他线程判断 INSTANCE==null会返回false,误认为当前对象已经初始化好。那我们只需要防止JIT编译器重排序即可,这里我们借助volatile关键字来实现。

public class Singleton {
    private volatile static Singleton INSTANCE;
    private Singleton() {
         System.out.println("init----");
    }
    public static Singleton getInstance(){
        if (INSTANCE==null){
            synchronized (Singleton.class){
                if (INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
        return INSTANCE;
    }

    public void sayHello(){
        System.out.println("hello!");
    }

}

这种写法虽然实现了对象的延迟初始化,但是仍不能避免通过反射破坏单例的缺点。

Initialization-on-demand holder idiom

这种实现方式主要借助JVM加载class的特性来实现的,当JVM加载的类没有任何静态变量要去初始化,那么该类只会在真正被通过方法或变量被用到才进行初始化,这样我们就可以实现延迟初始化的目的。那同时还要保证线程安全性,我在如何写一个jstack检测不到的死锁里提到,对象在初始化的时候会在执行cinit函数,会加入同步逻辑,保证初始化的线程安全。当一个线程在初始化的过程,其他要初始化的线程执行只能进行等待,初始化完成后,被唤醒。

下面是具体的代码:

public class Singleton {
    private static class InstanceHolder{
        public static Singleton INSTANCE=new Singleton();

    }
    public static Singleton getInstance(){
        return InstanceHolder.INSTANCE;
    }
    private Singleton() {
            System.out.println("init----");
    }
    public void sayHello(){
            System.out.println("hello!");
    }
}

http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom 这里面有对该方式的详细解释。 这种方式实现线程安全和对象延迟初始化两个方面。

借助AtomicReference CAS实现

借助AtomicReference的CAS来实现单例,同样可以实现对象延迟初始化和线程安全。

public class Singleton {
    private static AtomicReference<Singleton> INSTANCE = new AtomicReference();
    private Singleton() {
        System.out.println("init----");
    }
    public static Singleton getInstance(){
        while (INSTANCE.get()==null){
            INSTANCE.compareAndSet(null,new Singleton());
        }
        return INSTANCE.get();
    }

    public void sayHello(){
        System.out.println("hello!");
    }
}

上面这种代码,在测试的时候会出现对象会被初始化多次的缺点,对代码稍微改进后就可以避免多次初始化的缺点:

public class Singleton {
    private static AtomicReference<Boolean> isInit = new AtomicReference(false);
    private volatile static Singleton INSTANCE;
    private Singleton() {
        System.out.println("init----");
    }
    public static Singleton getInstance(){
       //不要使用 !isInit.get()来判断,否则会拿到空的实例
        while(INSTANCE==null){
            if(isInit.compareAndSet(false,true)){
                INSTANCE=new Singleton();
                break;
            }
        }
        return INSTANCE;
    }

    public void sayHello(){
        System.out.println("hello!");
    }
}

End

通过上面的五种单例的写法,可以看出要写出一个线程安全的延迟初始化的单例还是有一定的难度,需要我们对类加载初始化和多线程都要有了解才可以。
https://my.oschina.net/robinyao/blog/813252

本文是个人在2016写的最后一篇了文章,算是给自己一个安慰,2016整体技术学习力度还不是很够,工作一直很忙没有系统的去学习知识,在2016的尾巴补几篇文章,聊以自慰,哈哈,给2016画个句号,不枉2016年初设定的目标。

迎接2017.。。。希望在2017技术上能有所突破。

阿里巴巴中间件 招聘 资深java工程师/技术专家 ,个人微信:yaozb091

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!