单例模式里面其实也是包含很多知识点的,整理一下有助于知识的融会贯通。
单例模式的解决的痛点就是节约资源,节省时间从两个方面看:
1.由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的.
2.因为不需要频繁创建对象,我们的GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间 单例模式的缺点:简单的单例模式设计开发都比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度。
线程安全 | 并发性能好 | 可以延迟加载 | 序列化/反序列化安全 | 能抵御反射攻击 | |
---|---|---|---|---|---|
饿汉式 | Y | Y | |||
懒汉不加锁 | Y | Y | |||
懒汉加锁的 | Y | Y | |||
DCL | Y | Y | Y | ||
静态内部类 | Y | Y | Y | ||
枚举 | Y | Y | Y | Y |
懒汉式
懒汉式就是他很懒,只有在用的时候才进行实例化。
1. 单线程
public class Singleton {
private static Singleton singleton;
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
但是上面这个方法在多线程的情况下就不行了。如果多个线程同时运行到判空的地方。而且单例的确没有被创建。那么两个线程都会创建一个单例。那此时就不是单例了。
2. 直接使用synchronized但是效率低
public class Singleton {
private static Singleton singleton;
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这样确实可以保证线程安全,但是加锁释放锁是比较耗费性能的。所以这个方法不推荐。
3. 双重检验
public class Singleton {
private static Singleton singleton;
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public static Singleton getInstance() {
// 线程并发问题只有在初始化单例的时候会出现,所以如果实例存在那么就不用加锁了。
if (singleton == null) {
synchronized (Singleton.class) {
// 实例不存在的时候,先获取锁,但是此时有可能其他线程已经实例化过了。所以再判空。
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4. 加volatile
public class Singleton {
// 这个volatile很关键,下面细讲
private volatile static Singleton singleton;
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public static Singleton getInstance() {
// 线程并发问题只有在初始化单例的时候会出现,所以如果实例存在那么就不用加锁了。
if (singleton == null) {
synchronized (Singleton.class) {
// 实例不存在的时候,先获取锁,但是此时有可能其他线程已经实例化过了。所以再判空。
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么需要volatile呢?
我们都知道volatile有内存可见性和防止指令重排序的功能。
首先创建对象分为三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对象的引用。
但是JVM可能会对代码进行重排序,所以真正的执行顺序可能是1->3->2。
那么当第一个线程抢到锁执行初始化对象的时候,发生了重排序,这个时候对象还没初始化,但是对象的引用已经不为空了。
当第二个线程遇到第一个判空时,就会直接返回对象,但是第一个线程此时还没执行完初始化对象,就会造成第二个线程拿到的是一个空对象。造成空指针问题。
5. 静态内部类方法
public class Singleton {
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public static Singleton getInstance() {
return StaticSingletonHolder.singelton;
}
// 一个私有的静态内部类,用于初始化一个静态final实例
private static class StaticSingletonHolder {
private static final Singleton singleton = new Singleton();
}
}
加载一个类时,其中内部类不会同时被加载。一个类被加载,仅仅当某个静态成员被调用时发生。由于在调用 StaticSingleton.getInstance() 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的;由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。
优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。
饿汉式
1. 饿汉式
public class Singleton {
// 这个volatile很关键,下面细讲
private static final Singleton singleton = new Singleton();
// 私有的构造方法,不能在外部类实例化
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
在类初始化时,已经自行实例化。
instance什么时候被初始化?
Singleton类被加载的时候就会被初始化,java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。 生成这4条指令最常见的java代码场景是:
1)使用new关键字实例化对象
2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
4)调用一个类的静态方法
class的生命周期?
class的生命周期一般来说会经历加载、连接、初始化、使用、和卸载五个阶段
class的加载机制
这里可以聊下classloader的双亲委派模型。
2. 枚举方法
枚举默认就是线程安全的,所以不需要担心DCL。但能防止反序列化导致重新创建新的对象。即使使用反射机制也无法实例化一个枚举量。
public class Singleton {
enum Single {
SINGLE;
private Single() {
}
}
}