您的当前位置:首页正文

Java 单例实现

来源:华拓网

饿汉式

这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的

public class Singleton {
    // 类加载时就初始化
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance(){
        return instance;
    }
}

缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用

懒汉式,线程不安全

public class Singleton {
    private static Singleton instance;
    private Singleton () {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例

懒汉式,线程安全

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁

双重检验锁

public class Singleton {  
    private volatile static Singleton instance;  
    private Singleton () {}   
    public static Singleton getSingleton() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}  

称其为双重检查锁,是因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了

而 instance = new Singleton() 这句,并非是一个原子操作,事实上在 JVM 中这句话做了下面 3 件事:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错

所以需要将 instance 变量声明成 volatile

静态内部类

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton () {}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

静态内部类与外部类没有什么关系,外部类加载的时候,内部类不会被加载,静态内部类只是调用的时候用了外部类的名字而已,所以即使 Singleton 类被加载也不会创建单例对象

静态内部类 SingletonHolder 只有在 getInstance() 方法第一次被调用时,才会被加载,从而初始化它的静态域(创建 Singleton 的实例),因此该种方式实现了懒汉式的单例模式

不仅如此,根据 JVM 本身机制,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性

同时不用 synchronized,所以没有性能缺陷

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这个写法也是利用类的静态变量的唯一性,不过和上面的写法相比,不能实现懒加载

Enum

用枚举写单例最大的优点是简单

public enum Singleton {
    INSTANCE {

        @Override
        protected void read() {
            System.out.println("read");
        }

        @Override
        protected void write() {
            System.out.println("write");
        }

    };
    protected abstract void read();
    protected abstract void write();
}

以上是一个单例枚举的例子,要获取该实例只需要 Singleton.INSTANCE,并且此种方式可以保证该单例线程安全、防反射攻击、防止序列化生成新的实例

反编译后的类:

public abstract class Singleton extends Enum {

    private Singleton(String s, int i) {
        super(s, i);
    }

    protected abstract void read();

    protected abstract void write();

    public static Singleton[] values() {
        Singleton asingleton[];
        int i;
        Singleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static Singleton valueOf(String s) {
        return (Singleton)Enum.valueOf(singleton/Singleton, s);
    }

    Singleton(String s, int i, Singleton singleton) {
        this(s, i);
    }

    public static final Singleton INSTANCE;
    private static final Singleton ENUM$VALUES[];

    static {
        INSTANCE = new Singleton("INSTANCE", 0) {

            protected void read() {
                System.out.println("read");
            }

            protected void write() {
                System.out.println("write");
            }

        };
        ENUM$VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

看了这个类的真身后,可以知道:

  • 枚举类实现其实省略了 private 类型的构造函数
  • 枚举类的域其实是相应的 enum 类型的一个实例对象
  • 枚举类的域会在 static 方法块中被实例化,也就是说在 enum 被类加载器加载时被实例化,并非懒加载
  • enum 是 abstract 类,所以没法实例化,反射也无能为力
  • 关于线程安全的保证,其实是通过类加载机制来保证的,INSTANCE 是在 static 块中实例化的,JVM 加载类的过程显然是线程安全的
  • 而枚举可以反序列化是因为 Enum 实现了 readResolve 方法

对于一个标准的 enum 单例模式,最优秀的写法还是实现接口的形式:

// 定义单例模式中需要完成的代码逻辑
public interface MySingleton {
    void doSomething();
}

public enum Singleton implements MySingleton {
    INSTANCE {
        @Override
        public void doSomething() {
            System.out.println("complete singleton");
        }
    };

    public static MySingleton getInstance() {
        return Singleton.INSTANCE;
    }
}

当单例类被多个类加载器加载,如何还能保持单例

如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些 servlet 容器对每个 servlet 使用完全不同的类装载器,这样的话如果有两个 servlet 访问一个单例类,它们就都会有各自的实例

基于同样的原因,分布式系统和集群系统也都可能出现单例失效的情况

解决方法:用多个类加载器的父类来加载单例类

private static Class getClass(String classname) throws ClassNotFoundException {     
    // 线程上下文类加载器,未设置的话默认是应用程序类加载器
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
    
    if (classLoader == null)
        classLoader = Singleton.class.getClassLoader();
     
    return classLoader.loadClass(classname);
}  

单例类防止反序列化

public class Singleton implements java.io.Serializable {
    // ...
    private Object readResolve() {
        return INSTANCE;
    }
}

readResolve() 方法可以理解反序列化过程的出口,就是在反序列化完成得到对象前,把这个对象换成我们确定好的那个

反射破坏单例原则

在单例模式中,只对外提供工厂方法(获取单例),并私有化构造函数,来防止外面多余的创建。对于一般的外部调用来说,私有构造函数已经很安全了。但是一些特权用户可以通过反射来访问私有构造函数,然后打开访问权限 setAccessible(true),就可以访问私有构造函数了,这样破坏了单例的私有构造函数保护,创建了一个新的实例。如果要防御这样的反射侵入,可以修改构造函数,加上第二次实例化的检查,当发生创建第二个单例的请求时会抛出异常

private static int cntInstance = 0;
 
private Singleton() throws Exception {
    if (cntInstance++ > 1) {
        throw new Exception("can't create another singleton instance.");
    }
}