单例模式--java

概述

单例模式是创建型设计模式的一种。一个类只允许创建一个实例,那么这个类就是一个单例类,这种设计模式叫做单例模式。

单例的用处

从业务概念上来说,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,系统的配置信息。除此之外,还可以使用单例解决资源冲突问题。

举例来说,java的运行时环境Runtime类就是典型的单例类,使用了饿汉式的单例写法(后续会介绍)。

单例的实现

单例的实现主要包括饿汉式、懒汉式、双重检测、静态内部类、枚举。关注点在于:

  • 构造函数需要是private访问权限的,这样才能避免外部通过new创建实例
  • 考虑对象创建时的线程安全问题
  • 考虑是否支持延迟加载
  • 考虑getInstance()性能是否高(加锁的问题)

饿汉式

代码实现如下:

public class IdGenerator {
    
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();
    
    private IdGenerator(){
        
    };
    
    public static IdGenerator getInstance(){
        return instance;
    }
    
    public long getId(){
        return id.incrementAndGet();
    }
}

饿汉式在类加载的时候,instance静态实例就已经创建并初始化好了,所以instance实例的创建过程是线程安全的。这种写法较为简单,不支持延迟加载。

懒汉式

public class IdGenerator {

	private AtomicLong id = new AtomicLong(0);
	private static IdGenerator instance;
	
	private IdGenerator() {
	
	}
	
	public static synchronized IdGenerator getInstance() {
		if (instance == null) {
			instance = new IdGenerator();
		}
		return instance;
	}
	
	public long getId() {
		return id.incrementAndGet();
	}
}

懒汉式并没有提前实例化instance,而是在使用到的时候才实例化。支持延迟加载,但是实现过程中给getInstance()方法加了锁(synchronized ),并发度很低。

双重检测

public class IdGenerator {

	private AtomicLong id = new AtomicLong(0);
	private volatile static IdGenerator instance;
	
	private IdGenerator() {
	}
	
	public static IdGenerator getInstance() {
		if (instance == null) {
			//位置1
			synchronized(IdGenerator.class) { 
				if (instance == null) {
					//位置2
					instance = new IdGenerator();
				}
			}
		}
		return instance;
	}
	
	public long getId() {
		return id.incrementAndGet();
	}
}

双重检测并没有直接将锁加到整个方法上,而是在实例化的时候才需要加锁。这样解决了懒汉式并发度低的问题。

为什么在加锁之后还需要判断instance是否为空才进行实例化

假设此时有两个线程经过第一次if判断instance为空处于位置1,这时只有一个线程能够获取到锁,假设线程1获取到锁,进行实例化释放锁后,线程2获取到锁,如果此时没有第二次判断,则直接进行实例化,出现两次实例化过程,违反单例模式原则。

为什么需要给instance成员变量加上volatile关键字

因为在位置2实例化instance时可能发生指令重排序。
创建一个实例的过程一般如下:

  1. 申请一块内存
  2. 初始化
  3. instance指向这块内存

一旦发生3和2的重排序(这是由jvm控制的),就会导致IdGenerator对象被new出来并且赋值给instance后,还没来得及初始化,就被另一个线程使用。而volatile关键字可以通过内存屏障防止指令重排序。
上述描述在《java并发编程的艺术》一书中有被提及。
不过有人提出,目前只有低版本的java才存在这种问题,高版本java已经在jdk内部解决此问题(通过将对象new操作和初始化操作设计为原子操作)。本人技术有限,目前无法求证。如果有了解的朋友,欢迎交流解惑。

静态内部类

public class IdGenerator {

	private AtomicLong id = new AtomicLong(0);
	
	private IdGenerator() {
	
	}
	
	private static class SingletonHolder{
		private static final IdGenerator instance = new IdGenerator();
	}
	
	public static IdGenerator getInstance() {
		return SingletonHolder.instance;
	}
	public long getId() {
		return id.incrementAndGet();
	}
}

静态内部类既保证了线程安全,也能延迟加载。在IdGenerator被加载时并不会创建SingletonHolder实例对象。当调用getInstance()方法时,SingletonHolder才会被加载。此时才会创建instance。instance的唯一性、创建过程的线程安全性由jvm保证。

枚举

public enum IdGenerator {

	INSTANCE;

	private AtomicLong id = new AtomicLong(0);
	
	public long getId() {
		return id.incrementAndGet();
	}
}

通过枚举本身的特性保证了实例创建的线程安全性和实例的唯一性

单例的缺点

  1. 单例违背了基于接口而非实现的设计原则。例如上述Id生成器的例子,如果需要在不同的业务中使用不同的id生成方案,那么针对这个业务变化需要修改所有用到IdGenerator类的地方,代码改动较大。
  2. 单例扩展性,灵活性较差。如果存在需要传入属性来构造特定对象的需求,单例实现会变得复杂。

尽管存在一些问题,但设计模式本就应用在最合适的地方,对于出现的问题可以采用其他方式替代,如工厂模式,ioc容器等。

单例的唯一性

上述的单例实现实际上都是进程内唯一。

线程单例

线程唯一的单例实际上是以线程作为唯一标识,线程id相同则返回同一个对象;不同则返回不同的对象。可以使用一个键为线程id的map来存储对象.

public class IdGenerator{
	
	private AtomicLong id = new AtomicLong(0);
	private static final ConcurrentHashMap<Long, IdGenerator> instances 
			= new ConcurrentHashMap<>();
	
	private IdGenerator(){
	
	}
	
	public static IdGenerator getInstance() {
		Long currentThreadId = Thread.currentThread().getId();
		instances.putIfAbsent(currentThreadId, new IdGenerator());
		return instances.get(currentThreadId);
	}
	
	public long getId() {
		return id.incrementAndGet();
	}
}

java语言提供的ThreadLocal工具类底层就是map,也可以实现线程单例

集群单例

集群单例相当于多个进程构成的一个集合共享一个实例。实现时需要将这个单例对象序列化并存储到外部共享存储区。进程在使用这个单例对象时,需要先从外部共享存储区域将其读到内存并反序列化为对象使用。使用完成后再存储回外部共享存储区。

为保证任何时刻进程间只有一份对象,一个进程在获取对象时需要对对象加分布式锁,避免其他进程获取。
进程使用完对象后,需要显示的将对象从内存中删除并释放锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值