茴香豆有几种写法:
- 朋友,如果你真的是来看茴香豆的写法的话,那对不起!没有!我不会!
- 但是,关于单例,你知道有几种写法呢?
- 单例的懒汉式、饿汉式都是什么意思?枚举单例又为什么被大神推崇?还有那个变量前面莫名加上volatile是什么意思?
- 今天我们来看看单例设计模式的一些讨论。
单例概念:
- java中单例设计模式是一种常见的设计模式,单例设计模式的写法有好多种(正如茴香豆的茴字有多少种写法?):
- 懒汉式单例
- 饿汉式单例
- 枚举单例(这种写法比较少见,但是值得讨论和使用)
- 单例设计模式具备的特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
懒汉式单例:
- 注意点:
- 类内部私有封装一个自己的引用
- 构造器私有化
- 提供获取该类内部私有封装的唯一方法
- 缺点:
- 懒汉式单例设计的实现没有考虑过线程安全问题,它是非线程安全的,在并发环境下,很可能会出现多个SingleTon实例。
- 非线程安全实例:
- 懒汉式单例设计的实现没有考虑过线程安全问题,它是非线程安全的,在并发环境下,很可能会出现多个SingleTon实例。
###基本写法:
public class Person {//类内部封装自己的引用,该引用必须私有
private static Person person = null;//构造器私有化
private Person(){
}
public static Person getInstance() {//提供获取单例的唯一接口
if(person == null) {
person = new Person();
}
return person;
}
}
- 按理说,这种单例,其实只要在不涉及多线程的运用中,基本也就可以了,比如只要你确认你的单例调用,一直在UI主线程中使用的话,那完全不需要再修改,用就好了。
- 可惜事实往往很残酷,在程序运行期间,其实多线程常常是我们要考虑的问题。
###改造:
- 在获取方法中,加入同步机制:
- synchronized修饰获取方法:
//提供获取单例的唯一接口
public synchronized static Person getInstance() {
if(person == null) {
person =new Person();
}
return person;
}
- 上述例子就是一个线程安全的单例,但是同样还有缺陷:
- 虽然线程安全了,但是每次都要进行同步,因此会影响性能
- 因此我们还可以进一步改善,我们加上双重检查机制:
- 做了两次判空操作,确保了只有第一次调用单例的时候才会做同步,也避免了同步的性能损耗
//提供获取单例的唯一接口
public static Person getInstance() {
if(person ==null) {
synchronized (Person.class) {
if(person == null) {
person =new Person();
}
}
}
return person;
}
###静态内部类方案:
- 当然,不想用同步锁的方案,也是可以的,使用静态内部类并且加上final修饰的机制也可以实现单例:
- 静态内部类的方案,利用ClassLoader的机制类保证初始化单例对象的时候,只有一个线程,所以也是线程安全的,同时还没有性能的损耗。
public class Person{
//构造器私有化
private Person() {
}
//写一个静态内部类,用来提供单例对象
private static class LazyHolder {
public static final Person SINGLEINSTANCE = new Person();
}
//获取单例对象的方法
public static Person getInstance(){
return LazyHolder.SINGLEINSTANCE;
}
}
- 上述就是懒汉式单例的比较常见的写法,基本上来说,这几种写法在常用的开发中也就足够了。
- 但是,研究肯定不是简单地罗列方案就完事了,我们还要看看饿汉式单例,来做下对比。
饿汉式单例:
- 因为饿汉式单例是在类创建的同时,就已经创建好了一个静态的对象供给系统使用,以后不再改变,所以是天生线程安全的
- 实例:
public class Person {
//类内部封装自己的引用,该引用必须私有
private static Person person = new Person();
//构造器私有化
private Person(){
}
//提供获取单例的唯一接口
public static Person getInstance() {
return person;
}
}
- 饿汉式单例比较简单粗暴,比如一些程序只要运行就一定需要使用的单例,就不需要考虑饿汉式写法了。怎么方便怎么来,也不失为一种合适的方案。
饿汉式和懒汉式的区别:
- 说过了写法,我们还要来对比下两种单例的区别。
从名字上区分:
- 饿汉:
- 类一旦加载,就把单例初始化完成,保证取单例的时候,单例是绝对存在的
- 懒汉:
- 比较懒,只有当取单例的时候,才会去初始化这个单例对象
线程安全:
- 饿汉式:
- 天生线程安全,可以直接不用担心多线程的安全问题
- 懒汉式:
- 本身是非线程安全的,为了实现线程安全,需要额外做操作。
##抛出问题
- 上面所说的单例就已经完全没有问题了吗?在极限状态下,是否还会出现不可控的情况?
- 当然,他们能够解决大部分的问题。但是,说到安全,还不是绝对安全的。
- 比如,当这些单例,碰到反射的时候,单例就失效了。
- 因为私有化的构造函数,并不能安全地把反射机制阻隔在单例之外,如果你的单例能够被反射进行复制,并且需要避免这种情况的话,可以尝试使用以下的方案来设计。
枚举单例:
- 前面介绍了懒汉式单例、饿汉式单例,最近在网上看到有大神提出可以使用枚举类型创建单例。
- 优点:
- 枚举类型天生就是线程安全的,也不需要去考虑线程安全问题。所以,看来看去还是枚举单例用起来比较高大上。
- 缺点:
- 枚举其实就是Java的语法糖设计,枚举中的每一个单例,其实都是一个静态类的实现,在程序启动就已经实例化好了,也是一笔内存开销。
- 枚举单例属于饿汉式单例,有性能优化洁癖的同学,可能会不太喜欢。
- 且看下面代码实例:
public enum Person {
INSTANCE;
Person() {
//单例构造,默认私有
}
@Override
public String toString() {
return super.toString();
}
}
class test{
public void go(){
//直接使用枚举类型调用单例
Person.INSTANCE.toString();
}
}