设计模式——单例模式


前言

  昨天回顾了模板方法设计模式,今天回顾一下单例设计模式。单例模式是一种创建类型的常用设计模式,本文介绍五种单例模式实现,同时简要分析为什么有些实现是线程不安全的。


1.饿汉式——线程安全

  “饿汉式”,你可以把它可以想象一个“饿汉”,一个很饿的大汉一上来肯定要吃东西,所以饿汉式一开始直接创建类的实例。代码如下:

//饿汉式
public class SingleTon {

    //直接创建类的实例,只初始化一次
    private static SingleTon instance = new SingleTon();
    //私有化构造器
    private SingleTon(){

    }
    //返回创建好的实例
    public static SingleTon getInstance(){
        return instance;
    }
}
优点
  • 饿汉式是线程安全的,使用时没有延迟
缺点
  • 启动时即实例化,可能会造成资源浪费

2.懒汉式——线程不安全

  懒汉式,顾名思义,它很懒,所以一开始并不进行实例化,而是需要的时候才会进行实例化,因为是单例的,所以创建之前进行判断是否为空。代码如下:

//懒汉式——线程不安全
public class SingleTon {

    //先不实例化,需要的时候再进行实例化
    private static SingleTon instance = null;
    //私有化构造器
    private SingleTon() {

    }
    //创建并返回实例对象
    public static SingleTon getInstance() {
    	//判断是否为空
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}

这种写法在多线程情况下是线程不安全的,会导致多次实例化,违背了单例模式的初衷

TIMEThreadAThreadB
T1检测到instance为空,
T2检测到instance为空
T3初始化对象B
T4返回对象B
T5初始化对象A
T6返回对象A

  可以看到instance 被实例化了两次,并被不同对象持有,这完全违背了单例设计模式。

优点
  • 懒加载启动快,使用时才实例化,不会造成资源浪费
缺点
  • 非线程安全

3.懒汉式——线程安全(单锁)

  既然涉及到了线程安全,第一反应就是加锁,代码如下:

//懒汉式——线程安全(单锁)
public class SingleTon {

    private static SingleTon instance = null;
    private SingleTon() {

    }
    //加锁
    public static synchronized SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}

  加锁既可以实现延迟加载,又可以实现线程安全,但是因为synchronized为并发排他锁,并发性能差,实际上线程不安全只可能发生在第一次初始化时发生,之后再进行调用就没必要进行加锁。

优点
  • 懒加载启动快,使用时才实例化,不会造成资源浪费
  • 线程安全
缺点
  • synchronized锁会降低性能

4.懒汉式——线程安全(双检锁)

  通过分析我们知道仅仅初始化时候需要进行加锁,但是synchronized锁每次都加锁,会使性能减低,为了解决这一问题,下面使用双检锁来保证线程安全,先判断是否已经初始化,在决定要不要加锁。代码如下:

//懒汉式——线程安全(双检锁)
public class SingleTon {

    private volatile static SingleTon instance = null;
    private SingleTon() {

    }
    public static SingleTon getInstance() {
        //检测实例是否存在,如果不存在才会进行加锁
        if (instance == null) {
            //加锁
            synchronized (SingleTon.class) {
                //再次检测实例是否存在,不存在则进行初始化
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
优点
  • 懒加载,线程安全的同时保证性能
注:实例必须有volatile修饰,以保证初始化完全

  下面来分析一下为什么必须用volatile修饰,如果不用volatile修饰会发生什么。

instance = new SingleTon();

这句代码分为三个步骤,分别是:
  1.分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间
但是有些编译器会将第二步和第三步进行重排序,三个步骤变成了:
  1.分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象
然后分析一下在多线程情况下,考虑到重排序,分析如下

TIMEThreadAThread
Time1检查到instance为空
Time2获取锁
Time3再次检查到instance为空
Time4为instance分配内存空间
Time5将instance指向分配的内存空间
Time6检查到instance不为空
Time7访问instance(此时instance还未初始化完成)
Time8初始化instance

  由上表可知,如果实例对象没被volatile修饰,如果编译器重排序,就会造成在Time7时一样的情况,访问到的是一个为初始化完成的对象。所以必须要在实例对象前加volatile修饰,因为使用了volatile关键字之后,重排序被禁止,所有的写操作都发生在读操作之前,就不会出现上述情况了。

5.静态内部类——线程安全

  第五种方式,既有懒加载的有点,又在保证线程安全的同时无需加锁,这种方式通过静态内部类来实现的。代码如下:

/**
* 静态内部类——线程安全
* 静态成员内部类的实例与外部类的实例,没有绑定关系
* 只有被调用才会进行装载,从而实现懒加载
 */    
public class SingleTon {

   private static class SingleTonHolder {
       //静态初始化器,通过JVM来保证线程安全的
       private static SingleTon instance = new SingleTon();
   }
   private SingleTon() {
       
   }
   public static SingleTon getInstance() {
       return SingleTonHolder.instance;
   }
}
优点
  • 实现了延迟加载,保证线程安全的同时又避免加锁带来的性能影响

6.枚举类

  昨天评论区有人提出没有枚举类的实现方式,今天来加上。《effective java》书中说到“单元素的枚举类型已经成为实现Singleton的最佳方法”。书中讲到“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。”,这个时候为了解决这种这个问题就需要用到枚举类。代码演示如下:

public class SingleTon {

    //私有化构造器
    private SingleTon() {

    }

    //定义一个枚举类
     enum SingleTonEnum {
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;

        private SingleTon instance;

        //枚举类默认且强制私有化构造器
        SingleTonEnum() {
            instance = new SingleTon();
        }
        public SingleTon getInstance(){
            return instance;
        }
    }

    public static SingleTon getInstance(){
        return SingleTonEnum.INSTANCE.getInstance();
    }
}

//测试
public class Test {
    public static void main(String[] args) {
        System.out.println(SingleTon.getInstance());
        System.out.println(SingleTon.getInstance());
        System.out.println(SingleTon.getInstance() == SingleTon.getInstance());
    }
}

运行结果如下:
在这里插入图片描述

注:枚举类是实现单例模式的最佳方法,推荐使用

总结

Good Good Study,Day Day Up!

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值