单例模式的意义
单例模式属于创建型模式,它提供了一种创建对象的方式,确保只有单个对象被创建。单例模式的解决的痛点就是节约资源,节省时间从两个方面看:
- 由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的
- 因为不需要频繁创建对象,我们的GC压力也减轻了。而在GC中会有STW(stop the world),从这一方面也节约了GC的时间,单例模式的缺点,复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度。
饿汉模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance
}
}
扩展:
有可能会问instance什么时候被初始化?
Singleton类被加载的时候就会被初始化,java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 生成这4条指令最常见的java代码场景是:1)使用new关键字实例化对象2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)4)调用一个类的静态方法
class的生命周期?
class的生命周期一般来说会经历加载、连接、初始化、使用、和卸载五个阶段
class的加载机制
这里可以聊下classloader的双亲委派模型。
懒汉模式
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
加锁的懒汉模式:
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton newInstance(){
if(null == instance){ // Single Checked
instance = new Singleton();
}
return instance;
}
}
DCL(双重校验锁)
- 加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。
- synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。
- 因此就有了双重校验锁,先看下它的实现代码。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (null = instance) {
synchronized(Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
扩展:
为什么需要volatile? volatile有什么用?
- 首先要回答可见性,这个是毋庸质疑的,然后可能又会考到java内存模型。
- 防止指令重排序: 防止new Singleton时指令重排序导致其他线程获取到未初始化完的对象。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,然后使用,然后报错。 - 顺便也可以说下volatie原理用内存屏障
讲讲synchronized和volatile的区别
这里可以从synchroized能保证原子性,volatile不能保证说起,以及讲下synchroized是重量级锁,甚至可以所以下他和Lock的区别等等。
线程安全一般怎么实现的?
- 互斥同步。如lock,synchroized
- 非阻塞同步。如cas。
- 不同步。如threadLocal,局部变量。
静态内部类
public class Singleton {
private static class SingletonHolder {
public static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
- 这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。
- 不一样的是,它是在内部类里面去创建对象实例。
- 这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。
枚举
public class EnumSingleton{
private EnumSingleton(){}
public static EnumSingleton getInstance(){
return Singleton.INSTANCE.getInstance();
}
private static enum Singleton{
INSTANCE;
private EnumSingleton singleton;
//JVM会保证此方法绝对只调用一次
private Singleton(){
singleton = new EnumSingleton();
}
public EnumSingleton getInstance(){
return singleton;
}
}
public static void main(String[] args) {
EnumSingleton obj1 = EnumSingleton.getInstance();
EnumSingleton obj2 = EnumSingleton.getInstance();
//输出结果:obj1==obj2?true
System.out.println("obj1==obj2?" + (obj1==obj2));
}
}
- 首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。
- 同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
- 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
上面提到的四种实现单例的方式都有共同的缺点:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。