剑指offer第二版面试题2:实现Singleton模式(java)
面试题2:实现Singleton模式
题目:设计一个类,我们只能生成该类的一个实例。
由于第一题主要讲的是C++语言特性,因此直接跳过,开始记录第二题。
单例模式分为懒汉式(需要才去创建对象)和饿汉式(创建类的实例时就去创建对象)。
-
饿汉式
该模式在类被加载时就会实例化一个对象。
- 属性实例化对象
//饿汉模式:线程安全,耗费资源。 public class HugerSingletonTest { //final 该对象的引用不可修改 private static final HugerSingletonTest ourInstance = new HugerSingletonTest(); //static 可以通过HugerSingletonTest.getInstance()实现调用。 //HugerSingletonTest hunger = HugerSingletonTest.getInstance(); public static HugerSingletonTest getInstance() { return ourInstance; } private HugerSingletonTest() {}//private 防止其他类初始化该类 }
该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单)
PS:在看这块儿时发现static已经忘得差不多了,重新回顾了一下。。。static关键字的四种用法
-
懒汉式
该模式只在你需要对象时才会生成单例对象(比如调用getInstance方法)
-
非线程安全
public class Singleton { private static Singleton ourInstance; public static Singleton getInstance() { if (null == ourInstance) { ourInstance = new Singleton(); } return ourInstance; } private Singleton() {} }
分析:如果有两个线程同时调用getInstance()方法,则会创建两个实例化对象。所以是非线程安全的。
-
线程安全:给方法加锁(资源消耗较大)
public class Singleton { private static Singleton ourInstance; public synchronized static Singleton getInstance() { if (null == ourInstance) { ourInstance = new Singleton(); } return ourInstance; } private Singleton() {} }
分析:如果有多个线程调用getInstance()方法,当一个线程获取该方法,而其它线程必须等待,消耗资源。
这里的消耗资源主要是因为synchronized关键字加在了函数上。
-
线程安全:双重检查锁(DCL Double-Check懒汉式)
public class Singleton { //volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性 private volatile static Singleton ourInstance; public static Singleton getInstance() { if (null == ourInstance) { synchronized (Singleton.class) { if (null == ourInstance) { ourInstance = new Singleton(); } } } return ourInstance; } private Singleton() {} }
分析:为什么需要双重检查锁呢?因为第一次检查是确保之前是一个空对象,而非空对象就不需要同步了,空对象的线程然后进入同步代码块,如果不加第二次空对象检查,两个线程同时获取同步代码块,一个线程进入同步代码块,另一个线程就会等待,而这两个线程就会创建两个实例化对象,所以需要在线程进入同步代码块后再次进行空对象检查,才能确保只创建一个实例化对象。
相较于上一个,因为是在函数内部加锁,所以消耗资源较少。
volatile作用:
- 创建一个对象,往往包含三个过程:对于singleton = new Singleton(),这不是一个原子操作,在 JVM 中包含的三个过程。
- 1.给 singleton 分配内存。
- 2.调用 Singleton 的构造函数来初始化成员变量,形成实例。
- 3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)。
- 但是,由于JVM会进行指令重排序,所以上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被l另一个线程抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以这个线程会直接返回 instance,然后使用,那肯定就会报错了。
- 针对这种情况,我们有什么解决方法呢?那就是把singleton声明成 volatile。
- volatile的作用是:保证可见性、禁止指令重排序,但不能保证原子性。
-
线程安全:静态内部类(剑指强烈推荐的方法)
public class Singleton { private static class SingletonHodler { private static final Singleton ourInstance = new Singleton(); } public static Singleton getInstance() { return SingletonHodler.ourInstance; } private Singleton() {} }
分析:利用静态内部类的特性:只会加载一次。某个线程在调用该方法时会创建一个实例化对象。
下面的枚举和ThreadLocal我暂时看不懂,待到将来看到该部分时,再继续研究。
-
线程安全:枚举
enum SingletonTest { INSTANCE, INSTANCE("aa"), private String bb = "a"; public whateverMethod() { } public SingletonTest(String aa) { aa = bb; } }
分析:枚举的方式是《Effective Java》书中提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,但是在枚举中的其他任何方法的线程安全由程序员自己负责。还有防止上面的通过反射机制调用私用构造器。不过,由于Java1.5中才加入enum特性,所以使用的人并不多。
-
线程安全:使用ThreadLocal
public class Singleton { private static final ThreadLocal<Singleton> tlSingleton = new ThreadLocal<Singleton>() { @Override protected Singleton initialValue() { return new Singleton(); } // () -> return new Singleton(); lamda表达式 }; public static Singleton getInstance() { return tlSingleton.get(); } private Singleton() {} }
分析:ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
-
线程安全:CAS锁
public class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>(); /** * 用CAS确保线程安全 */ public static Singleton getInstance() { while (true) { Singleton current = INSTANCE.get(); if (current != null) { return current; } current = new Singleton(); if (INSTANCE.compareAndSet(null, current)) { return current; } } } private Singleton() {} }
分析:对AtomicReference和AtomicBoolean详解 compareAndSet详解
INSTANCE.compareAndSet(null, current)是将null与INSTANCE中的引用比较,如果相同,返回true,并将current赋给INSTANCE,如果不同,则返回false并且不赋值。此方法使用设置的内存语义更新值,就像将该变量声明为volatile一样,不改变执行语序。
当线程1与线程2同时到达current = new Singleton()时,假设线程1先进入if,此时INSTANCE为null,与形参null相同,因此将current赋值给INSTANCE,并返回current。然后线程2进入if,此时INSTANCE与null相比较为false,跳出循环,重新进入,再次get()后赋值为Singleton对象,在第一个if判断中跳出。
-