设计模式之单例

本节会介绍一下单例模式的定义,有哪几种创建方式,并会分别给出例子。

单例模式是什么

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。有以下三点需要注意:

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

为什么要用单例模式

在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例,也可以减少Jvm的GC次数,提升服务器性能。

在我们的系统中比如线程池、Spring中就用到了单例模式,Spring创建的Bean默认就是单例的。

什么情况下使用单例

在系统中如果某个类的对象只需要创建一次,后面可以复用这个对象:

  • 在开发中需要一个序列化生成器,就可以把这个类设计为单例。
  • 开发中经常会用到手机号归属地校验,通常都是把数据从数据库或者Excel中读取出来加载到内存中,这个缓存类就可以设计成单例。
  • 另外开发中也经常会用到的缓存也会设计成单例的,比如页面的某些静态数据就可以加载到内存中,通常这样的类也都会设计为单例的。

单例模式实现要点

因为单例类有一个实例,并且必须自己创建这个唯一的实例,另外单例类必须给其他对象提供这一实例,所以实现单例必须有如下关键部分:

  • 单例类的唯一对象的引用在类中为全局私有静态变量
  • 构造方法私有,防止外部通过new实例化
  • 提供一个静态方法返回唯一的实例

单例模式分类及实现

单例模式分为饿汉式和懒汉式:

  • 饿汉式:就是不管你用不用,因为饥渴难耐就自己主动创建出来了唯一的实例
  • 懒汉式:因为比较懒,所以只有在用到的时候会判断对象存不存在,存在就返回,不存在就进行实例化

饿汉式实现

饿汉式在Jvm加载类的时候就创建了唯一的实例,所以是线程安全的。优点是不用每次获取的时候再去判断了;缺点是有可能该对象并不会被用到,造成资源浪费。是属于空间换时间的方法,如果实例化过程比较耗时,可以采用懒汉式实现,因为如果实例化等待时间较久,会造成不好的影响。

方式1(线程安全):

//饿汉式单例
public class Singleton_h1 {

	//Jvm加载该类时就会自己实例化
	private static Singleton_h1 instance=new Singleton_h1();
	//构造方法私有,防止外部通过new实例化
	private Singleton_h1(){}
	//静态方法获取实例
	public static Singleton_h1 getInstance(){
		return instance;
	}
}

方式2:通过枚举实现(线程安全)

public enum Singleton_h2 {

	instance;
	
	Singleton_h2(){};
	
	public void doSomething(){
		System.out.println("hhhhhh");
	}
	
}

使用的时候直接通过 Singleton_h2.instance即可使用

懒汉式实现

获取实例时先进行判断,如果对象存在直接返回,如果不存在就创建对象并返回。好处就是只有用到了才进行实例化,不会浪费资源,坏处就是每次都要判断,而且还要注意线程安全问题。

1:静态内部类实现(线程安全):其实现原理就是访问外部类时,静态内部类不会被加载,只有访问静态内部类的属性或者方法时内部类才会被加载。

//饿汉式单例:静态内部类实现
public class Singleton_l1 {

	private Singleton_l1(){}
	
	//静态方法获取实例
	public static Singleton_l1 getInstance(){
		return SingletonHolder.instance;
	}
	
	//访问外部类时,静态内部类不会被加载
	private static class SingletonHolder{
		private static Singleton_l1 instance=new Singleton_l1();
	}
	
}

2:线程不安全的实现:多个线程同时请求的话会返回多个不同的对象

//懒汉式单例:线程不安全
public class Singleton_l2 {

	private static Singleton_l2 instance;
	//构造方法私有,防止外部通过new实例化
	private Singleton_l2(){}
	//静态方法获取实例:线程不安全
	public static Singleton_l2 getInstance(){
		if (instance == null) {  
	        instance = new Singleton_l2();  
	    }  
	    return instance;  
	}
}

3:线程安全的实现:每次调用都要获取锁,效率太低,更好的做法应该是已经实例化之后就不用获取锁了

//懒汉式单例:线程安全
public class Singleton_l3 {

	private static Singleton_l3 instance;
	//构造方法私有,防止外部通过new实例化
	private Singleton_l3(){}
	//静态方法获取实例:线程安全
	public static synchronized Singleton_l3 getInstance(){
		if (instance == null) {  
	        instance = new Singleton_l3();  
	    }  
	    return instance;  
	}
}

4:DCL双重校验锁的实现(线程安全):采用双重校验,只有实例为空的时候才需要获取锁,效率较上面第三种实现方式效率更高(里面的num属性和getNum方法是为了说明问题,跟单例没有关系)。

//懒汉式单例:双重校验锁
public class Singleton_l4 {

	private static volatile Singleton_l4 instance=null;
	private int num;
	
	//构造方法私有,防止外部通过new实例化
	private Singleton_l4(){
		this.num=new Random().nextInt(100)+1;//a
	}
	
	//静态方法获取实例:线程安全
	public static Singleton_l4 getInstance(){
		if (instance == null) {//1
			//实例为空,这时候需要获取锁
			synchronized (Singleton_l4.class) {//2
				//如果不加这层判断,则每个线程都会得到一个不同的实例
				if(instance==null){//3
					instance = new Singleton_l4();//4  
				}
			}
	    }  
	    return instance;  
	}
	
	public int getNum(){
		return this.num;
	}
}

双重校验锁实现的单例模式中有一个争议点就是volatile,很多人都会问明明都加了锁了,为什么还加个volatile干什么呢?

其实是因为在执行代码4处 instance = new Singleton_l4() 时其实是分几步执行的:

1:为对象分配内存空间

2:初始化默认值(区别于构造器方法的初始化)

3:执行构造初始化对象

4:连接引用和实例,即将instance引用执行对象的地址

正常按1、2、3、4的执行顺序是没有问题的,因为指令重排可能会导致4在3之前执行,即对象尚未构造完成,这时我们上面的代码中变量num的值为0,这时如果另外一个线程执行到代码1的时候读取到instance不为null直接返回,该返回的对象是一个不完整的对象,其调用getNum()的返回值是0,这样是错误的。

而volatile修饰的变量不会进行指令重排,会严格按照1、2、3、4执行,而且volatile可以保证可见性,当一个线程修改了变量会立即刷新到主存,在另一个线程内,读取该变量时会从从主存读取。

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值