设计模式--单例模式

单例模式(Singleton Pattern)是Java中最简单的设计模式之一,它属于创建型设计模式。单例模式设计到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,并且该类要提供一种访问这个唯一对象的方式。所以单例类有以下的几个规范:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

单例模式主要解决的问题是:当一个全局使用的类频繁地创建和销毁,其带来的系统损耗会很大。使用单例模式可以使系统中只有一个实例,只需创建一次,在需要的时候通过单例类提供的方法来调用这个实例。

单例模式的UML图

+ 表示被public修饰,- 表示被private修饰

单例模式的实现方式

单例模式的实现方式主要有这几种:

  1. 懒汉式
  2. 饿汉式
  3. 双重校验锁
  4. 静态内部类

我们按顺序来了解这几种方式。

懒汉式

懒汉式顾名思义就是等到需要用到这个实例时我再去实例化它。在懒汉式中,如果单例已经创建,则直接使用已经创建好的对象返回,否则创建一个新的对象返回。

public class Singleton2 {
	
	private static Singleton2 instance = null;
	
	private Singleton2(){
		
	}
	
	public static Singleton2 getInstance(){
		if(instance == null)
			instance = new Singleton2();
		return instance;
	}
}

上边时懒汉式最基本的实现方式,它最大的问题在于不支持多线程。因为当多线程调用getInstance()方法时,有多个线程同时进入if判断语句中,此时有可能instance正在实例化,但还没有完成实例化的过程,此时这几个线程都会去实例化这个类,使得失去了单例模式的效果。

当然,我们也可以通过加synchronized的方式来实现线程安全的懒汉式

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

我们解决了多线程同步的问题,但是又会有新的问题产生,synchronized会导致效率下降。这个问题,我们待会在双重校验锁中解决他。

饿汉式

首先我们先明白什么叫饿汉式,饿汉模式就是无论你是否需要用到这个单例类,都会在内存中先去实例化这个类,这个实例化过程在类加载的过程中(后边JVM的类加载过程会讲)完成。就像一个饿汉,只要有这个类,我就要吃掉(实例化)它。因此所有在类加载过程中完成单例类的实例化的方式都是饿汉式。下边我们简单介绍一种饿汉式

public class Singleton1 {
	
	private static Singleton1 instance = new Singleton1();
	
	private Singleton1(){
		
	}
	
	public static Singleton1 getInstance(){
		return instance;
	}
}

 

这种实现方式是最简单的一种实现方式,它是一种线程安全的创建方式,不需要加锁,执行效率会提高,但是因为它在类加载的过程中就初始化,如果在后续没有使用到这个单例类,就会造成内存浪费的问题。

这种实现方式适合单例占用内存比较小,在初始化时就会被用到,但是,如果单例占用的内存比较大,或者只在某特定的场景下才会用到,使用懒汉式会比饿汉式更适合。

双重校验锁

双重校验锁其实也是一种懒汉式,但它解决了懒汉式中直接在getInstance()方法上加synchronized所导致的效率问题。我们先来看看它的实现代码:
 

public class Singleton3 {

	private static Singleton3 instance = null;
	
	private Singleton3(){
		
	}
	
	public static Singleton3 getInstance(){
		if(instance == null){
			synchronized(Singleton3.class){
				if(instance == null){
					instance = new Singleton3();
				}
			}
		}
		return instance;
	}
}

通过双重校验和synchronized,我们解决了线程安全问题和效率问题,但是问题总是源源不断地。

首先要先说下Java中地指令重排优化,所谓指令重排优化时指在不改变源语义地情况下,通过调整指令地执行顺序让程运行得更快。由于指令重排优化地存在,导致初始化Singleton和将对象地址赋给instance字段地顺序时不确定。

例如:

线程A在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有完成初始化操作。线程B来调用newInstance()方法,得到的就是未初始化完全的单例对象,这就会导致系统出现异常行为。

而在JDK1.5之后增加了volatile关键字,volatile地一个语义就是禁止指令重排序优化,也就能够解决我们上边所讲到地问题。因此我们只需要给instance变量加上volatile关键字即可。

public class Singleton3 {

	private static volatile Singleton3 instance = null;
	
	private Singleton3(){
		
	}
	
	public static Singleton3 getInstance(){
		if(instance == null){
			synchronized(Singleton3.class){
				if(instance == null){
					instance = new Singleton3();
				}
			}
		}
		return instance;
	}
}

静态内部类

静态内部类也是一种懒汉式,能达到双重校验锁一样的功效,且实现更加简单。这种方式只适用于静态域的情况,而双重校验锁可以在实例域需要延迟初始化时使用。

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

这种方式同样利用了了加载机制来保证线程安全问题,即只会有一个实例。它与双重校验锁不同的地方在于,双重校验锁只要单例类被装载了,那么instance就会被实例化(并没有达到延迟加载的效果),而这种方式被装载时不一定会实例化,因为SingletonHolder没有被主动使用,只有调用了getInstance()才会显示装载Singleton Holder,从而实例化instance。

当实例化instance很消耗资源,想要延迟加载instance,又不希望Singleton加载时就实例化,此时这种方式就会比双重校验锁更加合适。


参考:https://blog.csdn.net/qq_24047659/article/details/86747024 
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值