茴香豆有几种写法:

  • 朋友,如果你真的是来看茴香豆的写法的话,那对不起!没有!我不会!
  • 但是,关于单例,你知道有几种写法呢?
  • 单例的懒汉式、饿汉式都是什么意思?枚举单例又为什么被大神推崇?还有那个变量前面莫名加上volatile是什么意思?
  • 今天我们来看看单例设计模式的一些讨论。

单例概念:

  • java中单例设计模式是一种常见的设计模式,单例设计模式的写法有好多种(正如茴香豆的茴字有多少种写法?):
    • 懒汉式单例
    • 饿汉式单例
    • 枚举单例(这种写法比较少见,但是值得讨论和使用)
  • 单例设计模式具备的特点:
    • 单例类只能有一个实例
    • 单例类必须自己创建自己的唯一实例
    • 单例类必须给所有其他对象提供这一实例

懒汉式单例:

  • 注意点:
    • 类内部私有封装一个自己的引用
    • 构造器私有化
    • 提供获取该类内部私有封装的唯一方法
  • 缺点:
    • 懒汉式单例设计的实现没有考虑过线程安全问题,它是非线程安全的,在并发环境下,很可能会出现多个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();
    }
}

 评论