23种设计模式之---单例模式(Singleton Pattern)

本文深入解析单例模式,包括其定义、特征、分类(饿汉式与懒汉式),以及在Spring IOC中的应用。探讨了不同实现方式的线程安全性和性能优劣,如同步方法、双重检验锁和静态内部类。

1.单例模式(创建型模式)

单例模式(Singleton):保证一个类仅有一个实例,并提供一个访问它的全局访问点. 这种设计模式属于创建型设计模式,提供了一种创建对象的最佳方式.

       通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象.一个最好的办法就是,让类自身负责保存它的唯一实例.这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法.

2.单例类的特征:

     1.单例类只能有一个实例;

     2.单例类必须自己创建自己的唯一实例;

     3.单例类必须给所有其他对象提供这一实例.

3.单例模式的分类: 饿汉式和懒汉式

饿汉式:饿汉式就是类一旦加载就把单例初始化完成,保证调用getInstance()方法的时候,单例是已经存在了的.

public class Singleton {
    /**
     *是否 Lazy 初始化:否
     *是否多线程安全:是
     *实现难度:易
     *描述:这种方式比较常用,但容易产生垃圾对象。
     *优点:没有加锁,执行效率会提高。
     *缺点:类加载时就初始化,浪费内存。
     *它基于 classloder 机制避免了多线程的同步问题,
     * 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
     * 在单例模式中大多数都是调用 getInstance 方法,
     * 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
     * 这时候初始化 instance 显然没有达到 lazy loading 的效果。
     */
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
        System.out.println("instance:"+instance);
        System.out.println("加载饿汉式....");
        return instance;
    }
}

懒汉式: 懒汉式比较懒,类加载时并不会创建这一实例,在调用getInstance()方法时,才会创建这一实例.

public class Singleton {
    /**
     *是否 Lazy 初始化:是
     *是否多线程安全:否
     *实现难度:易
     *描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
     *这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
     */
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4.饿汉式和懒汉式的区别:

       1.线程安全:

      饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

      懒汉式本身是非线程安全的,为了实现线程安全有几种写法(网上还有好多个线程安全的写法,可以参考):

(1).同步方法:

public class Singleton {
	//私有化构造
	private Singleton(){
		System.out.println("对象创建成功");
	}
	//全局对象
	private static Singleton singleton = null;	
	//调用getInstance方法才会创建对象
	public static synchronized Singleton getInstance(){
		//判断全局对象是否为空
		if(singleton == null){
			//如果为空,就创建该对象
			singleton = new Singleton();
		}
		//如果不为空,返回该对象
		return singleton;
	}
}

    虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance()方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

(2).双重检验锁

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile 保证编译器不进行优化
    private Singleton (){}
    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
   
}

关于volatile的作用,借鉴了一位大佬的评论:

(这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。" 二次检查这段代码, 为什么要二次检查? volatile在此的作用仅是保证可见性? 我想纠正一下, 内层的检查保证对象在并发时不会重复创建 和 ! 对象尚未初始化完成, 外层检查避免每一次获取对象都对资源进行加锁, 影响性能, 所以才会有了并发情况下的线程安全的懒汉单例, 即Double Check Lock; 而volatile 在此是禁止指令重排的作用, 保证先初始化, 再把对象引用赋值给instance变量. )

-------------------------------------------------------------------------------------------------

关于volatile的作用再次补充:

     instance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为instance 分配内存空间
  2. 初始化 instance
  3. 将instance 指向分配的内存地址 

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

-------------------------------------------------------------------------------------------------------------------------------

(3).静态内部类

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder是私有的,除了 getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK版本。

       2.资源加载和性能:

       饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。

       而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

5.懒汉式、饿汉式在spring IOC中的应用

在spring IOC中,bean在xml中可以配置为singleton,而且有一个lazy-init属性

lazy-init=true,设置延迟初始化, 在创建容器之后,在第一次从容器获取对象的时候创建单例的对象

如果没有配置或延迟初始化为默认值, 单例的对象会在创建容器的时候创建对象

---关于设计模式这块可以看下<<大话设计模式>>,比较简单易懂,适合刚接触设计模式的人学习.

附加关于synchronized关键字和volatile关键字的比较:

synchronized关键字和volatile关键字比较

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值