单例模式

2020-03-16  本文已影响0人  C调路过

前言

单例模式常用在一些全局唯一的管理类,避免对象重复创建,节省内存创建释放开销。

饿汉模式

每一次通过 new 创建的都是一个新的对象,为了保证全局唯一,我们就可以利用static关键字的特性,创建全局且线程共享的对象。这就是单例的第一种实现:饿汉模式

    public class Singleton {

        private static final Singleton instance = new Singleton();

        // 重要:写单例的第一件事就是将默认构造函数私有化,避免外部通过new来创建对象
        private Singleton() {
        }

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

懒汉模式

优秀的程序员懂得优化代码的时间空间,避免资源的浪费。在首次调用单例前,并没有初始化的必要,引出单例的第二种实现:懒汉模式

  1. 先私有化默认构造函数
  2. 在getInstance()方法中判断对象是否存在,没有就先new一个再返回,完美!
    public class Singleton2 {

        private static Singleton2 instance;

        // 重要:写单例的第一件事就是将默认构造函数私有化,避免外部通过new来创建对象
        private Singleton2() {

        }

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

为了模拟正常初始化的运行时间,在制造函数中给1ms的sleep。

        private Singleton2() {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

然后我们在Main函数中疯狂使用。

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Singleton2.getInstance().toString());
                }
            }).start();

        }

        -----------打印结果---------------
        singleton.Main$Singleton2@6d4bc03f
        singleton.Main$Singleton2@7f5b7c1b
        singleton.Main$Singleton2@b148489
        singleton.Main$Singleton2@30f51e1e
        singleton.Main$Singleton2@42f5503f
        singleton.Main$Singleton2@1bcc4803
        singleton.Main$Singleton2@7bae2ed5
        singleton.Main$Singleton2@701e866
        singleton.Main$Singleton2@4cf319d1
        singleton.Main$Singleton2@60174d9
        singleton.Main$Singleton2@71d9ab02
        singleton.Main$Singleton2@6d4bc03f
        singleton.Main$Singleton2@b20568f
        singleton.Main$Singleton2@71d9ab02
        singleton.Main$Singleton2@71d9ab02
        singleton.Main$Singleton2@71d9ab02
        singleton.Main$Singleton2@6299d4f3
        singleton.Main$Singleton2@9a662fb
        singleton.Main$Singleton2@1b73c3bb
        singleton.Main$Singleton2@3b637176
        singleton.Main$Singleton2@3b637176
        singleton.Main$Singleton2@3b637176
        singleton.Main$Singleton2@3b637176

会发现在多线程并发环境下,不能保证单例的线程安全。在第一次请求创建过程中,另外的线程进行读取,对象instance仍为空,开始了另一个对象的创建。
于是想到了给getInstance方法加锁,保证线程安全

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

但这样造成每次获取实例都要进行加锁耗时,我们想要的只是在第一次创建对象的时候加个锁。解决方式也很简单,于是给getInstance改写成给代码块加锁的方式。

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

也就是所说的双重检查锁(double checked locking)。

然而这并不是我们所见的标准DCL单例的写法,new Singleton2()创建对象并非是一个原子操作(类比 i = i + 1,首先是读取,加一,再赋值,需要多步完成),创建对象需要

  1. 分配内存
  2. 将对象指向内存
  3. 初始化对象

在不同的编译器可能对2,3步骤进行重排序,即先赋值给对象然后才进行初始化,会导致getInstance返回的实例为null的情况。于是instance对象加上volatile修饰,保证对象初始化和赋值操作不进行重排,避免创建了对象但是instance为空的情况。

    private volatile static Singleton2 instance;

静态内部类实现

需要熟悉类加载机制。静态内部类的方式即能保证在使用时创建,也能保证线程安全,但是不能在初始化时传递参数。如我们需要对单例设置如Context时,选择DCL的方式更为合适。

public static class Singleton3 {

    private Singleton3() {
        System.out.print("Singleton3初始化");
    }

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

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

其他

还有通过枚举类来创建单例的,感觉平时不会用到。
涉及知识点 static final synchronized关键字,类加载机制,原子性,重排序。

上一篇 下一篇

猜你喜欢

热点阅读