设计模式之单例模式
有时候需要控制一个类只创建一个对象,比如说创建这个对象比较消耗性能、或者这个类比较占用内存,那么就可以通过单例模式来设计这个类。
思考
单例模式的实质是控制实例数量,当需要控制一个类的实例只能有一个时,使用单例模式。
使用场景
- java中缓存的实现:正是一种懒汉式的方式,查询的时候初始化缓存一次,之后访问每次访问初始化的缓存对象
- 数据库连接池:
- 线程池:
代码实现
饿汉式
-
结构
- 私有化无参构造函数
- 创建一个static修饰的对象
- 提供一个公共的static修饰的方法返回实例对象
-
优点
- 线程安全
- 空间换时间
//饿汉式
public class Singleton {
//2定义一个静态变量来存储创建好的类实例直接在这里创建类实例,由虚拟机来保证只会创建一次
private static final Singleton instance = new Singleton();
//1:私有化构造方法,好在内部控制创建实例的数目
private Singleton(){
//初始化加载后创建对象,判断不为空,再次创建,抛出异常,防止反射创建
if (instance != null){
throw new IllegalStateException("Already instantiated");
}
}
//3:这个方法需要定义成类方法,也就是要加static
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
for(int i=0;i<3;i++){
System.out.println(Singleton.getInstance());
}
}
}
懒汉式
-
结构
- 私有化构造方法
- 提供一个公共的static修饰的方法获取实例
- 双重NULL判断,加锁创建实例对象返回
- 实例对象使用volatile修饰
-
优点
- 节省空间
- 加锁同步后性能低于饿汉式
public class Singleton {
//对保存实例的变量添加volatile的修饰,防止指令重排
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized(Singleton.class){
//再次检查实例是否存在,如果不存在才真的创建实例
if(instance == null){
//使用volatile修饰,避免重排序,锁内的代码也会发生重排序
instance = new Singleton();
}
}
}
return instance;
}
}
这里使用volatile的原因为了防止指令重排序,避免获取到一个未初始化完成的对象
instance=new Singleton();可以分为以下3步完成(伪代码)
memory=allocate(): // 1.分配对象内存空间
instance(memory): // 2.初始化对象
instance=memory; //3. 设置instance指向刚分配的内存地址,此时instance != null
步骤2依赖步骤1,步骤3不依赖步骤2,所以有可能发生指令重排序,先执行1,再执行3,再执行2,假如线程A执行完步骤3还没执行步骤2的时候,线程B进入判断 instance!=null,直接会获取一个未初始化的对象去使用
类级内部类实现单例
public class Singleton {
//类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,而且只有被调用到才会 //装载,从而实现了延迟加载
private static class SingletonHolder{
//静态初始化器,由JVM来保证线程安全,这里会发生重排序,但是是发生在SingletonHolder实例完成之前,不影响所以不用加volatile
private static final Singleton instance = new Singleton();
}
// 私有化构造方法
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
该模式依赖classloader类加载器线程安全来实现。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字
枚举实现单例
java中的枚举其实是一种多例的实现,在枚举中只定义一个实例,那么就是最简单的一种单例模式
public enum Singleton {
uniqueInstance("test");
private Singleton(String name){
}
}
使用CAS 无锁
public class Singleton {
private Singleton(){}
//使用AtomicReference保证内存可见性
private static AtomicReference<Singleton> instance = new AtomicReference<>();
public static Singleton getInstance(){
//死循环中compareAndSet
for(;;){
if(instance.get() == null){
Singleton singleton = new Singleton();
//compareAndSet成功 返回,否则继续循环
if(instance.compareAndSet(null, singleton)){
return instance.get();
}
}else{//已经创建成功则返回
return instance.get();
}
}
}
}
CAS既保证了线程安全,又避免了锁等待导致的线程切换和阻塞,性能相对好一些,但是如果一直创建对象不成功,则会导致死循环,CPU开销大。同时还有一个问题,当大量线程同时到达的时候,都会执行 Singleton singleton = new Singleton();导致大量Singleton对象创建。不建议使用这种方式
针对以上问题优化后CAS实现单例
public class Singleton {
private Singleton(){}
//使用 volatile 防止重排序
private static volatile Singleton instance = null;
private static AtomicBoolean flag = new AtomicBoolean(false);
public static Singleton getInstance(){
//死循环中compareAndSet
for(;;){
if(instance == null){
//compareAndSet成功 返回,否则继续循环
if(flag.compareAndSet(false, true)){
try {
instance = new Singleton();
}catch (Exception e){
//防止创建对象异常
if (instance == null){
flag.set(false);
}
}
return instance;
}
}else{//已经创建成功则返回
return instance;
}
}
}
}
网友评论