天天看点

Java实现6种单例模式-设计模式剑指Offer

剑指Offer

面试题2:实现Singleton模式

Java实现6种单例模式-设计模式剑指Offer

解法1:饿汉式(静态常量)[可用]

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

单例实体作为静态属性在类加载的过程中完成实例化,避免了多线程并发问题。

缺点:没有达到Lazy-Loading效果。

解法2:饿汉式(静态代码块)[可用]

public class Singleton {
    private Singleton() {
    }
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    public static Singleton getInstance() {
        return this.instance;
    }
}
           

同解法1单例实体作为静态属性在类加载的过程中的执行静态代码块完成实例化,避免了多线程并发问题。

缺点:没有达到Lazy-Loading效果。

解法3:懒汉式(同步方法)[不推荐用]

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

单例实体的初始化在同步代码块中进行,保证了线程安全。

缺点:效率过低,每次获得单例实体都需要进行同步。

解法4:懒汉式(双重检查)[推荐用]

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

为什么要用单例实体要用volatile修饰?

答:第9行 instance = new Singleton(); 这句代码并非是原子指令,从字节码可以看到创建并赋值一个对象实例,可以分为三步:

1.分配对象内存

2.调用构造器方法,执行初始化

3.将对象引用赋值给变量

虚拟机实际运行时,以上指令是可能发生重排序。以上步骤 2,3 可能发生重排序,但是并不会重排序步骤 1 的顺序,因为 2,3 指令需要依托 1 指令执行结果。Java 语言规定了,在线程执行程序时,需要保证重排序的结果不会改变此程序在单线程内的程序执行结果。这个重排序在没有改变在单线程内的程序执行结果的前提下,进行了重排序,这可以提高程序的执行性能。虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。比如:

线程A 线程B
T1 分配对象内存
T2 将对象引用赋值给变量
T3 判断对象是否为空
T4 由于已经赋值,对象不为空,访问对象,报空指针异常
T5 调用构造器方法,执行初始化
如果线程A获取到同步锁,并进入同步方法,进行创建对象实例,这个时候发生了指令重排序。当线程A执行到T3 时刻,线程B刚好进入,由于此时对象已经被赋值,不为空,所以线程B可以自由访问该对象。然而该对象还未初始化,所以线程B访问时将会发生异常。由于volatile关键词可以禁止对象创建时指令之间重排序,也就是2,3指令之间的重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

解法5:懒汉式(静态内部类)[推荐用]

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

和解法1,2的饿汉式采用的机制类似,但是又有不同。两者都是采用了类加载机制来保证初始化单例实例时只有一个线程进行操作,但不同的地方在解法1,2的饿汉式中是只要Singleton类被加载单例实例就会被实例化,没有Lazy-Loading的作用,而静态内部类的方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载Singleton类中的静态内部类SingletonInstance类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。

解法6:懒汉式(枚举法)[十分推荐]

public enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("doSomething");
    }
}
           

调用时:

public class Main {
    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }
}
           

利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。