**单例:**保证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 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。