彻底理解单例模式

  说起单例模式,很多人觉得很简单,不就那几行代码,没什么好理解,其实不然,单例模式的背后也会引出很多知识点。

1 饿汉式

饿汉式实现步骤:

  • 私有构造方法
  • 声明静态变量
  • 对外提供静态方法
public class EagerSingleton {
    private final static EagerSingleton singleton = new EagerSingleton();
    private EagerSingleton() {
    }
    public static EagerSingleton getInstance() {
        return singleton;
    }
}
复制代码

1.1 谈谈饿汉式单例模式为啥是线程安全的

  首先我们知道类加载过程有:加载、验证、准备、解析、初始化、使用和卸载。那么什么时候会触发这个类加载过程呢?

  • new 关键字创建对象
  • 访问类的静态变量(除了被final修饰的静态变量)访问类的静态方法
  • 子类调用父类的静态变量,子类不会被初始化,只有父类会被初始化

  其次 jvm 对类的加载也是通过一个个的线程来完成的,类的加载的过程也是线程安全的,可以理解 类加载的线程是互斥的。所以就能明白饿汉式单例模式为啥要通过静态变量的方式实现来提供实例。

2 懒汉式

双重检查锁 + volatile 关键字来实现

public class LazySingleton {
    private volatile static LazySingleton singleton = null;
    private LazySingleton() {
    }
    public static LazySingleton getInstance() {
        if (null != singleton) {
            synchronized (LazySingleton.class) {
              //这里为啥要加为空判断呢?
              //第一个线程释放锁创建了一个对象,第二线程进入时也创建了一个对象,相当于第一个对象被覆盖了  
              if (null != singleton) { 
                    //A线程刚好被指令重排序,就会先赋值,但还没有完成成员变量的初始化
                    singleton = new LazySingleton(); 
                }
            }
        }
        return singleton;
    }
} 
复制代码

2.1 懒汉式单例模式存在的问题

  首先我们要理解一个对象在内存中创建的过程: LazySingleton s = new LazySingleton();

  1. new 关键字触发类加载机制,已被加载的类不需要再次被加载
  2. 分配内存空间
  3. 将对象进行初始化(即给类的成员变量赋默认值)
  4. 将对象的应用地址指向栈内存中的变量

  JVM的即时编译器会对 代码的执行过程进行优化,包括代码的 执行顺序。所以说上面创对象的过程可能被优化成 1 - 2 - 4 - 3 (指令重排序),这样导致对象创建的不完整,第二个线程来调用获得的对象去使用就会报错。所以要使用 双重检查锁 + volatile 关键字来保证单例模式的正确性

2.2 如何保证原子性、有序性和可见性

  • 原子性

字节码操作的原子性,比如 i++ 它不是原子性,为什么?jvm是如何操作的

(1)先从局部变量表中获取 i 的值

(2)将 i 的值加入到操作栈中

(3)将操作栈中的 i 进行自加

(4)将自己的 i 值放入到操作栈

(5)将操作栈的栈顶元素取出来并放到局部变量表中

怎么解决?使用 锁 机制来保证原子性

  • 有序性
int x;
boolean y;

x=10;
y=false;
复制代码

此时 x和y 之间没有依赖性时,x和y的执行可以进行指令重排序。

int x=10;
int y=x+1;
复制代码

此时 x和y 之间有依赖性时,不可以进行指令重排序。 怎么解决指令重排?volidate 关键字,被它修饰的变量不能指令重排序

  • 可见性

image.png 线程A如果没有立即写入的主内存中,此时线程B读取到的 i 的值还是 10

怎么解决可见性问题?volidate 关键字,它可以禁止工作内存的使用,直接操作主内存

3 静态内部类

  通过静态内部类也可以简单地实现单例模式,但这种方式没有太多可讲的知识点。

public class StaticInnerSingleton {
    private StaticInnerSingleton() {
    }
    /**
     * jvm 在类加载的时候是互斥,由此保证了线程安全的
     */
    private static class InnerSingleton {
        private static StaticInnerSingleton s = new StaticInnerSingleton();
    }
    public static StaticInnerSingleton getInstance() {
        return InnerSingleton.s;
    }
}
复制代码

猜你喜欢

转载自juejin.im/post/7111256226238824461