单例设计模式

**单例:**保证java程序中,只有一个实例存在,适用于全局统一管理的场景。
(1)饿汉模式:在初始化的时候就创建类实例。适合单例占用内存少,初始化时就会用到的情况。

public class Singleton{  
    private static Singleton instance = new Singleton();  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return instance;  
    }  
}  

(2)懒汉模式(常用):在需要时才创建。可以使用synchronized+volatile对创建实例的getInstance方法进行加锁和防止指令重排序。

public class Singleton {  
	//使用volatile保证多线程下的可见性
    private static volatile Singleton instance = null;  
    //私有化构造器
    private Singleton(){}  
    //静态方法获取单例对象
    public static Singleton getInstance() {  
        if (instance == null) {  
        	//由于是静态方法,所以加锁是要锁整个类,因为静态方法的调用是类层面的,不是对象层面的
            synchronized (Singleton.class) {  
            	//加完锁之后还要再判断一次单例对象是否已经被其他线程创建,避免重复创建
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
} 

(3)静态内部类:通过内部类创建实例,不使用到内部类时不会创建单例对象,也避免了创建对象时的线程安全问题。

public class Singleton{  
    private static class SingletonHolder{  
        public static Singleton instance = new Singleton();  
    }  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return SingletonHolder.instance;  
    }  
} 

(4)枚举(推荐):上面几种方法中,可以使用反射强行调用私有构造器,或者序列化时会新创建一个新对象,枚举可以防止这两个情况。

public enum Singleton{  
    instance;  
    public void whateverMethod(){}      
} 

(5)Spring的单例注册表:由于以上模式的构造方法是私有的,不可继承,Spring为实现单例类可继承,使用的是单例注册表的方式:①使用map实现注册表;②使用protect修饰构造方法;

Import java.util.HashMap;   
 
Public class RegSingleton{
	//使用一个map来当注册表
   Static private HashMap registry=new HashMap();    
   //静态块,在类被加载时自动执行    
    Static{    
     	RegSingleton rs=new RegSingleton();    
     	Registry.put(rs.getClass().getName(),rs);    
   }    
	//受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点    
	Protected RegSingleton(){}    
	//静态工厂方法,返回此类的唯一实例    
	public static RegSingleton getInstance(String name){    
    	if(name==null){    
      		name=RegSingleton;    
    	}if(registry.get(name)==null){    
	      	try{    
	          registry.put(name,Class.forName(name).newInstance());    
	       	}Catch(Exception ex){
	       		ex.printStackTrace();
	       	}    
    	}    
    	Return (RegSingleton)registry.get(name);    
	}    
}  

参考《Spring的单例实现原理

为什么要使用volitile关键字

这里思考一个问题,为什么懒汉模式中药使用volitile关键字?
这里主要是使用了volatile关键字的禁止指令重排序的作用,防止代码出现并发异常。
What❓难道不是因为可见性问题吗?担心其他线程无法及时获取到singleton对象而导致重复创建?
这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

第一步,是给 singleton 分配内存空间;
第二步,开始调用 Singleton 的构造函数等,来初始化 singleton;
第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。

这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错,详细流程如下图所示:
在这里插入图片描述
线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。
这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。
使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值