单例模式
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
为什么要保证只存在一份对象呢?
因为有些对象管理的内存数据可能会很多, 可能有些项目里就一个对象运行起来就吃上百G的内存空间, 如果不小心多new了几个, 那系统可能直接崩溃了.
饿汉模式实现单例
类加载的同时, 创建实例.
class Singleton { private static Singleton singleton = new Singleton(); private Singleton() {} //不允许外部调用构造方法 public static Singleton getSingleton() { return singleton; //将创建好的实例返回 } }
这里只是单纯的读操作, 因此该模式是线程安全的.
懒汉模式实现单例
核心思想 :非必要不创建.
加载的时候不创建实例. 第一次使用的时候才创建实例.
单线程版
class Singleton { private static Singleton singleton = null; private Singleton() {} //修改了构造方法的访问修饰权限符, 只有在类内部才能访问构造方法 public static Singleton getSingleton() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
上面的懒汉模式实现是线程不安全的.
为什么不安全呢?
比如两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 它两都 new 出了对象, 这与我们预期的创建一个对象不符, 所以线程不安全.
我们可以通过加锁来解决这一问题, 下面是多线程版 , 线程安全.
多线程版
怎么加锁呢?
看看这样加锁是否可行:
class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getSingleton() { if(singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton; } }
显然这样是不行的, 当两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 然后通过竞争锁, 竞争成功的线程先 new 对象, 另一个线程后 new 对象, 这也是 new 了多个对象, 与预期不符.
我们再来看看给方法加锁:
class Singleton { private static Singleton singleton; private Singleton() {} public synchronized static Singleton getSingleton() { if(singleton == null) { singleton = new Singleton(); } return singleton; } }
当多个线程同时调用 getSingleton 方法时, 通过竞争锁, 竞争成功的线程先进去创建完对象出来后, 其他线程再来获取对象就不会再创建对象了.
其实这里还有一个问题, 那就是指令重排序问题, 什么意思呢?
举个例字 :
假设我们有两个线程 t1 和 t2,
t1 有个操作是: s = new Student();
t2 有个操作是: if(s != null) { s.learn(); }
这个操作 : s = new Student(); 大体可以分为三个步骤:
- 申请内存空间
- 调用构造方法(初始化内存的数据)
- 把对象的引用赋值给 s (内存地址的赋值)
如果是单线程, 此处进行指令重排序,步骤2和步骤3是可以调换顺序的, 重排序后可能就是132执行了, 这对单线程结果没影响, 但多线程就不行了.
回到上面的 t1 和 t2, 如果 t1 的操作进行指令重排序, 就会先申请内存, 然后把这个内存地址赋值给 s (注意这里还没有调用构造方法, 没有new对象), 这时 s 不是 null , 这个时候如果线程 t2 刚好进行 if 判断则会直接进入, 然后调用 s 的方法, 因为 s 里没对象, 就会抛出空指针异常.
这也是上面给方法加锁代码存在的问题, 如果得到的 singleton 里没对象就调用这里面的方法, 那就会产生空指针异常.
如何避免发生指令重排序呢?
我们可以加个 volatile 来禁止指令重排序, 在下面代码中体现.
多线程版优化
class Singleton { private volatile static Singleton singleton; //禁止对singleton进行指令重排序 private Singleton() {} public static Singleton getSingleton() { //注意这里有两个 if 判断是否为空!!! if(singleton == null) { synchronized (Singleton.class) { if(singleton == null) { singleton = new Singleton(); } } } return singleton; } }
为什么要两个 if 判断呢?
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了.
这样可以让加锁操作只在第一次创建实例的时候出现.
小结
单例模式线程安全问题 :
- 饿汉模式, 天然就是安全的, 只是读操作.
- 懒汉模式, 不安全的, 有读也有写.
如何将懒汉模式变安全:
- 加锁, 把 if new 变为原子操作.
- 双重 if, 减少不必要的加锁操作.
- 使用 volatile 禁止指令重排序, 保证后续线程拿到的是完整对象.