前言
在写着篇文章之前,我有点纠结是否要把单例模式也写出来,因为单例模式看起来很简单,随手就能写出来一个单例模式,但是后来我真正的了解了一下单例模式,发现它并不像我想象中的那么简单,有好多种实现方式并且每一种实现方式有不同的应用场景和不同的优缺点。经此教训,我也明白了任何看上去很简单的东西都有可能超乎想象,同时也要改掉自己眼高手低的毛病,自勉。
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。当一个需要全局使用的对象频繁的创建销毁时,这样的操作是没有必要的,可以使用单例模式来创建这个全局对象的唯一实例来供所有使用这个全局对象的场景使用。
单例模式的特点
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式的优点
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
单例模式的缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
单例模式实现的关键
将构造函数私有化,使外部不能创建这个类的对象,然后在内部提供一个方法来提供这个类的对象。
单例模式的分类
1、懒汉式单例模式(在调用方法时创建类的实例)
2、饿汉式单例模式(在类被装载的时候创建实例)
单例模式的实现
方法一
这种方式是比较常见的一种方式,性能较高,也避免了多线程问题,但是容易造成类在不经意间被加载而使类进行了实例化,从而造成了垃圾对象。
方式二
这种方式是最基本的实现方式,但是最大的问题是不支持多线程,在多线程环境下不能保证“单例”。
为什么这么说呢?我们用代码来检验一下这种模式在多线程下的发挥
思路:我们先来创建三个线程,在线程中调用Single2的getInstance方法获取对象,然后输出线程名和获取的对象的hashCode(唯一对象的hashCode也是唯一的),看三个线程获取的对象的hashCode是否一致,就能看出来获取的对象是不是唯一的那个对象。
线程类:
线程测试类:
输出结果:
似乎没什么不一样,这样就没造成线程安全问题,但是线程安全与否不是在某一次的测试中就能说明的,只要是在任何一次的测试中不通过,这就是不安全的,在当前的代码中,三个线程顺序的通过了getInstance方法,那就让我们改造代码来加大概率,在getInstance方法中sleep片刻。
输出结果:
这样三个线程获取的分别是不同的对象,这是因为第一个线程判断了instance为空但是还没有创建对象就sleep了,然后在sleep的时候第二个线程进来了,判断instance为空,第三个线程同理,然后就造成了三个线程分别创建了三个对象,这样就不符合单例模式的特点。所以如果要在多线程场景下使用单例模式的时候,一定不要使用这种方式。
方式三
这种方式使用synchronized对getInstance方法加锁,保证进入getInstance方法中的只能有一个线程,这样保证了在多线程场景下,获取的仍是唯一的一个实例,但是因为加锁,造成了性能不高,安全和性能是对立的,当对getInstance的性能要求不高或是不经常调用但是要保证线程安全的情况下使用。
方式四
这种方式使用双检锁/双重校验锁(DCL,即 double-checked locking),保证在线程安全的前提下保持高性能,当getInstance性能对程序很关键时可以使用此方式。
方式五
使用静态内部类的方式保证在类装载的时候不被实例化(内部类并没有被使用,从而不会实例化),只有显式调用getInstance方法时才会装载内部类从而实例化,防止类在意外装载时被实例化而造成垃圾对象。
方式六
这种方式是《Effective Java》作者 Josh Bloch 提倡的方式,使用枚举的方式,代码简洁,同时自动支持序列化机制,绝对防止多次实例化,是实现单例模式的最佳方式,但是还没有被广泛使用。这种写法可能会让人感到生疏,以后我会出一篇文章来介绍一下枚举,顺便说一下,《Effective Java》这个书挺不错的,使我学习到了很多,我最近也在第三遍读这本书了,如果你渴望提升自己的Java技术,那就看这本书绝对没错。
单例模式的选择
在开发中,我们一般使用第一种方式,如果需要明确实现懒加载的话,可以使用第五种方式,如果涉及到反序列化的情况,可以使用第六种。还是要根据不同的场景使用不同的模式,毕竟招式是死的,人是活的。
完整代码实现:https://github.com/pyctay/singleton.git
同时欢迎关注我的微信公众号:猿人族永不为奴。
二维码: